seo 0.2.20 → 0.2.22

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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
2
- import { n as version, t as load } from "./esm-DvJj2-vq.js";
2
+ import { n as version, t as load } from "./esm-insIkRuo.js";
3
3
  import { Headers, fetch as fetch$1 } from "undici";
4
4
  import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
5
  import { arch, homedir, platform, totalmem } from "node:os";
@@ -2249,10 +2249,10 @@ function aiReferralSourceForValue(value) {
2249
2249
  }
2250
2250
  //#endregion
2251
2251
  //#region packages/core/src/analyze/ai-referrals-analysis.ts
2252
- function compareText$18(left, right) {
2252
+ function compareText$19(left, right) {
2253
2253
  return left < right ? -1 : left > right ? 1 : 0;
2254
2254
  }
2255
- function metric(row, name) {
2255
+ function metric$1(row, name) {
2256
2256
  const value = row[name];
2257
2257
  const parsed = Number(value);
2258
2258
  if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `Google Analytics returned an invalid ${name} value.`);
@@ -2287,8 +2287,8 @@ function analyzeAiReferralRows(input) {
2287
2287
  ...emptyMetrics()
2288
2288
  };
2289
2289
  addMetrics(total, {
2290
- sessions: metric(row, "sessions"),
2291
- eventCount: metric(row, "eventCount")
2290
+ sessions: metric$1(row, "sessions"),
2291
+ eventCount: metric$1(row, "eventCount")
2292
2292
  });
2293
2293
  total.observedSessionSources.add(row.sessionSource ?? "");
2294
2294
  sourceTotals.set(definition.id, total);
@@ -2300,8 +2300,8 @@ function analyzeAiReferralRows(input) {
2300
2300
  const definition = aiReferralSourceForValue(row.sessionSource ?? "");
2301
2301
  if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics detail rows contained an unexpected session source.");
2302
2302
  const metrics = {
2303
- sessions: metric(row, "sessions"),
2304
- eventCount: metric(row, "eventCount")
2303
+ sessions: metric$1(row, "sessions"),
2304
+ eventCount: metric$1(row, "eventCount")
2305
2305
  };
2306
2306
  addMetrics(detailTotal, metrics);
2307
2307
  const landingPage = row.landingPagePlusQueryString || "(not set)";
@@ -2338,7 +2338,7 @@ function analyzeAiReferralRows(input) {
2338
2338
  const generalCaveat = "Google Analytics only attributes visits that arrive with a recognized session source; some AI visits appear as direct, unassigned, or another source.";
2339
2339
  const possiblyTruncated = input.sourceDiscovery.truncated || input.detail.truncated || input.totalUsersEvidence.truncated;
2340
2340
  const allLandingPages = [...pageTotals.entries()].map(([landingPage, metrics]) => {
2341
- const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$18(left[0], right[0]))[0]?.[0];
2341
+ const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$19(left[0], right[0]))[0]?.[0];
2342
2342
  const topSource = topSourceId ? sourceTotals.get(topSourceId)?.definition : void 0;
2343
2343
  if (!topSource) throw new SeoError("PROVIDER_UNAVAILABLE", "Could not resolve the top AI referral source.");
2344
2344
  return {
@@ -2352,7 +2352,7 @@ function analyzeAiReferralRows(input) {
2352
2352
  label: topSource.label
2353
2353
  }
2354
2354
  };
2355
- }).sort((left, right) => right.sessions - left.sessions || compareText$18(left.landingPage, right.landingPage));
2355
+ }).sort((left, right) => right.sessions - left.sessions || compareText$19(left.landingPage, right.landingPage));
2356
2356
  const landingPages = allLandingPages.slice(0, input.resultLimit);
2357
2357
  const selection = {
2358
2358
  limit: input.resultLimit,
@@ -2397,19 +2397,19 @@ function analyzeAiReferralRows(input) {
2397
2397
  id: source.definition.id,
2398
2398
  label: source.definition.label,
2399
2399
  source: source.definition.label,
2400
- observedSessionSources: [...source.observedSessionSources].sort(compareText$18),
2400
+ observedSessionSources: [...source.observedSessionSources].sort(compareText$19),
2401
2401
  sessions: source.sessions,
2402
2402
  eventCount: source.eventCount,
2403
2403
  totalUsers: null,
2404
2404
  shareOfAiSessions: total.sessions ? source.sessions / total.sessions : 0,
2405
2405
  share: total.sessions ? source.sessions / total.sessions : 0
2406
- })).sort((left, right) => right.sessions - left.sessions || compareText$18(left.id, right.id)),
2406
+ })).sort((left, right) => right.sessions - left.sessions || compareText$19(left.id, right.id)),
2407
2407
  landingPages,
2408
2408
  daily: [...dailyTotals.entries()].map(([date, metrics]) => ({
2409
2409
  date,
2410
2410
  ...metrics,
2411
2411
  totalUsers: null
2412
- })).sort((left, right) => compareText$18(left.date, right.date)),
2412
+ })).sort((left, right) => compareText$19(left.date, right.date)),
2413
2413
  caveats: [
2414
2414
  generalCaveat,
2415
2415
  "Attribution uses Google Analytics sessionSource only; landing-page text, medium text, and event referrers are never treated as referral sources.",
@@ -2499,7 +2499,7 @@ function sessionSourceInListFilter(values) {
2499
2499
  //#endregion
2500
2500
  //#region packages/core/src/analyze/ai-referrals.ts
2501
2501
  const DEFAULT_MAX_ROWS$1 = 1e5;
2502
- const MAX_ROWS = 1e5;
2502
+ const MAX_ROWS$1 = 1e5;
2503
2503
  const DEFAULT_RESULT_LIMIT = 25;
2504
2504
  const MAX_RESULT_LIMIT = 1e3;
2505
2505
  function validIsoDate$1(value) {
@@ -2525,7 +2525,7 @@ function reportRange(input) {
2525
2525
  function maxRows(input) {
2526
2526
  if (input.maxRows !== void 0 && input.limit !== void 0 && input.maxRows !== input.limit) throw new SeoError("INVALID_INPUT", "Use maxRows or the legacy limit option, not both.");
2527
2527
  const value = input.maxRows ?? input.limit ?? DEFAULT_MAX_ROWS$1;
2528
- if (!Number.isInteger(value) || value < 1 || value > MAX_ROWS) throw new SeoError("INVALID_INPUT", `maxRows must be a whole number between 1 and ${MAX_ROWS}.`);
2528
+ if (!Number.isInteger(value) || value < 1 || value > MAX_ROWS$1) throw new SeoError("INVALID_INPUT", `maxRows must be a whole number between 1 and ${MAX_ROWS$1}.`);
2529
2529
  return value;
2530
2530
  }
2531
2531
  function resultLimit(input) {
@@ -5775,7 +5775,7 @@ const AUDIT_PAGE_RULE_IDS = [
5775
5775
  "canonical_outside_head",
5776
5776
  "canonical_mismatch"
5777
5777
  ];
5778
- const defaultDependencies$16 = {
5778
+ const defaultDependencies$17 = {
5779
5779
  fetchPage,
5780
5780
  queryPageMetrics,
5781
5781
  now: () => /* @__PURE__ */ new Date()
@@ -5806,7 +5806,7 @@ function buildRecommendations(report) {
5806
5806
  });
5807
5807
  return recommendations;
5808
5808
  }
5809
- async function auditPage(input, dependencies = defaultDependencies$16) {
5809
+ async function auditPage(input, dependencies = defaultDependencies$17) {
5810
5810
  const fetched = await dependencies.fetchPage(input.url, {
5811
5811
  js: input.js,
5812
5812
  refresh: input.refresh
@@ -5948,7 +5948,7 @@ function tokenStem(token) {
5948
5948
  function tokensMatch(a, b) {
5949
5949
  return a === b || tokenStem(a) === tokenStem(b);
5950
5950
  }
5951
- function includesTokenSequence(queryTokens, termTokens) {
5951
+ function includesTokenSequence$1(queryTokens, termTokens) {
5952
5952
  if (!termTokens.length || termTokens.length > queryTokens.length) return false;
5953
5953
  for (let index = 0; index <= queryTokens.length - termTokens.length; index++) if (termTokens.every((termToken, offset) => tokensMatch(queryTokens[index + offset] ?? "", termToken))) return true;
5954
5954
  return false;
@@ -5977,7 +5977,7 @@ function isBrandQuery(query, brandTerms) {
5977
5977
  const compactMatches = (compactTermLength >= 6 || /[^\p{ASCII}]/u.test(compactTerm) && compactTermLength >= 2) && compactQuery.includes(compactTerm);
5978
5978
  const termTokens = normalizedTerm.split(" ");
5979
5979
  if (termTokens.length === 1) return queryTokens.some((queryToken) => tokensMatch(queryToken, normalizedTerm)) || compactMatches;
5980
- return includesTokenSequence(queryTokens, termTokens) || compactMatches;
5980
+ return includesTokenSequence$1(queryTokens, termTokens) || compactMatches;
5981
5981
  });
5982
5982
  }
5983
5983
  function shouldExcludeBrandQuery(input) {
@@ -6289,7 +6289,7 @@ function resolveQueryOpportunityInput(input, now) {
6289
6289
  }
6290
6290
  //#endregion
6291
6291
  //#region packages/core/src/analyze/query-opportunity-rows.ts
6292
- function compareText$17(left, right) {
6292
+ function compareText$18(left, right) {
6293
6293
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
6294
6294
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
6295
6295
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
@@ -6299,9 +6299,9 @@ function compareText$17(left, right) {
6299
6299
  return leftPoints.length - rightPoints.length;
6300
6300
  }
6301
6301
  function compareQueryOpportunityRows(left, right) {
6302
- return right.impressions - left.impressions || right.clicks - left.clicks || left.position - right.position || compareText$17(left.query, right.query);
6302
+ return right.impressions - left.impressions || right.clicks - left.clicks || left.position - right.position || compareText$18(left.query, right.query);
6303
6303
  }
6304
- function validRow$3(row, query) {
6304
+ function validRow$4(row, query) {
6305
6305
  return row.keys.length === 1 && Boolean(query) && Number.isFinite(row.clicks) && Number.isFinite(row.impressions) && Number.isFinite(row.ctr) && Number.isFinite(row.position) && row.clicks >= 0 && row.impressions > 0 && row.clicks <= row.impressions && row.ctr >= 0 && row.ctr <= 1 && row.position > 0;
6306
6306
  }
6307
6307
  function signature(row) {
@@ -6311,7 +6311,7 @@ function normalizeQueryOpportunityRows(rows, selection) {
6311
6311
  const grouped = /* @__PURE__ */ new Map();
6312
6312
  for (const row of rows) {
6313
6313
  const query = row.keys[0]?.trim() ?? "";
6314
- if (!validRow$3(row, query)) {
6314
+ if (!validRow$4(row, query)) {
6315
6315
  selection.invalidRows++;
6316
6316
  continue;
6317
6317
  }
@@ -7323,11 +7323,11 @@ function selectBenchmarkRows$1(input) {
7323
7323
  }
7324
7324
  //#endregion
7325
7325
  //#region packages/core/src/analyze/page-opportunities-analysis.ts
7326
- function compareText$16(left, right) {
7326
+ function compareText$17(left, right) {
7327
7327
  return left < right ? -1 : left > right ? 1 : 0;
7328
7328
  }
7329
7329
  function compareItems$2(left, right) {
7330
- return (right.estimatedCtrClickShortfall ?? -1) - (left.estimatedCtrClickShortfall ?? -1) || right.impressions - left.impressions || left.position - right.position || compareText$16(left.query, right.query) || compareText$16(left.url, right.url);
7330
+ return (right.estimatedCtrClickShortfall ?? -1) - (left.estimatedCtrClickShortfall ?? -1) || right.impressions - left.impressions || left.position - right.position || compareText$17(left.query, right.query) || compareText$17(left.url, right.url);
7331
7331
  }
7332
7332
  function analyzeRow(input) {
7333
7333
  const query = input.row.keys[0]?.trim() ?? "";
@@ -7489,7 +7489,7 @@ function pageOpportunityRecommendations(analysis) {
7489
7489
  //#endregion
7490
7490
  //#region packages/core/src/analyze/page-opportunities.ts
7491
7491
  const MAX_BENCHMARK_ROWS = 1e5;
7492
- const defaultDependencies$15 = {
7492
+ const defaultDependencies$16 = {
7493
7493
  searchAnalytics: querySearchAnalytics,
7494
7494
  fetch: fetchPage,
7495
7495
  extract: extractPage,
@@ -7561,7 +7561,7 @@ function reportCaveats$1(input) {
7561
7561
  input.benchmarkTruncated ? `The site-wide benchmark reached its ${MAX_BENCHMARK_ROWS.toLocaleString("en-US")}-row cap, so provenance is complete but the peer sample is truncated.` : ""
7562
7562
  ].filter(Boolean);
7563
7563
  }
7564
- async function pageOpportunitiesReport(input, dependencies = defaultDependencies$15) {
7564
+ async function pageOpportunitiesReport(input, dependencies = defaultDependencies$16) {
7565
7565
  const url = assertUrlMatchesGscProperty(input.site, input.url);
7566
7566
  const days = reportDays$1(input.days);
7567
7567
  const range = defaultDateRange$1(days);
@@ -11358,7 +11358,7 @@ function absoluteHttpUrl(value) {
11358
11358
  return false;
11359
11359
  }
11360
11360
  }
11361
- function urlKey(value) {
11361
+ function urlKey$1(value) {
11362
11362
  if (!value) return void 0;
11363
11363
  try {
11364
11364
  const url = new URL(value);
@@ -11557,7 +11557,7 @@ function auditCrawlPages(pages, opts = {}) {
11557
11557
  for (const page of pages) {
11558
11558
  if (!isAuditableHtmlPage(page)) continue;
11559
11559
  for (const value of [page.url, page.finalUrl]) {
11560
- const key = urlKey(value);
11560
+ const key = urlKey$1(value);
11561
11561
  if (key) pageByUrl.set(key, page);
11562
11562
  }
11563
11563
  }
@@ -11699,7 +11699,7 @@ function auditCrawlPages(pages, opts = {}) {
11699
11699
  canonicalRaw: page.canonicalRaw,
11700
11700
  canonical: page.canonical
11701
11701
  }));
11702
- const canonicalTarget = pageByUrl.get(urlKey(page.canonical) ?? "");
11702
+ const canonicalTarget = pageByUrl.get(urlKey$1(page.canonical) ?? "");
11703
11703
  if (canonicalTarget?.canonical && !sameUrl(canonicalTarget.canonical, page.canonical)) issues.push(issue("canonical_chain", page, page.canonical, {
11704
11704
  canonical: page.canonical,
11705
11705
  nextCanonical: canonicalTarget.canonical,
@@ -11874,7 +11874,7 @@ const SENSITIVE_FIELD_NAMES = /* @__PURE__ */ new Set([
11874
11874
  "refresh-token"
11875
11875
  ]);
11876
11876
  const SENSITIVE_PARAM_PATTERN = /^(access_?token|auth|api_?key|client_?secret|code|jwt|key|password|refresh_?token|secret|session|sig|signature|token)$/i;
11877
- function uniqueSorted(values = []) {
11877
+ function uniqueSorted$1(values = []) {
11878
11878
  return [...new Set(values.filter(Boolean))].sort();
11879
11879
  }
11880
11880
  function normalizeFetchRate(input, strategy) {
@@ -11894,13 +11894,13 @@ function normalizeCrawlConfig(input) {
11894
11894
  mode: input.mode ?? (strategy === "health" ? "sitemap" : "site"),
11895
11895
  strategy,
11896
11896
  ...input.sitemapUrl ? { sitemapUrl: new URL(input.sitemapUrl).toString() } : {},
11897
- urls: uniqueSorted(input.urls ?? []),
11897
+ urls: uniqueSorted$1(input.urls ?? []),
11898
11898
  maxPages: input.maxPages ?? 500,
11899
11899
  maxDepth: input.maxDepth ?? 16,
11900
11900
  concurrency,
11901
11901
  timeoutMs: input.timeoutMs ?? 2e4,
11902
- include: uniqueSorted(input.include ?? []),
11903
- exclude: uniqueSorted(input.exclude ?? []),
11902
+ include: uniqueSorted$1(input.include ?? []),
11903
+ exclude: uniqueSorted$1(input.exclude ?? []),
11904
11904
  respectRobots: input.respectRobots ?? true,
11905
11905
  useSitemap: input.useSitemap ?? true,
11906
11906
  checkExternal: strategy === "health" ? false : input.checkExternal ?? true,
@@ -12024,19 +12024,19 @@ function sanitizeIssues(issues) {
12024
12024
  function sanitizeMessages(values = []) {
12025
12025
  return values.map((value) => sanitizeTenantString(value));
12026
12026
  }
12027
- function compareText$15(left, right) {
12027
+ function compareText$16(left, right) {
12028
12028
  return left < right ? -1 : left > right ? 1 : 0;
12029
12029
  }
12030
12030
  function sortPages(pages) {
12031
- return [...pages].sort((left, right) => compareText$15(left.url, right.url));
12031
+ return [...pages].sort((left, right) => compareText$16(left.url, right.url));
12032
12032
  }
12033
12033
  function sortRequests(requests) {
12034
12034
  return [...requests].sort((left, right) => {
12035
- const byUrl = compareText$15(left.requestedUrl, right.requestedUrl);
12035
+ const byUrl = compareText$16(left.requestedUrl, right.requestedUrl);
12036
12036
  if (byUrl) return byUrl;
12037
- const byOutcome = compareText$15(left.outcome, right.outcome);
12037
+ const byOutcome = compareText$16(left.outcome, right.outcome);
12038
12038
  if (byOutcome) return byOutcome;
12039
- return compareText$15(left.outcome === "response" ? `${left.status}:${left.finalUrl}` : left.outcome === "skipped" ? left.reason : `${left.failureKind}:${left.error}`, right.outcome === "response" ? `${right.status}:${right.finalUrl}` : right.outcome === "skipped" ? right.reason : `${right.failureKind}:${right.error}`);
12039
+ return compareText$16(left.outcome === "response" ? `${left.status}:${left.finalUrl}` : left.outcome === "skipped" ? left.reason : `${left.failureKind}:${left.error}`, right.outcome === "response" ? `${right.status}:${right.finalUrl}` : right.outcome === "skipped" ? right.reason : `${right.failureKind}:${right.error}`);
12040
12040
  });
12041
12041
  }
12042
12042
  function summarizeCrawlReport(input) {
@@ -12258,8 +12258,8 @@ function createCrawlReport(input) {
12258
12258
  ...input.externalLinkVerification ? { externalLinkVerification: sanitizeTenantValue(input.externalLinkVerification) } : {},
12259
12259
  ...input.siteChecks ? { siteChecks: sanitizeTenantValue(input.siteChecks) } : {},
12260
12260
  ...input.agentDiscovery ? { agentDiscovery: sanitizeTenantValue(input.agentDiscovery) } : {},
12261
- warnings: sanitizeMessages(input.warnings).sort(compareText$15),
12262
- caveats: sanitizeMessages(input.caveats).sort(compareText$15)
12261
+ warnings: sanitizeMessages(input.warnings).sort(compareText$16),
12262
+ caveats: sanitizeMessages(input.caveats).sort(compareText$16)
12263
12263
  };
12264
12264
  }
12265
12265
  function normalizeLoadedCrawlReport(report) {
@@ -12289,8 +12289,8 @@ function normalizeLoadedCrawlReport(report) {
12289
12289
  pages,
12290
12290
  issues,
12291
12291
  issueGroups: groupCrawlIssues(issues),
12292
- warnings: sanitizeMessages(report.warnings).sort(compareText$15),
12293
- caveats: sanitizeMessages(report.caveats).sort(compareText$15)
12292
+ warnings: sanitizeMessages(report.warnings).sort(compareText$16),
12293
+ caveats: sanitizeMessages(report.caveats).sort(compareText$16)
12294
12294
  };
12295
12295
  }
12296
12296
  //#endregion
@@ -14154,7 +14154,7 @@ async function fetchSitemapUrls(input) {
14154
14154
  }
14155
14155
  //#endregion
14156
14156
  //#region packages/core/src/analyze/workflows/analytics-value.ts
14157
- const defaultDependencies$14 = { runGa4Report };
14157
+ const defaultDependencies$15 = { runGa4Report };
14158
14158
  function normalizePath(value) {
14159
14159
  if (!value || value === "(not set)") return "";
14160
14160
  const [path = ""] = value.split("?");
@@ -14167,12 +14167,12 @@ function urlPath(url) {
14167
14167
  return "";
14168
14168
  }
14169
14169
  }
14170
- function compareText$14(left, right) {
14170
+ function compareText$15(left, right) {
14171
14171
  return left < right ? -1 : left > right ? 1 : 0;
14172
14172
  }
14173
14173
  function landingPageValuesFromRows(rows) {
14174
14174
  const orderedRows = [...rows].sort((left, right) => {
14175
- return compareText$14(normalizePath(left.landingPagePlusQueryString ?? ""), normalizePath(right.landingPagePlusQueryString ?? "")) || compareText$14(left.landingPagePlusQueryString ?? "", right.landingPagePlusQueryString ?? "") || compareText$14(left.sessions ?? "", right.sessions ?? "") || compareText$14(left.totalUsers ?? "", right.totalUsers ?? "") || compareText$14(left.conversions ?? "", right.conversions ?? "");
14175
+ return compareText$15(normalizePath(left.landingPagePlusQueryString ?? ""), normalizePath(right.landingPagePlusQueryString ?? "")) || compareText$15(left.landingPagePlusQueryString ?? "", right.landingPagePlusQueryString ?? "") || compareText$15(left.sessions ?? "", right.sessions ?? "") || compareText$15(left.totalUsers ?? "", right.totalUsers ?? "") || compareText$15(left.conversions ?? "", right.conversions ?? "");
14176
14176
  });
14177
14177
  const values = /* @__PURE__ */ new Map();
14178
14178
  for (const row of orderedRows) {
@@ -14214,7 +14214,7 @@ function landingPageRankingPolicy(input) {
14214
14214
  warnings
14215
14215
  };
14216
14216
  }
14217
- async function fetchLandingPageValues(input, dependencies = defaultDependencies$14) {
14217
+ async function fetchLandingPageValues(input, dependencies = defaultDependencies$15) {
14218
14218
  const retainedRowLimit = input.limit ?? 5e3;
14219
14219
  if (!input.propertyId) return {
14220
14220
  values: /* @__PURE__ */ new Map(),
@@ -14984,7 +14984,7 @@ async function crawlSite(input, dependencies) {
14984
14984
  const REPORT_BASELINE_MAX_PAGES = 100;
14985
14985
  const REPORT_BASELINE_MAX_DEPTH = 4;
14986
14986
  const REPORT_BASELINE_MAX_AGE_MS = 10080 * 60 * 1e3;
14987
- const defaultDependencies$13 = {
14987
+ const defaultDependencies$14 = {
14988
14988
  latestCrawlReport,
14989
14989
  crawlSite,
14990
14990
  saveCrawlReport: (report) => saveCrawlReport(report, crawlReportStore, { retention: "baseline" }),
@@ -15033,7 +15033,7 @@ function crawlInput$1(input) {
15033
15033
  refresh: input.refresh ?? false
15034
15034
  };
15035
15035
  }
15036
- async function resolveTechnicalBaseline(input, dependencies = defaultDependencies$13) {
15036
+ async function resolveTechnicalBaseline(input, dependencies = defaultDependencies$14) {
15037
15037
  if (input.crawl === false) return {
15038
15038
  status: "skipped",
15039
15039
  reason: "Technical crawl evidence was skipped with --no-crawl."
@@ -16858,7 +16858,7 @@ function countLabel(count, singular, pluralLabel = `${singular}s`) {
16858
16858
  //#region packages/core/src/analyze/crawler/okf.ts
16859
16859
  const OKF_DEFAULT_CONCEPTS = 500;
16860
16860
  const OKF_MAX_CONCEPTS = 5e3;
16861
- function compareText$13(left, right) {
16861
+ function compareText$14(left, right) {
16862
16862
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
16863
16863
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
16864
16864
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
@@ -16948,7 +16948,7 @@ function okfTitle(value, report) {
16948
16948
  }
16949
16949
  function selectedOkfPages(report, requestedLimit) {
16950
16950
  const limit = okfConceptLimit(requestedLimit);
16951
- const eligible = report.pages.filter((page) => page.indexable && page.status >= 200 && page.status < 300 && !page.blocked && !page.error).sort((left, right) => (right.searchMetrics?.clicks ?? 0) - (left.searchMetrics?.clicks ?? 0) || (right.searchMetrics?.impressions ?? 0) - (left.searchMetrics?.impressions ?? 0) || (right.internalLinkAuthorityScore ?? 0) - (left.internalLinkAuthorityScore ?? 0) || (right.internalInlinkCount ?? 0) - (left.internalInlinkCount ?? 0) || compareText$13(left.finalUrl, right.finalUrl));
16951
+ const eligible = report.pages.filter((page) => page.indexable && page.status >= 200 && page.status < 300 && !page.blocked && !page.error).sort((left, right) => (right.searchMetrics?.clicks ?? 0) - (left.searchMetrics?.clicks ?? 0) || (right.searchMetrics?.impressions ?? 0) - (left.searchMetrics?.impressions ?? 0) || (right.internalLinkAuthorityScore ?? 0) - (left.internalLinkAuthorityScore ?? 0) || (right.internalInlinkCount ?? 0) - (left.internalInlinkCount ?? 0) || compareText$14(left.finalUrl, right.finalUrl));
16952
16952
  const unique = /* @__PURE__ */ new Map();
16953
16953
  for (const page of eligible) if (!unique.has(page.finalUrl)) unique.set(page.finalUrl, page);
16954
16954
  const uniquePages = [...unique.values()];
@@ -18446,7 +18446,7 @@ const CTR_DEFAULT_MIN_IMPRESSIONS = 200;
18446
18446
  const CTR_MAX_MIN_IMPRESSIONS = 1e9;
18447
18447
  const CTR_DEFAULT_LIMIT = 25;
18448
18448
  const CTR_MAX_LIMIT = 100;
18449
- function compareText$12(left, right) {
18449
+ function compareText$13(left, right) {
18450
18450
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
18451
18451
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
18452
18452
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
@@ -18455,7 +18455,7 @@ function compareText$12(left, right) {
18455
18455
  }
18456
18456
  return leftPoints.length - rightPoints.length;
18457
18457
  }
18458
- function boundedInteger$6(value, fallback, minimum, maximum) {
18458
+ function boundedInteger$7(value, fallback, minimum, maximum) {
18459
18459
  if (value === void 0 || !Number.isFinite(value)) return fallback;
18460
18460
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
18461
18461
  }
@@ -18494,7 +18494,7 @@ function aggregateCtrRows(rows, selection) {
18494
18494
  weightedPosition: 0,
18495
18495
  sourceRows: 0
18496
18496
  };
18497
- if (compareText$12(query, group.query) < 0) group.query = query;
18497
+ if (compareText$13(query, group.query) < 0) group.query = query;
18498
18498
  group.clicks += row.clicks;
18499
18499
  group.impressions += row.impressions;
18500
18500
  group.weightedPosition += row.position * row.impressions;
@@ -18532,8 +18532,8 @@ function initialSelection(sourceRows) {
18532
18532
  };
18533
18533
  }
18534
18534
  function analyzeCtrUnderperformersFromRows(input) {
18535
- const minImpressions = boundedInteger$6(input.minImpressions, 200, 1, CTR_MAX_MIN_IMPRESSIONS);
18536
- const limit = boundedInteger$6(input.limit, 25, 1, 100);
18535
+ const minImpressions = boundedInteger$7(input.minImpressions, 200, 1, CTR_MAX_MIN_IMPRESSIONS);
18536
+ const limit = boundedInteger$7(input.limit, 25, 1, 100);
18537
18537
  const selection = initialSelection(input.rows.length);
18538
18538
  const benchmarkRows = aggregateCtrRows(input.rows, selection).filter((row) => {
18539
18539
  const query = row.keys[0] ?? "";
@@ -18598,7 +18598,7 @@ function analyzeCtrUnderperformersFromRows(input) {
18598
18598
  confidence: "low"
18599
18599
  }
18600
18600
  };
18601
- }).filter((item) => item.actualCtr < item.expectedCtr * .6).sort((left, right) => right.clickShortfall - left.clickShortfall || compareText$12(left.query, right.query) || compareText$12(left.url, right.url));
18601
+ }).filter((item) => item.actualCtr < item.expectedCtr * .6).sort((left, right) => right.clickShortfall - left.clickShortfall || compareText$13(left.query, right.query) || compareText$13(left.url, right.url));
18602
18602
  const items = eligibleItems.slice(0, limit);
18603
18603
  selection.eligibleUnderperformers = eligibleItems.length;
18604
18604
  selection.returnedUnderperformers = items.length;
@@ -18614,15 +18614,15 @@ function analyzeCtrUnderperformersFromRows(input) {
18614
18614
  }
18615
18615
  //#endregion
18616
18616
  //#region packages/core/src/analyze/ctr-underperformers.ts
18617
- const MAX_GSC_ROWS$4 = 1e5;
18617
+ const MAX_GSC_ROWS$5 = 1e5;
18618
18618
  function plural$4(count, singular, pluralLabel = `${singular}s`) {
18619
18619
  return count === 1 ? singular : pluralLabel;
18620
18620
  }
18621
- const defaultDependencies$12 = {
18621
+ const defaultDependencies$13 = {
18622
18622
  searchAnalytics: querySearchAnalytics,
18623
18623
  now: () => /* @__PURE__ */ new Date()
18624
18624
  };
18625
- async function ctrUnderperformersReport(input, dependencies = defaultDependencies$12) {
18625
+ async function ctrUnderperformersReport(input, dependencies = defaultDependencies$13) {
18626
18626
  const minImpressions = integerOption$2({
18627
18627
  value: input.minImpressions,
18628
18628
  fallback: 200,
@@ -18644,7 +18644,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
18644
18644
  dimensions: ["query", "page"],
18645
18645
  type: "web",
18646
18646
  dataState: "final",
18647
- maxRows: MAX_GSC_ROWS$4
18647
+ maxRows: MAX_GSC_ROWS$5
18648
18648
  }, { refresh: input.refresh });
18649
18649
  const { items, totalClickShortfall, returnedClickShortfall, selection } = analyzeCtrUnderperformersFromRows({
18650
18650
  rows: source.rows,
@@ -18655,7 +18655,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
18655
18655
  includeBrand: input.includeBrand
18656
18656
  });
18657
18657
  const top = items[0];
18658
- const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$4;
18658
+ const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$5;
18659
18659
  const partialValidation = selection.invalidRows > 0;
18660
18660
  const dataStatus = selection.sourceRows === 0 ? "empty" : possiblyTruncated || partialValidation ? "partial" : selection.eligibleUnderperformers === 0 ? "filtered" : "complete";
18661
18661
  const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
@@ -18672,7 +18672,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
18672
18672
  dataState: "final",
18673
18673
  rowsFetched: source.rowsFetched,
18674
18674
  calls: source.calls,
18675
- maxRows: MAX_GSC_ROWS$4,
18675
+ maxRows: MAX_GSC_ROWS$5,
18676
18676
  possiblyTruncated,
18677
18677
  completeness,
18678
18678
  validation: {
@@ -18702,7 +18702,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
18702
18702
  "CTR benchmarks and calculated click shortfalls are directional heuristics, not forecasts or proof that clicks are available. Validate the search intent and live SERP before editing.",
18703
18703
  ...selection.invalidRows ? [`${selection.invalidRows} invalid provider ${plural$4(selection.invalidRows, "row")} ${selection.invalidRows === 1 ? "was" : "were"} excluded, so this report is partial.`] : [],
18704
18704
  ...selection.duplicateRows ? [`${selection.duplicateRows} repeated query/page ${plural$4(selection.duplicateRows, "row")} ${selection.duplicateRows === 1 ? "was" : "were"} aggregated before CTR and average position were calculated.`] : [],
18705
- ...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$4} was reached, so lower-ranked source rows may be absent.`] : [],
18705
+ ...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$5} was reached, so lower-ranked source rows may be absent.`] : [],
18706
18706
  ...selection.limitedUnderperformers ? [`${selection.limitedUnderperformers} eligible ${plural$4(selection.limitedUnderperformers, "underperformer")} ${selection.limitedUnderperformers === 1 ? "was" : "were"} omitted by the output limit.`] : []
18707
18707
  ],
18708
18708
  recommendations: top ? [
@@ -18829,7 +18829,7 @@ function diagnosisDataStatus(input) {
18829
18829
  function fixed$1(value) {
18830
18830
  return Number(value.toFixed(3));
18831
18831
  }
18832
- function compareText$11(left, right) {
18832
+ function compareText$12(left, right) {
18833
18833
  return left < right ? -1 : left > right ? 1 : 0;
18834
18834
  }
18835
18835
  function validMetric$2(value) {
@@ -18909,8 +18909,8 @@ function compareRetainedSegmentRows(input) {
18909
18909
  else unmatched.push(unmatchedRow(row, "before"));
18910
18910
  }
18911
18911
  for (const [key, row] of after.rows) if (!before.rows.has(key)) unmatched.push(unmatchedRow(row, "after"));
18912
- matched.sort((left, right) => Math.abs(right.clickDelta) - Math.abs(left.clickDelta) || compareText$11(left.key, right.key));
18913
- unmatched.sort((left, right) => right.clicks - left.clicks || compareText$11(left.retainedIn, right.retainedIn) || compareText$11(left.key, right.key));
18912
+ matched.sort((left, right) => Math.abs(right.clickDelta) - Math.abs(left.clickDelta) || compareText$12(left.key, right.key));
18913
+ unmatched.sort((left, right) => right.clicks - left.clicks || compareText$12(left.retainedIn, right.retainedIn) || compareText$12(left.key, right.key));
18914
18914
  return {
18915
18915
  before,
18916
18916
  after,
@@ -19226,7 +19226,7 @@ function unavailableSegmentImpactReport(input) {
19226
19226
  //#endregion
19227
19227
  //#region packages/core/src/analyze/segment-impact.ts
19228
19228
  const DEFAULT_MAX_ROWS = 1e5;
19229
- const defaultDependencies$11 = {
19229
+ const defaultDependencies$12 = {
19230
19230
  searchAnalytics: querySearchAnalytics,
19231
19231
  now: () => /* @__PURE__ */ new Date()
19232
19232
  };
@@ -19266,7 +19266,7 @@ function compareSegmentRows(input) {
19266
19266
  generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
19267
19267
  });
19268
19268
  }
19269
- async function segmentImpact(input, dependencies = defaultDependencies$11) {
19269
+ async function segmentImpact(input, dependencies = defaultDependencies$12) {
19270
19270
  validateSegmentSite(input.site);
19271
19271
  const now = dependencies.now();
19272
19272
  const ranges = resolveSegmentRanges({
@@ -19667,13 +19667,13 @@ const MINIMUM_PAGE_SHARE = .1;
19667
19667
  const MINIMUM_PAGE_IMPRESSIONS = 10;
19668
19668
  const MAXIMUM_DOMINANT_SHARE = .8;
19669
19669
  const MAX_SUPPRESSIONS = 100;
19670
- function boundedInteger$5(value, fallback, minimum, maximum) {
19670
+ function boundedInteger$6(value, fallback, minimum, maximum) {
19671
19671
  if (value === void 0 || !Number.isFinite(value)) return fallback;
19672
19672
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
19673
19673
  }
19674
19674
  function analyzeCannibalRows(input) {
19675
- const minImpressions = boundedInteger$5(input.minImpressions, 50, 0, 1e9);
19676
- const limit = boundedInteger$5(input.limit, 25, 1, 100);
19675
+ const minImpressions = boundedInteger$6(input.minImpressions, 50, 0, 1e9);
19676
+ const limit = boundedInteger$6(input.limit, 25, 1, 100);
19677
19677
  const selection = {
19678
19678
  sourceRows: input.rows.length,
19679
19679
  invalidRows: 0,
@@ -19821,8 +19821,8 @@ const CANNIBAL_MINIMUM_PAGE_SHARE = MINIMUM_PAGE_SHARE;
19821
19821
  const CANNIBAL_MAXIMUM_DOMINANT_SHARE = MAXIMUM_DOMINANT_SHARE;
19822
19822
  //#endregion
19823
19823
  //#region packages/core/src/analyze/site-diagnostics/cannibal.ts
19824
- const MAX_GSC_ROWS$3 = 1e5;
19825
- const defaultDependencies$10 = {
19824
+ const MAX_GSC_ROWS$4 = 1e5;
19825
+ const defaultDependencies$11 = {
19826
19826
  searchAnalytics: querySearchAnalytics,
19827
19827
  now: () => /* @__PURE__ */ new Date()
19828
19828
  };
@@ -19835,7 +19835,7 @@ function verdict$4(input) {
19835
19835
  const result = `${input.returned} of ${input.eligible} multi-URL query candidate${input.eligible === 1 ? "" : "s"} returned for intent and technical review.`;
19836
19836
  return input.status === "partial" ? `${result} Provider evidence is partial.` : result;
19837
19837
  }
19838
- async function cannibalReport(input, dependencies = defaultDependencies$10) {
19838
+ async function cannibalReport(input, dependencies = defaultDependencies$11) {
19839
19839
  const days = integerOption$2({
19840
19840
  value: input.days,
19841
19841
  fallback: 28,
@@ -19865,7 +19865,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
19865
19865
  ...range,
19866
19866
  type: "web",
19867
19867
  dataState: "final",
19868
- maxRows: MAX_GSC_ROWS$3
19868
+ maxRows: MAX_GSC_ROWS$4
19869
19869
  };
19870
19870
  const [pageExposure, propertyDemand] = await Promise.all([dependencies.searchAnalytics(input.site, {
19871
19871
  ...requestBase,
@@ -19888,8 +19888,8 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
19888
19888
  brandTerms: input.brandTerms,
19889
19889
  includeBrand: input.includeBrand
19890
19890
  });
19891
- const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$3;
19892
- const propertyTruncated = propertyDemand.rowsFetched >= MAX_GSC_ROWS$3;
19891
+ const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$4;
19892
+ const propertyTruncated = propertyDemand.rowsFetched >= MAX_GSC_ROWS$4;
19893
19893
  const possiblyTruncated = pageTruncated || propertyTruncated;
19894
19894
  const partialValidation = analysis.selection.invalidRows + analysis.selection.propertyInvalidRows > 0;
19895
19895
  const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
@@ -19914,7 +19914,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
19914
19914
  invalidRows: analysis.selection.invalidRows
19915
19915
  },
19916
19916
  calls: pageExposure.calls,
19917
- maxRows: MAX_GSC_ROWS$3,
19917
+ maxRows: MAX_GSC_ROWS$4,
19918
19918
  possiblyTruncated: pageTruncated
19919
19919
  },
19920
19920
  propertyDemand: {
@@ -19926,7 +19926,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
19926
19926
  invalidRows: analysis.selection.propertyInvalidRows
19927
19927
  },
19928
19928
  calls: propertyDemand.calls,
19929
- maxRows: MAX_GSC_ROWS$3,
19929
+ maxRows: MAX_GSC_ROWS$4,
19930
19930
  possiblyTruncated: propertyTruncated
19931
19931
  },
19932
19932
  completeness
@@ -19999,7 +19999,7 @@ function normalizeUrl(value) {
19999
19999
  return;
20000
20000
  }
20001
20001
  }
20002
- function validRow$2(row, query, url) {
20002
+ function validRow$3(row, query, url) {
20003
20003
  return Boolean(query && url) && Number.isFinite(row.clicks) && row.clicks >= 0 && Number.isFinite(row.impressions) && row.impressions > 0 && row.clicks <= row.impressions && Number.isFinite(row.ctr) && row.ctr >= 0 && row.ctr <= 1 && Number.isFinite(row.position) && row.position > 0;
20004
20004
  }
20005
20005
  function aggregateDecayRows(rows) {
@@ -20008,7 +20008,7 @@ function aggregateDecayRows(rows) {
20008
20008
  for (const row of rows) {
20009
20009
  const query = row.keys[0]?.trim() ?? "";
20010
20010
  const url = normalizeUrl(row.keys[1]?.trim() ?? "");
20011
- if (!validRow$2(row, query, url)) {
20011
+ if (!validRow$3(row, query, url)) {
20012
20012
  invalid++;
20013
20013
  continue;
20014
20014
  }
@@ -20234,8 +20234,8 @@ function analyzeDecay(input) {
20234
20234
  }
20235
20235
  //#endregion
20236
20236
  //#region packages/core/src/analyze/site-diagnostics/decaying.ts
20237
- const MAX_GSC_ROWS$2 = 1e5;
20238
- const defaultDependencies$9 = {
20237
+ const MAX_GSC_ROWS$3 = 1e5;
20238
+ const defaultDependencies$10 = {
20239
20239
  searchAnalytics: querySearchAnalytics,
20240
20240
  now: () => /* @__PURE__ */ new Date()
20241
20241
  };
@@ -20246,7 +20246,7 @@ function verdict$3(report) {
20246
20246
  if (!report.selection.eligibleRows) return "No observed retained query/page declines matched the report filters.";
20247
20247
  return `${report.selection.returnedRows} of ${report.selection.eligibleRows} observed retained query/page declines returned for review.`;
20248
20248
  }
20249
- async function decayingReport(input, dependencies = defaultDependencies$9) {
20249
+ async function decayingReport(input, dependencies = defaultDependencies$10) {
20250
20250
  if (input.comparison !== void 0 && input.comparison !== "previous-period" && input.comparison !== "year-over-year") throw new SeoError("INVALID_INPUT", "comparison must be previous-period or year-over-year.");
20251
20251
  if (input.brandTerms && (input.brandTerms.length > 20 || input.brandTerms.some((term) => !term.trim() || term.trim().length > 200))) throw new SeoError("INVALID_INPUT", "brandTerms must contain at most 20 non-empty terms of 200 characters or fewer.");
20252
20252
  const days = integerOption$2({
@@ -20299,7 +20299,7 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
20299
20299
  aggregationType: "auto",
20300
20300
  type: "web",
20301
20301
  dataState: "final",
20302
- maxRows: MAX_GSC_ROWS$2
20302
+ maxRows: MAX_GSC_ROWS$3
20303
20303
  };
20304
20304
  const [current, previous] = await Promise.all([dependencies.searchAnalytics(input.site, {
20305
20305
  ...request,
@@ -20322,8 +20322,8 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
20322
20322
  brandTerms: input.brandTerms,
20323
20323
  includeBrand: input.includeBrand
20324
20324
  });
20325
- const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$2;
20326
- const previousTruncated = previous.rowsFetched >= MAX_GSC_ROWS$2;
20325
+ const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$3;
20326
+ const previousTruncated = previous.rowsFetched >= MAX_GSC_ROWS$3;
20327
20327
  const possiblyTruncated = currentTruncated || previousTruncated;
20328
20328
  const dataStatus = current.rowsFetched === 0 && previous.rowsFetched === 0 ? "empty" : current.rowsFetched === 0 || previous.rowsFetched === 0 || possiblyTruncated || analysis.selection.currentInvalidRows > 0 || analysis.selection.previousInvalidRows > 0 || analysis.selection.currentRowNotRetained > 0 ? "partial" : analysis.selection.eligibleRows === 0 ? "filtered" : "complete";
20329
20329
  const reportBase = {
@@ -20350,13 +20350,13 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
20350
20350
  current: {
20351
20351
  rowsFetched: current.rowsFetched,
20352
20352
  calls: current.calls,
20353
- maxRows: MAX_GSC_ROWS$2,
20353
+ maxRows: MAX_GSC_ROWS$3,
20354
20354
  possiblyTruncated: currentTruncated
20355
20355
  },
20356
20356
  previous: {
20357
20357
  rowsFetched: previous.rowsFetched,
20358
20358
  calls: previous.calls,
20359
- maxRows: MAX_GSC_ROWS$2,
20359
+ maxRows: MAX_GSC_ROWS$3,
20360
20360
  possiblyTruncated: previousTruncated
20361
20361
  },
20362
20362
  completeness: possiblyTruncated ? "possibly-truncated" : "retained-query-rows-only"
@@ -20415,7 +20415,7 @@ function normalizedQuery(value) {
20415
20415
  function groupKey$1(item) {
20416
20416
  return `${item.template.id}:${normalizedQuery(item.query)}`;
20417
20417
  }
20418
- function compareText$10(left, right) {
20418
+ function compareText$11(left, right) {
20419
20419
  return left < right ? -1 : left > right ? 1 : 0;
20420
20420
  }
20421
20421
  function groupAction(input) {
@@ -20451,11 +20451,11 @@ function groupQuickWins(items) {
20451
20451
  technicalUrls
20452
20452
  });
20453
20453
  return group;
20454
- }).filter((group) => group !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$10(left.id, right.id));
20454
+ }).filter((group) => group !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$11(left.id, right.id));
20455
20455
  }
20456
20456
  //#endregion
20457
20457
  //#region packages/core/src/analyze/site-diagnostics/quick-wins-analysis-primitives.ts
20458
- function boundedInteger$4(value, fallback, minimum, maximum) {
20458
+ function boundedInteger$5(value, fallback, minimum, maximum) {
20459
20459
  if (value === void 0 || !Number.isFinite(value)) return fallback;
20460
20460
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
20461
20461
  }
@@ -20467,7 +20467,7 @@ function validHttpUrl(value) {
20467
20467
  return false;
20468
20468
  }
20469
20469
  }
20470
- function validRow$1(row, query, url) {
20470
+ function validRow$2(row, query, url) {
20471
20471
  return row.keys.length >= 2 && Boolean(query) && validHttpUrl(url) && Number.isFinite(row.clicks) && row.clicks >= 0 && Number.isFinite(row.impressions) && row.impressions > 0 && row.clicks <= row.impressions && Number.isFinite(row.ctr) && row.ctr >= 0 && row.ctr <= 1 && Number.isFinite(row.position) && row.position >= 1;
20472
20472
  }
20473
20473
  function selectBenchmarkRows(input) {
@@ -20489,7 +20489,7 @@ function selectBenchmarkRows(input) {
20489
20489
  for (const row of input.rows) {
20490
20490
  const query = row.keys[0]?.trim() ?? "";
20491
20491
  const url = row.keys[1]?.trim() ?? "";
20492
- if (!validRow$1(row, query, url)) selection.invalidRows++;
20492
+ if (!validRow$2(row, query, url)) selection.invalidRows++;
20493
20493
  else if (row.position > 10) selection.outsideBenchmarkPositionRows++;
20494
20494
  else if (isLowActionabilityQuery(query)) selection.lowActionabilityRows++;
20495
20495
  else if (shouldExcludeBrandQuery({
@@ -20563,11 +20563,11 @@ function quickWinItem(input) {
20563
20563
  }
20564
20564
  };
20565
20565
  }
20566
- function compareText$9(left, right) {
20566
+ function compareText$10(left, right) {
20567
20567
  return left < right ? -1 : left > right ? 1 : 0;
20568
20568
  }
20569
20569
  function compareQuickWinItems(left, right) {
20570
- return right.estimatedCtrClickShortfall - left.estimatedCtrClickShortfall || right.impressions - left.impressions || compareText$9(left.query, right.query) || compareText$9(left.url, right.url);
20570
+ return right.estimatedCtrClickShortfall - left.estimatedCtrClickShortfall || right.impressions - left.impressions || compareText$10(left.query, right.query) || compareText$10(left.url, right.url);
20571
20571
  }
20572
20572
  function roundedSum(values) {
20573
20573
  return Number(values.reduce((sum, value) => sum + value, 0).toFixed(2));
@@ -20580,8 +20580,8 @@ const DEFAULT_LIMIT$3 = 25;
20580
20580
  const MAX_LIMIT$3 = 100;
20581
20581
  const FALLBACK_SOURCE = "builtin_position_ctr_curve_v1";
20582
20582
  function analyzeQuickWinsFromRows(input) {
20583
- const minImpressions = boundedInteger$4(input.minImpressions, DEFAULT_MIN_IMPRESSIONS$2, 0, MAX_MIN_IMPRESSIONS$2);
20584
- const limit = boundedInteger$4(input.limit, DEFAULT_LIMIT$3, 1, MAX_LIMIT$3);
20583
+ const minImpressions = boundedInteger$5(input.minImpressions, DEFAULT_MIN_IMPRESSIONS$2, 0, MAX_MIN_IMPRESSIONS$2);
20584
+ const limit = boundedInteger$5(input.limit, DEFAULT_LIMIT$3, 1, MAX_LIMIT$3);
20585
20585
  const selected = selectBenchmarkRows(input);
20586
20586
  const benchmarkContext = createCtrBenchmarkContext(selected.rows, {
20587
20587
  samplePopulation: "all_qualified_url_samples",
@@ -20682,7 +20682,7 @@ function analyzeQuickWinsFromRows(input) {
20682
20682
  }
20683
20683
  //#endregion
20684
20684
  //#region packages/core/src/analyze/site-diagnostics/quick-wins-templates.ts
20685
- function compareText$8(left, right) {
20685
+ function compareText$9(left, right) {
20686
20686
  return left < right ? -1 : left > right ? 1 : 0;
20687
20687
  }
20688
20688
  function unique$2(values) {
@@ -20708,7 +20708,7 @@ function quickWinTemplateRecommendations(items) {
20708
20708
  action: technicalUrls > 0 ? `Fix verified technical issues on ${technicalUrls} affected URLs before testing shared template changes.` : `Compare live results and verified page evidence for ${urls.length} affected URLs before testing a shared title, snippet, or heading change.`,
20709
20709
  evidence: `${templateItems.length} eligible rows across ${urls.length} distinct URLs share this recognised template. Example queries: ${queries.join("; ")}.`
20710
20710
  };
20711
- }).filter((item) => item !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$8(left.templateId, right.templateId));
20711
+ }).filter((item) => item !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$9(left.templateId, right.templateId));
20712
20712
  }
20713
20713
  function quickWinTemplateSummaries(items) {
20714
20714
  const templates = /* @__PURE__ */ new Map();
@@ -20727,17 +20727,17 @@ function quickWinTemplateSummaries(items) {
20727
20727
  label: item.label,
20728
20728
  rowCount: item.rows,
20729
20729
  urlCount: item.urls.size,
20730
- sampleUrls: [...item.urls].sort(compareText$8).slice(0, 3)
20731
- })).sort((left, right) => right.urlCount - left.urlCount || right.rowCount - left.rowCount || compareText$8(left.id, right.id)).slice(0, 5);
20730
+ sampleUrls: [...item.urls].sort(compareText$9).slice(0, 3)
20731
+ })).sort((left, right) => right.urlCount - left.urlCount || right.rowCount - left.rowCount || compareText$9(left.id, right.id)).slice(0, 5);
20732
20732
  }
20733
20733
  //#endregion
20734
20734
  //#region packages/core/src/analyze/site-diagnostics/quick-wins.ts
20735
- const DEFAULT_DAYS$2 = 28;
20736
- const MAX_DAYS$2 = 548;
20735
+ const DEFAULT_DAYS$3 = 28;
20736
+ const MAX_DAYS$4 = 548;
20737
20737
  const DEFAULT_VERIFY_LIMIT$1 = 5;
20738
20738
  const MAX_VERIFY_LIMIT = 100;
20739
20739
  const MAX_SOURCE_ROWS$3 = 1e5;
20740
- const defaultDependencies$8 = {
20740
+ const defaultDependencies$9 = {
20741
20741
  searchAnalytics: querySearchAnalytics,
20742
20742
  fetch: fetchPage,
20743
20743
  extract: extractPage,
@@ -20756,13 +20756,13 @@ function applyVerification$1(item, coverage) {
20756
20756
  evidenceRef: `${item.recommendation.evidenceRef} ${coverage.summary}`
20757
20757
  };
20758
20758
  }
20759
- async function quickWinsReport(input, dependencies = defaultDependencies$8) {
20760
- const explicitRange = explicitDateRange(input, MAX_DAYS$2);
20759
+ async function quickWinsReport(input, dependencies = defaultDependencies$9) {
20760
+ const explicitRange = explicitDateRange(input, MAX_DAYS$4);
20761
20761
  const days = explicitRange?.days ?? integerOption$2({
20762
20762
  value: input.days,
20763
- fallback: DEFAULT_DAYS$2,
20763
+ fallback: DEFAULT_DAYS$3,
20764
20764
  minimum: 1,
20765
- maximum: MAX_DAYS$2,
20765
+ maximum: MAX_DAYS$4,
20766
20766
  label: "Days"
20767
20767
  });
20768
20768
  const verifyLimit = integerOption$2({
@@ -20912,7 +20912,7 @@ async function quickWinsReport(input, dependencies = defaultDependencies$8) {
20912
20912
  }
20913
20913
  //#endregion
20914
20914
  //#region packages/core/src/analyze/striking-distance-analysis-primitives.ts
20915
- function compareText$7(left, right) {
20915
+ function compareText$8(left, right) {
20916
20916
  const leftPoints = [...left].map((character) => character.codePointAt(0) ?? 0);
20917
20917
  const rightPoints = [...right].map((character) => character.codePointAt(0) ?? 0);
20918
20918
  const length = Math.min(leftPoints.length, rightPoints.length);
@@ -20978,7 +20978,7 @@ function strikingDistanceItem(row) {
20978
20978
  };
20979
20979
  }
20980
20980
  function compareStrikingDistanceItems(left, right) {
20981
- return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$7(left.query, right.query) || compareText$7(left.url, right.url);
20981
+ return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$8(left.query, right.query) || compareText$8(left.url, right.url);
20982
20982
  }
20983
20983
  function groupStrikingDistanceItems(items) {
20984
20984
  const grouped = /* @__PURE__ */ new Map();
@@ -21015,7 +21015,7 @@ function groupStrikingDistanceItems(items) {
21015
21015
  action: shared ? `Review the sampled ${first.template.label.toLowerCase()} URLs for a recurring technical, content, or internal-link pattern before changing the shared template.` : `Review these URLs individually; the current evidence does not support a shared-template change.`
21016
21016
  }
21017
21017
  };
21018
- }).filter((group) => group !== void 0).sort((left, right) => right.totalImpressions - left.totalImpressions || right.uniqueUrls - left.uniqueUrls || compareText$7(left.id, right.id));
21018
+ }).filter((group) => group !== void 0).sort((left, right) => right.totalImpressions - left.totalImpressions || right.uniqueUrls - left.uniqueUrls || compareText$8(left.id, right.id));
21019
21019
  }
21020
21020
  //#endregion
21021
21021
  //#region packages/core/src/analyze/striking-distance-analysis.ts
@@ -21129,22 +21129,22 @@ function analyzeStrikingDistanceRows(input) {
21129
21129
  }
21130
21130
  //#endregion
21131
21131
  //#region packages/core/src/analyze/striking-distance.ts
21132
- const DEFAULT_DAYS$1 = 28;
21133
- const MAX_DAYS$1 = 548;
21132
+ const DEFAULT_DAYS$2 = 28;
21133
+ const MAX_DAYS$3 = 548;
21134
21134
  const DEFAULT_VERIFY_LIMIT = 5;
21135
21135
  const MAX_SOURCE_ROWS$2 = 1e5;
21136
- const defaultDependencies$7 = {
21136
+ const defaultDependencies$8 = {
21137
21137
  searchAnalytics: querySearchAnalytics,
21138
21138
  verifyContent: verifyQueryContent,
21139
21139
  now: () => /* @__PURE__ */ new Date()
21140
21140
  };
21141
- function boundedInteger$3(value, fallback, minimum, maximum) {
21141
+ function boundedInteger$4(value, fallback, minimum, maximum) {
21142
21142
  if (value === void 0 || !Number.isFinite(value)) return fallback;
21143
21143
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
21144
21144
  }
21145
21145
  function reportDays(value) {
21146
- if (value === void 0) return DEFAULT_DAYS$1;
21147
- if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS$1) throw new SeoError("INVALID_INPUT", `Days must be a whole number between 1 and ${MAX_DAYS$1}.`);
21146
+ if (value === void 0) return DEFAULT_DAYS$2;
21147
+ if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS$3) throw new SeoError("INVALID_INPUT", `Days must be a whole number between 1 and ${MAX_DAYS$3}.`);
21148
21148
  return value;
21149
21149
  }
21150
21150
  function dateRange(days, now) {
@@ -21183,7 +21183,7 @@ function verdict$2(analysis) {
21183
21183
  if (analysis.dataStatus === "filtered") return "No retained query/page rows met the report filters.";
21184
21184
  return `${countLabel(analysis.summary.eligibleRows, "eligible query/page row")} found; ${countLabel(analysis.summary.returnedRows, "row")} returned for review.`;
21185
21185
  }
21186
- async function strikingDistance(input, dependencies = defaultDependencies$7) {
21186
+ async function strikingDistance(input, dependencies = defaultDependencies$8) {
21187
21187
  const days = reportDays(input.days);
21188
21188
  const generatedAt = dependencies.now().toISOString();
21189
21189
  const range = dateRange(days, new Date(generatedAt));
@@ -21202,7 +21202,7 @@ async function strikingDistance(input, dependencies = defaultDependencies$7) {
21202
21202
  brandTerms: input.brandTerms,
21203
21203
  includeBrand: input.includeBrand
21204
21204
  });
21205
- const verifyLimit = boundedInteger$3(input.verifyLimit, DEFAULT_VERIFY_LIMIT, 0, analysis.limit);
21205
+ const verifyLimit = boundedInteger$4(input.verifyLimit, DEFAULT_VERIFY_LIMIT, 0, analysis.limit);
21206
21206
  const verificationRequested = input.verifyContent === true || input.verifyContent !== false && input.verifyLimit !== void 0;
21207
21207
  const attempted = verificationRequested ? Math.min(verifyLimit, analysis.items.length) : 0;
21208
21208
  const items = await Promise.all(analysis.items.map(async (item, index) => {
@@ -22030,7 +22030,7 @@ function emptyQuickWins(input) {
22030
22030
  warnings: []
22031
22031
  };
22032
22032
  }
22033
- const defaultDependencies$6 = {
22033
+ const defaultDependencies$7 = {
22034
22034
  trafficAnomaly,
22035
22035
  updateCorrelation,
22036
22036
  segmentImpact,
@@ -22041,7 +22041,7 @@ const defaultDependencies$6 = {
22041
22041
  };
22042
22042
  async function diagnoseProperty(input, dependencies = {}) {
22043
22043
  const providers = {
22044
- ...defaultDependencies$6,
22044
+ ...defaultDependencies$7,
22045
22045
  ...dependencies
22046
22046
  };
22047
22047
  const limit = input.limit ?? 10;
@@ -22989,6 +22989,42 @@ async function providerRequestJson(input) {
22989
22989
  return parsed.data;
22990
22990
  }
22991
22991
  //#endregion
22992
+ //#region packages/core/src/providers/dataforseo/account-pricing.ts
22993
+ function unitPrice(components) {
22994
+ const prices = components ?? [];
22995
+ const total = (type) => {
22996
+ const matching = prices.filter((item) => item.cost_type === type);
22997
+ return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
22998
+ };
22999
+ return {
23000
+ perRequestMicros: total("per_request"),
23001
+ perResultMicros: total("per_result")
23002
+ };
23003
+ }
23004
+ function dataForSeoAccountPrices(account) {
23005
+ return {
23006
+ keywordOverviewPrice: unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal),
23007
+ keywordDiscoveryPrices: {
23008
+ ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
23009
+ related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
23010
+ suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
23011
+ },
23012
+ domainResearchPrices: {
23013
+ domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
23014
+ rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
23015
+ rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
23016
+ serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
23017
+ },
23018
+ linkPrices: {
23019
+ summary: unitPrice(account.price?.backlinks?.summary?.live?.priority_normal),
23020
+ backlinks: unitPrice(account.price?.backlinks?.backlinks?.live?.priority_normal),
23021
+ referringDomains: unitPrice(account.price?.backlinks?.referring_domains?.live?.priority_normal)
23022
+ },
23023
+ serpLiveAdvancedPrice: unitPrice(account.price?.serp?.live?.advanced?.priority_normal),
23024
+ serpTaskPostPrice: unitPrice(account.price?.serp?.task_post?.priority_normal)
23025
+ };
23026
+ }
23027
+ //#endregion
22992
23028
  //#region packages/core/src/providers/dataforseo/account-schema.ts
22993
23029
  const userDataMoneySchema = z.object({
22994
23030
  total: z.number().finite().nonnegative().optional(),
@@ -23006,6 +23042,11 @@ const priceComponentSchema = z.object({
23006
23042
  const normalPriorityPriceSchema = z.object({ priority_normal: z.array(priceComponentSchema).max(20).optional() }).passthrough();
23007
23043
  const livePriceSchema = z.object({ live: normalPriorityPriceSchema.optional() }).passthrough();
23008
23044
  const userDataPriceSchema = z.object({
23045
+ backlinks: z.object({
23046
+ summary: livePriceSchema.optional(),
23047
+ backlinks: livePriceSchema.optional(),
23048
+ referring_domains: livePriceSchema.optional()
23049
+ }).passthrough().optional(),
23009
23050
  dataforseo_labs: z.object({
23010
23051
  keyword_overview: livePriceSchema.optional(),
23011
23052
  keyword_ideas: livePriceSchema.optional(),
@@ -23063,7 +23104,7 @@ function discoveryNextCursor(response) {
23063
23104
  //#region packages/core/src/providers/dataforseo/schema.ts
23064
23105
  const MAX_PROVIDER_MONTHLY_SEARCH_ROWS = 120;
23065
23106
  const nullableNumber$1 = z.number().finite().nullable().optional();
23066
- const nullableCount$1 = z.union([z.number().finite(), z.string().trim().min(1)]).nullable().optional();
23107
+ const nullableCount$2 = z.union([z.number().finite(), z.string().trim().min(1)]).nullable().optional();
23067
23108
  const monthlySearchSchema$1 = z.object({
23068
23109
  year: z.number().int(),
23069
23110
  month: z.number().int(),
@@ -23079,7 +23120,7 @@ const keywordOverviewItemSchema = z.object({
23079
23120
  last_updated_time: z.string().trim().min(1).nullable().optional()
23080
23121
  }).nullish(),
23081
23122
  keyword_properties: z.object({ keyword_difficulty: nullableNumber$1 }).nullish(),
23082
- serp_info: z.object({ se_results_count: nullableCount$1 }).nullish(),
23123
+ serp_info: z.object({ se_results_count: nullableCount$2 }).nullish(),
23083
23124
  search_intent_info: z.object({ main_intent: z.enum([
23084
23125
  "informational",
23085
23126
  "navigational",
@@ -23152,37 +23193,37 @@ const dataForSeoDiscoveryResponseSchema = z.object({
23152
23193
  //#region packages/core/src/providers/dataforseo/domain-schema.ts
23153
23194
  const nullableNumber = z.number().finite().nullable().optional();
23154
23195
  const nullableNonnegativeNumber = z.number().finite().nonnegative().nullable().optional();
23155
- const nullableCount = z.number().int().nonnegative().nullable().optional();
23196
+ const nullableCount$1 = z.number().int().nonnegative().nullable().optional();
23156
23197
  const rankingMetricsSchema = z.object({
23157
- pos_1: nullableCount,
23158
- pos_2_3: nullableCount,
23159
- pos_4_10: nullableCount,
23160
- pos_11_20: nullableCount,
23161
- pos_21_30: nullableCount,
23162
- pos_31_40: nullableCount,
23163
- pos_41_50: nullableCount,
23164
- pos_51_60: nullableCount,
23165
- pos_61_70: nullableCount,
23166
- pos_71_80: nullableCount,
23167
- pos_81_90: nullableCount,
23168
- pos_91_100: nullableCount,
23198
+ pos_1: nullableCount$1,
23199
+ pos_2_3: nullableCount$1,
23200
+ pos_4_10: nullableCount$1,
23201
+ pos_11_20: nullableCount$1,
23202
+ pos_21_30: nullableCount$1,
23203
+ pos_31_40: nullableCount$1,
23204
+ pos_41_50: nullableCount$1,
23205
+ pos_51_60: nullableCount$1,
23206
+ pos_61_70: nullableCount$1,
23207
+ pos_71_80: nullableCount$1,
23208
+ pos_81_90: nullableCount$1,
23209
+ pos_91_100: nullableCount$1,
23169
23210
  etv: nullableNonnegativeNumber,
23170
- count: nullableCount,
23211
+ count: nullableCount$1,
23171
23212
  estimated_paid_traffic_cost: nullableNonnegativeNumber,
23172
- is_new: nullableCount,
23173
- is_up: nullableCount,
23174
- is_down: nullableCount,
23175
- is_lost: nullableCount
23213
+ is_new: nullableCount$1,
23214
+ is_up: nullableCount$1,
23215
+ is_down: nullableCount$1,
23216
+ is_lost: nullableCount$1
23176
23217
  }).passthrough();
23177
23218
  const metricsSchema = z.object({ organic: rankingMetricsSchema.nullable().optional() }).passthrough();
23178
- const paidResponseSchema = z.object({
23219
+ const paidResponseSchema$1 = z.object({
23179
23220
  status_code: z.number().int(),
23180
23221
  status_message: z.string().max(1e3),
23181
23222
  cost: z.number().finite().nonnegative().optional(),
23182
23223
  tasks_count: z.number().int().nonnegative(),
23183
23224
  tasks_error: z.number().int().nonnegative()
23184
23225
  }).passthrough();
23185
- const taskFields = {
23226
+ const taskFields$1 = {
23186
23227
  id: z.string().trim().min(1).max(100).optional(),
23187
23228
  status_code: z.number().int(),
23188
23229
  status_message: z.string().max(1e3),
@@ -23202,8 +23243,8 @@ const overviewResultSchema = z.object({
23202
23243
  items_count: z.number().int().nonnegative().optional(),
23203
23244
  items: z.array(overviewItemSchema).max(1e3).nullable().optional()
23204
23245
  }).passthrough();
23205
- const dataForSeoDomainOverviewResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23206
- ...taskFields,
23246
+ const dataForSeoDomainOverviewResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
23247
+ ...taskFields$1,
23207
23248
  result: z.array(overviewResultSchema).max(10).nullable().optional()
23208
23249
  }).passthrough()).max(10) });
23209
23250
  const rankedSerpItemSchema = z.object({
@@ -23223,8 +23264,8 @@ const rankedKeywordsResultSchema = z.object({
23223
23264
  items_count: z.number().int().nonnegative().optional(),
23224
23265
  items: z.array(rankedKeywordItemSchema).max(1e3).nullable().optional()
23225
23266
  }).passthrough();
23226
- const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23227
- ...taskFields,
23267
+ const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
23268
+ ...taskFields$1,
23228
23269
  result: z.array(rankedKeywordsResultSchema).max(10).nullable().optional()
23229
23270
  }).passthrough()).max(10) });
23230
23271
  const rankingPageItemSchema = z.object({
@@ -23237,8 +23278,8 @@ const rankingPagesResultSchema = z.object({
23237
23278
  items_count: z.number().int().nonnegative().optional(),
23238
23279
  items: z.array(rankingPageItemSchema).max(1e3).nullable().optional()
23239
23280
  }).passthrough();
23240
- const dataForSeoRankingPagesResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23241
- ...taskFields,
23281
+ const dataForSeoRankingPagesResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
23282
+ ...taskFields$1,
23242
23283
  result: z.array(rankingPagesResultSchema).max(10).nullable().optional()
23243
23284
  }).passthrough()).max(10) });
23244
23285
  const competitorItemSchema = z.object({
@@ -23246,9 +23287,9 @@ const competitorItemSchema = z.object({
23246
23287
  avg_position: nullableNumber,
23247
23288
  median_position: nullableNumber,
23248
23289
  etv: nullableNonnegativeNumber,
23249
- keywords_count: nullableCount,
23290
+ keywords_count: nullableCount$1,
23250
23291
  visibility: nullableNonnegativeNumber,
23251
- relevant_serp_items: nullableCount,
23292
+ relevant_serp_items: nullableCount$1,
23252
23293
  keywords_positions: z.record(z.string().trim().min(1).max(80), z.array(z.number().int().positive()).max(100)).nullable().optional()
23253
23294
  }).passthrough();
23254
23295
  const competitorsResultSchema = z.object({
@@ -23257,8 +23298,8 @@ const competitorsResultSchema = z.object({
23257
23298
  items_count: z.number().int().nonnegative().optional(),
23258
23299
  items: z.array(competitorItemSchema).max(1e3).nullable().optional()
23259
23300
  }).passthrough();
23260
- const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23261
- ...taskFields,
23301
+ const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
23302
+ ...taskFields$1,
23262
23303
  result: z.array(competitorsResultSchema).max(10).nullable().optional()
23263
23304
  }).passthrough()).max(10) });
23264
23305
  const DOMAIN_OVERVIEW_PATH = "v3/dataforseo_labs/google/domain_rank_overview/live";
@@ -23448,6 +23489,209 @@ function serpCompetitorsPaidRequest(input, ttlMs) {
23448
23489
  };
23449
23490
  }
23450
23491
  //#endregion
23492
+ //#region packages/core/src/providers/dataforseo/link-schema.ts
23493
+ const nullableCount = z.number().int().nonnegative().nullable().optional();
23494
+ const nullableScore = z.number().finite().nonnegative().nullable().optional();
23495
+ const nullableText = z.string().max(2e4).nullable().optional();
23496
+ const paidResponseSchema = z.object({
23497
+ status_code: z.number().int(),
23498
+ status_message: z.string().max(1e3),
23499
+ cost: z.number().finite().nonnegative().optional(),
23500
+ tasks_count: z.number().int().nonnegative(),
23501
+ tasks_error: z.number().int().nonnegative()
23502
+ }).passthrough();
23503
+ const taskFields = {
23504
+ id: z.string().trim().min(1).max(100).optional(),
23505
+ status_code: z.number().int(),
23506
+ status_message: z.string().max(1e3),
23507
+ cost: z.number().finite().nonnegative().optional(),
23508
+ result_count: z.number().int().nonnegative().optional()
23509
+ };
23510
+ const dataForSeoLinkSummaryItemSchema = z.object({
23511
+ target: z.string().trim().min(1).max(2048).optional(),
23512
+ rank: nullableScore,
23513
+ backlinks: nullableCount,
23514
+ referring_pages: nullableCount,
23515
+ referring_domains: nullableCount,
23516
+ broken_backlinks: nullableCount,
23517
+ broken_pages: nullableCount,
23518
+ backlinks_spam_score: nullableScore,
23519
+ info: z.object({ target_spam_score: nullableScore }).passthrough().nullable().optional()
23520
+ }).passthrough();
23521
+ const dataForSeoLinkSummaryResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23522
+ ...taskFields,
23523
+ result: z.array(dataForSeoLinkSummaryItemSchema).max(10).nullable().optional()
23524
+ }).passthrough()).max(10) });
23525
+ const dataForSeoBacklinkItemSchema = z.object({
23526
+ domain_from: nullableText,
23527
+ url_from: nullableText,
23528
+ url_to: nullableText,
23529
+ anchor: nullableText,
23530
+ item_type: z.string().max(100).nullable().optional(),
23531
+ dofollow: z.boolean().nullable().optional(),
23532
+ rank: nullableScore,
23533
+ domain_from_rank: nullableScore,
23534
+ page_from_rank: nullableScore,
23535
+ backlink_spam_score: nullableScore,
23536
+ backlinks_spam_score: nullableScore,
23537
+ first_seen: z.string().max(100).nullable().optional(),
23538
+ last_visited: z.string().max(100).nullable().optional(),
23539
+ lost_date: z.string().max(100).nullable().optional(),
23540
+ is_lost: z.boolean().nullable().optional(),
23541
+ is_indirect_link: z.boolean().nullable().optional(),
23542
+ links_count: nullableCount,
23543
+ group_count: nullableCount,
23544
+ rel_attributes: z.array(z.string().trim().min(1).max(100)).max(100).nullable().optional(),
23545
+ attributes: z.array(z.string().trim().min(1).max(100)).max(100).nullable().optional()
23546
+ }).passthrough();
23547
+ const backlinkResultSchema = z.object({
23548
+ target: z.string().trim().min(1).max(2048).optional(),
23549
+ total_count: nullableCount,
23550
+ items_count: nullableCount,
23551
+ search_after_token: z.string().max(2e4).nullable().optional(),
23552
+ items: z.array(dataForSeoBacklinkItemSchema).max(1e3).nullable().optional()
23553
+ }).passthrough();
23554
+ const dataForSeoBacklinksResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23555
+ ...taskFields,
23556
+ result: z.array(backlinkResultSchema).max(10).nullable().optional()
23557
+ }).passthrough()).max(10) });
23558
+ const dataForSeoReferringDomainItemSchema = z.object({
23559
+ domain: z.string().trim().min(1).max(2048).nullable().optional(),
23560
+ backlinks: nullableCount,
23561
+ referring_pages: nullableCount,
23562
+ broken_backlinks: nullableCount,
23563
+ broken_pages: nullableCount,
23564
+ rank: nullableScore,
23565
+ backlinks_spam_score: nullableScore,
23566
+ first_seen: z.string().max(100).nullable().optional()
23567
+ }).passthrough();
23568
+ const referringDomainsResultSchema = z.object({
23569
+ target: z.string().trim().min(1).max(2048).optional(),
23570
+ total_count: nullableCount,
23571
+ items_count: nullableCount,
23572
+ items: z.array(dataForSeoReferringDomainItemSchema).max(1e3).nullable().optional()
23573
+ }).passthrough();
23574
+ const dataForSeoReferringDomainsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
23575
+ ...taskFields,
23576
+ result: z.array(referringDomainsResultSchema).max(10).nullable().optional()
23577
+ }).passthrough()).max(10) });
23578
+ const MAX_LINK_ROWS$1 = 1e3;
23579
+ const MAX_LINK_OFFSET = 2e4;
23580
+ const LINK_ENDPOINTS = {
23581
+ summary: "v3/backlinks/summary/live",
23582
+ backlinks: "v3/backlinks/backlinks/live",
23583
+ referringDomains: "v3/backlinks/referring_domains/live"
23584
+ };
23585
+ function resultRows(response) {
23586
+ return response.tasks.reduce((taskTotal, task) => taskTotal + (task.result ?? []).reduce((resultTotal, result) => resultTotal + (result.items?.length ?? 0), 0), 0);
23587
+ }
23588
+ function summaryRows(response) {
23589
+ return response.tasks.reduce((total, task) => total + (task.result?.length ?? 0), 0);
23590
+ }
23591
+ function commonRequest(input) {
23592
+ return {
23593
+ target: input.target,
23594
+ include_subdomains: input.scope === "domain" ? input.includeSubdomains : false,
23595
+ include_indirect_links: true,
23596
+ exclude_internal_backlinks: true,
23597
+ backlinks_status_type: "live",
23598
+ rank_scale: "one_hundred"
23599
+ };
23600
+ }
23601
+ function validatePage(input) {
23602
+ if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 1e3) throw new ProviderError({
23603
+ provider: "dataforseo",
23604
+ operation: input.operation,
23605
+ code: "configuration",
23606
+ message: `Link row limit must be from 1 to ${MAX_LINK_ROWS$1}.`
23607
+ });
23608
+ if (!Number.isSafeInteger(input.offset) || input.offset < 0 || input.offset > 2e4) throw new ProviderError({
23609
+ provider: "dataforseo",
23610
+ operation: input.operation,
23611
+ code: "configuration",
23612
+ message: `Link row offset must be from 0 to ${MAX_LINK_OFFSET}.`
23613
+ });
23614
+ }
23615
+ function linkSummaryPaidRequest(input, ttlMs) {
23616
+ return {
23617
+ operation: "link-summary",
23618
+ capability: "link-summary",
23619
+ endpoint: LINK_ENDPOINTS.summary,
23620
+ request: commonRequest(input),
23621
+ schema: dataForSeoLinkSummaryResponseSchema,
23622
+ requestedRows: 1,
23623
+ price: (account) => account.linkPrices.summary,
23624
+ context: input.context,
23625
+ ttlMs,
23626
+ refresh: input.refresh,
23627
+ rowCount: summaryRows
23628
+ };
23629
+ }
23630
+ function backlinksPaidRequest(input, ttlMs) {
23631
+ validatePage({
23632
+ limit: input.limit,
23633
+ offset: input.offset,
23634
+ operation: "backlinks"
23635
+ });
23636
+ if (input.orderBy.length < 1 || input.orderBy.length > 3) throw new ProviderError({
23637
+ provider: "dataforseo",
23638
+ operation: "backlinks",
23639
+ code: "configuration",
23640
+ message: "Backlink rows need from 1 to 3 sort rules."
23641
+ });
23642
+ return {
23643
+ operation: "backlinks",
23644
+ capability: "backlinks",
23645
+ endpoint: LINK_ENDPOINTS.backlinks,
23646
+ request: {
23647
+ ...commonRequest(input),
23648
+ mode: input.mode,
23649
+ backlinks_status_type: input.status,
23650
+ limit: input.limit,
23651
+ offset: input.offset,
23652
+ order_by: input.orderBy
23653
+ },
23654
+ schema: dataForSeoBacklinksResponseSchema,
23655
+ requestedRows: input.limit,
23656
+ price: (account) => account.linkPrices.backlinks,
23657
+ context: input.context,
23658
+ ttlMs,
23659
+ refresh: input.refresh,
23660
+ rowCount: resultRows
23661
+ };
23662
+ }
23663
+ function referringDomainsPaidRequest(input, ttlMs) {
23664
+ validatePage({
23665
+ limit: input.limit,
23666
+ offset: input.offset,
23667
+ operation: "referring-domains"
23668
+ });
23669
+ if (input.orderBy.length < 1 || input.orderBy.length > 3) throw new ProviderError({
23670
+ provider: "dataforseo",
23671
+ operation: "referring-domains",
23672
+ code: "configuration",
23673
+ message: "Referring-domain rows need from 1 to 3 sort rules."
23674
+ });
23675
+ return {
23676
+ operation: "referring-domains",
23677
+ capability: "referring-domains",
23678
+ endpoint: LINK_ENDPOINTS.referringDomains,
23679
+ request: {
23680
+ ...commonRequest(input),
23681
+ limit: input.limit,
23682
+ offset: input.offset,
23683
+ order_by: input.orderBy
23684
+ },
23685
+ schema: dataForSeoReferringDomainsResponseSchema,
23686
+ requestedRows: input.limit,
23687
+ price: (account) => account.linkPrices.referringDomains,
23688
+ context: input.context,
23689
+ ttlMs,
23690
+ refresh: input.refresh,
23691
+ rowCount: resultRows
23692
+ };
23693
+ }
23694
+ //#endregion
23451
23695
  //#region packages/core/src/providers/dataforseo/paid-request.ts
23452
23696
  function usdToMicros(value) {
23453
23697
  if (value === void 0) return null;
@@ -23971,41 +24215,6 @@ const MAX_KEYWORD_CHARACTERS = 80;
23971
24215
  const MAX_KEYWORD_WORDS = 10;
23972
24216
  const MAX_DISCOVERY_SEEDS$1 = 5;
23973
24217
  const MAX_DISCOVERY_ROWS$1 = 100;
23974
- function unitPrice(components) {
23975
- const prices = components ?? [];
23976
- const total = (type) => {
23977
- const matching = prices.filter((item) => item.cost_type === type);
23978
- return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
23979
- };
23980
- return {
23981
- perRequestMicros: total("per_request"),
23982
- perResultMicros: total("per_result")
23983
- };
23984
- }
23985
- function keywordOverviewPrice(account) {
23986
- return unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal);
23987
- }
23988
- function keywordDiscoveryPrices(account) {
23989
- return {
23990
- ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
23991
- related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
23992
- suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
23993
- };
23994
- }
23995
- function domainResearchPrices(account) {
23996
- return {
23997
- domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
23998
- rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
23999
- rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
24000
- serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
24001
- };
24002
- }
24003
- function serpLiveAdvancedPrice(account) {
24004
- return unitPrice(account.price?.serp?.live?.advanced?.priority_normal);
24005
- }
24006
- function serpTaskPostPrice(account) {
24007
- return unitPrice(account.price?.serp?.task_post?.priority_normal);
24008
- }
24009
24218
  function responseTaskIds(response) {
24010
24219
  return [...new Set(response.tasks.flatMap((task) => task.id ? [task.id] : []))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
24011
24220
  }
@@ -24025,6 +24234,7 @@ var DataForSeoClient = class {
24025
24234
  serpTtlMs;
24026
24235
  accountPricingTtlMs;
24027
24236
  domainResearchTtlMs;
24237
+ linkTtlMs;
24028
24238
  spendLimits;
24029
24239
  accountPricing;
24030
24240
  accountPricingRequest;
@@ -24041,6 +24251,7 @@ var DataForSeoClient = class {
24041
24251
  this.serpTtlMs = options.serpTtlMs ?? DEFAULT_SERP_TTL_MS;
24042
24252
  this.accountPricingTtlMs = options.accountPricingTtlMs ?? DEFAULT_ACCOUNT_PRICING_TTL_MS;
24043
24253
  this.domainResearchTtlMs = options.domainResearchTtlMs ?? 6048e5;
24254
+ this.linkTtlMs = options.linkTtlMs ?? 864e5;
24044
24255
  this.spendLimits = options.spendLimits;
24045
24256
  }
24046
24257
  async getCredentials(operation) {
@@ -24123,11 +24334,7 @@ var DataForSeoClient = class {
24123
24334
  accountDailySpendMicros: dataForSeoUsdToMicros(account.money?.statistics?.day?.total),
24124
24335
  accountDailySpendPeriod: account.money?.statistics?.day?.value ?? null,
24125
24336
  accountDailyLimitMicros: dataForSeoUsdToMicros(account.money?.limits?.day?.total),
24126
- keywordOverviewPrice: keywordOverviewPrice(account),
24127
- keywordDiscoveryPrices: keywordDiscoveryPrices(account),
24128
- domainResearchPrices: domainResearchPrices(account),
24129
- serpLiveAdvancedPrice: serpLiveAdvancedPrice(account),
24130
- serpTaskPostPrice: serpTaskPostPrice(account),
24337
+ ...dataForSeoAccountPrices(account),
24131
24338
  backlinksSubscriptionExpiresAt: account.backlinks_subscription_expiry_date ?? null,
24132
24339
  aiMentionsSubscriptionExpiresAt: account.llm_mentions_subscription_expiry_date ?? null,
24133
24340
  apiVersion: response.version ?? null,
@@ -24190,6 +24397,15 @@ var DataForSeoClient = class {
24190
24397
  async serpCompetitors(input) {
24191
24398
  return this.paidPost(serpCompetitorsPaidRequest(input, this.domainResearchTtlMs));
24192
24399
  }
24400
+ async linkSummary(input) {
24401
+ return this.paidPost(linkSummaryPaidRequest(input, this.linkTtlMs));
24402
+ }
24403
+ async backlinks(input) {
24404
+ return this.paidPost(backlinksPaidRequest(input, this.linkTtlMs));
24405
+ }
24406
+ async referringDomains(input) {
24407
+ return this.paidPost(referringDomainsPaidRequest(input, this.linkTtlMs));
24408
+ }
24193
24409
  async keywordOverview(input) {
24194
24410
  const keywords = input.keywords.map((keyword) => keyword.trim());
24195
24411
  if (keywords.length < 1 || keywords.length > MAX_KEYWORDS_PER_OVERVIEW_REQUEST || keywords.some((keyword) => keyword.length < 1 || keyword.length > MAX_KEYWORD_CHARACTERS || keyword.split(/\s+/u).length > MAX_KEYWORD_WORDS)) throw new ProviderError({
@@ -24533,20 +24749,20 @@ function compareCodepoints$3(left, right) {
24533
24749
  function normalizedKeyword$2(keyword) {
24534
24750
  return keyword.trim().replace(/\s+/gu, " ").toLowerCase();
24535
24751
  }
24536
- function missing$1(field) {
24752
+ function missing$2(field) {
24537
24753
  return unavailableValue("missing", `DataForSEO omitted ${field}.`);
24538
24754
  }
24539
24755
  function numericValue(values, field, valid) {
24540
24756
  const present = values.filter((value) => value !== null && value !== void 0);
24541
- if (present.length === 0) return missing$1(field);
24757
+ if (present.length === 0) return missing$2(field);
24542
24758
  const numbers = present.filter((value) => typeof value === "number" && valid(value));
24543
24759
  if (numbers.length !== present.length) return unavailableValue("invalid", `DataForSEO returned an invalid ${field}.`);
24544
24760
  const unique = [...new Set(numbers)];
24545
24761
  return unique.length === 1 ? observedValue(unique[0]) : unavailableValue("invalid", `DataForSEO returned conflicting ${field} values.`);
24546
24762
  }
24547
- function stringValue(values, field, normalize) {
24763
+ function stringValue$1(values, field, normalize) {
24548
24764
  const present = values.filter((value) => value !== null && value !== void 0);
24549
- if (present.length === 0) return missing$1(field);
24765
+ if (present.length === 0) return missing$2(field);
24550
24766
  const strings = present.flatMap((value) => {
24551
24767
  if (typeof value !== "string") return [];
24552
24768
  const normalized = normalize(value);
@@ -24558,7 +24774,7 @@ function stringValue(values, field, normalize) {
24558
24774
  }
24559
24775
  function monthlySearchesValue(rows) {
24560
24776
  const histories = rows.map((row) => row.keyword_info?.monthly_searches).filter((value) => value !== null && value !== void 0);
24561
- if (histories.length === 0) return missing$1("monthlySearches");
24777
+ if (histories.length === 0) return missing$2("monthlySearches");
24562
24778
  const byMonth = /* @__PURE__ */ new Map();
24563
24779
  let invalid = false;
24564
24780
  for (const history of histories) for (const item of history) {
@@ -24591,27 +24807,27 @@ function resultCountValues(rows) {
24591
24807
  function metricForKeyword(keyword, rows) {
24592
24808
  if (rows.length === 0) return {
24593
24809
  keyword,
24594
- monthlySearchVolume: missing$1("monthlySearchVolume"),
24595
- monthlySearches: missing$1("monthlySearches"),
24596
- searchVolumeUpdatedAt: missing$1("searchVolumeUpdatedAt"),
24597
- cpcUsd: missing$1("cpcUsd"),
24598
- paidCompetition: missing$1("paidCompetition"),
24599
- keywordDifficulty: missing$1("keywordDifficulty"),
24600
- intent: missing$1("intent"),
24601
- resultCount: missing$1("resultCount")
24810
+ monthlySearchVolume: missing$2("monthlySearchVolume"),
24811
+ monthlySearches: missing$2("monthlySearches"),
24812
+ searchVolumeUpdatedAt: missing$2("searchVolumeUpdatedAt"),
24813
+ cpcUsd: missing$2("cpcUsd"),
24814
+ paidCompetition: missing$2("paidCompetition"),
24815
+ keywordDifficulty: missing$2("keywordDifficulty"),
24816
+ intent: missing$2("intent"),
24817
+ resultCount: missing$2("resultCount")
24602
24818
  };
24603
24819
  return {
24604
24820
  keyword,
24605
24821
  monthlySearchVolume: numericValue(rows.map((row) => row.keyword_info?.search_volume), "monthlySearchVolume", (value) => Number.isSafeInteger(value) && value >= 0),
24606
24822
  monthlySearches: monthlySearchesValue(rows),
24607
- searchVolumeUpdatedAt: stringValue(rows.map((row) => row.keyword_info?.last_updated_time), "searchVolumeUpdatedAt", (value) => {
24823
+ searchVolumeUpdatedAt: stringValue$1(rows.map((row) => row.keyword_info?.last_updated_time), "searchVolumeUpdatedAt", (value) => {
24608
24824
  const timestamp = Date.parse(value);
24609
24825
  return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
24610
24826
  }),
24611
24827
  cpcUsd: numericValue(rows.map((row) => row.keyword_info?.cpc), "cpcUsd", (value) => Number.isFinite(value) && value >= 0),
24612
24828
  paidCompetition: numericValue(rows.map((row) => row.keyword_info?.competition), "paidCompetition", (value) => Number.isFinite(value) && value >= 0 && value <= 1),
24613
24829
  keywordDifficulty: numericValue(rows.map((row) => row.keyword_properties?.keyword_difficulty), "keywordDifficulty", (value) => Number.isFinite(value) && value >= 0 && value <= 100),
24614
- intent: stringValue(rows.map((row) => row.search_intent_info?.main_intent), "intent", (value) => value.trim().toLowerCase() || null),
24830
+ intent: stringValue$1(rows.map((row) => row.search_intent_info?.main_intent), "intent", (value) => value.trim().toLowerCase() || null),
24615
24831
  resultCount: numericValue(resultCountValues(rows), "resultCount", (value) => Number.isSafeInteger(value) && value >= 0)
24616
24832
  };
24617
24833
  }
@@ -24664,21 +24880,21 @@ const COMPETITOR_RESULT_TYPES = /* @__PURE__ */ new Set([
24664
24880
  "featured_snippet",
24665
24881
  "local_pack"
24666
24882
  ]);
24667
- function missing(field) {
24883
+ function missing$1(field) {
24668
24884
  return unavailableValue("missing", `DataForSEO omitted ${field}.`);
24669
24885
  }
24670
- function numberValue(value, field) {
24671
- return value === null || value === void 0 ? missing(field) : observedValue(value);
24886
+ function numberValue$1(value, field) {
24887
+ return value === null || value === void 0 ? missing$1(field) : observedValue(value);
24672
24888
  }
24673
24889
  function countValue(value, field) {
24674
- return numberValue(value, field);
24890
+ return numberValue$1(value, field);
24675
24891
  }
24676
24892
  function sumBuckets(metrics, fields, label) {
24677
24893
  const values = fields.map((field) => metrics[field]);
24678
- return values.some((value) => value === null || value === void 0) ? missing(label) : observedValue(values.reduce((sum, value) => sum + (typeof value === "number" ? value : 0), 0));
24894
+ return values.some((value) => value === null || value === void 0) ? missing$1(label) : observedValue(values.reduce((sum, value) => sum + (typeof value === "number" ? value : 0), 0));
24679
24895
  }
24680
24896
  function rankingDistribution(metrics) {
24681
- if (!metrics) return missing("organic ranking distribution");
24897
+ if (!metrics) return missing$1("organic ranking distribution");
24682
24898
  const buckets = {
24683
24899
  first: sumBuckets(metrics, ["pos_1"], "position 1 rankings"),
24684
24900
  top3: sumBuckets(metrics, ["pos_1", "pos_2_3"], "top 3 rankings"),
@@ -24729,9 +24945,9 @@ function rankingDistribution(metrics) {
24729
24945
  }
24730
24946
  function organicFootprint(metrics) {
24731
24947
  return {
24732
- estimatedMonthlyTraffic: numberValue(metrics?.etv, "estimated organic monthly traffic"),
24948
+ estimatedMonthlyTraffic: numberValue$1(metrics?.etv, "estimated organic monthly traffic"),
24733
24949
  rankedKeywords: countValue(metrics?.count, "ranked organic keywords"),
24734
- estimatedMonthlyTrafficCostUsd: numberValue(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
24950
+ estimatedMonthlyTrafficCostUsd: numberValue$1(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
24735
24951
  rankings: rankingDistribution(metrics),
24736
24952
  newRankings: countValue(metrics?.is_new, "new rankings"),
24737
24953
  improvedRankings: countValue(metrics?.is_up, "improved rankings"),
@@ -24786,7 +25002,7 @@ function researchMarket(input, operation) {
24786
25002
  });
24787
25003
  return parsed;
24788
25004
  }
24789
- function rowLimit(limit, offset = 0) {
25005
+ function rowLimit$1(limit, offset = 0) {
24790
25006
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
24791
25007
  provider: "dataforseo",
24792
25008
  operation: "domain-research",
@@ -24810,7 +25026,7 @@ function resultTypes(input, allowed) {
24810
25026
  });
24811
25027
  return normalized;
24812
25028
  }
24813
- function safeUrl$1(value) {
25029
+ function safeUrl$2(value) {
24814
25030
  if (!value) return null;
24815
25031
  try {
24816
25032
  const url = new URL(value);
@@ -24830,7 +25046,7 @@ function dedupeBy(rows, key) {
24830
25046
  }
24831
25047
  return [...grouped.entries()].sort(([left], [right]) => compareCodepoints$3(left, right)).map(([, matches]) => [...matches].sort((left, right) => compareCodepoints$3(JSON.stringify(left), JSON.stringify(right)))[0]);
24832
25048
  }
24833
- function observedNumber(value) {
25049
+ function observedNumber$1(value) {
24834
25050
  return value.state === "observed" ? value.value : -1;
24835
25051
  }
24836
25052
  function duplicateWarning(available, retained, label) {
@@ -24841,7 +25057,7 @@ function duplicateWarning(available, retained, label) {
24841
25057
  message: `${duplicates} duplicate ${label} row${duplicates === 1 ? "" : "s"} were collapsed deterministically.`
24842
25058
  }] : [];
24843
25059
  }
24844
- function totalRows(values) {
25060
+ function totalRows$1(values) {
24845
25061
  const present = values.filter((value) => value !== null && value !== void 0);
24846
25062
  return present.length ? Math.max(...present) : null;
24847
25063
  }
@@ -24857,7 +25073,7 @@ function coverage(input) {
24857
25073
  nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
24858
25074
  };
24859
25075
  }
24860
- function requestContext(reportId, supplied) {
25076
+ function requestContext$1(reportId, supplied) {
24861
25077
  return supplied ?? {
24862
25078
  reportId,
24863
25079
  reportRunId: randomUUID()
@@ -24906,7 +25122,7 @@ async function dataForSeoDomainOverview(client, input) {
24906
25122
  languageCode: market.languageCode.split("-")[0],
24907
25123
  ...location,
24908
25124
  refresh: input.refresh,
24909
- context: requestContext("domain-overview", input.context)
25125
+ context: requestContext$1("domain-overview", input.context)
24910
25126
  });
24911
25127
  const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
24912
25128
  const matchingResult = results.find((row) => !row.target || domain(row.target) === target);
@@ -24998,7 +25214,7 @@ async function dataForSeoRankedKeywords(client, input) {
24998
25214
  const market = researchMarket(input.market, "ranked-keywords");
24999
25215
  const target = rankedTarget(input.target);
25000
25216
  const offset = input.offset ?? 0;
25001
- rowLimit(input.limit, offset);
25217
+ rowLimit$1(input.limit, offset);
25002
25218
  const types = resultTypes(input.resultTypes, RANKED_RESULT_TYPES);
25003
25219
  const excluded = excludedTerms(input);
25004
25220
  const requestedFilters = filters$1(input, excluded);
@@ -25018,7 +25234,7 @@ async function dataForSeoRankedKeywords(client, input) {
25018
25234
  limit: input.limit,
25019
25235
  offset,
25020
25236
  refresh: input.refresh,
25021
- context: requestContext("ranked-keywords", input.context)
25237
+ context: requestContext$1("ranked-keywords", input.context)
25022
25238
  });
25023
25239
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
25024
25240
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
@@ -25027,7 +25243,7 @@ async function dataForSeoRankedKeywords(client, input) {
25027
25243
  const keywordData = row.keyword_data;
25028
25244
  const serp = row.ranked_serp_element?.serp_item;
25029
25245
  const keyword = keywordData?.keyword ? normalizedKeyword$2(keywordData.keyword) : "";
25030
- const url = safeUrl$1(serp?.url);
25246
+ const url = safeUrl$2(serp?.url);
25031
25247
  if (!keywordData || !keyword || !url || !serp?.rank_group || !serp.rank_absolute || !serp.type) {
25032
25248
  invalidRows += 1;
25033
25249
  return [];
@@ -25038,11 +25254,11 @@ async function dataForSeoRankedKeywords(client, input) {
25038
25254
  rankGroup: serp.rank_group,
25039
25255
  rankAbsolute: serp.rank_absolute,
25040
25256
  resultType: serp.type,
25041
- estimatedMonthlyTraffic: numberValue(serp.etv, "estimated keyword traffic")
25257
+ estimatedMonthlyTraffic: numberValue$1(serp.etv, "estimated keyword traffic")
25042
25258
  }];
25043
25259
  });
25044
- const rows = dedupeBy(mappedRows, (row) => `${row.keyword}\0${row.url}\0${row.resultType}`).sort((left, right) => observedNumber(right.monthlySearchVolume) - observedNumber(left.monthlySearchVolume) || left.rankGroup - right.rankGroup || compareCodepoints$3(left.keyword, right.keyword) || compareCodepoints$3(left.url, right.url));
25045
- const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
25260
+ const rows = dedupeBy(mappedRows, (row) => `${row.keyword}\0${row.url}\0${row.resultType}`).sort((left, right) => observedNumber$1(right.monthlySearchVolume) - observedNumber$1(left.monthlySearchVolume) || left.rankGroup - right.rankGroup || compareCodepoints$3(left.keyword, right.keyword) || compareCodepoints$3(left.url, right.url));
25261
+ const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
25046
25262
  return domainEvidence({
25047
25263
  capability: "ranked-keywords",
25048
25264
  data: {
@@ -25115,7 +25331,7 @@ async function dataForSeoRankingPages(client, input) {
25115
25331
  const market = researchMarket(input.market, "ranking-pages");
25116
25332
  const target = domain(input.domain);
25117
25333
  const offset = input.offset ?? 0;
25118
- rowLimit(input.limit, offset);
25334
+ rowLimit$1(input.limit, offset);
25119
25335
  const requestedFilters = filters(input);
25120
25336
  const orderBy = ["metrics.organic.etv,desc", "page_address,asc"];
25121
25337
  const snapshot = await client.rankingPages({
@@ -25127,13 +25343,13 @@ async function dataForSeoRankingPages(client, input) {
25127
25343
  limit: input.limit,
25128
25344
  offset,
25129
25345
  refresh: input.refresh,
25130
- context: requestContext("ranking-pages", input.context)
25346
+ context: requestContext$1("ranking-pages", input.context)
25131
25347
  });
25132
25348
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
25133
25349
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
25134
25350
  let invalidRows = 0;
25135
25351
  const mappedRows = rawRows.flatMap((row) => {
25136
- const url = safeUrl$1(row.page_address);
25352
+ const url = safeUrl$2(row.page_address);
25137
25353
  if (!url) {
25138
25354
  invalidRows += 1;
25139
25355
  return [];
@@ -25143,8 +25359,8 @@ async function dataForSeoRankingPages(client, input) {
25143
25359
  organic: organicFootprint(row.metrics?.organic)
25144
25360
  }];
25145
25361
  });
25146
- const rows = dedupeBy(mappedRows, (row) => row.url).sort((left, right) => observedNumber(right.organic.estimatedMonthlyTraffic) - observedNumber(left.organic.estimatedMonthlyTraffic) || compareCodepoints$3(left.url, right.url));
25147
- const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
25362
+ const rows = dedupeBy(mappedRows, (row) => row.url).sort((left, right) => observedNumber$1(right.organic.estimatedMonthlyTraffic) - observedNumber$1(left.organic.estimatedMonthlyTraffic) || compareCodepoints$3(left.url, right.url));
25363
+ const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
25148
25364
  return domainEvidence({
25149
25365
  capability: "relevant-pages",
25150
25366
  data: {
@@ -25189,7 +25405,7 @@ function keywords(input) {
25189
25405
  async function dataForSeoSerpCompetitors(client, input) {
25190
25406
  const market = researchMarket(input.market, "serp-competitors");
25191
25407
  const offset = input.offset ?? 0;
25192
- rowLimit(input.limit, offset);
25408
+ rowLimit$1(input.limit, offset);
25193
25409
  const requestedKeywords = keywords(input.keywords);
25194
25410
  const types = resultTypes(input.resultTypes, COMPETITOR_RESULT_TYPES);
25195
25411
  const orderBy = [
@@ -25207,7 +25423,7 @@ async function dataForSeoSerpCompetitors(client, input) {
25207
25423
  limit: input.limit,
25208
25424
  offset,
25209
25425
  refresh: input.refresh,
25210
- context: requestContext("serp-competitors", input.context)
25426
+ context: requestContext$1("serp-competitors", input.context)
25211
25427
  });
25212
25428
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
25213
25429
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
@@ -25230,16 +25446,16 @@ async function dataForSeoSerpCompetitors(client, input) {
25230
25446
  return [{
25231
25447
  domain: normalizedDomain,
25232
25448
  matchedKeywords: row.keywords_count,
25233
- averagePosition: numberValue(row.avg_position, "average position"),
25234
- medianPosition: numberValue(row.median_position, "median position"),
25235
- visibility: numberValue(row.visibility, "visibility"),
25236
- estimatedMonthlyTraffic: numberValue(row.etv, "estimated monthly traffic"),
25449
+ averagePosition: numberValue$1(row.avg_position, "average position"),
25450
+ medianPosition: numberValue$1(row.median_position, "median position"),
25451
+ visibility: numberValue$1(row.visibility, "visibility"),
25452
+ estimatedMonthlyTraffic: numberValue$1(row.etv, "estimated monthly traffic"),
25237
25453
  relevantResults: countValue(row.relevant_serp_items, "relevant result count"),
25238
25454
  keywordPositions
25239
25455
  }];
25240
25456
  });
25241
- const rows = dedupeBy(mappedRows, (row) => row.domain).sort((left, right) => observedNumber(right.visibility) - observedNumber(left.visibility) || right.matchedKeywords - left.matchedKeywords || compareCodepoints$3(left.domain, right.domain));
25242
- const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
25457
+ const rows = dedupeBy(mappedRows, (row) => row.domain).sort((left, right) => observedNumber$1(right.visibility) - observedNumber$1(left.visibility) || right.matchedKeywords - left.matchedKeywords || compareCodepoints$3(left.domain, right.domain));
25458
+ const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
25243
25459
  return domainEvidence({
25244
25460
  capability: "serp-competitors",
25245
25461
  data: {
@@ -25363,7 +25579,7 @@ function resolveProvider(input) {
25363
25579
  //#endregion
25364
25580
  //#region packages/core/src/analyze/domain-research/shared.ts
25365
25581
  const MAX_GSC_DOMAIN_ROWS = 1e5;
25366
- function compareText$6(left, right) {
25582
+ function compareText$7(left, right) {
25367
25583
  return left < right ? -1 : left > right ? 1 : 0;
25368
25584
  }
25369
25585
  function normalizedKeyword$1(value) {
@@ -25480,8 +25696,8 @@ function aggregateGscQueries(rows) {
25480
25696
  clicks: item.clicks,
25481
25697
  impressions: item.impressions,
25482
25698
  averagePosition: item.positionWeight ? item.weightedPosition / item.positionWeight : null,
25483
- urls: [...item.urls].sort(compareText$6).slice(0, 5)
25484
- })).sort((left, right) => right.impressions - left.impressions || compareText$6(left.query, right.query));
25699
+ urls: [...item.urls].sort(compareText$7).slice(0, 5)
25700
+ })).sort((left, right) => right.impressions - left.impressions || compareText$7(left.query, right.query));
25485
25701
  }
25486
25702
  async function acquireGscQueries(input) {
25487
25703
  const range = finalGscDateRange(input.days, input.now);
@@ -25847,7 +26063,7 @@ function pseoQueryPatterns(rows) {
25847
26063
  //#region packages/core/src/analyze/pseo/templates.ts
25848
26064
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
25849
26065
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
25850
- function compareText$5(left, right) {
26066
+ function compareText$6(left, right) {
25851
26067
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
25852
26068
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
25853
26069
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
@@ -25903,7 +26119,7 @@ function valueTokens(values) {
25903
26119
  const tokens = value.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
25904
26120
  for (const token of tokens.filter((item) => item.length > 1 || /[^\p{ASCII}]/u.test(item))) counts.set(token, (counts.get(token) ?? 0) + 1);
25905
26121
  }
25906
- return [...counts.entries()].sort((a, b) => b[1] - a[1] || compareText$5(a[0], b[0])).slice(0, 8).map(([token]) => token);
26122
+ return [...counts.entries()].sort((a, b) => b[1] - a[1] || compareText$6(a[0], b[0])).slice(0, 8).map(([token]) => token);
25907
26123
  }
25908
26124
  function templateShape(signature, urls) {
25909
26125
  const signatureParts = signature.split("/").filter(Boolean);
@@ -25952,7 +26168,7 @@ function collectClusters(urls, signatures) {
25952
26168
  return bySignature;
25953
26169
  }
25954
26170
  function clusterPseoTemplates(urls, opts = {}) {
25955
- const uniqueUrls = [...new Set(urls)].sort(compareText$5);
26171
+ const uniqueUrls = [...new Set(urls)].sort(compareText$6);
25956
26172
  const minUrls = opts.minUrls ?? 3;
25957
26173
  const minShare = opts.minShare ?? .01;
25958
26174
  const sampleSize = opts.sampleSize ?? 5;
@@ -25970,15 +26186,15 @@ function clusterPseoTemplates(urls, opts = {}) {
25970
26186
  shape: templateShape(signature, clusterUrls),
25971
26187
  urlCount: clusterUrls.length,
25972
26188
  share: uniqueUrls.length ? clusterUrls.length / uniqueUrls.length : 0,
25973
- sampleUrls: clusterUrls.sort(compareText$5).slice(0, sampleSize)
25974
- })).filter((cluster) => cluster.shape.variableSegments.length > 0 && cluster.urlCount >= minUrls && cluster.share >= minShare).sort((a, b) => b.urlCount - a.urlCount || compareText$5(a.signature, b.signature)).slice(0, opts.limit ?? 50);
26189
+ sampleUrls: clusterUrls.sort(compareText$6).slice(0, sampleSize)
26190
+ })).filter((cluster) => cluster.shape.variableSegments.length > 0 && cluster.urlCount >= minUrls && cluster.share >= minShare).sort((a, b) => b.urlCount - a.urlCount || compareText$6(a.signature, b.signature)).slice(0, opts.limit ?? 50);
25975
26191
  }
25976
26192
  //#endregion
25977
26193
  //#region packages/core/src/analyze/domain-research/gap-analysis.ts
25978
26194
  const MAX_RELEVANCE_QUERY_REFS = 3;
25979
26195
  const MAX_FIRST_PARTY_PATTERN_URLS = 1e4;
25980
26196
  function terms(value) {
25981
- return [...new Set(pseoQueryTerms(value).map(canonicalPseoTerm).filter(Boolean))].sort(compareText$6);
26197
+ return [...new Set(pseoQueryTerms(value).map(canonicalPseoTerm).filter(Boolean))].sort(compareText$7);
25982
26198
  }
25983
26199
  function observedVolume$1(row) {
25984
26200
  return value(row.monthlySearchVolume) ?? -1;
@@ -26022,7 +26238,7 @@ function relevanceForKeyword(keyword, acquisition, tokenIndex) {
26022
26238
  query: row.query,
26023
26239
  overlap: terms(row.query).filter((token) => keywordTerms.includes(token)).length,
26024
26240
  impressions: row.impressions
26025
- })).filter((row) => row.overlap > 0).sort((left, right) => right.overlap - left.overlap || right.impressions - left.impressions || compareText$6(left.query, right.query)).slice(0, MAX_RELEVANCE_QUERY_REFS).map((row) => row.query);
26241
+ })).filter((row) => row.overlap > 0).sort((left, right) => right.overlap - left.overlap || right.impressions - left.impressions || compareText$7(left.query, right.query)).slice(0, MAX_RELEVANCE_QUERY_REFS).map((row) => row.query);
26026
26242
  return {
26027
26243
  state: sharedTokens.length >= 2 ? "observed-overlap" : sharedTokens.length === 1 ? "weak-overlap" : "unavailable",
26028
26244
  sharedTokens,
@@ -26044,10 +26260,10 @@ function repeatedCompetitorPatterns(rows) {
26044
26260
  urlCount: pattern.urlCount,
26045
26261
  sampleUrls: pattern.sampleUrls,
26046
26262
  evidenceRefs: items.flatMap((item) => pattern.sampleUrls.includes(item.row.url) ? [item.evidenceRef] : [])
26047
- }))).sort((left, right) => right.urlCount - left.urlCount || compareText$6(left.domain, right.domain) || compareText$6(left.signature, right.signature));
26263
+ }))).sort((left, right) => right.urlCount - left.urlCount || compareText$7(left.domain, right.domain) || compareText$7(left.signature, right.signature));
26048
26264
  }
26049
26265
  function ownPatterns(acquisition) {
26050
- return clusterPseoTemplates([...new Set(acquisition.sourceRows.rows.flatMap((row) => row.urls))].sort(compareText$6).slice(0, MAX_FIRST_PARTY_PATTERN_URLS), {
26266
+ return clusterPseoTemplates([...new Set(acquisition.sourceRows.rows.flatMap((row) => row.urls))].sort(compareText$7).slice(0, MAX_FIRST_PARTY_PATTERN_URLS), {
26051
26267
  minUrls: 3,
26052
26268
  minShare: 0,
26053
26269
  limit: 50,
@@ -26088,10 +26304,10 @@ function analyzeCompetitorGap(acquisition) {
26088
26304
  const byDomain = /* @__PURE__ */ new Map();
26089
26305
  for (const item of rows) {
26090
26306
  const current = byDomain.get(item.domain);
26091
- if (!current || item.row.rankGroup < current.row.rankGroup || item.row.rankGroup === current.row.rankGroup && compareText$6(item.row.url, current.row.url) < 0) byDomain.set(item.domain, item);
26307
+ if (!current || item.row.rankGroup < current.row.rankGroup || item.row.rankGroup === current.row.rankGroup && compareText$7(item.row.url, current.row.url) < 0) byDomain.set(item.domain, item);
26092
26308
  }
26093
- const retainedRows = [...byDomain.values()].sort((left, right) => left.row.rankGroup - right.row.rankGroup || compareText$6(left.domain, right.domain));
26094
- const best = [...retainedRows].sort((left, right) => observedVolume$1(right.row) - observedVolume$1(left.row) || left.row.rankGroup - right.row.rankGroup || compareText$6(left.domain, right.domain))[0];
26309
+ const retainedRows = [...byDomain.values()].sort((left, right) => left.row.rankGroup - right.row.rankGroup || compareText$7(left.domain, right.domain));
26310
+ const best = [...retainedRows].sort((left, right) => observedVolume$1(right.row) - observedVolume$1(left.row) || left.row.rankGroup - right.row.rankGroup || compareText$7(left.domain, right.domain))[0];
26095
26311
  const firstParty = firstPartyByQuery.get(keyword);
26096
26312
  const relevance = relevanceForKeyword(keyword, acquisition, tokenIndex.index);
26097
26313
  const ownRank = ownRanks.get(keyword) ?? null;
@@ -26135,7 +26351,7 @@ function analyzeCompetitorGap(acquisition) {
26135
26351
  },
26136
26352
  evidenceRefs: retainedRows.map((item) => item.evidenceRef)
26137
26353
  };
26138
- }).sort((left, right) => classificationOrder(left.classification) - classificationOrder(right.classification) || right.competitorCount - left.competitorCount || (value(right.monthlySearchVolume) ?? -1) - (value(left.monthlySearchVolume) ?? -1) || Math.min(...left.competitors.map((item) => item.rank)) - Math.min(...right.competitors.map((item) => item.rank)) || compareText$6(left.keyword, right.keyword));
26354
+ }).sort((left, right) => classificationOrder(left.classification) - classificationOrder(right.classification) || right.competitorCount - left.competitorCount || (value(right.monthlySearchVolume) ?? -1) - (value(left.monthlySearchVolume) ?? -1) || Math.min(...left.competitors.map((item) => item.rank)) - Math.min(...right.competitors.map((item) => item.rank)) || compareText$7(left.keyword, right.keyword));
26139
26355
  return {
26140
26356
  rawRows,
26141
26357
  candidates,
@@ -26154,7 +26370,7 @@ function analyzeCompetitorGap(acquisition) {
26154
26370
  }
26155
26371
  //#endregion
26156
26372
  //#region packages/core/src/analyze/domain-research/gap-report.ts
26157
- const MAX_FINDINGS = 10;
26373
+ const MAX_FINDINGS$2 = 10;
26158
26374
  const MAX_SOURCE_BRIEFS = 5;
26159
26375
  function count(candidates, classification) {
26160
26376
  return candidates.filter((item) => item.classification === classification).length;
@@ -26169,7 +26385,7 @@ function dataStatus(input) {
26169
26385
  function findings$2(input) {
26170
26386
  const result = [];
26171
26387
  for (const [index, candidate] of input.candidates.entries()) {
26172
- if (result.length >= MAX_FINDINGS) break;
26388
+ if (result.length >= MAX_FINDINGS$2) break;
26173
26389
  if (candidate.classification === "relevant-gap-candidate") result.push({
26174
26390
  code: "relevant-competitor-gap",
26175
26391
  evidenceRefs: [`candidates[${index}]`, ...candidate.evidenceRefs],
@@ -26184,7 +26400,7 @@ function findings$2(input) {
26184
26400
  });
26185
26401
  }
26186
26402
  for (const [index, pattern] of input.patterns.entries()) {
26187
- if (result.length >= MAX_FINDINGS) break;
26403
+ if (result.length >= MAX_FINDINGS$2) break;
26188
26404
  result.push({
26189
26405
  code: "competitor-template-pattern",
26190
26406
  evidenceRefs: [`repeatedCompetitorPatterns[${index}]`, ...pattern.evidenceRefs],
@@ -26607,7 +26823,7 @@ async function rankingPagesReport(input, dependencies = {}) {
26607
26823
  impressions: row.impressions,
26608
26824
  averagePosition: row.position
26609
26825
  }] : [];
26610
- }).sort((left, right) => right.impressions - left.impressions || compareText$6(left.url, right.url));
26826
+ }).sort((left, right) => right.impressions - left.impressions || compareText$7(left.url, right.url));
26611
26827
  const firstPartyStatus = !input.site ? "not-requested" : (firstPartyResponse?.rowsFetched ?? 0) >= MAX_GSC_PAGE_ROWS ? "partial" : firstPartyResponse?.rows.length === 0 ? "empty" : "complete";
26612
26828
  const findings = patterns.slice(0, 5).map((pattern, index) => ({
26613
26829
  code: "repeated-ranking-page-pattern",
@@ -26688,7 +26904,7 @@ async function serpCompetitorsReport(input, dependencies = {}) {
26688
26904
  const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
26689
26905
  const market = validatedMarket(input.market);
26690
26906
  const providerId = validatedProvider(input.provider);
26691
- const keywords = [...new Set(input.keywords.map(normalizedKeyword$1))].filter(Boolean).sort(compareText$6);
26907
+ const keywords = [...new Set(input.keywords.map(normalizedKeyword$1))].filter(Boolean).sort(compareText$7);
26692
26908
  if (keywords.length < 2 || keywords.length > 200 || keywords.some((keyword) => keyword.length > 80 || keyword.split(/\s+/u).length > 10)) throw new SeoError("INVALID_INPUT", "SERP competitor research requires 2 to 200 keywords of at most 80 characters and 10 words.");
26693
26909
  const target = input.targetDomain ? normalizeDomain$1(input.targetDomain) : null;
26694
26910
  const declared = declaredCompetitors(input.declaredCompetitors);
@@ -27231,7 +27447,7 @@ function measurementWindow(input) {
27231
27447
  }
27232
27448
  //#endregion
27233
27449
  //#region packages/core/src/analyze/experiments/measure-change.ts
27234
- const defaultDependencies$5 = {
27450
+ const defaultDependencies$6 = {
27235
27451
  searchAnalytics: querySearchAnalytics,
27236
27452
  googleAnalyticsReport: runGa4Report,
27237
27453
  contentGroup: getContentGroup,
@@ -27249,7 +27465,7 @@ function counterfactualDelta(input) {
27249
27465
  const expectedAfter = input.treatmentBefore * (input.controlAfter / input.controlBefore);
27250
27466
  return fixed(input.treatmentAfter - expectedAfter);
27251
27467
  }
27252
- async function measureChange(input, dependencies = defaultDependencies$5) {
27468
+ async function measureChange(input, dependencies = defaultDependencies$6) {
27253
27469
  const now = dependencies.now();
27254
27470
  const change = (input.id ? getChange(input.id) : void 0) ?? {
27255
27471
  id: "adhoc",
@@ -27974,14 +28190,14 @@ async function verifyInternalLinkCandidate(input) {
27974
28190
  }
27975
28191
  //#endregion
27976
28192
  //#region packages/core/src/analyze/internal-links.ts
27977
- const MAX_GSC_ROWS$1 = 1e5;
27978
- const defaultDependencies$4 = {
28193
+ const MAX_GSC_ROWS$2 = 1e5;
28194
+ const defaultDependencies$5 = {
27979
28195
  searchAnalytics: querySearchAnalytics,
27980
28196
  fetch: fetchPage,
27981
28197
  extract: extractPage,
27982
28198
  now: () => /* @__PURE__ */ new Date()
27983
28199
  };
27984
- async function internalLinksReport(input, dependencies = defaultDependencies$4) {
28200
+ async function internalLinksReport(input, dependencies = defaultDependencies$5) {
27985
28201
  const targetPage = new URL(assertUrlMatchesGscProperty(input.site, input.targetUrl));
27986
28202
  targetPage.hash = "";
27987
28203
  const targetUrl = targetPage.toString();
@@ -28030,7 +28246,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
28030
28246
  dimensions: ["query", "page"],
28031
28247
  type: "web",
28032
28248
  dataState: "final",
28033
- maxRows: MAX_GSC_ROWS$1,
28249
+ maxRows: MAX_GSC_ROWS$2,
28034
28250
  dimensionFilterGroups: [{
28035
28251
  groupType: "and",
28036
28252
  filters: [{
@@ -28062,7 +28278,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
28062
28278
  dimensions: ["query", "page"],
28063
28279
  type: "web",
28064
28280
  dataState: "final",
28065
- maxRows: MAX_GSC_ROWS$1
28281
+ maxRows: MAX_GSC_ROWS$2
28066
28282
  }, { refresh: input.refresh }) : {
28067
28283
  rows: [],
28068
28284
  calls: 0,
@@ -28107,8 +28323,8 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
28107
28323
  else checkedSources += 1;
28108
28324
  }
28109
28325
  const uncheckedCandidates = Math.max(0, analysis.candidates.length - attemptedSources);
28110
- const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$1);
28111
- const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS$1;
28326
+ const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$2);
28327
+ const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS$2;
28112
28328
  const invalidRows = analysis.selection.targetInvalidRows + analysis.selection.sourceInvalidRows;
28113
28329
  const structuredWarnings = uniqueInternalLinksWarnings(warnings);
28114
28330
  const dataStatus = target.verification === "technical-issue" ? "target-technical-issue" : target.verification === "failed" ? "partial" : targetRows.length === 0 ? "empty" : targetTruncated || sourceTruncated || invalidRows > 0 ? "partial" : queryCandidates && sourceResult.rows.length === 0 ? "source-empty" : analysis.selection.targetEligibleQueries === 0 || analysis.candidates.length === 0 ? "filtered" : failedChecks > 0 || uncheckedCandidates > 0 ? "partial" : "complete";
@@ -28149,18 +28365,18 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
28149
28365
  pageFilter: alias,
28150
28366
  rowsFetched: result.rowsFetched,
28151
28367
  calls: result.calls,
28152
- possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$1
28368
+ possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$2
28153
28369
  })),
28154
28370
  rowsFetched: targetRequests.reduce((sum, { result }) => sum + result.rowsFetched, 0),
28155
28371
  calls: targetRequests.reduce((sum, { result }) => sum + result.calls, 0),
28156
- maxRowsPerRequest: MAX_GSC_ROWS$1,
28372
+ maxRowsPerRequest: MAX_GSC_ROWS$2,
28157
28373
  possiblyTruncated: targetTruncated
28158
28374
  },
28159
28375
  candidates: {
28160
28376
  queried: queryCandidates,
28161
28377
  rowsFetched: sourceResult.rowsFetched,
28162
28378
  calls: sourceResult.calls,
28163
- maxRows: MAX_GSC_ROWS$1,
28379
+ maxRows: MAX_GSC_ROWS$2,
28164
28380
  possiblyTruncated: sourceTruncated
28165
28381
  },
28166
28382
  completeness: !queryCandidates ? "not-queried" : targetTruncated || sourceTruncated ? "possibly-truncated" : "retained-rows-only"
@@ -28179,7 +28395,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
28179
28395
  minImpressions,
28180
28396
  resultLimit: limit,
28181
28397
  checkLimit,
28182
- maxGscRowsPerRequest: MAX_GSC_ROWS$1
28398
+ maxGscRowsPerRequest: MAX_GSC_ROWS$2
28183
28399
  },
28184
28400
  target: {
28185
28401
  requestedUrl: target.requestedUrl,
@@ -28469,7 +28685,7 @@ function providerReportError(error) {
28469
28685
  if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
28470
28686
  throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
28471
28687
  }
28472
- function validateInput$3(input) {
28688
+ function validateInput$4(input) {
28473
28689
  if (input.keywords.length < 1 || input.keywords.length > MAX_REPORT_KEYWORDS || input.keywords.some((keyword) => {
28474
28690
  const value = keyword.trim();
28475
28691
  return value.length < 1 || value.length > 80 || value.split(/\s+/u).length > 10;
@@ -28503,7 +28719,7 @@ function findings$1(analysis) {
28503
28719
  }).sort((left, right) => Number(right.code === "recent-demand-increase") - Number(left.code === "recent-demand-increase") || right.recentAverage - left.recentAverage || compareCodepoints$2(left.keyword, right.keyword)).slice(0, 10).map(({ recentAverage: _recentAverage, ...finding }) => finding);
28504
28720
  }
28505
28721
  async function keywordMetricsReport(input, dependencies = {}) {
28506
- const validated = validateInput$3(input);
28722
+ const validated = validateInput$4(input);
28507
28723
  let candidates;
28508
28724
  try {
28509
28725
  candidates = dependencies.candidates ?? await defaultCandidates$3();
@@ -28801,16 +29017,16 @@ function benchmarkDetails(benchmark) {
28801
29017
  positiveUrlSamples: benchmark.positiveUrlSamples
28802
29018
  };
28803
29019
  }
28804
- function boundedInteger$2(input) {
29020
+ function boundedInteger$3(input) {
28805
29021
  const value = Number.isFinite(input.value) ? input.value ?? 0 : input.fallback;
28806
29022
  return Math.min(input.maximum ?? Number.POSITIVE_INFINITY, Math.max(1, Math.floor(value)));
28807
29023
  }
28808
29024
  function analyzeQueryClustersFromRows(input) {
28809
- const minImpressions = boundedInteger$2({
29025
+ const minImpressions = boundedInteger$3({
28810
29026
  value: input.minImpressions,
28811
29027
  fallback: 25
28812
29028
  });
28813
- const limit = boundedInteger$2({
29029
+ const limit = boundedInteger$3({
28814
29030
  value: input.limit,
28815
29031
  fallback: 25,
28816
29032
  maximum: 100
@@ -28929,7 +29145,7 @@ function integerInRange(value, fallback, minimum, maximum) {
28929
29145
  if (value === void 0 || !Number.isFinite(value)) return fallback;
28930
29146
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
28931
29147
  }
28932
- function compareText$4(left, right) {
29148
+ function compareText$5(left, right) {
28933
29149
  const leftPoints = [...left].map((character) => character.codePointAt(0) ?? 0);
28934
29150
  const rightPoints = [...right].map((character) => character.codePointAt(0) ?? 0);
28935
29151
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
@@ -28948,7 +29164,7 @@ function normalizedHttpUrl(value) {
28948
29164
  return;
28949
29165
  }
28950
29166
  }
28951
- function validRow(row, query, url) {
29167
+ function validRow$1(row, query, url) {
28952
29168
  return Boolean(query && url) && Number.isFinite(row.clicks) && Number.isFinite(row.impressions) && Number.isFinite(row.ctr) && Number.isFinite(row.position) && row.clicks >= 0 && row.impressions > 0 && row.clicks <= row.impressions && row.ctr >= 0 && row.ctr <= 1 && row.position > 0;
28953
29169
  }
28954
29170
  function queryFromRow(row) {
@@ -28961,7 +29177,7 @@ function queryFromRow(row) {
28961
29177
  };
28962
29178
  }
28963
29179
  function compareQueries(left, right) {
28964
- return right.impressions - left.impressions || left.position - right.position || right.clicks - left.clicks || compareText$4(left.query, right.query);
29180
+ return right.impressions - left.impressions || left.position - right.position || right.clicks - left.clicks || compareText$5(left.query, right.query);
28965
29181
  }
28966
29182
  function recommendation(input) {
28967
29183
  return {
@@ -29007,7 +29223,7 @@ function pageItem(url, rows) {
29007
29223
  };
29008
29224
  }
29009
29225
  function compareItems$1(left, right) {
29010
- return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$4(left.url, right.url);
29226
+ return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$5(left.url, right.url);
29011
29227
  }
29012
29228
  function analyzeSecondPageRows(input) {
29013
29229
  const minImpressions = integerInRange(input.minImpressions, DEFAULT_MIN_IMPRESSIONS, 0, MAX_MIN_IMPRESSIONS);
@@ -29028,7 +29244,7 @@ function analyzeSecondPageRows(input) {
29028
29244
  for (const row of input.rows) {
29029
29245
  const query = row.keys[0]?.trim() ?? "";
29030
29246
  const url = normalizedHttpUrl(row.keys[1]?.trim() ?? "");
29031
- if (!validRow(row, query, url)) selection.invalidRows++;
29247
+ if (!validRow$1(row, query, url)) selection.invalidRows++;
29032
29248
  else if (row.position <= 10 || row.position > 20) selection.outsidePositionRows++;
29033
29249
  else if (shouldExcludeBrandQuery({
29034
29250
  query,
@@ -29119,8 +29335,8 @@ function analyzeSecondPageRows(input) {
29119
29335
  }
29120
29336
  //#endregion
29121
29337
  //#region packages/core/src/analyze/keyword-opportunities.ts
29122
- const DEFAULT_DAYS = 28;
29123
- const MAX_DAYS = 548;
29338
+ const DEFAULT_DAYS$1 = 28;
29339
+ const MAX_DAYS$2 = 548;
29124
29340
  const DEFAULT_REPORT_LIMIT = 10;
29125
29341
  const MAX_REPORT_LIMIT = 25;
29126
29342
  const DEFAULT_KEYWORD_LIMIT = 30;
@@ -29137,7 +29353,7 @@ const SOURCE_ORDER = [
29137
29353
  "second-page",
29138
29354
  "striking-distance"
29139
29355
  ];
29140
- function compareText$3(left, right) {
29356
+ function compareText$4(left, right) {
29141
29357
  return left < right ? -1 : left > right ? 1 : 0;
29142
29358
  }
29143
29359
  function rounded(value, precision = 3) {
@@ -29149,7 +29365,7 @@ function integerOption$1(input) {
29149
29365
  if (!Number.isInteger(input.value) || input.value < input.minimum || input.value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be a whole number between ${input.minimum} and ${input.maximum}.`);
29150
29366
  return input.value;
29151
29367
  }
29152
- function validateInput$2(input) {
29368
+ function validateInput$3(input) {
29153
29369
  const site = input.site.trim();
29154
29370
  if (!site || site.length > 2048) throw new SeoError("INVALID_INPUT", "Use a valid Search Console property.");
29155
29371
  if (input.minImpressions !== void 0 && (!Number.isInteger(input.minImpressions) || input.minImpressions < 0 || input.minImpressions > 1e9)) throw new SeoError("INVALID_INPUT", "Minimum impressions must be a whole number between 0 and 1000000000.");
@@ -29167,9 +29383,9 @@ function validateInput$2(input) {
29167
29383
  site,
29168
29384
  days: integerOption$1({
29169
29385
  value: input.days,
29170
- fallback: DEFAULT_DAYS,
29386
+ fallback: DEFAULT_DAYS$1,
29171
29387
  minimum: 1,
29172
- maximum: MAX_DAYS,
29388
+ maximum: MAX_DAYS$2,
29173
29389
  label: "Days"
29174
29390
  }),
29175
29391
  limit: integerOption$1({
@@ -29281,7 +29497,7 @@ function aggregateCandidates(rows) {
29281
29497
  impressions: rounded(row.impressions),
29282
29498
  ctr: rounded(row.ctr, 4),
29283
29499
  averagePosition: rounded(row.position, 2)
29284
- })).sort((left, right) => right.impressions - left.impressions || left.averagePosition - right.averagePosition || compareText$3(left.url, right.url));
29500
+ })).sort((left, right) => right.impressions - left.impressions || left.averagePosition - right.averagePosition || compareText$4(left.url, right.url));
29285
29501
  return [keyword, {
29286
29502
  keyword,
29287
29503
  clicks: rounded(clicks),
@@ -29515,7 +29731,7 @@ function nextSteps(input) {
29515
29731
  return steps;
29516
29732
  }
29517
29733
  async function keywordOpportunitiesReport(input, dependencies = {}) {
29518
- const validated = validateInput$2(input);
29734
+ const validated = validateInput$3(input);
29519
29735
  const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
29520
29736
  const generatedAt = now.toISOString();
29521
29737
  const range = finalGscDateRange(validated.days, now);
@@ -29883,7 +30099,7 @@ function providerError$3(error) {
29883
30099
  if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
29884
30100
  throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
29885
30101
  }
29886
- function validateInput$1(input) {
30102
+ function validateInput$2(input) {
29887
30103
  if (input.seeds.length < 1 || input.seeds.length > MAX_RESEARCH_SEEDS || input.seeds.some((seed) => {
29888
30104
  const value = seed.trim();
29889
30105
  return value.length < 1 || value.length > 80 || value.split(/\s+/u).length > 10;
@@ -29936,7 +30152,7 @@ async function keywordResearchReport(input, dependencies = {}) {
29936
30152
  "suggestions"
29937
30153
  ];
29938
30154
  const limit = input.limit ?? 50;
29939
- const validated = validateInput$1({
30155
+ const validated = validateInput$2({
29940
30156
  ...input,
29941
30157
  sources,
29942
30158
  limit
@@ -30019,6 +30235,864 @@ async function keywordResearchReport(input, dependencies = {}) {
30019
30235
  };
30020
30236
  }
30021
30237
  //#endregion
30238
+ //#region packages/core/src/analyze/local-search/intent.ts
30239
+ const NEARBY_PHRASES = [
30240
+ "closest to me",
30241
+ "in my area",
30242
+ "near me",
30243
+ "nearby"
30244
+ ];
30245
+ const UK_POSTCODE = /\b[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}\b/iu;
30246
+ const US_ZIP_CODE = /\b\d{5}(?:-\d{4})?\b/u;
30247
+ const US_ZIP_CONTEXT = /\b(?:around|near|nearby|postal(?:\s+code)?|zip(?:\s+code)?)\b/iu;
30248
+ function normalizeLocalSearchText(value) {
30249
+ return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLowerCase();
30250
+ }
30251
+ function tokens(value) {
30252
+ return normalizeLocalSearchText(value).match(/[\p{L}\p{N}]+/gu) ?? [];
30253
+ }
30254
+ function includesTokenSequence(source, candidate) {
30255
+ if (candidate.length === 0 || candidate.length > source.length) return false;
30256
+ return source.some((_, start) => candidate.every((token, offset) => source[start + offset] === token));
30257
+ }
30258
+ function termMatcher(terms) {
30259
+ const root = {
30260
+ children: /* @__PURE__ */ new Map(),
30261
+ terms: []
30262
+ };
30263
+ for (const term of terms) {
30264
+ let node = root;
30265
+ for (const token of tokens(term)) {
30266
+ const child = node.children.get(token) ?? {
30267
+ children: /* @__PURE__ */ new Map(),
30268
+ terms: []
30269
+ };
30270
+ node.children.set(token, child);
30271
+ node = child;
30272
+ }
30273
+ node.terms.push(term);
30274
+ }
30275
+ return (source) => {
30276
+ const matches = /* @__PURE__ */ new Set();
30277
+ for (let start = 0; start < source.length; start++) {
30278
+ let node = root;
30279
+ for (let index = start; index < source.length; index++) {
30280
+ node = node.children.get(source[index]);
30281
+ if (!node) break;
30282
+ for (const term of node.terms) matches.add(term);
30283
+ }
30284
+ }
30285
+ return [...matches].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
30286
+ };
30287
+ }
30288
+ function normalizeLocationTerms(values = []) {
30289
+ return [...new Set(values.map(normalizeLocalSearchText).filter(Boolean))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
30290
+ }
30291
+ function classifyLocalIntent(query, locationTerms) {
30292
+ return createLocalIntentClassifier(locationTerms)(query);
30293
+ }
30294
+ function createLocalIntentClassifier(locationTerms) {
30295
+ const matchLocations = termMatcher(locationTerms);
30296
+ return (query) => {
30297
+ const queryTokens = tokens(normalizeLocalSearchText(query));
30298
+ const matchedLocations = matchLocations(queryTokens);
30299
+ const matchedNearby = NEARBY_PHRASES.filter((phrase) => includesTokenSequence(queryTokens, tokens(phrase)));
30300
+ const ukPostcode = UK_POSTCODE.exec(query)?.[0] ?? null;
30301
+ const usZipCode = US_ZIP_CONTEXT.test(query) && US_ZIP_CODE.test(query) ? US_ZIP_CODE.exec(query)?.[0] ?? null : null;
30302
+ const postalCode = ukPostcode ?? usZipCode;
30303
+ const classes = [];
30304
+ if (matchedLocations.length > 0) classes.push("named-location");
30305
+ if (matchedNearby.length > 0) classes.push("nearby");
30306
+ if (postalCode) classes.push("postal-code");
30307
+ if (classes.length === 0) return null;
30308
+ return {
30309
+ heuristic: true,
30310
+ method: "explicit-local-intent-v1",
30311
+ classes,
30312
+ matchedTerms: [
30313
+ ...matchedLocations,
30314
+ ...matchedNearby,
30315
+ ...postalCode ? [normalizeLocalSearchText(postalCode)] : []
30316
+ ]
30317
+ };
30318
+ };
30319
+ }
30320
+ //#endregion
30321
+ //#region packages/core/src/analyze/local-search/analysis.ts
30322
+ function compareText$3(left, right) {
30323
+ return left < right ? -1 : left > right ? 1 : 0;
30324
+ }
30325
+ function validRow(row) {
30326
+ const query = row.keys[0]?.trim() ?? "";
30327
+ const page = row.keys[1]?.trim() ?? "";
30328
+ let validPage = false;
30329
+ try {
30330
+ validPage = ["http:", "https:"].includes(new URL(page).protocol);
30331
+ } catch {
30332
+ validPage = false;
30333
+ }
30334
+ if (row.keys.length !== 2 || !query || !validPage || !Number.isFinite(row.clicks) || !Number.isFinite(row.impressions) || !Number.isFinite(row.ctr) || !Number.isFinite(row.position) || row.clicks < 0 || row.impressions <= 0 || row.clicks > row.impressions || row.ctr < 0 || row.ctr > 1 || row.position <= 0) return null;
30335
+ return {
30336
+ query,
30337
+ page,
30338
+ clicks: row.clicks,
30339
+ impressions: row.impressions,
30340
+ ctr: row.ctr,
30341
+ position: row.position
30342
+ };
30343
+ }
30344
+ function rowSignature(row) {
30345
+ return `${row.clicks}\u0000${row.impressions}\u0000${row.ctr}\u0000${row.position}`;
30346
+ }
30347
+ function normalizeRows(rows, selection) {
30348
+ const grouped = /* @__PURE__ */ new Map();
30349
+ for (const source of rows) {
30350
+ const row = validRow(source);
30351
+ if (!row) {
30352
+ selection.invalidRows++;
30353
+ continue;
30354
+ }
30355
+ const key = `${row.query}\u0000${row.page}`;
30356
+ grouped.set(key, [...grouped.get(key) ?? [], row]);
30357
+ }
30358
+ const result = [];
30359
+ for (const group of grouped.values()) {
30360
+ if (new Set(group.map(rowSignature)).size > 1) {
30361
+ selection.conflictingRows += group.length;
30362
+ continue;
30363
+ }
30364
+ selection.exactDuplicateRows += group.length - 1;
30365
+ const row = group[0];
30366
+ if (row) result.push(row);
30367
+ }
30368
+ return result;
30369
+ }
30370
+ function opportunityAction(position, pageCount) {
30371
+ if (pageCount > 1) return "review-page-overlap";
30372
+ if (position <= 3) return "protect-visibility";
30373
+ if (position <= 20) return "improve-existing-page";
30374
+ return "investigate-relevance";
30375
+ }
30376
+ function aggregateQueries(input) {
30377
+ const grouped = /* @__PURE__ */ new Map();
30378
+ for (const row of input.rows) {
30379
+ if (isLowActionabilityQuery(row.query)) {
30380
+ input.selection.lowActionabilityRows++;
30381
+ continue;
30382
+ }
30383
+ if (shouldExcludeBrandQuery({
30384
+ query: row.query,
30385
+ siteUrl: input.site,
30386
+ brandTerms: input.brandTerms,
30387
+ includeBrand: input.includeBrand
30388
+ })) {
30389
+ input.selection.brandRows++;
30390
+ continue;
30391
+ }
30392
+ grouped.set(row.query, [...grouped.get(row.query) ?? [], row]);
30393
+ }
30394
+ const opportunities = [];
30395
+ for (const [query, rows] of grouped) {
30396
+ const intent = input.classify(query);
30397
+ if (!intent) {
30398
+ input.selection.nonLocalRows += rows.length;
30399
+ continue;
30400
+ }
30401
+ const impressions = rows.reduce((sum, row) => sum + row.impressions, 0);
30402
+ if (impressions < input.minImpressions) {
30403
+ input.selection.belowMinimumRows += rows.length;
30404
+ continue;
30405
+ }
30406
+ const clicks = rows.reduce((sum, row) => sum + row.clicks, 0);
30407
+ const averagePosition = rows.reduce((sum, row) => sum + row.position * row.impressions, 0) / impressions;
30408
+ const pages = rows.map((row) => ({
30409
+ url: row.page,
30410
+ clicks: row.clicks,
30411
+ impressions: row.impressions,
30412
+ ctr: row.impressions ? row.clicks / row.impressions : 0,
30413
+ averagePosition: row.position
30414
+ })).sort((left, right) => right.impressions - left.impressions || right.clicks - left.clicks || left.averagePosition - right.averagePosition || compareText$3(left.url, right.url));
30415
+ opportunities.push({
30416
+ query,
30417
+ clicks,
30418
+ impressions,
30419
+ ctr: impressions ? clicks / impressions : 0,
30420
+ averagePosition,
30421
+ intent,
30422
+ action: opportunityAction(averagePosition, pages.length),
30423
+ pages,
30424
+ pageCoverage: {
30425
+ available: pages.length,
30426
+ returned: Math.min(3, pages.length),
30427
+ omitted: Math.max(0, pages.length - 3)
30428
+ }
30429
+ });
30430
+ }
30431
+ return opportunities.sort((left, right) => right.impressions - left.impressions || right.clicks - left.clicks || left.averagePosition - right.averagePosition || compareText$3(left.query, right.query));
30432
+ }
30433
+ function localTemplates(opportunities) {
30434
+ const clusters = clusterPseoTemplates(opportunities.flatMap((item) => item.pages.map((page) => page.url)), {
30435
+ minUrls: 2,
30436
+ minShare: 0,
30437
+ limit: 10,
30438
+ sampleSize: 5
30439
+ });
30440
+ return clusters.map((cluster) => {
30441
+ const matched = opportunities.flatMap((item) => item.pages.filter((page) => templateForUrl(page.url, clusters) === cluster.signature).map((page) => ({
30442
+ query: item.query,
30443
+ page
30444
+ })));
30445
+ return {
30446
+ heuristic: true,
30447
+ signature: cluster.signature,
30448
+ urlCount: cluster.urlCount,
30449
+ sampleUrls: cluster.sampleUrls,
30450
+ queryCount: new Set(matched.map((item) => item.query)).size,
30451
+ clicks: matched.reduce((sum, item) => sum + item.page.clicks, 0),
30452
+ impressions: matched.reduce((sum, item) => sum + item.page.impressions, 0)
30453
+ };
30454
+ });
30455
+ }
30456
+ function analyzeLocalSearchRows(input) {
30457
+ const selection = {
30458
+ sourceRows: input.rows.length,
30459
+ invalidRows: 0,
30460
+ exactDuplicateRows: 0,
30461
+ conflictingRows: 0,
30462
+ lowActionabilityRows: 0,
30463
+ brandRows: 0,
30464
+ nonLocalRows: 0,
30465
+ belowMinimumRows: 0,
30466
+ eligibleQueries: 0,
30467
+ returnedQueries: 0,
30468
+ omittedQueries: 0,
30469
+ limit: input.limit,
30470
+ minImpressions: input.minImpressions
30471
+ };
30472
+ const normalized = normalizeRows(input.rows, selection);
30473
+ const eligible = aggregateQueries({
30474
+ ...input,
30475
+ rows: normalized,
30476
+ selection,
30477
+ classify: createLocalIntentClassifier(input.locationTerms)
30478
+ });
30479
+ selection.eligibleQueries = eligible.length;
30480
+ selection.returnedQueries = Math.min(input.limit, eligible.length);
30481
+ selection.omittedQueries = Math.max(0, eligible.length - input.limit);
30482
+ return {
30483
+ selection,
30484
+ eligibleSummary: {
30485
+ clicks: eligible.reduce((sum, item) => sum + item.clicks, 0),
30486
+ impressions: eligible.reduce((sum, item) => sum + item.impressions, 0),
30487
+ namedLocationQueries: eligible.filter((item) => item.intent.classes.includes("named-location")).length,
30488
+ nearbyQueries: eligible.filter((item) => item.intent.classes.includes("nearby")).length,
30489
+ postalCodeQueries: eligible.filter((item) => item.intent.classes.includes("postal-code")).length,
30490
+ pageOverlapQueries: eligible.filter((item) => item.action === "review-page-overlap").length
30491
+ },
30492
+ eligiblePageCount: new Set(eligible.flatMap((item) => item.pages.map((page) => page.url))).size,
30493
+ opportunities: eligible.slice(0, input.limit).map((item) => ({
30494
+ ...item,
30495
+ pages: item.pages.slice(0, 3)
30496
+ })),
30497
+ templates: localTemplates(eligible)
30498
+ };
30499
+ }
30500
+ //#endregion
30501
+ //#region packages/core/src/providers/dataforseo/serp-snapshot.ts
30502
+ const MAX_SERP_DEPTH = 100;
30503
+ const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
30504
+ function safeUrl$1(value) {
30505
+ if (!value) return null;
30506
+ try {
30507
+ const parsed = new URL(value);
30508
+ if (!["http:", "https:"].includes(parsed.protocol)) return null;
30509
+ parsed.username = "";
30510
+ parsed.password = "";
30511
+ return parsed.toString();
30512
+ } catch {
30513
+ return null;
30514
+ }
30515
+ }
30516
+ function validOrganic(item) {
30517
+ return item.type === "organic" && Number.isSafeInteger(item.rank_group) && (item.rank_group ?? 0) > 0 && Number.isSafeInteger(item.rank_absolute) && (item.rank_absolute ?? 0) > 0 && Number.isSafeInteger(item.page) && (item.page ?? 0) > 0 && Boolean(item.domain?.trim()) && safeUrl$1(item.url) !== null;
30518
+ }
30519
+ function organicResult(item) {
30520
+ if (!validOrganic(item)) return null;
30521
+ const url = safeUrl$1(item.url);
30522
+ if (!url) return null;
30523
+ return {
30524
+ rankGroup: item.rank_group,
30525
+ rankAbsolute: item.rank_absolute,
30526
+ page: item.page,
30527
+ domain: item.domain.trim().toLowerCase(),
30528
+ url,
30529
+ title: item.title ?? null,
30530
+ description: item.description ?? null,
30531
+ isFeaturedSnippet: item.is_featured_snippet ?? null
30532
+ };
30533
+ }
30534
+ function checkedAt(value, fallback) {
30535
+ const timestamp = Date.parse(value);
30536
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
30537
+ }
30538
+ function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
30539
+ const market = searchMarketSchema.parse(input.market);
30540
+ const keyword = normalizedKeyword$2(input.keyword);
30541
+ const location = locationRequest(market, "serp-snapshot");
30542
+ const result = snapshot.response.tasks.flatMap((task) => task.result ?? []).find((item) => normalizedKeyword$2(item.keyword) === keyword);
30543
+ if (!result) throw new ProviderError({
30544
+ provider: "dataforseo",
30545
+ operation: "serp-snapshot",
30546
+ code: "invalid-response",
30547
+ message: "DataForSEO returned no matching SERP result."
30548
+ });
30549
+ const warnings = [...snapshot.warnings];
30550
+ const items = result.items ?? [];
30551
+ const organicItems = items.filter((item) => item.type === "organic");
30552
+ const mappedOrganicResults = organicItems.flatMap((item) => {
30553
+ const mapped = organicResult(item);
30554
+ return mapped ? [mapped] : [];
30555
+ }).sort((left, right) => left.rankAbsolute - right.rankAbsolute || left.rankGroup - right.rankGroup || compareCodepoints$3(left.url, right.url));
30556
+ const organicResults = mappedOrganicResults.slice(0, input.depth);
30557
+ const invalidRows = organicItems.length - mappedOrganicResults.length;
30558
+ const resultCapped = mappedOrganicResults.length > input.depth;
30559
+ const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
30560
+ if (invalidRows > 0) warnings.push({
30561
+ code: "invalid-organic-results",
30562
+ field: "organicResults",
30563
+ message: `DataForSEO returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank, domain, or URL.`
30564
+ });
30565
+ if (!Number.isFinite(Date.parse(result.datetime))) warnings.push({
30566
+ code: "invalid-serp-observation-time",
30567
+ field: "checkedAt",
30568
+ message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
30569
+ });
30570
+ const checkUrl = safeUrl$1(result.check_url);
30571
+ if (result.check_url && !checkUrl) warnings.push({
30572
+ code: "invalid-serp-check-url",
30573
+ field: "checkUrl",
30574
+ message: "DataForSEO returned an invalid SERP check URL."
30575
+ });
30576
+ if (invalidResultCount) warnings.push({
30577
+ code: "invalid-serp-result-count",
30578
+ field: "resultCount",
30579
+ message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
30580
+ });
30581
+ const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
30582
+ return {
30583
+ schemaVersion: 1,
30584
+ provider: "dataforseo",
30585
+ capability: "serp-snapshot",
30586
+ data: {
30587
+ keyword,
30588
+ effectiveKeyword: normalizedKeyword$2(result.spell?.keyword ?? result.keyword),
30589
+ searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
30590
+ checkedAt: checkedAt(result.datetime, snapshot.observedAt),
30591
+ checkUrl,
30592
+ resultCount: invalidResultCount ? null : result.se_results_count ?? null,
30593
+ pagesCount: result.pages_count === void 0 ? null : result.pages_count,
30594
+ features,
30595
+ organicResults
30596
+ },
30597
+ observedAt: snapshot.observedAt,
30598
+ market: {
30599
+ ...market,
30600
+ device: market.device ?? "desktop"
30601
+ },
30602
+ coverage: {
30603
+ requestedRows: input.depth,
30604
+ returnedRows: snapshot.returnedRows,
30605
+ retainedRows: organicResults.length,
30606
+ invalidRows,
30607
+ providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
30608
+ completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
30609
+ nextCursor: null
30610
+ },
30611
+ cache: snapshot.cache,
30612
+ cost: snapshot.cost,
30613
+ request: {
30614
+ operation: "serp-snapshot",
30615
+ endpoint,
30616
+ limit: input.depth,
30617
+ filters: {
30618
+ countryCode: market.countryCode,
30619
+ languageCode: market.languageCode,
30620
+ device: market.device ?? "desktop",
30621
+ ...location.locationCode !== void 0 ? { locationCode: location.locationCode } : { locationName: location.locationName }
30622
+ },
30623
+ sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
30624
+ },
30625
+ warnings
30626
+ };
30627
+ }
30628
+ var DataForSeoSerpSnapshotProvider = class {
30629
+ provider = "dataforseo";
30630
+ capabilitySupport = [{
30631
+ capability: "serp-snapshot",
30632
+ status: "available",
30633
+ markets: [{
30634
+ searchEngines: ["google"],
30635
+ devices: ["desktop", "mobile"],
30636
+ location: "any"
30637
+ }]
30638
+ }];
30639
+ client;
30640
+ constructor(options = {}) {
30641
+ this.client = options.client ?? new DataForSeoClient(options);
30642
+ }
30643
+ async serpSnapshot(input) {
30644
+ const market = searchMarketSchema.parse(input.market);
30645
+ if (market.searchEngine !== "google") throw new ProviderError({
30646
+ provider: "dataforseo",
30647
+ operation: "serp-snapshot",
30648
+ code: "configuration",
30649
+ message: "DataForSEO SERP snapshots currently support Google."
30650
+ });
30651
+ if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_SERP_DEPTH) throw new ProviderError({
30652
+ provider: "dataforseo",
30653
+ operation: "serp-snapshot",
30654
+ code: "configuration",
30655
+ message: "SERP depth must be from 1 to 100."
30656
+ });
30657
+ const keyword = normalizedKeyword$2(input.keyword);
30658
+ if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new ProviderError({
30659
+ provider: "dataforseo",
30660
+ operation: "serp-snapshot",
30661
+ code: "configuration",
30662
+ message: "SERP snapshots require a keyword of at most 80 characters and 10 words."
30663
+ });
30664
+ const context = input.context ?? {
30665
+ reportId: "serp-results",
30666
+ reportRunId: randomUUID()
30667
+ };
30668
+ const location = locationRequest(market, "serp-snapshot");
30669
+ return mapDataForSeoSerpSnapshot(input, await this.client.serpLive({
30670
+ keyword,
30671
+ languageCode: market.languageCode.split("-")[0],
30672
+ ...location,
30673
+ device: market.device ?? "desktop",
30674
+ depth: input.depth,
30675
+ refresh: input.refresh,
30676
+ context
30677
+ }));
30678
+ }
30679
+ };
30680
+ //#endregion
30681
+ //#region packages/core/src/analyze/serp-results.ts
30682
+ const MAX_REPORT_DEPTH = 100;
30683
+ function compareCodepoints(left, right) {
30684
+ return left < right ? -1 : left > right ? 1 : 0;
30685
+ }
30686
+ function snapshotProvider(candidate) {
30687
+ return "serpSnapshot" in candidate && typeof candidate.serpSnapshot === "function" ? candidate : null;
30688
+ }
30689
+ async function defaultCandidates$1() {
30690
+ return [{
30691
+ adapter: new DataForSeoSerpSnapshotProvider(),
30692
+ connected: Boolean(await readDataForSeoCredentials()),
30693
+ priority: 10
30694
+ }];
30695
+ }
30696
+ function providerError$2(error) {
30697
+ if (!(error instanceof ProviderError)) throw error;
30698
+ if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
30699
+ if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
30700
+ throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
30701
+ }
30702
+ function validateInput$1(input) {
30703
+ const keyword = input.keyword.trim().replace(/\s+/gu, " ");
30704
+ if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new SeoError("INVALID_INPUT", "SERP results requires a keyword of at most 80 characters and 10 words.");
30705
+ if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_REPORT_DEPTH) throw new SeoError("INVALID_INPUT", `SERP result depth must be from 1 to ${MAX_REPORT_DEPTH}.`);
30706
+ const market = searchMarketSchema.safeParse(input.market);
30707
+ if (!market.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
30708
+ const provider = input.provider ? providerIdSchema.safeParse(input.provider) : void 0;
30709
+ if (provider && !provider.success) throw new SeoError("INVALID_INPUT", "Use a supported SERP provider.");
30710
+ return {
30711
+ keyword,
30712
+ market: market.data,
30713
+ provider: provider?.data
30714
+ };
30715
+ }
30716
+ function reportStatus$1(evidence) {
30717
+ if (evidence.coverage.completeness === "unavailable" || evidence.coverage.completeness === "invalid") return "unavailable";
30718
+ return evidence.coverage.completeness === "complete" ? "complete" : "partial";
30719
+ }
30720
+ function domainSummaries(snapshot) {
30721
+ const byDomain = /* @__PURE__ */ new Map();
30722
+ for (const result of snapshot.organicResults) byDomain.set(result.domain, [...byDomain.get(result.domain) ?? [], result.rankAbsolute]);
30723
+ return [...byDomain.entries()].map(([domain, ranks]) => ({
30724
+ domain,
30725
+ resultCount: ranks.length,
30726
+ ranks: [...ranks].sort((left, right) => left - right)
30727
+ })).sort((left, right) => right.resultCount - left.resultCount || (left.ranks[0] ?? Number.MAX_SAFE_INTEGER) - (right.ranks[0] ?? Number.MAX_SAFE_INTEGER) || compareCodepoints(left.domain, right.domain));
30728
+ }
30729
+ function reportFindings(snapshot, domains) {
30730
+ const findings = [];
30731
+ if (snapshot.effectiveKeyword !== snapshot.keyword) findings.push({
30732
+ code: "query-correction",
30733
+ evidenceRef: "evidence.data.effectiveKeyword",
30734
+ principle: "A corrected query changes the observed result context and does not prove the original wording has the same intent.",
30735
+ detail: `The provider observed results for ${snapshot.effectiveKeyword} after the requested query ${snapshot.keyword}.`
30736
+ });
30737
+ for (const [index, domain] of domains.entries()) {
30738
+ if (domain.resultCount <= 1) continue;
30739
+ findings.push({
30740
+ code: "repeated-domain",
30741
+ evidenceRef: `domains[${index}]`,
30742
+ principle: "Repeated results are descriptive snapshot evidence, not a domain strength or quality score.",
30743
+ detail: `${domain.domain} appears ${domain.resultCount} times at absolute ranks ${domain.ranks.join(", ")}.`
30744
+ });
30745
+ }
30746
+ return findings.slice(0, 10);
30747
+ }
30748
+ async function serpResultsReport(input, dependencies = {}) {
30749
+ const depth = input.depth ?? 10;
30750
+ const validated = validateInput$1({
30751
+ ...input,
30752
+ depth
30753
+ });
30754
+ let candidates;
30755
+ try {
30756
+ candidates = dependencies.candidates ?? await defaultCandidates$1();
30757
+ } catch (error) {
30758
+ return providerError$2(error);
30759
+ }
30760
+ const resolution = resolveProvider({
30761
+ capability: "serp-snapshot",
30762
+ market: validated.market,
30763
+ candidates,
30764
+ provider: validated.provider
30765
+ });
30766
+ if (resolution.status === "unavailable") throw new SeoError("PROVIDER_UNAVAILABLE", resolution.reason === "provider-not-connected" ? "No connected provider can return live SERP results. Run `seo providers dataforseo connect` first." : validated.provider ? `${validated.provider} cannot return live SERP results for this market.` : "No configured provider can return live SERP results for this market.");
30767
+ const provider = snapshotProvider(resolution.provider);
30768
+ if (!provider) throw new SeoError("PROVIDER_UNAVAILABLE", "The selected provider has no live SERP implementation.");
30769
+ let evidence;
30770
+ try {
30771
+ evidence = await provider.serpSnapshot({
30772
+ keyword: validated.keyword,
30773
+ market: validated.market,
30774
+ depth,
30775
+ refresh: input.refresh,
30776
+ context: {
30777
+ projectId: input.context?.projectId ?? input.projectId,
30778
+ reportId: input.context?.reportId ?? "serp-results",
30779
+ reportRunId: input.context?.reportRunId ?? randomUUID()
30780
+ }
30781
+ });
30782
+ } catch (error) {
30783
+ return providerError$2(error);
30784
+ }
30785
+ const domains = domainSummaries(evidence.data);
30786
+ const dataStatus = reportStatus$1(evidence);
30787
+ const generatedAt = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
30788
+ const correctedQuery = evidence.data.effectiveKeyword !== evidence.data.keyword;
30789
+ return {
30790
+ schemaVersion: 1,
30791
+ generatedAt,
30792
+ dataStatus,
30793
+ market: evidence.market,
30794
+ summary: {
30795
+ keyword: evidence.data.keyword,
30796
+ effectiveKeyword: evidence.data.effectiveKeyword,
30797
+ requestedDepth: depth,
30798
+ organicResults: evidence.data.organicResults.length,
30799
+ uniqueDomains: domains.length,
30800
+ observedFeatures: evidence.data.features.length,
30801
+ correctedQuery,
30802
+ verdict: `${evidence.data.organicResults.length} organic results across ${domains.length} domains were retained from the ${evidence.market.device ?? "desktop"} snapshot.`
30803
+ },
30804
+ evidence,
30805
+ domains,
30806
+ findings: reportFindings(evidence.data, domains),
30807
+ caveats: [
30808
+ "This is one market, location, language, and device-specific result snapshot; it is not rank history.",
30809
+ "Search results can change between checks and may differ from a person's signed-in or personalized results.",
30810
+ "The provider result count is an estimate and may be missing; retained organic rows are bounded by the requested depth.",
30811
+ "Repeated domains, result features, titles, and snippets do not establish content quality, authority, or ranking feasibility."
30812
+ ],
30813
+ nextSteps: [
30814
+ "Review the retained pages and result types before deciding whether the query fits an existing page, a new page, or no page.",
30815
+ "Keep Search Console average position separate from these exact snapshot ranks when comparing first-party performance.",
30816
+ "Repeat the same market and device check later if the decision depends on result stability."
30817
+ ]
30818
+ };
30819
+ }
30820
+ //#endregion
30821
+ //#region packages/core/src/analyze/local-search/report.ts
30822
+ const MAX_DAYS$1 = 548;
30823
+ const MAX_ROWS = 5e4;
30824
+ function boundedInteger$2(input) {
30825
+ const value = input.value ?? input.fallback;
30826
+ if (!Number.isSafeInteger(value) || value < input.minimum || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be a whole number between ${input.minimum} and ${input.maximum}.`);
30827
+ return value;
30828
+ }
30829
+ function validateInput(input) {
30830
+ const site = input.site.trim();
30831
+ if (!site) throw new SeoError("INVALID_INPUT", "site must not be empty.");
30832
+ if (input.locationTerms && (input.locationTerms.length > 100 || input.locationTerms.some((term) => !term.trim() || term.length > 100))) throw new SeoError("INVALID_INPUT", "locationTerms must contain at most 100 non-empty terms of 100 characters or fewer.");
30833
+ if (input.brandTerms && (input.brandTerms.length > 20 || input.brandTerms.some((term) => !term.trim() || term.length > 200))) throw new SeoError("INVALID_INPUT", "brandTerms must contain at most 20 non-empty terms of 200 characters or fewer.");
30834
+ const includeSerps = input.includeSerps ?? false;
30835
+ let market;
30836
+ if (input.market) {
30837
+ const parsed = searchMarketSchema.safeParse(input.market);
30838
+ if (!parsed.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
30839
+ market = parsed.data;
30840
+ }
30841
+ if (includeSerps && !market?.location) throw new SeoError("INVALID_INPUT", "Local SERP evidence requires a country, language, and canonical location.");
30842
+ if (!includeSerps && (input.market || input.provider || input.serpLimit !== void 0 || input.serpDepth !== void 0)) throw new SeoError("INVALID_INPUT", "Set includeSerps to true before passing market, provider, or SERP options.");
30843
+ return {
30844
+ site,
30845
+ days: boundedInteger$2({
30846
+ value: input.days,
30847
+ fallback: 28,
30848
+ minimum: 1,
30849
+ maximum: MAX_DAYS$1,
30850
+ label: "days"
30851
+ }),
30852
+ locationTerms: normalizeLocationTerms(input.locationTerms),
30853
+ minImpressions: boundedInteger$2({
30854
+ value: input.minImpressions,
30855
+ fallback: 1,
30856
+ minimum: 0,
30857
+ maximum: 1e9,
30858
+ label: "minImpressions"
30859
+ }),
30860
+ limit: boundedInteger$2({
30861
+ value: input.limit,
30862
+ fallback: 25,
30863
+ minimum: 1,
30864
+ maximum: 100,
30865
+ label: "limit"
30866
+ }),
30867
+ maxRows: boundedInteger$2({
30868
+ value: input.maxRows,
30869
+ fallback: MAX_ROWS,
30870
+ minimum: 1,
30871
+ maximum: MAX_ROWS,
30872
+ label: "maxRows"
30873
+ }),
30874
+ includeSerps,
30875
+ market,
30876
+ serpLimit: boundedInteger$2({
30877
+ value: input.serpLimit,
30878
+ fallback: 3,
30879
+ minimum: 1,
30880
+ maximum: 3,
30881
+ label: "serpLimit"
30882
+ }),
30883
+ serpDepth: boundedInteger$2({
30884
+ value: input.serpDepth,
30885
+ fallback: 10,
30886
+ minimum: 1,
30887
+ maximum: 20,
30888
+ label: "serpDepth"
30889
+ })
30890
+ };
30891
+ }
30892
+ function emptyCost() {
30893
+ return {
30894
+ currency: "USD",
30895
+ estimatedMicros: 0,
30896
+ actualMicros: 0,
30897
+ taskIds: []
30898
+ };
30899
+ }
30900
+ function aggregateCosts(costs) {
30901
+ const estimated = costs.map((cost) => cost.estimatedMicros);
30902
+ const actual = costs.map((cost) => cost.actualMicros);
30903
+ return {
30904
+ currency: "USD",
30905
+ estimatedMicros: estimated.some((value) => value === null) ? null : estimated.reduce((sum, value) => sum + (value ?? 0), 0),
30906
+ actualMicros: actual.some((value) => value === null) ? null : actual.reduce((sum, value) => sum + (value ?? 0), 0),
30907
+ taskIds: [...new Set(costs.flatMap((cost) => cost.taskIds))].sort()
30908
+ };
30909
+ }
30910
+ function costsWithUnknownRequest(costs) {
30911
+ return {
30912
+ currency: "USD",
30913
+ estimatedMicros: null,
30914
+ actualMicros: null,
30915
+ taskIds: [...new Set(costs.flatMap((cost) => cost.taskIds))].sort()
30916
+ };
30917
+ }
30918
+ async function acquireSerps(input) {
30919
+ const selected = input.queries.slice(0, input.limit);
30920
+ const selection = {
30921
+ availableQueries: input.queries.length,
30922
+ requestedQueries: selected.length,
30923
+ omittedQueries: Math.max(0, input.queries.length - selected.length),
30924
+ limit: input.limit,
30925
+ depth: input.depth,
30926
+ method: "highest-impression-local-queries-v1"
30927
+ };
30928
+ if (!input.requested) return {
30929
+ requested: false,
30930
+ status: "not-requested",
30931
+ selection,
30932
+ market: null,
30933
+ reports: [],
30934
+ cost: emptyCost(),
30935
+ reason: "Local SERP snapshots were not requested, so no paid provider work was attempted."
30936
+ };
30937
+ if (selected.length === 0) return {
30938
+ requested: true,
30939
+ status: "skipped",
30940
+ selection,
30941
+ market: input.market ?? null,
30942
+ reports: [],
30943
+ cost: emptyCost(),
30944
+ reason: "No retained local-intent query was available for a SERP snapshot."
30945
+ };
30946
+ if (!input.market) throw new SeoError("INVALID_INPUT", "Local SERP evidence requires a market.");
30947
+ const reports = [];
30948
+ try {
30949
+ for (const query of selected) reports.push(await input.run({
30950
+ keyword: query,
30951
+ market: input.market,
30952
+ depth: input.depth,
30953
+ provider: input.provider,
30954
+ projectId: input.projectId,
30955
+ context: {
30956
+ reportId: "local-search-demand",
30957
+ reportRunId: input.reportRunId
30958
+ },
30959
+ refresh: input.refresh
30960
+ }));
30961
+ } catch (error) {
30962
+ if (error instanceof SeoError && (error.code === "PROVIDER_UNAVAILABLE" || error.code === "RATE_LIMITED")) return {
30963
+ requested: true,
30964
+ status: reports.length > 0 ? "partial" : "unavailable",
30965
+ selection,
30966
+ market: input.market,
30967
+ reports,
30968
+ cost: costsWithUnknownRequest(reports.map((report) => report.evidence.cost)),
30969
+ reason: "First-party local demand remains available, but not every requested SERP snapshot could be acquired.",
30970
+ error: {
30971
+ code: error.code,
30972
+ message: error.message,
30973
+ retryable: error.retryable
30974
+ }
30975
+ };
30976
+ throw error;
30977
+ }
30978
+ return {
30979
+ requested: true,
30980
+ status: reports.every((report) => report.dataStatus === "complete") ? "complete" : "partial",
30981
+ selection,
30982
+ market: input.market,
30983
+ reports,
30984
+ cost: aggregateCosts(reports.map((report) => report.evidence.cost))
30985
+ };
30986
+ }
30987
+ async function localSearchReport(input, dependencies = {}) {
30988
+ const options = validateInput(input);
30989
+ const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
30990
+ const generatedAt = now.toISOString();
30991
+ const range = finalGscDateRange(options.days, now);
30992
+ const source = await (dependencies.searchAnalytics ?? querySearchAnalytics)(options.site, {
30993
+ ...range,
30994
+ dimensions: ["query", "page"],
30995
+ type: "web",
30996
+ dataState: "final",
30997
+ maxRows: options.maxRows
30998
+ }, { refresh: input.refresh });
30999
+ const analysis = analyzeLocalSearchRows({
31000
+ rows: source.rows,
31001
+ site: options.site,
31002
+ locationTerms: options.locationTerms,
31003
+ brandTerms: input.brandTerms,
31004
+ includeBrand: input.includeBrand,
31005
+ minImpressions: options.minImpressions,
31006
+ limit: options.limit
31007
+ });
31008
+ const serpEvidence = await acquireSerps({
31009
+ requested: options.includeSerps,
31010
+ queries: analysis.opportunities.map((item) => item.query),
31011
+ market: options.market,
31012
+ provider: input.provider,
31013
+ projectId: input.projectId,
31014
+ refresh: input.refresh,
31015
+ limit: options.serpLimit,
31016
+ depth: options.serpDepth,
31017
+ reportRunId: randomUUID(),
31018
+ run: dependencies.serpResults ?? serpResultsReport
31019
+ });
31020
+ const possiblyTruncated = source.rowsFetched >= options.maxRows;
31021
+ const sourceEmpty = source.rows.length === 0;
31022
+ const filtered = !sourceEmpty && analysis.selection.eligibleQueries === 0;
31023
+ const partial = possiblyTruncated || serpEvidence.status === "partial" || serpEvidence.status === "unavailable";
31024
+ const opportunities = analysis.opportunities;
31025
+ const localPackSnapshots = serpEvidence.reports.filter((report) => report.evidence.data.features.includes("local_pack")).length;
31026
+ const dataStatus = sourceEmpty ? "empty" : partial ? "partial" : filtered ? "filtered" : "complete";
31027
+ const warnings = [];
31028
+ if (possiblyTruncated) warnings.push(`Search Console reached the ${options.maxRows}-row retention cap. Lower-volume local queries may be missing.`);
31029
+ if (analysis.selection.invalidRows > 0) warnings.push(`${analysis.selection.invalidRows} malformed Search Console rows were excluded.`);
31030
+ if (analysis.selection.conflictingRows > 0) warnings.push(`${analysis.selection.conflictingRows} conflicting duplicate Search Console rows were excluded.`);
31031
+ if (serpEvidence.error) warnings.push(serpEvidence.error.message);
31032
+ const nextSteps = [];
31033
+ if (!options.includeSerps) nextSteps.push("If the shortlisted queries justify paid verification, rerun with includeSerps true and an exact location and device to inspect current local result features and organic competitors.");
31034
+ else if (serpEvidence.status === "unavailable") nextSteps.push("Check the provider connection, market support, and local spend limits before retrying the missing SERP snapshots.");
31035
+ if (analysis.templates.length > 0) nextSteps.push("Run pseo-audit on the strongest repeated local page pattern before expanding it; verify source fields, page uniqueness, indexability, and representative output.");
31036
+ if (opportunities.some((item) => item.action === "review-page-overlap")) nextSteps.push("Review overlapping pages query by query before consolidating anything; multiple retained pages do not by themselves prove harmful cannibalisation.");
31037
+ nextSteps.push("Use location terms for the actual service area and repeat the report for materially different markets or devices instead of treating one area as representative.");
31038
+ return {
31039
+ schemaVersion: 1,
31040
+ methodology: "local-search-demand-v1",
31041
+ site: options.site,
31042
+ generatedAt,
31043
+ range,
31044
+ rangeDays: options.days,
31045
+ dataStatus,
31046
+ source: {
31047
+ provider: "google-search-console",
31048
+ dimensions: ["query", "page"],
31049
+ searchType: "web",
31050
+ dataState: "final",
31051
+ rowsFetched: source.rowsFetched,
31052
+ calls: source.calls,
31053
+ maxRows: options.maxRows,
31054
+ possiblyTruncated,
31055
+ completeness: possiblyTruncated ? "possibly-truncated" : "retained-query-page-rows-only"
31056
+ },
31057
+ methodologyDetails: {
31058
+ intentMethod: "explicit-local-intent-v1",
31059
+ suppliedLocationTerms: options.locationTerms,
31060
+ automaticPatterns: [
31061
+ "nearby-phrases",
31062
+ "uk-postcodes",
31063
+ "contextual-us-zip-codes"
31064
+ ],
31065
+ opportunityOrder: "impressions-clicks-position-query-v1",
31066
+ templateMethod: "pseo-url-template-clustering-v1"
31067
+ },
31068
+ selection: analysis.selection,
31069
+ summary: {
31070
+ localQueries: analysis.selection.eligibleQueries,
31071
+ returnedQueries: opportunities.length,
31072
+ pages: analysis.eligiblePageCount,
31073
+ ...analysis.eligibleSummary,
31074
+ templates: analysis.templates.length,
31075
+ serpSnapshots: serpEvidence.reports.length,
31076
+ localPackSnapshots,
31077
+ verdict: sourceEmpty ? "Search Console returned no retained query/page rows for this date window." : filtered ? "No retained query/page rows matched the supplied terms or automatic local-intent patterns." : `${countLabel(analysis.selection.eligibleQueries, "local-intent query")} found; ${countLabel(opportunities.length, "query")} returned for review.`
31078
+ },
31079
+ opportunities,
31080
+ templates: analysis.templates,
31081
+ serpEvidence,
31082
+ warnings,
31083
+ caveats: [
31084
+ "Search Console exposes retained top query/page rows and does not guarantee every query. A row cap means lower-volume local demand may be missing.",
31085
+ "Named-place, proximity, and postal-code matching are explicit heuristics. They can miss implicit local intent and postal-looking non-location terms.",
31086
+ "A place in a query describes query wording, not the searcher's physical location. Search Console average position is not an exact local rank.",
31087
+ "A Google Analytics city or region describes measured users under Analytics rules; it is not joined here because it cannot prove which search query brought a user.",
31088
+ "A local_pack feature in a live snapshot proves only that the result type appeared in that market, device, and observation. It does not expose or validate every business listing.",
31089
+ "Repeated URL patterns and page overlap are review signals, not proof that more pages should exist or that consolidation is required.",
31090
+ input.includeBrand ? "Brand queries were included when they matched the local-intent rules." : "Brand-like queries were excluded using the site and supplied brand terms."
31091
+ ],
31092
+ nextSteps
31093
+ };
31094
+ }
31095
+ //#endregion
30022
31096
  //#region packages/core/src/analyze/monitoring/crawl-recommendations.ts
30023
31097
  function statusMovedToError(item) {
30024
31098
  return item.kind !== "removed" && (item.after?.status ?? 0) >= 400 && (item.before?.status ?? 200) < 400;
@@ -30707,7 +31781,7 @@ function analyzeIndexCoverageSignals(input) {
30707
31781
  const MAX_SITEMAP_DOCUMENTS = 20;
30708
31782
  const MAX_SEARCH_ROWS = 25e4;
30709
31783
  const MAX_SITEMAP_URLS = 25e4;
30710
- const defaultDependencies$3 = {
31784
+ const defaultDependencies$4 = {
30711
31785
  loadCrawl: loadCrawlReport,
30712
31786
  latestCrawl: latestCrawlReport,
30713
31787
  searchAnalytics: querySearchAnalytics,
@@ -30739,7 +31813,7 @@ function sitemapCompleteness(input) {
30739
31813
  if (input.inventoryTruncated) return "truncated";
30740
31814
  return input.results.every((result) => result.dataStatus === "complete") ? "complete" : "partial";
30741
31815
  }
30742
- async function indexCoverageSignals(input, dependencies = defaultDependencies$3) {
31816
+ async function indexCoverageSignals(input, dependencies = defaultDependencies$4) {
30743
31817
  const days = integerOption$2({
30744
31818
  value: input.days,
30745
31819
  fallback: 90,
@@ -31223,7 +32297,7 @@ function pruneIndexWatchSnapshots(rootSite, url, database = getDb()) {
31223
32297
  WHERE attempts.attempt_rank > ? AND latest_success.id IS NULL
31224
32298
  )`).run(rootSite, url, rootSite, url, 20).changes;
31225
32299
  }
31226
- const defaultDependencies$2 = {
32300
+ const defaultDependencies$3 = {
31227
32301
  inspectUrl,
31228
32302
  now: () => /* @__PURE__ */ new Date(),
31229
32303
  store: {
@@ -31268,7 +32342,7 @@ function isFatalBatchError(error) {
31268
32342
  "AUTH_REQUIRED"
31269
32343
  ].includes(error.code);
31270
32344
  }
31271
- async function indexWatch(input, dependencies = defaultDependencies$2) {
32345
+ async function indexWatch(input, dependencies = defaultDependencies$3) {
31272
32346
  const languageCode = validateLanguageCode(input.languageCode);
31273
32347
  const dailyLimit = integerOption$2({
31274
32348
  value: input.dailyLimit,
@@ -33450,9 +34524,9 @@ function pseoSampleUrls(report) {
33450
34524
  }
33451
34525
  //#endregion
33452
34526
  //#region packages/core/src/analyze/pseo/audit.ts
33453
- const MAX_GSC_ROWS = 5e4;
34527
+ const MAX_GSC_ROWS$1 = 5e4;
33454
34528
  const DEFAULT_SITEMAP_URLS = 5e4;
33455
- const defaultDependencies$1 = {
34529
+ const defaultDependencies$2 = {
33456
34530
  searchAnalytics: querySearchAnalytics,
33457
34531
  fetchPage,
33458
34532
  inspectUrl,
@@ -33654,7 +34728,7 @@ async function inspectSample(input) {
33654
34728
  };
33655
34729
  }
33656
34730
  }
33657
- async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$1) {
34731
+ async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$2) {
33658
34732
  const options = pseoAuditOptions(input);
33659
34733
  const now = dependencies.now();
33660
34734
  const generatedAt = now.toISOString();
@@ -33673,7 +34747,7 @@ async function acquirePseoAuditEvidence(input, dependencies = defaultDependencie
33673
34747
  type: "web",
33674
34748
  dataState: "final",
33675
34749
  aggregationType: "auto",
33676
- maxRows: MAX_GSC_ROWS
34750
+ maxRows: MAX_GSC_ROWS$1
33677
34751
  };
33678
34752
  input.progress?.(`Fetching ${options.days} days of GSC page and query/page rows`);
33679
34753
  const [pageResponse, queryPageResponse] = await Promise.all([dependencies.searchAnalytics(input.site, {
@@ -33724,7 +34798,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
33724
34798
  minimumTemplateImpressions: options.minimumTemplateImpressions,
33725
34799
  crawlSamplesPerTemplate: options.crawlSamples,
33726
34800
  inspectionSamplesPerTemplate: options.inspectSamples,
33727
- maxRowsPerRequest: MAX_GSC_ROWS,
34801
+ maxRowsPerRequest: MAX_GSC_ROWS$1,
33728
34802
  pageRowsFetched: acquisition.pageRowsFetched,
33729
34803
  queryPageRowsFetched: acquisition.queryPageRowsFetched,
33730
34804
  sitemapsRequested: input.sitemaps?.length ?? 0,
@@ -33735,7 +34809,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
33735
34809
  caveats: [`Data freshness: ${input.refresh ? "fresh fetch requested; caches bypassed where supported" : "local cache allowed; use --refresh to bypass cached provider and HTTP data"}.`, input.sitemaps?.length ? `Sitemaps: ${countLabel(input.sitemaps.length, "sitemap URL")} requested.` : "Sitemaps: none provided; template discovery used retained GSC page rows."]
33736
34810
  });
33737
34811
  }
33738
- async function pseoAuditReport(input, dependencies = defaultDependencies$1) {
34812
+ async function pseoAuditReport(input, dependencies = defaultDependencies$2) {
33739
34813
  const acquisition = await acquirePseoAuditEvidence(input, dependencies);
33740
34814
  const sampleUrls = pseoSampleUrls(buildPseoAuditFromAcquisition(input, acquisition));
33741
34815
  const topQueries = topQueryByUrl(acquisition.queryPageRows, input);
@@ -34574,326 +35648,6 @@ function pseoOpportunityVerdict(input) {
34574
35648
  return `${expansions} template-expansion candidate${expansions === 1 ? "" : "s"} and ${input.competitors.length} observed competitor domain${input.competitors.length === 1 ? "" : "s"} were retained for review.`;
34575
35649
  }
34576
35650
  //#endregion
34577
- //#region packages/core/src/providers/dataforseo/serp-snapshot.ts
34578
- const MAX_SERP_DEPTH = 100;
34579
- const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
34580
- function safeUrl(value) {
34581
- if (!value) return null;
34582
- try {
34583
- const parsed = new URL(value);
34584
- if (!["http:", "https:"].includes(parsed.protocol)) return null;
34585
- parsed.username = "";
34586
- parsed.password = "";
34587
- return parsed.toString();
34588
- } catch {
34589
- return null;
34590
- }
34591
- }
34592
- function validOrganic(item) {
34593
- return item.type === "organic" && Number.isSafeInteger(item.rank_group) && (item.rank_group ?? 0) > 0 && Number.isSafeInteger(item.rank_absolute) && (item.rank_absolute ?? 0) > 0 && Number.isSafeInteger(item.page) && (item.page ?? 0) > 0 && Boolean(item.domain?.trim()) && safeUrl(item.url) !== null;
34594
- }
34595
- function organicResult(item) {
34596
- if (!validOrganic(item)) return null;
34597
- const url = safeUrl(item.url);
34598
- if (!url) return null;
34599
- return {
34600
- rankGroup: item.rank_group,
34601
- rankAbsolute: item.rank_absolute,
34602
- page: item.page,
34603
- domain: item.domain.trim().toLowerCase(),
34604
- url,
34605
- title: item.title ?? null,
34606
- description: item.description ?? null,
34607
- isFeaturedSnippet: item.is_featured_snippet ?? null
34608
- };
34609
- }
34610
- function checkedAt(value, fallback) {
34611
- const timestamp = Date.parse(value);
34612
- return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
34613
- }
34614
- function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
34615
- const market = searchMarketSchema.parse(input.market);
34616
- const keyword = normalizedKeyword$2(input.keyword);
34617
- const location = locationRequest(market, "serp-snapshot");
34618
- const result = snapshot.response.tasks.flatMap((task) => task.result ?? []).find((item) => normalizedKeyword$2(item.keyword) === keyword);
34619
- if (!result) throw new ProviderError({
34620
- provider: "dataforseo",
34621
- operation: "serp-snapshot",
34622
- code: "invalid-response",
34623
- message: "DataForSEO returned no matching SERP result."
34624
- });
34625
- const warnings = [...snapshot.warnings];
34626
- const items = result.items ?? [];
34627
- const organicItems = items.filter((item) => item.type === "organic");
34628
- const mappedOrganicResults = organicItems.flatMap((item) => {
34629
- const mapped = organicResult(item);
34630
- return mapped ? [mapped] : [];
34631
- }).sort((left, right) => left.rankAbsolute - right.rankAbsolute || left.rankGroup - right.rankGroup || compareCodepoints$3(left.url, right.url));
34632
- const organicResults = mappedOrganicResults.slice(0, input.depth);
34633
- const invalidRows = organicItems.length - mappedOrganicResults.length;
34634
- const resultCapped = mappedOrganicResults.length > input.depth;
34635
- const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
34636
- if (invalidRows > 0) warnings.push({
34637
- code: "invalid-organic-results",
34638
- field: "organicResults",
34639
- message: `DataForSEO returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank, domain, or URL.`
34640
- });
34641
- if (!Number.isFinite(Date.parse(result.datetime))) warnings.push({
34642
- code: "invalid-serp-observation-time",
34643
- field: "checkedAt",
34644
- message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
34645
- });
34646
- const checkUrl = safeUrl(result.check_url);
34647
- if (result.check_url && !checkUrl) warnings.push({
34648
- code: "invalid-serp-check-url",
34649
- field: "checkUrl",
34650
- message: "DataForSEO returned an invalid SERP check URL."
34651
- });
34652
- if (invalidResultCount) warnings.push({
34653
- code: "invalid-serp-result-count",
34654
- field: "resultCount",
34655
- message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
34656
- });
34657
- const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
34658
- return {
34659
- schemaVersion: 1,
34660
- provider: "dataforseo",
34661
- capability: "serp-snapshot",
34662
- data: {
34663
- keyword,
34664
- effectiveKeyword: normalizedKeyword$2(result.spell?.keyword ?? result.keyword),
34665
- searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
34666
- checkedAt: checkedAt(result.datetime, snapshot.observedAt),
34667
- checkUrl,
34668
- resultCount: invalidResultCount ? null : result.se_results_count ?? null,
34669
- pagesCount: result.pages_count === void 0 ? null : result.pages_count,
34670
- features,
34671
- organicResults
34672
- },
34673
- observedAt: snapshot.observedAt,
34674
- market: {
34675
- ...market,
34676
- device: market.device ?? "desktop"
34677
- },
34678
- coverage: {
34679
- requestedRows: input.depth,
34680
- returnedRows: snapshot.returnedRows,
34681
- retainedRows: organicResults.length,
34682
- invalidRows,
34683
- providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
34684
- completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
34685
- nextCursor: null
34686
- },
34687
- cache: snapshot.cache,
34688
- cost: snapshot.cost,
34689
- request: {
34690
- operation: "serp-snapshot",
34691
- endpoint,
34692
- limit: input.depth,
34693
- filters: {
34694
- countryCode: market.countryCode,
34695
- languageCode: market.languageCode,
34696
- device: market.device ?? "desktop",
34697
- ...location.locationCode !== void 0 ? { locationCode: location.locationCode } : { locationName: location.locationName }
34698
- },
34699
- sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
34700
- },
34701
- warnings
34702
- };
34703
- }
34704
- var DataForSeoSerpSnapshotProvider = class {
34705
- provider = "dataforseo";
34706
- capabilitySupport = [{
34707
- capability: "serp-snapshot",
34708
- status: "available",
34709
- markets: [{
34710
- searchEngines: ["google"],
34711
- devices: ["desktop", "mobile"],
34712
- location: "any"
34713
- }]
34714
- }];
34715
- client;
34716
- constructor(options = {}) {
34717
- this.client = options.client ?? new DataForSeoClient(options);
34718
- }
34719
- async serpSnapshot(input) {
34720
- const market = searchMarketSchema.parse(input.market);
34721
- if (market.searchEngine !== "google") throw new ProviderError({
34722
- provider: "dataforseo",
34723
- operation: "serp-snapshot",
34724
- code: "configuration",
34725
- message: "DataForSEO SERP snapshots currently support Google."
34726
- });
34727
- if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_SERP_DEPTH) throw new ProviderError({
34728
- provider: "dataforseo",
34729
- operation: "serp-snapshot",
34730
- code: "configuration",
34731
- message: "SERP depth must be from 1 to 100."
34732
- });
34733
- const keyword = normalizedKeyword$2(input.keyword);
34734
- if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new ProviderError({
34735
- provider: "dataforseo",
34736
- operation: "serp-snapshot",
34737
- code: "configuration",
34738
- message: "SERP snapshots require a keyword of at most 80 characters and 10 words."
34739
- });
34740
- const context = input.context ?? {
34741
- reportId: "serp-results",
34742
- reportRunId: randomUUID()
34743
- };
34744
- const location = locationRequest(market, "serp-snapshot");
34745
- return mapDataForSeoSerpSnapshot(input, await this.client.serpLive({
34746
- keyword,
34747
- languageCode: market.languageCode.split("-")[0],
34748
- ...location,
34749
- device: market.device ?? "desktop",
34750
- depth: input.depth,
34751
- refresh: input.refresh,
34752
- context
34753
- }));
34754
- }
34755
- };
34756
- //#endregion
34757
- //#region packages/core/src/analyze/serp-results.ts
34758
- const MAX_REPORT_DEPTH = 100;
34759
- function compareCodepoints(left, right) {
34760
- return left < right ? -1 : left > right ? 1 : 0;
34761
- }
34762
- function snapshotProvider(candidate) {
34763
- return "serpSnapshot" in candidate && typeof candidate.serpSnapshot === "function" ? candidate : null;
34764
- }
34765
- async function defaultCandidates$1() {
34766
- return [{
34767
- adapter: new DataForSeoSerpSnapshotProvider(),
34768
- connected: Boolean(await readDataForSeoCredentials()),
34769
- priority: 10
34770
- }];
34771
- }
34772
- function providerError$2(error) {
34773
- if (!(error instanceof ProviderError)) throw error;
34774
- if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
34775
- if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
34776
- throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
34777
- }
34778
- function validateInput(input) {
34779
- const keyword = input.keyword.trim().replace(/\s+/gu, " ");
34780
- if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new SeoError("INVALID_INPUT", "SERP results requires a keyword of at most 80 characters and 10 words.");
34781
- if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_REPORT_DEPTH) throw new SeoError("INVALID_INPUT", `SERP result depth must be from 1 to ${MAX_REPORT_DEPTH}.`);
34782
- const market = searchMarketSchema.safeParse(input.market);
34783
- if (!market.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
34784
- const provider = input.provider ? providerIdSchema.safeParse(input.provider) : void 0;
34785
- if (provider && !provider.success) throw new SeoError("INVALID_INPUT", "Use a supported SERP provider.");
34786
- return {
34787
- keyword,
34788
- market: market.data,
34789
- provider: provider?.data
34790
- };
34791
- }
34792
- function reportStatus$1(evidence) {
34793
- if (evidence.coverage.completeness === "unavailable" || evidence.coverage.completeness === "invalid") return "unavailable";
34794
- return evidence.coverage.completeness === "complete" ? "complete" : "partial";
34795
- }
34796
- function domainSummaries(snapshot) {
34797
- const byDomain = /* @__PURE__ */ new Map();
34798
- for (const result of snapshot.organicResults) byDomain.set(result.domain, [...byDomain.get(result.domain) ?? [], result.rankAbsolute]);
34799
- return [...byDomain.entries()].map(([domain, ranks]) => ({
34800
- domain,
34801
- resultCount: ranks.length,
34802
- ranks: [...ranks].sort((left, right) => left - right)
34803
- })).sort((left, right) => right.resultCount - left.resultCount || (left.ranks[0] ?? Number.MAX_SAFE_INTEGER) - (right.ranks[0] ?? Number.MAX_SAFE_INTEGER) || compareCodepoints(left.domain, right.domain));
34804
- }
34805
- function reportFindings(snapshot, domains) {
34806
- const findings = [];
34807
- if (snapshot.effectiveKeyword !== snapshot.keyword) findings.push({
34808
- code: "query-correction",
34809
- evidenceRef: "evidence.data.effectiveKeyword",
34810
- principle: "A corrected query changes the observed result context and does not prove the original wording has the same intent.",
34811
- detail: `The provider observed results for ${snapshot.effectiveKeyword} after the requested query ${snapshot.keyword}.`
34812
- });
34813
- for (const [index, domain] of domains.entries()) {
34814
- if (domain.resultCount <= 1) continue;
34815
- findings.push({
34816
- code: "repeated-domain",
34817
- evidenceRef: `domains[${index}]`,
34818
- principle: "Repeated results are descriptive snapshot evidence, not a domain strength or quality score.",
34819
- detail: `${domain.domain} appears ${domain.resultCount} times at absolute ranks ${domain.ranks.join(", ")}.`
34820
- });
34821
- }
34822
- return findings.slice(0, 10);
34823
- }
34824
- async function serpResultsReport(input, dependencies = {}) {
34825
- const depth = input.depth ?? 10;
34826
- const validated = validateInput({
34827
- ...input,
34828
- depth
34829
- });
34830
- let candidates;
34831
- try {
34832
- candidates = dependencies.candidates ?? await defaultCandidates$1();
34833
- } catch (error) {
34834
- return providerError$2(error);
34835
- }
34836
- const resolution = resolveProvider({
34837
- capability: "serp-snapshot",
34838
- market: validated.market,
34839
- candidates,
34840
- provider: validated.provider
34841
- });
34842
- if (resolution.status === "unavailable") throw new SeoError("PROVIDER_UNAVAILABLE", resolution.reason === "provider-not-connected" ? "No connected provider can return live SERP results. Run `seo providers dataforseo connect` first." : validated.provider ? `${validated.provider} cannot return live SERP results for this market.` : "No configured provider can return live SERP results for this market.");
34843
- const provider = snapshotProvider(resolution.provider);
34844
- if (!provider) throw new SeoError("PROVIDER_UNAVAILABLE", "The selected provider has no live SERP implementation.");
34845
- let evidence;
34846
- try {
34847
- evidence = await provider.serpSnapshot({
34848
- keyword: validated.keyword,
34849
- market: validated.market,
34850
- depth,
34851
- refresh: input.refresh,
34852
- context: {
34853
- projectId: input.context?.projectId ?? input.projectId,
34854
- reportId: input.context?.reportId ?? "serp-results",
34855
- reportRunId: input.context?.reportRunId ?? randomUUID()
34856
- }
34857
- });
34858
- } catch (error) {
34859
- return providerError$2(error);
34860
- }
34861
- const domains = domainSummaries(evidence.data);
34862
- const dataStatus = reportStatus$1(evidence);
34863
- const generatedAt = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
34864
- const correctedQuery = evidence.data.effectiveKeyword !== evidence.data.keyword;
34865
- return {
34866
- schemaVersion: 1,
34867
- generatedAt,
34868
- dataStatus,
34869
- market: evidence.market,
34870
- summary: {
34871
- keyword: evidence.data.keyword,
34872
- effectiveKeyword: evidence.data.effectiveKeyword,
34873
- requestedDepth: depth,
34874
- organicResults: evidence.data.organicResults.length,
34875
- uniqueDomains: domains.length,
34876
- observedFeatures: evidence.data.features.length,
34877
- correctedQuery,
34878
- verdict: `${evidence.data.organicResults.length} organic results across ${domains.length} domains were retained from the ${evidence.market.device ?? "desktop"} snapshot.`
34879
- },
34880
- evidence,
34881
- domains,
34882
- findings: reportFindings(evidence.data, domains),
34883
- caveats: [
34884
- "This is one market, location, language, and device-specific result snapshot; it is not rank history.",
34885
- "Search results can change between checks and may differ from a person's signed-in or personalized results.",
34886
- "The provider result count is an estimate and may be missing; retained organic rows are bounded by the requested depth.",
34887
- "Repeated domains, result features, titles, and snippets do not establish content quality, authority, or ranking feasibility."
34888
- ],
34889
- nextSteps: [
34890
- "Review the retained pages and result types before deciding whether the query fits an existing page, a new page, or no page.",
34891
- "Keep Search Console average position separate from these exact snapshot ranks when comparing first-party performance.",
34892
- "Repeat the same market and device check later if the decision depends on result stability."
34893
- ]
34894
- };
34895
- }
34896
- //#endregion
34897
35651
  //#region packages/core/src/analyze/pseo-opportunities/serps.ts
34898
35652
  function selectSerpKeywords(candidates, limit) {
34899
35653
  const seedRefs = /* @__PURE__ */ new Set();
@@ -37191,7 +37945,7 @@ function groupedMappings(evidence) {
37191
37945
  //#endregion
37192
37946
  //#region packages/core/src/analyze/second-page.ts
37193
37947
  const MAX_SOURCE_ROWS = 1e5;
37194
- const defaultDependencies = {
37948
+ const defaultDependencies$1 = {
37195
37949
  searchAnalytics: querySearchAnalytics,
37196
37950
  fetch: fetchPage,
37197
37951
  extract: extractPage,
@@ -37268,7 +38022,7 @@ function reportRecommendations(items) {
37268
38022
  "Treat the priority score as a triage heuristic, not an estimated traffic lift."
37269
38023
  ]);
37270
38024
  }
37271
- async function secondPage(input, dependencies = defaultDependencies) {
38025
+ async function secondPage(input, dependencies = defaultDependencies$1) {
37272
38026
  const days = integerOption({
37273
38027
  value: input.range,
37274
38028
  fallback: 28,
@@ -40434,10 +41188,10 @@ const DEFAULT_TARGET_LIMIT = 20;
40434
41188
  const MAX_TARGET_LIMIT = 50;
40435
41189
  const DEFAULT_DETAIL_PAGES = 1;
40436
41190
  const MAX_DETAIL_PAGES = 3;
40437
- const DEFAULT_ROW_LIMIT$2 = 500;
40438
- const MAX_ROW_LIMIT$2 = 1e3;
41191
+ const DEFAULT_ROW_LIMIT$3 = 500;
41192
+ const MAX_ROW_LIMIT$3 = 1e3;
40439
41193
  const MAX_CONCURRENCY = 3;
40440
- function integer(value, fallback, max, name) {
41194
+ function integer$1(value, fallback, max, name) {
40441
41195
  const resolved = value ?? fallback;
40442
41196
  if (!Number.isInteger(resolved) || resolved < 1 || resolved > max) throw new SeoError("INVALID_INPUT", `${name} must be between 1 and ${max}.`);
40443
41197
  return resolved;
@@ -40498,9 +41252,9 @@ async function targetDetails(input) {
40498
41252
  }
40499
41253
  async function collectBingLinkEvidence(input) {
40500
41254
  if (!input.site.trim()) throw new SeoError("INVALID_INPUT", "Pass a Bing Webmaster site URL.");
40501
- const targetLimit = integer(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
40502
- const detailPages = integer(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
40503
- const rowLimit = integer(input.rowLimit, DEFAULT_ROW_LIMIT$2, MAX_ROW_LIMIT$2, "Bing link row limit");
41255
+ const targetLimit = integer$1(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
41256
+ const detailPages = integer$1(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
41257
+ const rowLimit = integer$1(input.rowLimit, DEFAULT_ROW_LIMIT$3, MAX_ROW_LIMIT$3, "Bing link row limit");
40504
41258
  const client = input.client ?? (await createBingWebmasterClient()).client;
40505
41259
  const countRows = [];
40506
41260
  let countPagesRequested = 0;
@@ -40621,6 +41375,884 @@ async function collectBingLinkEvidence(input) {
40621
41375
  };
40622
41376
  }
40623
41377
  //#endregion
41378
+ //#region packages/core/src/links/context-sources.ts
41379
+ function urlKey(value) {
41380
+ try {
41381
+ const url = new URL(value);
41382
+ if (!["http:", "https:"].includes(url.protocol)) return null;
41383
+ url.hash = "";
41384
+ return url.toString();
41385
+ } catch {
41386
+ return null;
41387
+ }
41388
+ }
41389
+ function compareTargets(left, right) {
41390
+ return right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0);
41391
+ }
41392
+ function linkTargetCounts(evidence) {
41393
+ if (evidence.targetCounts.length) return [...evidence.targetCounts].sort(compareTargets);
41394
+ const counts = /* @__PURE__ */ new Map();
41395
+ for (const row of evidence.rows) counts.set(row.targetUrl, (counts.get(row.targetUrl) ?? 0) + 1);
41396
+ return [...counts].map(([targetUrl, observedLinks]) => ({
41397
+ targetUrl,
41398
+ observedLinks
41399
+ })).sort(compareTargets);
41400
+ }
41401
+ function crawlForLinkTargets(input) {
41402
+ const values = /* @__PURE__ */ new Map();
41403
+ if (!input.report) {
41404
+ for (const target of input.targets) values.set(target.targetUrl, {
41405
+ state: "unavailable",
41406
+ reason: "No matching saved crawl was available."
41407
+ });
41408
+ return {
41409
+ values,
41410
+ matched: 0
41411
+ };
41412
+ }
41413
+ const pages = /* @__PURE__ */ new Map();
41414
+ for (const page of input.report.pages) for (const value of [page.url, page.finalUrl]) {
41415
+ const key = urlKey(value);
41416
+ if (key && !pages.has(key)) pages.set(key, page);
41417
+ }
41418
+ const issues = /* @__PURE__ */ new Map();
41419
+ for (const issue of input.report.issues) {
41420
+ const key = urlKey(issue.url);
41421
+ if (!key) continue;
41422
+ const ids = issues.get(key) ?? /* @__PURE__ */ new Set();
41423
+ ids.add(issue.ruleId);
41424
+ issues.set(key, ids);
41425
+ }
41426
+ let matched = 0;
41427
+ for (const target of input.targets) {
41428
+ const key = urlKey(target.targetUrl);
41429
+ const page = key ? pages.get(key) : void 0;
41430
+ if (!page) {
41431
+ values.set(target.targetUrl, {
41432
+ state: "not-observed",
41433
+ reason: "The target URL was not present in the retained crawl pages."
41434
+ });
41435
+ continue;
41436
+ }
41437
+ matched += 1;
41438
+ values.set(target.targetUrl, {
41439
+ state: "observed",
41440
+ reportId: input.report.id,
41441
+ observedAt: input.report.generatedAt,
41442
+ status: page.status,
41443
+ finalUrl: page.finalUrl,
41444
+ indexable: page.indexable,
41445
+ canonical: page.canonical ?? null,
41446
+ issueIds: [...issues.get(key) ?? []].sort()
41447
+ });
41448
+ }
41449
+ return {
41450
+ values,
41451
+ matched
41452
+ };
41453
+ }
41454
+ function aggregateLinkSearchRows(rows) {
41455
+ const values = /* @__PURE__ */ new Map();
41456
+ for (const row of rows) {
41457
+ const page = row.keys[0];
41458
+ const key = page ? urlKey(page) : null;
41459
+ if (!key) continue;
41460
+ const current = values.get(key) ?? {
41461
+ clicks: 0,
41462
+ impressions: 0,
41463
+ positionWeight: 0
41464
+ };
41465
+ current.clicks += row.clicks;
41466
+ current.impressions += row.impressions;
41467
+ current.positionWeight += row.position * row.impressions;
41468
+ values.set(key, current);
41469
+ }
41470
+ return values;
41471
+ }
41472
+ //#endregion
41473
+ //#region packages/core/src/links/context-findings.ts
41474
+ const MAX_FINDINGS$1 = 50;
41475
+ function findingPriority(input) {
41476
+ const searchValue = input.search.state === "observed" && (input.search.clicks > 0 || input.search.impressions >= 100);
41477
+ if (input.code === "linked-broken-target" || searchValue) return "high";
41478
+ if (input.observedLinks >= 5) return "medium";
41479
+ return "low";
41480
+ }
41481
+ function finding(input) {
41482
+ const crawl = input.row.crawl;
41483
+ if (crawl.state !== "observed") throw new Error("A link finding requires observed crawl evidence.");
41484
+ const searchEvidence = input.row.searchConsole.state === "observed" ? ` Search Console retained ${input.row.searchConsole.clicks.toFixed(0)} clicks and ${input.row.searchConsole.impressions.toFixed(0)} impressions for this page in the selected window.` : "";
41485
+ const common = {
41486
+ priority: findingPriority({
41487
+ code: input.code,
41488
+ observedLinks: input.row.observedLinks,
41489
+ crawl,
41490
+ search: input.row.searchConsole
41491
+ }),
41492
+ heuristic: true,
41493
+ targetUrl: input.row.targetUrl,
41494
+ observedLinks: input.row.observedLinks,
41495
+ evidenceRefs: [input.row.targetUrl, crawl.reportId]
41496
+ };
41497
+ if (input.code === "linked-broken-target") return {
41498
+ ...common,
41499
+ code: input.code,
41500
+ principle: "Linked target pages should resolve intentionally.",
41501
+ evidence: `The saved crawl observed status ${crawl.status} for a target with ${input.row.observedLinks} retained representative links.${searchEvidence}`,
41502
+ action: "Confirm whether the page should exist. Restore it or add one direct redirect to a genuinely equivalent live page when the missing URL is unintended.",
41503
+ verify: "Run redirect-trace for the target, then recrawl it and confirm a stable intended response."
41504
+ };
41505
+ if (input.code === "linked-redirect-target") return {
41506
+ ...common,
41507
+ code: input.code,
41508
+ principle: "External links should reach a stable final destination.",
41509
+ evidence: `The saved crawl observed ${input.row.targetUrl} resolving to ${crawl.finalUrl}.${searchEvidence}`,
41510
+ action: "Check that the redirect is intentional, direct and semantically equivalent. Update links you control to the final URL and ask important external sources to update only when the change is worth the effort.",
41511
+ verify: "Run redirect-trace and confirm one stable hop to the intended indexable page."
41512
+ };
41513
+ if (input.code === "linked-canonical-conflict") return {
41514
+ ...common,
41515
+ code: input.code,
41516
+ principle: "Canonical hints should agree with the intended linked page.",
41517
+ evidence: `The saved crawl observed canonical ${crawl.canonical} on a linked target that resolved to ${crawl.finalUrl}.${searchEvidence}`,
41518
+ action: "Confirm the preferred URL. Align the canonical and redirect only if the current disagreement is unintended.",
41519
+ verify: "Audit the final page and inspect the declared and Google-selected canonical before changing it."
41520
+ };
41521
+ return {
41522
+ ...common,
41523
+ code: input.code,
41524
+ principle: "A linked page should be indexable when search visibility is intended.",
41525
+ evidence: `The saved crawl marked a linked target as non-indexable.${searchEvidence}`,
41526
+ action: "Confirm whether non-indexing is intentional. Remove the blocking signal or redirect to an equivalent indexable page only when this URL should appear in search.",
41527
+ verify: "Audit the page, then use URL Inspection when the intended state is indexable."
41528
+ };
41529
+ }
41530
+ function linkTargetFindings(rows) {
41531
+ return rows.flatMap((row) => {
41532
+ const crawl = row.crawl;
41533
+ if (crawl.state !== "observed") return [];
41534
+ if (crawl.status >= 400) return [finding({
41535
+ row,
41536
+ code: "linked-broken-target"
41537
+ })];
41538
+ if (urlKey(crawl.finalUrl) !== urlKey(row.targetUrl)) return [finding({
41539
+ row,
41540
+ code: "linked-redirect-target"
41541
+ })];
41542
+ if (crawl.canonical && urlKey(crawl.canonical) !== urlKey(crawl.finalUrl)) return [finding({
41543
+ row,
41544
+ code: "linked-canonical-conflict"
41545
+ })];
41546
+ if (!crawl.indexable) return [finding({
41547
+ row,
41548
+ code: "linked-non-indexable-target"
41549
+ })];
41550
+ return [];
41551
+ }).sort((left, right) => ({
41552
+ high: 0,
41553
+ medium: 1,
41554
+ low: 2
41555
+ })[left.priority] - {
41556
+ high: 0,
41557
+ medium: 1,
41558
+ low: 2
41559
+ }[right.priority] || right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0)).slice(0, MAX_FINDINGS$1);
41560
+ }
41561
+ //#endregion
41562
+ //#region packages/core/src/links/context.ts
41563
+ const DEFAULT_DAYS = 90;
41564
+ const MAX_DAYS = 548;
41565
+ const DEFAULT_CONTEXT_LIMIT = 100;
41566
+ const MAX_CONTEXT_LIMIT = 100;
41567
+ const MAX_GSC_ROWS = 25e3;
41568
+ const defaultDependencies = {
41569
+ latestCrawl: latestCrawlReport,
41570
+ searchAnalytics: querySearchAnalytics,
41571
+ now: () => /* @__PURE__ */ new Date()
41572
+ };
41573
+ function integer(input) {
41574
+ const value = input.value ?? input.fallback;
41575
+ if (!Number.isSafeInteger(value) || value < 1 || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be between 1 and ${input.maximum}.`);
41576
+ return value;
41577
+ }
41578
+ function searchEvidence(input) {
41579
+ if (!input.site) return {
41580
+ state: "unavailable",
41581
+ reason: "No Search Console property was selected for this report."
41582
+ };
41583
+ if (input.error) return {
41584
+ state: "unavailable",
41585
+ reason: input.error
41586
+ };
41587
+ if (!input.aggregate) return {
41588
+ state: "not-retained",
41589
+ reason: input.retainedLimitReached ? `The bounded Search Console response reached ${MAX_GSC_ROWS} rows and did not retain this page.` : "Search Console did not retain a page row for this URL in the selected window."
41590
+ };
41591
+ return {
41592
+ state: "observed",
41593
+ clicks: input.aggregate.clicks,
41594
+ impressions: input.aggregate.impressions,
41595
+ ctr: input.aggregate.impressions > 0 ? input.aggregate.clicks / input.aggregate.impressions : 0,
41596
+ position: input.aggregate.impressions > 0 ? input.aggregate.positionWeight / input.aggregate.impressions : 0
41597
+ };
41598
+ }
41599
+ async function linkTargetContext(input, dependencies = defaultDependencies) {
41600
+ const days = integer({
41601
+ value: input.days,
41602
+ fallback: DEFAULT_DAYS,
41603
+ maximum: MAX_DAYS,
41604
+ label: "Link context days"
41605
+ });
41606
+ const limit = integer({
41607
+ value: input.limit,
41608
+ fallback: DEFAULT_CONTEXT_LIMIT,
41609
+ maximum: MAX_CONTEXT_LIMIT,
41610
+ label: "Link context limit"
41611
+ });
41612
+ const available = linkTargetCounts(input.evidence);
41613
+ const selected = available.slice(0, limit);
41614
+ const crawlLookup = input.crawlSite ?? input.searchConsoleSite;
41615
+ const crawl = crawlLookup ? dependencies.latestCrawl(crawlLookup) : void 0;
41616
+ const crawlValues = crawlForLinkTargets({
41617
+ report: crawl,
41618
+ targets: selected
41619
+ });
41620
+ const warnings = [];
41621
+ const range = finalGscDateRange(days, dependencies.now());
41622
+ let searchRows = /* @__PURE__ */ new Map();
41623
+ let searchCalls = 0;
41624
+ let searchRowsFetched = 0;
41625
+ let searchError = null;
41626
+ if (input.searchConsoleSite) try {
41627
+ const result = await dependencies.searchAnalytics(input.searchConsoleSite, {
41628
+ ...range,
41629
+ dimensions: ["page"],
41630
+ type: "web",
41631
+ dataState: "final",
41632
+ rowLimit: MAX_GSC_ROWS,
41633
+ maxRows: MAX_GSC_ROWS
41634
+ }, { refresh: input.refresh });
41635
+ searchRows = aggregateLinkSearchRows(result.rows);
41636
+ searchCalls = result.calls;
41637
+ searchRowsFetched = result.rowsFetched;
41638
+ } catch (error) {
41639
+ searchError = error instanceof Error ? error.message : "Search Console evidence could not be loaded.";
41640
+ warnings.push(`Search Console context was unavailable. ${searchError}`);
41641
+ }
41642
+ let searchMatches = 0;
41643
+ const retainedLimitReached = searchRowsFetched >= MAX_GSC_ROWS;
41644
+ const rows = selected.map((target) => {
41645
+ const aggregate = searchRows.get(urlKey(target.targetUrl) ?? "");
41646
+ if (aggregate) searchMatches += 1;
41647
+ return {
41648
+ ...target,
41649
+ crawl: crawlValues.values.get(target.targetUrl) ?? {
41650
+ state: "not-observed",
41651
+ reason: "The target URL was not present in the retained crawl pages."
41652
+ },
41653
+ searchConsole: searchEvidence({
41654
+ site: input.searchConsoleSite,
41655
+ error: searchError,
41656
+ retainedLimitReached,
41657
+ aggregate
41658
+ })
41659
+ };
41660
+ });
41661
+ if (!crawl) warnings.push("No matching saved crawl was available, so target response and indexability checks were skipped.");
41662
+ if (retainedLimitReached) warnings.push(`Search Console reached the ${MAX_GSC_ROWS}-row acquisition limit. Missing target rows remain not retained rather than zero.`);
41663
+ const unavailableSources = Number(!crawl) + Number(!input.searchConsoleSite);
41664
+ return {
41665
+ schemaVersion: 1,
41666
+ dataStatus: unavailableSources === 2 ? "unavailable" : unavailableSources > 0 || searchError || retainedLimitReached ? "partial" : "complete",
41667
+ selection: {
41668
+ availableTargets: available.length,
41669
+ returnedTargets: rows.length,
41670
+ omittedTargets: Math.max(0, available.length - rows.length),
41671
+ limit
41672
+ },
41673
+ provenance: {
41674
+ crawl: {
41675
+ status: crawl ? "joined" : "unavailable",
41676
+ reportId: crawl?.id ?? null,
41677
+ observedAt: crawl?.generatedAt ?? null,
41678
+ availablePages: crawl?.pages.length ?? 0,
41679
+ matchedTargets: crawlValues.matched
41680
+ },
41681
+ searchConsole: {
41682
+ status: !input.searchConsoleSite || searchError ? "unavailable" : retainedLimitReached ? "partial" : "joined",
41683
+ site: input.searchConsoleSite ?? null,
41684
+ range: input.searchConsoleSite ? {
41685
+ ...range,
41686
+ days
41687
+ } : null,
41688
+ calls: searchCalls,
41689
+ rowsFetched: searchRowsFetched,
41690
+ retainedRowLimit: MAX_GSC_ROWS,
41691
+ retainedRowLimitReached: retainedLimitReached,
41692
+ matchedTargets: searchMatches
41693
+ }
41694
+ },
41695
+ rows,
41696
+ findings: linkTargetFindings(rows),
41697
+ warnings
41698
+ };
41699
+ }
41700
+ //#endregion
41701
+ //#region packages/core/src/providers/dataforseo/link-research-shared.ts
41702
+ function linkTarget(value, requestedScope = "domain") {
41703
+ const raw = value.trim();
41704
+ if (!raw || raw.length > 2048) throw invalidTarget();
41705
+ if (requestedScope === "page") try {
41706
+ const url = new URL(raw);
41707
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error();
41708
+ url.hash = "";
41709
+ return {
41710
+ target: url.toString(),
41711
+ scope: "page"
41712
+ };
41713
+ } catch {
41714
+ throw invalidTarget();
41715
+ }
41716
+ try {
41717
+ const hostname = new URL(raw.includes("://") ? raw : `https://${raw}`).hostname.toLowerCase().replace(/^www\./u, "").replace(/\.$/u, "");
41718
+ if (!hostname || hostname.length > 253 || hostname.includes("..") || !hostname.includes(".") || !/^[a-z0-9.-]+$/u.test(hostname)) throw new Error();
41719
+ return {
41720
+ target: hostname,
41721
+ scope: "domain"
41722
+ };
41723
+ } catch {
41724
+ throw invalidTarget();
41725
+ }
41726
+ }
41727
+ function invalidTarget() {
41728
+ return new ProviderError({
41729
+ provider: "dataforseo",
41730
+ operation: "link-evidence",
41731
+ code: "configuration",
41732
+ message: "Use a valid domain, or choose page scope and pass an absolute URL."
41733
+ });
41734
+ }
41735
+ function linkRowLimit(limit, offset = 0) {
41736
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
41737
+ provider: "dataforseo",
41738
+ operation: "link-evidence",
41739
+ code: "configuration",
41740
+ message: `Link row limit must be from 1 to ${MAX_LINK_ROWS$1}.`
41741
+ });
41742
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > 2e4) throw new ProviderError({
41743
+ provider: "dataforseo",
41744
+ operation: "link-evidence",
41745
+ code: "configuration",
41746
+ message: `Link row offset must be from 0 to ${MAX_LINK_OFFSET}.`
41747
+ });
41748
+ }
41749
+ function requestContext(reportId, supplied) {
41750
+ return supplied ?? {
41751
+ reportId,
41752
+ reportRunId: randomUUID()
41753
+ };
41754
+ }
41755
+ function missing(field) {
41756
+ return unavailableValue("missing", `DataForSEO omitted ${field}.`);
41757
+ }
41758
+ function numberValue(value, field) {
41759
+ return value === null || value === void 0 ? missing(field) : observedValue(value);
41760
+ }
41761
+ function stringValue(value, field) {
41762
+ return value ? observedValue(value) : missing(field);
41763
+ }
41764
+ function metric(id, label, value) {
41765
+ return value === null || value === void 0 ? [] : [{
41766
+ provider: "dataforseo",
41767
+ id,
41768
+ label,
41769
+ value,
41770
+ scale: {
41771
+ minimum: 0,
41772
+ maximum: 100
41773
+ }
41774
+ }];
41775
+ }
41776
+ function safeUrl(value) {
41777
+ if (!value) return null;
41778
+ try {
41779
+ const url = new URL(value);
41780
+ if (!["http:", "https:"].includes(url.protocol)) return null;
41781
+ url.username = "";
41782
+ url.password = "";
41783
+ url.hash = "";
41784
+ return url.toString();
41785
+ } catch {
41786
+ return null;
41787
+ }
41788
+ }
41789
+ function normalizedDate(value) {
41790
+ if (!value) return null;
41791
+ const parsed = Date.parse(value);
41792
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
41793
+ }
41794
+ function uniqueSorted(values = []) {
41795
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort(compareCodepoints$3);
41796
+ }
41797
+ function totalRows(values) {
41798
+ const present = values.filter((value) => value !== null && value !== void 0);
41799
+ return present.length ? Math.max(...present) : null;
41800
+ }
41801
+ function linkCoverage(input) {
41802
+ const hasMore = input.providerTotalComparable !== false && input.providerTotalRows !== null ? input.offset + input.returnedRows < input.providerTotalRows : input.returnedRows >= input.requestedRows;
41803
+ return {
41804
+ requestedRows: input.requestedRows,
41805
+ returnedRows: input.returnedRows,
41806
+ retainedRows: input.retainedRows,
41807
+ invalidRows: input.invalidRows,
41808
+ providerTotalRows: input.providerTotalRows,
41809
+ completeness: input.invalidRows > 0 ? "partial" : hasMore ? "capped" : input.filtered ? "filtered" : "complete",
41810
+ nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
41811
+ };
41812
+ }
41813
+ function linkEvidence(input) {
41814
+ return {
41815
+ schemaVersion: 1,
41816
+ provider: "dataforseo",
41817
+ capability: input.capability,
41818
+ data: input.data,
41819
+ observedAt: input.snapshot.observedAt,
41820
+ market: null,
41821
+ coverage: input.coverage,
41822
+ cache: input.snapshot.cache,
41823
+ cost: input.snapshot.cost,
41824
+ request: {
41825
+ operation: input.capability,
41826
+ endpoint: input.endpoint,
41827
+ limit: input.limit,
41828
+ filters: input.filters,
41829
+ sort: input.sort
41830
+ },
41831
+ warnings: input.warnings
41832
+ };
41833
+ }
41834
+ function mappingWarnings(input) {
41835
+ return [
41836
+ ...input.snapshotWarnings,
41837
+ ...input.invalidRows ? [{
41838
+ code: `invalid-${input.label}-rows`,
41839
+ field: "data.rows",
41840
+ message: `${input.invalidRows} ${input.label} row${input.invalidRows === 1 ? "" : "s"} lacked required fields and were omitted.`
41841
+ }] : [],
41842
+ ...input.duplicateRows ? [{
41843
+ code: `duplicate-${input.label}-rows`,
41844
+ field: "data.rows",
41845
+ message: `${input.duplicateRows} duplicate ${input.label} row${input.duplicateRows === 1 ? "" : "s"} were collapsed deterministically.`
41846
+ }] : []
41847
+ ];
41848
+ }
41849
+ //#endregion
41850
+ //#region packages/core/src/providers/dataforseo/backlinks.ts
41851
+ function metricValue(row, id) {
41852
+ return row.metrics.find((item) => item.id === id)?.value ?? -1;
41853
+ }
41854
+ function compareRows(left, right) {
41855
+ return metricValue(right, "backlink-rank") - metricValue(left, "backlink-rank") || metricValue(right, "source-domain-rank") - metricValue(left, "source-domain-rank") || compareCodepoints$3(left.sourceDomain, right.sourceDomain) || compareCodepoints$3(left.sourceUrl, right.sourceUrl) || compareCodepoints$3(left.targetUrl, right.targetUrl) || compareCodepoints$3(left.anchorText ?? "", right.anchorText ?? "");
41856
+ }
41857
+ function dedupe(rows) {
41858
+ const sorted = [...rows].sort(compareRows);
41859
+ const seen = /* @__PURE__ */ new Set();
41860
+ return sorted.filter((row) => {
41861
+ const key = `${row.sourceUrl}\0${row.targetUrl}\0${row.anchorText ?? ""}`;
41862
+ if (seen.has(key)) return false;
41863
+ seen.add(key);
41864
+ return true;
41865
+ });
41866
+ }
41867
+ async function dataForSeoBacklinks(client, input) {
41868
+ const normalized = linkTarget(input.target, input.scope);
41869
+ const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
41870
+ const offset = input.offset ?? 0;
41871
+ linkRowLimit(input.limit, offset);
41872
+ const mode = input.mode ?? "representative";
41873
+ const status = input.status ?? "live";
41874
+ const providerMode = mode === "representative" ? "one_per_domain" : "as_is";
41875
+ const orderBy = [
41876
+ "rank,desc",
41877
+ "domain_from_rank,desc",
41878
+ "url_from,asc"
41879
+ ];
41880
+ const snapshot = await client.backlinks({
41881
+ ...normalized,
41882
+ includeSubdomains,
41883
+ mode: providerMode,
41884
+ status,
41885
+ limit: input.limit,
41886
+ offset,
41887
+ orderBy,
41888
+ refresh: input.refresh,
41889
+ context: requestContext("link-evidence", input.context)
41890
+ });
41891
+ const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
41892
+ const rawRows = results.flatMap((result) => result.items ?? []);
41893
+ let invalidRows = 0;
41894
+ const mapped = rawRows.flatMap((row) => {
41895
+ const sourceUrl = safeUrl(row.url_from);
41896
+ const targetUrl = safeUrl(row.url_to);
41897
+ if (!sourceUrl || !targetUrl) {
41898
+ invalidRows += 1;
41899
+ return [];
41900
+ }
41901
+ return [{
41902
+ sourceUrl,
41903
+ sourceDomain: new URL(sourceUrl).hostname.toLowerCase(),
41904
+ targetUrl,
41905
+ anchorText: row.anchor ?? null,
41906
+ linkType: row.item_type ?? null,
41907
+ dofollow: row.dofollow ?? null,
41908
+ attributes: uniqueSorted(row.rel_attributes ?? row.attributes ?? []),
41909
+ firstSeenAt: normalizedDate(row.first_seen),
41910
+ lastSeenAt: normalizedDate(row.lost_date ?? row.last_visited),
41911
+ state: row.is_lost || row.lost_date ? "lost" : "live",
41912
+ indirect: row.is_indirect_link ?? null,
41913
+ linksFromPage: row.links_count ?? null,
41914
+ linksFromDomain: row.group_count ?? null,
41915
+ metrics: [
41916
+ ...metric("backlink-rank", "DataForSEO backlink rank", row.rank),
41917
+ ...metric("source-domain-rank", "DataForSEO source domain rank", row.domain_from_rank),
41918
+ ...metric("source-page-rank", "DataForSEO source page rank", row.page_from_rank),
41919
+ ...metric("backlink-spam-score", "DataForSEO backlink spam score", row.backlink_spam_score ?? row.backlinks_spam_score)
41920
+ ]
41921
+ }];
41922
+ });
41923
+ const rows = dedupe(mapped);
41924
+ const duplicateRows = mapped.length - rows.length;
41925
+ const providerTotalRows = totalRows(results.map((result) => result.total_count));
41926
+ return linkEvidence({
41927
+ capability: "backlinks",
41928
+ data: {
41929
+ target: normalized.target,
41930
+ mode,
41931
+ rows,
41932
+ totalRows: providerTotalRows
41933
+ },
41934
+ snapshot,
41935
+ coverage: linkCoverage({
41936
+ requestedRows: input.limit,
41937
+ returnedRows: rawRows.length,
41938
+ retainedRows: rows.length,
41939
+ invalidRows,
41940
+ providerTotalRows,
41941
+ offset,
41942
+ filtered: mode === "representative" || status !== "all",
41943
+ providerTotalComparable: mode !== "representative"
41944
+ }),
41945
+ endpoint: LINK_ENDPOINTS.backlinks,
41946
+ limit: input.limit,
41947
+ filters: {
41948
+ scope: normalized.scope,
41949
+ includeSubdomains,
41950
+ mode,
41951
+ status,
41952
+ includeIndirectLinks: true,
41953
+ excludeInternalBacklinks: true,
41954
+ offset
41955
+ },
41956
+ sort: orderBy,
41957
+ warnings: mappingWarnings({
41958
+ snapshotWarnings: snapshot.warnings,
41959
+ invalidRows,
41960
+ duplicateRows,
41961
+ label: "backlink"
41962
+ })
41963
+ });
41964
+ }
41965
+ //#endregion
41966
+ //#region packages/core/src/providers/dataforseo/link-summary.ts
41967
+ async function dataForSeoLinkSummary(client, input) {
41968
+ const normalized = linkTarget(input.target, input.scope);
41969
+ const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
41970
+ const snapshot = await client.linkSummary({
41971
+ ...normalized,
41972
+ includeSubdomains,
41973
+ refresh: input.refresh,
41974
+ context: requestContext("link-evidence", input.context)
41975
+ });
41976
+ const rows = snapshot.response.tasks.flatMap((task) => task.result ?? []);
41977
+ const row = [...rows].sort((left, right) => compareCodepoints$3(left.target ?? "", right.target ?? ""))[0];
41978
+ const data = {
41979
+ target: normalized.target,
41980
+ scope: normalized.scope,
41981
+ backlinks: numberValue(row?.backlinks, "total backlinks"),
41982
+ referringDomains: numberValue(row?.referring_domains, "referring domains"),
41983
+ referringPages: numberValue(row?.referring_pages, "referring pages"),
41984
+ brokenBacklinks: numberValue(row?.broken_backlinks, "broken backlinks"),
41985
+ brokenPages: numberValue(row?.broken_pages, "broken referring pages"),
41986
+ metrics: [
41987
+ ...metric("rank", "DataForSEO rank", row?.rank),
41988
+ ...metric("backlinks-spam-score", "DataForSEO backlink spam score", row?.backlinks_spam_score),
41989
+ ...metric("target-spam-score", "DataForSEO target spam score", row?.info?.target_spam_score)
41990
+ ]
41991
+ };
41992
+ const retainedRows = row ? 1 : 0;
41993
+ return linkEvidence({
41994
+ capability: "link-summary",
41995
+ data,
41996
+ snapshot,
41997
+ coverage: {
41998
+ requestedRows: 1,
41999
+ returnedRows: rows.length,
42000
+ retainedRows,
42001
+ invalidRows: 0,
42002
+ providerTotalRows: rows.length,
42003
+ completeness: row ? "complete" : "unavailable",
42004
+ nextCursor: null
42005
+ },
42006
+ endpoint: LINK_ENDPOINTS.summary,
42007
+ limit: 1,
42008
+ filters: {
42009
+ scope: normalized.scope,
42010
+ includeSubdomains,
42011
+ status: "live",
42012
+ includeIndirectLinks: true,
42013
+ excludeInternalBacklinks: true
42014
+ },
42015
+ sort: [],
42016
+ warnings: [
42017
+ ...snapshot.warnings,
42018
+ ...rows.length > 1 ? [{
42019
+ code: "multiple-link-summary-rows",
42020
+ field: "data",
42021
+ message: "DataForSEO returned more than one summary row. One row was retained deterministically."
42022
+ }] : [],
42023
+ ...!row ? [{
42024
+ code: "missing-link-summary-row",
42025
+ field: "data",
42026
+ message: "DataForSEO returned no summary row. Counts remain unavailable rather than zero."
42027
+ }] : []
42028
+ ]
42029
+ });
42030
+ }
42031
+ //#endregion
42032
+ //#region packages/core/src/providers/dataforseo/referring-domains.ts
42033
+ function observedNumber(row) {
42034
+ return row.backlinks.state === "observed" ? row.backlinks.value : -1;
42035
+ }
42036
+ async function dataForSeoReferringDomains(client, input) {
42037
+ const normalized = linkTarget(input.target, input.scope);
42038
+ const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
42039
+ const offset = input.offset ?? 0;
42040
+ linkRowLimit(input.limit, offset);
42041
+ const orderBy = [
42042
+ "backlinks,desc",
42043
+ "rank,desc",
42044
+ "domain,asc"
42045
+ ];
42046
+ const snapshot = await client.referringDomains({
42047
+ ...normalized,
42048
+ includeSubdomains,
42049
+ limit: input.limit,
42050
+ offset,
42051
+ orderBy,
42052
+ refresh: input.refresh,
42053
+ context: requestContext("link-evidence", input.context)
42054
+ });
42055
+ const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
42056
+ const rawRows = results.flatMap((result) => result.items ?? []);
42057
+ let invalidRows = 0;
42058
+ const mapped = rawRows.flatMap((row) => {
42059
+ const domain = row.domain?.trim().toLowerCase();
42060
+ if (!domain) {
42061
+ invalidRows += 1;
42062
+ return [];
42063
+ }
42064
+ const firstSeenAt = normalizedDate(row.first_seen);
42065
+ return [{
42066
+ domain,
42067
+ backlinks: numberValue(row.backlinks, "domain backlinks"),
42068
+ referringPages: numberValue(row.referring_pages, "domain referring pages"),
42069
+ brokenBacklinks: numberValue(row.broken_backlinks, "domain broken backlinks"),
42070
+ brokenPages: numberValue(row.broken_pages, "domain broken pages"),
42071
+ firstSeenAt: stringValue(firstSeenAt, "domain first seen date"),
42072
+ metrics: [...metric("source-domain-rank", "DataForSEO source domain rank", row.rank), ...metric("domain-backlinks-spam-score", "DataForSEO domain backlink spam score", row.backlinks_spam_score)]
42073
+ }];
42074
+ });
42075
+ const byDomain = /* @__PURE__ */ new Map();
42076
+ for (const row of mapped.sort((left, right) => observedNumber(right) - observedNumber(left) || compareCodepoints$3(left.domain, right.domain))) if (!byDomain.has(row.domain)) byDomain.set(row.domain, row);
42077
+ const rows = [...byDomain.values()];
42078
+ const duplicateRows = mapped.length - rows.length;
42079
+ const providerTotalRows = totalRows(results.map((result) => result.total_count));
42080
+ return linkEvidence({
42081
+ capability: "referring-domains",
42082
+ data: {
42083
+ target: normalized.target,
42084
+ rows,
42085
+ totalRows: providerTotalRows
42086
+ },
42087
+ snapshot,
42088
+ coverage: linkCoverage({
42089
+ requestedRows: input.limit,
42090
+ returnedRows: rawRows.length,
42091
+ retainedRows: rows.length,
42092
+ invalidRows,
42093
+ providerTotalRows,
42094
+ offset,
42095
+ filtered: true
42096
+ }),
42097
+ endpoint: LINK_ENDPOINTS.referringDomains,
42098
+ limit: input.limit,
42099
+ filters: {
42100
+ scope: normalized.scope,
42101
+ includeSubdomains,
42102
+ status: "live",
42103
+ includeIndirectLinks: true,
42104
+ excludeInternalBacklinks: true,
42105
+ offset
42106
+ },
42107
+ sort: orderBy,
42108
+ warnings: mappingWarnings({
42109
+ snapshotWarnings: snapshot.warnings,
42110
+ invalidRows,
42111
+ duplicateRows,
42112
+ label: "referring-domain"
42113
+ })
42114
+ });
42115
+ }
42116
+ //#endregion
42117
+ //#region packages/core/src/providers/dataforseo/link-research.ts
42118
+ var DataForSeoLinkProvider = class {
42119
+ provider = "dataforseo";
42120
+ capabilitySupport = [
42121
+ "link-summary",
42122
+ "referring-domains",
42123
+ "backlinks"
42124
+ ].map((capability) => ({
42125
+ capability,
42126
+ status: "available",
42127
+ markets: "all"
42128
+ }));
42129
+ client;
42130
+ constructor(options = {}) {
42131
+ this.client = options.client ?? new DataForSeoClient(options);
42132
+ }
42133
+ linkSummary(input) {
42134
+ return dataForSeoLinkSummary(this.client, input);
42135
+ }
42136
+ backlinks(input) {
42137
+ return dataForSeoBacklinks(this.client, input);
42138
+ }
42139
+ referringDomains(input) {
42140
+ return dataForSeoReferringDomains(this.client, input);
42141
+ }
42142
+ };
42143
+ //#endregion
42144
+ //#region packages/core/src/links/dataforseo.ts
42145
+ const DEFAULT_ROW_LIMIT$2 = 100;
42146
+ const MAX_ROW_LIMIT$2 = 500;
42147
+ function rowLimit(value) {
42148
+ const limit = value ?? DEFAULT_ROW_LIMIT$2;
42149
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_ROW_LIMIT$2) throw new SeoError("INVALID_INPUT", `DataForSEO link row limit must be between 1 and ${MAX_ROW_LIMIT$2}.`);
42150
+ return limit;
42151
+ }
42152
+ async function collectDataForSeoLinkEvidence(input) {
42153
+ const limit = rowLimit(input.rowLimit);
42154
+ const provider = input.provider ?? new DataForSeoLinkProvider();
42155
+ const context = input.context ?? {
42156
+ reportId: "link-evidence",
42157
+ reportRunId: randomUUID()
42158
+ };
42159
+ const summary = await provider.linkSummary({
42160
+ target: input.target,
42161
+ scope: input.scope,
42162
+ includeSubdomains: input.includeSubdomains,
42163
+ refresh: input.refresh,
42164
+ context
42165
+ });
42166
+ const backlinks = await provider.backlinks({
42167
+ target: summary.data.target,
42168
+ scope: summary.data.scope,
42169
+ includeSubdomains: input.includeSubdomains,
42170
+ mode: "representative",
42171
+ status: "live",
42172
+ limit,
42173
+ refresh: input.refresh,
42174
+ context
42175
+ });
42176
+ const rows = backlinks.data.rows.map((row) => ({
42177
+ sourceUrl: row.sourceUrl,
42178
+ sourceDomain: row.sourceDomain,
42179
+ targetUrl: row.targetUrl,
42180
+ anchorText: row.anchorText ?? void 0,
42181
+ firstSeenAt: row.firstSeenAt ?? void 0,
42182
+ lastSeenAt: row.lastSeenAt ?? void 0,
42183
+ nofollow: row.dofollow === null ? void 0 : !row.dofollow,
42184
+ linkType: row.linkType ?? void 0,
42185
+ attributes: row.attributes,
42186
+ state: row.state,
42187
+ indirect: row.indirect ?? void 0,
42188
+ linksFromPage: row.linksFromPage ?? void 0,
42189
+ linksFromDomain: row.linksFromDomain ?? void 0,
42190
+ providerMetrics: row.metrics
42191
+ }));
42192
+ const counts = /* @__PURE__ */ new Map();
42193
+ for (const row of rows) counts.set(row.targetUrl, (counts.get(row.targetUrl) ?? 0) + 1);
42194
+ const targetCounts = [...counts].map(([targetUrl, observedLinks]) => ({
42195
+ targetUrl,
42196
+ observedLinks
42197
+ })).sort((left, right) => right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0));
42198
+ const completeness = backlinks.coverage.completeness;
42199
+ const representative = backlinks.data.mode === "representative";
42200
+ const partial = completeness !== "complete" || representative;
42201
+ return {
42202
+ rows,
42203
+ targetCounts,
42204
+ provenance: {
42205
+ provider: "dataforseo",
42206
+ observedAt: backlinks.observedAt > summary.observedAt ? backlinks.observedAt : summary.observedAt,
42207
+ cached: summary.cache.status === "hit" && backlinks.cache.status === "hit",
42208
+ suppliedRows: backlinks.coverage.returnedRows ?? rows.length,
42209
+ validRows: rows.length,
42210
+ invalidRows: backlinks.coverage.invalidRows,
42211
+ duplicateRows: Math.max(0, (backlinks.coverage.returnedRows ?? rows.length) - backlinks.coverage.invalidRows - rows.length),
42212
+ capped: ["capped", "partial"].includes(completeness),
42213
+ rowLimit: limit,
42214
+ completeness: partial ? "partial" : "complete",
42215
+ providerRequests: {
42216
+ methods: [summary.request.endpoint, backlinks.request.endpoint],
42217
+ maxConcurrentRequests: 1
42218
+ },
42219
+ providerCoverage: {
42220
+ targetCountRows: {
42221
+ returnedRows: targetCounts.length,
42222
+ retainedRows: targetCounts.length,
42223
+ invalidRows: 0
42224
+ },
42225
+ detailRows: {
42226
+ returnedRows: backlinks.coverage.returnedRows ?? rows.length,
42227
+ retainedRows: rows.length,
42228
+ invalidRows: backlinks.coverage.invalidRows
42229
+ },
42230
+ summaryRows: {
42231
+ returnedRows: summary.coverage.returnedRows ?? 0,
42232
+ retainedRows: summary.coverage.retainedRows ?? 0,
42233
+ invalidRows: summary.coverage.invalidRows
42234
+ },
42235
+ backlinkRows: {
42236
+ returnedRows: backlinks.coverage.returnedRows ?? rows.length,
42237
+ retainedRows: rows.length,
42238
+ invalidRows: backlinks.coverage.invalidRows,
42239
+ providerTotalRows: backlinks.coverage.providerTotalRows
42240
+ }
42241
+ }
42242
+ },
42243
+ externalProvider: {
42244
+ summary,
42245
+ backlinks
42246
+ },
42247
+ warnings: [
42248
+ ...summary.warnings.map((warning) => warning.message),
42249
+ ...backlinks.warnings.map((warning) => warning.message),
42250
+ ...representative ? ["The link list retains one representative backlink per referring domain. Provider summary counts remain separate from retained rows."] : [],
42251
+ ...!representative && partial ? ["The link list is bounded or partial. Provider summary counts remain separate from retained rows."] : []
42252
+ ]
42253
+ };
42254
+ }
42255
+ //#endregion
40624
42256
  //#region packages/core/src/links/import.ts
40625
42257
  const DEFAULT_ROW_LIMIT$1 = 1e4;
40626
42258
  const MAX_ROW_LIMIT$1 = 1e5;
@@ -40809,6 +42441,10 @@ async function importLinkEvidence(input) {
40809
42441
  //#region packages/core/src/links/report.ts
40810
42442
  const DEFAULT_OUTPUT_LIMIT = 100;
40811
42443
  const MAX_OUTPUT_LIMIT = 500;
42444
+ const MAX_TARGET_COUNTS = 100;
42445
+ const MAX_CONTEXT_ROWS = 100;
42446
+ const MAX_FINDINGS = 50;
42447
+ const MAX_STRUCTURED_DETAIL_ROWS = 750;
40812
42448
  function boundedLimit(value) {
40813
42449
  const limit = value ?? DEFAULT_OUTPUT_LIMIT;
40814
42450
  if (!Number.isInteger(limit) || limit < 1 || limit > MAX_OUTPUT_LIMIT) throw new SeoError("INVALID_INPUT", `Link report limit must be between 1 and ${MAX_OUTPUT_LIMIT}.`);
@@ -40820,7 +42456,7 @@ function targetCounts(rows) {
40820
42456
  return [...counts].map(([targetUrl, observedLinks]) => ({
40821
42457
  targetUrl,
40822
42458
  observedLinks
40823
- })).sort((a, b) => b.observedLinks - a.observedLinks || a.targetUrl.localeCompare(b.targetUrl, "en"));
42459
+ })).sort((a, b) => b.observedLinks - a.observedLinks || (a.targetUrl < b.targetUrl ? -1 : a.targetUrl > b.targetUrl ? 1 : 0));
40824
42460
  }
40825
42461
  function linkEvidenceReport(input) {
40826
42462
  const limit = boundedLimit(input.limit);
@@ -40828,8 +42464,24 @@ function linkEvidenceReport(input) {
40828
42464
  const domains = new Set(input.evidence.rows.map((row) => row.sourceDomain));
40829
42465
  const observedTargets = targetCounts(input.evidence.rows);
40830
42466
  const counts = input.evidence.targetCounts.length ? input.evidence.targetCounts : observedTargets;
42467
+ const providerEvidence = input.evidence.externalProvider ? {
42468
+ summary: input.evidence.externalProvider.summary,
42469
+ backlinks: {
42470
+ ...input.evidence.externalProvider.backlinks,
42471
+ data: {
42472
+ target: input.evidence.externalProvider.backlinks.data.target,
42473
+ mode: input.evidence.externalProvider.backlinks.data.mode,
42474
+ totalRows: input.evidence.externalProvider.backlinks.data.totalRows,
42475
+ retainedRows: input.evidence.externalProvider.backlinks.data.rows.length
42476
+ }
42477
+ }
42478
+ } : null;
42479
+ const targetCountsOutput = counts.slice(0, MAX_TARGET_COUNTS);
42480
+ const contextRows = input.targetContext?.rows.slice(0, MAX_CONTEXT_ROWS) ?? [];
42481
+ const findings = input.targetContext?.findings.slice(0, MAX_FINDINGS) ?? [];
42482
+ const returnedDetailRows = rows.length + targetCountsOutput.length + contextRows.length + findings.length;
40831
42483
  return {
40832
- schemaVersion: 1,
42484
+ schemaVersion: 2,
40833
42485
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
40834
42486
  dataStatus: input.evidence.provenance.completeness === "complete" ? "complete" : "partial",
40835
42487
  summary: {
@@ -40838,6 +42490,8 @@ function linkEvidenceReport(input) {
40838
42490
  targetPages: new Set(input.evidence.rows.map((row) => row.targetUrl)).size,
40839
42491
  providerTargetPages: counts.length
40840
42492
  },
42493
+ providerSummary: providerEvidence?.summary.data ?? null,
42494
+ providerEvidence,
40841
42495
  provenance: input.evidence.provenance,
40842
42496
  selection: {
40843
42497
  availableRows: input.evidence.rows.length,
@@ -40845,13 +42499,45 @@ function linkEvidenceReport(input) {
40845
42499
  omittedRows: Math.max(0, input.evidence.rows.length - rows.length),
40846
42500
  limit
40847
42501
  },
40848
- targetCounts: counts.slice(0, 100),
42502
+ outputBudget: {
42503
+ unit: "structured-detail-rows",
42504
+ limit: MAX_STRUCTURED_DETAIL_ROWS,
42505
+ returned: returnedDetailRows,
42506
+ omitted: Math.max(0, input.evidence.rows.length - rows.length + Math.max(0, counts.length - targetCountsOutput.length) + Math.max(0, (input.targetContext?.rows.length ?? 0) - contextRows.length) + Math.max(0, (input.targetContext?.findings.length ?? 0) - findings.length)),
42507
+ sections: {
42508
+ links: {
42509
+ limit: MAX_OUTPUT_LIMIT,
42510
+ returned: rows.length
42511
+ },
42512
+ targetCounts: {
42513
+ limit: MAX_TARGET_COUNTS,
42514
+ returned: targetCountsOutput.length
42515
+ },
42516
+ targetContext: {
42517
+ limit: MAX_CONTEXT_ROWS,
42518
+ returned: contextRows.length
42519
+ },
42520
+ findings: {
42521
+ limit: MAX_FINDINGS,
42522
+ returned: findings.length
42523
+ }
42524
+ }
42525
+ },
42526
+ targetCounts: targetCountsOutput,
40849
42527
  links: rows,
40850
- warnings: input.evidence.warnings,
42528
+ targetContext: input.targetContext ? {
42529
+ ...input.targetContext,
42530
+ rows: contextRows,
42531
+ findings
42532
+ } : null,
42533
+ findings,
42534
+ warnings: [...input.evidence.warnings, ...input.targetContext?.warnings ?? []],
40851
42535
  caveats: [
40852
42536
  "This is bounded provider or imported evidence, not a complete backlink index.",
40853
42537
  "A missing link is not proof that no link exists, and an observed link is not a quality or ranking verdict.",
40854
- "Referring-domain counts describe the retained rows only."
42538
+ "Referring-domain counts describe the retained rows only.",
42539
+ ...providerEvidence ? ["Provider profile counts describe its current index. The representative link rows are a separate bounded sample.", "Provider rank and spam metrics keep their provider names and scales. They are context, not ranking factors or universal authority scores."] : [],
42540
+ ...input.targetContext ? ["Search Console page rows are retained first-party evidence. Missing or capped rows are never treated as zero.", "Saved crawl findings describe the recorded crawl time. Verify the live target before changing it."] : []
40855
42541
  ]
40856
42542
  };
40857
42543
  }
@@ -41840,6 +43526,7 @@ const TELEMETRY_REPORTS = [
41840
43526
  "crawl-history",
41841
43527
  "crawler-rules",
41842
43528
  "llms-txt-audit",
43529
+ "local-search-demand",
41843
43530
  "generate-llms-txt",
41844
43531
  "measure-change",
41845
43532
  "monthly-report",
@@ -42183,6 +43870,6 @@ function isoDate(daysAgo = 0) {
42183
43870
  return date.toISOString().slice(0, 10);
42184
43871
  }
42185
43872
  //#endregion
42186
- export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoProvider, DataForSeoRankTrackingCollector, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, RANK_TRACKING_LIMITS, RANK_TRACKING_SCHEMA_SQL, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, activeRankTrackingRun, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, competitorKeywordGapReport, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, domainOverviewReport, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, enforceRankTrackingLogicalBytes, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, executeRankTracking, expectedCtrForPosition, explainOkfValidation, explainRule, failRankTrackingTask, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getOrCreateRankTrackingConfiguration, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRankTrackingRun, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, latestRankTrackingRun, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, mapDataForSeoSerpSnapshot, markRankTasksPosted, markRankTasksPosting, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, priorComparableRankTrackingRun, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, rankObservations, rankTrackingLogicalBytes, rankTrackingReport, rankTrackingTasks, rankedKeywordsReport, rankingPagesReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, recoverRankTaskReceipt, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, refreshRankTrackingRun, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, saveRankObservation, saveRankObservations, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpCompetitorsReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, startRankTrackingRun, strikingDistance, submitIndexNow, summarizeCrawlReport, targetMatchesDomain, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
43873
+ export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoLinkProvider, DataForSeoProvider, DataForSeoRankTrackingCollector, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, RANK_TRACKING_LIMITS, RANK_TRACKING_SCHEMA_SQL, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, activeRankTrackingRun, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzeLocalSearchRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, classifyLocalIntent, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, collectDataForSeoLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, competitorKeywordGapReport, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLocalIntentClassifier, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, domainOverviewReport, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, enforceRankTrackingLogicalBytes, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, executeRankTracking, expectedCtrForPosition, explainOkfValidation, explainRule, failRankTrackingTask, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getOrCreateRankTrackingConfiguration, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRankTrackingRun, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, latestRankTrackingRun, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, linkTargetContext, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, localSearchReport, loginWithLoopback, makeRunId, mapDataForSeoSerpSnapshot, markRankTasksPosted, markRankTasksPosting, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeLocalSearchText, normalizeLocationTerms, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, priorComparableRankTrackingRun, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, rankObservations, rankTrackingLogicalBytes, rankTrackingReport, rankTrackingTasks, rankedKeywordsReport, rankingPagesReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, recoverRankTaskReceipt, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, refreshRankTrackingRun, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, saveRankObservation, saveRankObservations, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpCompetitorsReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, startRankTrackingRun, strikingDistance, submitIndexNow, summarizeCrawlReport, targetMatchesDomain, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
42187
43874
 
42188
43875
  //# sourceMappingURL=index.js.map