seo 0.2.20 → 0.2.21

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/mcp.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-CSE-hgHf.js";
3
3
  import { Headers, fetch as fetch$1 } from "undici";
4
4
  import { chmodSync, createReadStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { arch, homedir, platform, totalmem } from "node:os";
@@ -1814,7 +1814,7 @@ function aiReferralSourceForValue(value) {
1814
1814
  function compareText$18(left, right) {
1815
1815
  return left < right ? -1 : left > right ? 1 : 0;
1816
1816
  }
1817
- function metric(row, name) {
1817
+ function metric$1(row, name) {
1818
1818
  const value = row[name];
1819
1819
  const parsed = Number(value);
1820
1820
  if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `Google Analytics returned an invalid ${name} value.`);
@@ -1849,8 +1849,8 @@ function analyzeAiReferralRows(input) {
1849
1849
  ...emptyMetrics()
1850
1850
  };
1851
1851
  addMetrics(total, {
1852
- sessions: metric(row, "sessions"),
1853
- eventCount: metric(row, "eventCount")
1852
+ sessions: metric$1(row, "sessions"),
1853
+ eventCount: metric$1(row, "eventCount")
1854
1854
  });
1855
1855
  total.observedSessionSources.add(row.sessionSource ?? "");
1856
1856
  sourceTotals.set(definition.id, total);
@@ -1862,8 +1862,8 @@ function analyzeAiReferralRows(input) {
1862
1862
  const definition = aiReferralSourceForValue(row.sessionSource ?? "");
1863
1863
  if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics detail rows contained an unexpected session source.");
1864
1864
  const metrics = {
1865
- sessions: metric(row, "sessions"),
1866
- eventCount: metric(row, "eventCount")
1865
+ sessions: metric$1(row, "sessions"),
1866
+ eventCount: metric$1(row, "eventCount")
1867
1867
  };
1868
1868
  addMetrics(detailTotal, metrics);
1869
1869
  const landingPage = row.landingPagePlusQueryString || "(not set)";
@@ -5323,7 +5323,7 @@ function estimateSerpTitleWidth(title) {
5323
5323
  }
5324
5324
  //#endregion
5325
5325
  //#region packages/core/dist/analyze/audit-page.js
5326
- const defaultDependencies$15 = {
5326
+ const defaultDependencies$16 = {
5327
5327
  fetchPage,
5328
5328
  queryPageMetrics,
5329
5329
  now: () => /* @__PURE__ */ new Date()
@@ -5354,7 +5354,7 @@ function buildRecommendations(report) {
5354
5354
  });
5355
5355
  return recommendations;
5356
5356
  }
5357
- async function auditPage(input, dependencies = defaultDependencies$15) {
5357
+ async function auditPage(input, dependencies = defaultDependencies$16) {
5358
5358
  const fetched = await dependencies.fetchPage(input.url, {
5359
5359
  js: input.js,
5360
5360
  refresh: input.refresh
@@ -6928,7 +6928,7 @@ function pageOpportunityRecommendations(analysis) {
6928
6928
  //#endregion
6929
6929
  //#region packages/core/dist/analyze/page-opportunities.js
6930
6930
  const MAX_BENCHMARK_ROWS = 1e5;
6931
- const defaultDependencies$14 = {
6931
+ const defaultDependencies$15 = {
6932
6932
  searchAnalytics: querySearchAnalytics,
6933
6933
  fetch: fetchPage,
6934
6934
  extract: extractPage,
@@ -7000,7 +7000,7 @@ function reportCaveats$1(input) {
7000
7000
  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.` : ""
7001
7001
  ].filter(Boolean);
7002
7002
  }
7003
- async function pageOpportunitiesReport(input, dependencies = defaultDependencies$14) {
7003
+ async function pageOpportunitiesReport(input, dependencies = defaultDependencies$15) {
7004
7004
  const url = assertUrlMatchesGscProperty(input.site, input.url);
7005
7005
  const days = reportDays$1(input.days);
7006
7006
  const range = defaultDateRange$1(days);
@@ -10787,7 +10787,7 @@ function absoluteHttpUrl(value) {
10787
10787
  return false;
10788
10788
  }
10789
10789
  }
10790
- function urlKey(value) {
10790
+ function urlKey$1(value) {
10791
10791
  if (!value) return void 0;
10792
10792
  try {
10793
10793
  const url = new URL(value);
@@ -10986,7 +10986,7 @@ function auditCrawlPages(pages, opts = {}) {
10986
10986
  for (const page of pages) {
10987
10987
  if (!isAuditableHtmlPage(page)) continue;
10988
10988
  for (const value of [page.url, page.finalUrl]) {
10989
- const key = urlKey(value);
10989
+ const key = urlKey$1(value);
10990
10990
  if (key) pageByUrl.set(key, page);
10991
10991
  }
10992
10992
  }
@@ -11128,7 +11128,7 @@ function auditCrawlPages(pages, opts = {}) {
11128
11128
  canonicalRaw: page.canonicalRaw,
11129
11129
  canonical: page.canonical
11130
11130
  }));
11131
- const canonicalTarget = pageByUrl.get(urlKey(page.canonical) ?? "");
11131
+ const canonicalTarget = pageByUrl.get(urlKey$1(page.canonical) ?? "");
11132
11132
  if (canonicalTarget?.canonical && !sameUrl(canonicalTarget.canonical, page.canonical)) issues.push(issue("canonical_chain", page, page.canonical, {
11133
11133
  canonical: page.canonical,
11134
11134
  nextCanonical: canonicalTarget.canonical,
@@ -11302,7 +11302,7 @@ const SENSITIVE_FIELD_NAMES = /* @__PURE__ */ new Set([
11302
11302
  "refresh-token"
11303
11303
  ]);
11304
11304
  const SENSITIVE_PARAM_PATTERN = /^(access_?token|auth|api_?key|client_?secret|code|jwt|key|password|refresh_?token|secret|session|sig|signature|token)$/i;
11305
- function uniqueSorted(values = []) {
11305
+ function uniqueSorted$1(values = []) {
11306
11306
  return [...new Set(values.filter(Boolean))].sort();
11307
11307
  }
11308
11308
  function normalizeFetchRate(input, strategy) {
@@ -11322,13 +11322,13 @@ function normalizeCrawlConfig(input) {
11322
11322
  mode: input.mode ?? (strategy === "health" ? "sitemap" : "site"),
11323
11323
  strategy,
11324
11324
  ...input.sitemapUrl ? { sitemapUrl: new URL(input.sitemapUrl).toString() } : {},
11325
- urls: uniqueSorted(input.urls ?? []),
11325
+ urls: uniqueSorted$1(input.urls ?? []),
11326
11326
  maxPages: input.maxPages ?? 500,
11327
11327
  maxDepth: input.maxDepth ?? 16,
11328
11328
  concurrency,
11329
11329
  timeoutMs: input.timeoutMs ?? 2e4,
11330
- include: uniqueSorted(input.include ?? []),
11331
- exclude: uniqueSorted(input.exclude ?? []),
11330
+ include: uniqueSorted$1(input.include ?? []),
11331
+ exclude: uniqueSorted$1(input.exclude ?? []),
11332
11332
  respectRobots: input.respectRobots ?? true,
11333
11333
  useSitemap: input.useSitemap ?? true,
11334
11334
  checkExternal: strategy === "health" ? false : input.checkExternal ?? true,
@@ -13573,7 +13573,7 @@ async function fetchSitemapUrls(input) {
13573
13573
  }
13574
13574
  //#endregion
13575
13575
  //#region packages/core/dist/analyze/workflows/analytics-value.js
13576
- const defaultDependencies$13 = { runGa4Report };
13576
+ const defaultDependencies$14 = { runGa4Report };
13577
13577
  function normalizePath(value) {
13578
13578
  if (!value || value === "(not set)") return "";
13579
13579
  const [path = ""] = value.split("?");
@@ -13633,7 +13633,7 @@ function landingPageRankingPolicy(input) {
13633
13633
  warnings
13634
13634
  };
13635
13635
  }
13636
- async function fetchLandingPageValues(input, dependencies = defaultDependencies$13) {
13636
+ async function fetchLandingPageValues(input, dependencies = defaultDependencies$14) {
13637
13637
  const retainedRowLimit = input.limit ?? 5e3;
13638
13638
  if (!input.propertyId) return {
13639
13639
  values: /* @__PURE__ */ new Map(),
@@ -16601,15 +16601,15 @@ function analyzeCtrUnderperformersFromRows(input) {
16601
16601
  }
16602
16602
  //#endregion
16603
16603
  //#region packages/core/dist/analyze/ctr-underperformers.js
16604
- const MAX_GSC_ROWS$4 = 1e5;
16604
+ const MAX_GSC_ROWS$5 = 1e5;
16605
16605
  function plural$3(count, singular, pluralLabel = `${singular}s`) {
16606
16606
  return count === 1 ? singular : pluralLabel;
16607
16607
  }
16608
- const defaultDependencies$12 = {
16608
+ const defaultDependencies$13 = {
16609
16609
  searchAnalytics: querySearchAnalytics,
16610
16610
  now: () => /* @__PURE__ */ new Date()
16611
16611
  };
16612
- async function ctrUnderperformersReport(input, dependencies = defaultDependencies$12) {
16612
+ async function ctrUnderperformersReport(input, dependencies = defaultDependencies$13) {
16613
16613
  const minImpressions = integerOption$2({
16614
16614
  value: input.minImpressions,
16615
16615
  fallback: 200,
@@ -16631,7 +16631,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
16631
16631
  dimensions: ["query", "page"],
16632
16632
  type: "web",
16633
16633
  dataState: "final",
16634
- maxRows: MAX_GSC_ROWS$4
16634
+ maxRows: MAX_GSC_ROWS$5
16635
16635
  }, { refresh: input.refresh });
16636
16636
  const { items, totalClickShortfall, returnedClickShortfall, selection } = analyzeCtrUnderperformersFromRows({
16637
16637
  rows: source.rows,
@@ -16642,7 +16642,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
16642
16642
  includeBrand: input.includeBrand
16643
16643
  });
16644
16644
  const top = items[0];
16645
- const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$4;
16645
+ const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$5;
16646
16646
  const partialValidation = selection.invalidRows > 0;
16647
16647
  const dataStatus = selection.sourceRows === 0 ? "empty" : possiblyTruncated || partialValidation ? "partial" : selection.eligibleUnderperformers === 0 ? "filtered" : "complete";
16648
16648
  const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
@@ -16659,7 +16659,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
16659
16659
  dataState: "final",
16660
16660
  rowsFetched: source.rowsFetched,
16661
16661
  calls: source.calls,
16662
- maxRows: MAX_GSC_ROWS$4,
16662
+ maxRows: MAX_GSC_ROWS$5,
16663
16663
  possiblyTruncated,
16664
16664
  completeness,
16665
16665
  validation: {
@@ -16689,7 +16689,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
16689
16689
  "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.",
16690
16690
  ...selection.invalidRows ? [`${selection.invalidRows} invalid provider ${plural$3(selection.invalidRows, "row")} ${selection.invalidRows === 1 ? "was" : "were"} excluded, so this report is partial.`] : [],
16691
16691
  ...selection.duplicateRows ? [`${selection.duplicateRows} repeated query/page ${plural$3(selection.duplicateRows, "row")} ${selection.duplicateRows === 1 ? "was" : "were"} aggregated before CTR and average position were calculated.`] : [],
16692
- ...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$4} was reached, so lower-ranked source rows may be absent.`] : [],
16692
+ ...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$5} was reached, so lower-ranked source rows may be absent.`] : [],
16693
16693
  ...selection.limitedUnderperformers ? [`${selection.limitedUnderperformers} eligible ${plural$3(selection.limitedUnderperformers, "underperformer")} ${selection.limitedUnderperformers === 1 ? "was" : "were"} omitted by the output limit.`] : []
16694
16694
  ],
16695
16695
  recommendations: top ? [
@@ -17202,11 +17202,11 @@ function unavailableSegmentImpactReport(input) {
17202
17202
  //#endregion
17203
17203
  //#region packages/core/dist/analyze/segment-impact.js
17204
17204
  const DEFAULT_MAX_ROWS = 1e5;
17205
- const defaultDependencies$11 = {
17205
+ const defaultDependencies$12 = {
17206
17206
  searchAnalytics: querySearchAnalytics,
17207
17207
  now: () => /* @__PURE__ */ new Date()
17208
17208
  };
17209
- async function segmentImpact(input, dependencies = defaultDependencies$11) {
17209
+ async function segmentImpact(input, dependencies = defaultDependencies$12) {
17210
17210
  validateSegmentSite(input.site);
17211
17211
  const now = dependencies.now();
17212
17212
  const ranges = resolveSegmentRanges({
@@ -17759,8 +17759,8 @@ function analyzeCannibalRows(input) {
17759
17759
  }
17760
17760
  //#endregion
17761
17761
  //#region packages/core/dist/analyze/site-diagnostics/cannibal.js
17762
- const MAX_GSC_ROWS$3 = 1e5;
17763
- const defaultDependencies$10 = {
17762
+ const MAX_GSC_ROWS$4 = 1e5;
17763
+ const defaultDependencies$11 = {
17764
17764
  searchAnalytics: querySearchAnalytics,
17765
17765
  now: () => /* @__PURE__ */ new Date()
17766
17766
  };
@@ -17773,7 +17773,7 @@ function verdict$4(input) {
17773
17773
  const result = `${input.returned} of ${input.eligible} multi-URL query candidate${input.eligible === 1 ? "" : "s"} returned for intent and technical review.`;
17774
17774
  return input.status === "partial" ? `${result} Provider evidence is partial.` : result;
17775
17775
  }
17776
- async function cannibalReport(input, dependencies = defaultDependencies$10) {
17776
+ async function cannibalReport(input, dependencies = defaultDependencies$11) {
17777
17777
  const days = integerOption$2({
17778
17778
  value: input.days,
17779
17779
  fallback: 28,
@@ -17803,7 +17803,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
17803
17803
  ...range,
17804
17804
  type: "web",
17805
17805
  dataState: "final",
17806
- maxRows: MAX_GSC_ROWS$3
17806
+ maxRows: MAX_GSC_ROWS$4
17807
17807
  };
17808
17808
  const [pageExposure, propertyDemand] = await Promise.all([dependencies.searchAnalytics(input.site, {
17809
17809
  ...requestBase,
@@ -17826,8 +17826,8 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
17826
17826
  brandTerms: input.brandTerms,
17827
17827
  includeBrand: input.includeBrand
17828
17828
  });
17829
- const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$3;
17830
- const propertyTruncated = propertyDemand.rowsFetched >= MAX_GSC_ROWS$3;
17829
+ const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$4;
17830
+ const propertyTruncated = propertyDemand.rowsFetched >= MAX_GSC_ROWS$4;
17831
17831
  const possiblyTruncated = pageTruncated || propertyTruncated;
17832
17832
  const partialValidation = analysis.selection.invalidRows + analysis.selection.propertyInvalidRows > 0;
17833
17833
  const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
@@ -17852,7 +17852,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
17852
17852
  invalidRows: analysis.selection.invalidRows
17853
17853
  },
17854
17854
  calls: pageExposure.calls,
17855
- maxRows: MAX_GSC_ROWS$3,
17855
+ maxRows: MAX_GSC_ROWS$4,
17856
17856
  possiblyTruncated: pageTruncated
17857
17857
  },
17858
17858
  propertyDemand: {
@@ -17864,7 +17864,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
17864
17864
  invalidRows: analysis.selection.propertyInvalidRows
17865
17865
  },
17866
17866
  calls: propertyDemand.calls,
17867
- maxRows: MAX_GSC_ROWS$3,
17867
+ maxRows: MAX_GSC_ROWS$4,
17868
17868
  possiblyTruncated: propertyTruncated
17869
17869
  },
17870
17870
  completeness
@@ -18172,8 +18172,8 @@ function analyzeDecay(input) {
18172
18172
  }
18173
18173
  //#endregion
18174
18174
  //#region packages/core/dist/analyze/site-diagnostics/decaying.js
18175
- const MAX_GSC_ROWS$2 = 1e5;
18176
- const defaultDependencies$9 = {
18175
+ const MAX_GSC_ROWS$3 = 1e5;
18176
+ const defaultDependencies$10 = {
18177
18177
  searchAnalytics: querySearchAnalytics,
18178
18178
  now: () => /* @__PURE__ */ new Date()
18179
18179
  };
@@ -18184,7 +18184,7 @@ function verdict$3(report) {
18184
18184
  if (!report.selection.eligibleRows) return "No observed retained query/page declines matched the report filters.";
18185
18185
  return `${report.selection.returnedRows} of ${report.selection.eligibleRows} observed retained query/page declines returned for review.`;
18186
18186
  }
18187
- async function decayingReport(input, dependencies = defaultDependencies$9) {
18187
+ async function decayingReport(input, dependencies = defaultDependencies$10) {
18188
18188
  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.");
18189
18189
  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.");
18190
18190
  const days = integerOption$2({
@@ -18237,7 +18237,7 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
18237
18237
  aggregationType: "auto",
18238
18238
  type: "web",
18239
18239
  dataState: "final",
18240
- maxRows: MAX_GSC_ROWS$2
18240
+ maxRows: MAX_GSC_ROWS$3
18241
18241
  };
18242
18242
  const [current, previous] = await Promise.all([dependencies.searchAnalytics(input.site, {
18243
18243
  ...request,
@@ -18260,8 +18260,8 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
18260
18260
  brandTerms: input.brandTerms,
18261
18261
  includeBrand: input.includeBrand
18262
18262
  });
18263
- const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$2;
18264
- const previousTruncated = previous.rowsFetched >= MAX_GSC_ROWS$2;
18263
+ const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$3;
18264
+ const previousTruncated = previous.rowsFetched >= MAX_GSC_ROWS$3;
18265
18265
  const possiblyTruncated = currentTruncated || previousTruncated;
18266
18266
  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";
18267
18267
  const reportBase = {
@@ -18288,13 +18288,13 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
18288
18288
  current: {
18289
18289
  rowsFetched: current.rowsFetched,
18290
18290
  calls: current.calls,
18291
- maxRows: MAX_GSC_ROWS$2,
18291
+ maxRows: MAX_GSC_ROWS$3,
18292
18292
  possiblyTruncated: currentTruncated
18293
18293
  },
18294
18294
  previous: {
18295
18295
  rowsFetched: previous.rowsFetched,
18296
18296
  calls: previous.calls,
18297
- maxRows: MAX_GSC_ROWS$2,
18297
+ maxRows: MAX_GSC_ROWS$3,
18298
18298
  possiblyTruncated: previousTruncated
18299
18299
  },
18300
18300
  completeness: possiblyTruncated ? "possibly-truncated" : "retained-query-rows-only"
@@ -18670,12 +18670,12 @@ function quickWinTemplateSummaries(items) {
18670
18670
  }
18671
18671
  //#endregion
18672
18672
  //#region packages/core/dist/analyze/site-diagnostics/quick-wins.js
18673
- const DEFAULT_DAYS$2 = 28;
18674
- const MAX_DAYS$2 = 548;
18673
+ const DEFAULT_DAYS$3 = 28;
18674
+ const MAX_DAYS$3 = 548;
18675
18675
  const DEFAULT_VERIFY_LIMIT$1 = 5;
18676
18676
  const MAX_VERIFY_LIMIT = 100;
18677
18677
  const MAX_SOURCE_ROWS$3 = 1e5;
18678
- const defaultDependencies$8 = {
18678
+ const defaultDependencies$9 = {
18679
18679
  searchAnalytics: querySearchAnalytics,
18680
18680
  fetch: fetchPage,
18681
18681
  extract: extractPage,
@@ -18694,13 +18694,13 @@ function applyVerification$1(item, coverage) {
18694
18694
  evidenceRef: `${item.recommendation.evidenceRef} ${coverage.summary}`
18695
18695
  };
18696
18696
  }
18697
- async function quickWinsReport(input, dependencies = defaultDependencies$8) {
18698
- const explicitRange = explicitDateRange(input, MAX_DAYS$2);
18697
+ async function quickWinsReport(input, dependencies = defaultDependencies$9) {
18698
+ const explicitRange = explicitDateRange(input, MAX_DAYS$3);
18699
18699
  const days = explicitRange?.days ?? integerOption$2({
18700
18700
  value: input.days,
18701
- fallback: DEFAULT_DAYS$2,
18701
+ fallback: DEFAULT_DAYS$3,
18702
18702
  minimum: 1,
18703
- maximum: MAX_DAYS$2,
18703
+ maximum: MAX_DAYS$3,
18704
18704
  label: "Days"
18705
18705
  });
18706
18706
  const verifyLimit = integerOption$2({
@@ -19067,11 +19067,11 @@ function analyzeStrikingDistanceRows(input) {
19067
19067
  }
19068
19068
  //#endregion
19069
19069
  //#region packages/core/dist/analyze/striking-distance.js
19070
- const DEFAULT_DAYS$1 = 28;
19071
- const MAX_DAYS$1 = 548;
19070
+ const DEFAULT_DAYS$2 = 28;
19071
+ const MAX_DAYS$2 = 548;
19072
19072
  const DEFAULT_VERIFY_LIMIT = 5;
19073
19073
  const MAX_SOURCE_ROWS$2 = 1e5;
19074
- const defaultDependencies$7 = {
19074
+ const defaultDependencies$8 = {
19075
19075
  searchAnalytics: querySearchAnalytics,
19076
19076
  verifyContent: verifyQueryContent,
19077
19077
  now: () => /* @__PURE__ */ new Date()
@@ -19081,8 +19081,8 @@ function boundedInteger$3(value, fallback, minimum, maximum) {
19081
19081
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
19082
19082
  }
19083
19083
  function reportDays(value) {
19084
- if (value === void 0) return DEFAULT_DAYS$1;
19085
- 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}.`);
19084
+ if (value === void 0) return DEFAULT_DAYS$2;
19085
+ if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS$2) throw new SeoError("INVALID_INPUT", `Days must be a whole number between 1 and ${MAX_DAYS$2}.`);
19086
19086
  return value;
19087
19087
  }
19088
19088
  function dateRange(days, now) {
@@ -19121,7 +19121,7 @@ function verdict$2(analysis) {
19121
19121
  if (analysis.dataStatus === "filtered") return "No retained query/page rows met the report filters.";
19122
19122
  return `${countLabel(analysis.summary.eligibleRows, "eligible query/page row")} found; ${countLabel(analysis.summary.returnedRows, "row")} returned for review.`;
19123
19123
  }
19124
- async function strikingDistance(input, dependencies = defaultDependencies$7) {
19124
+ async function strikingDistance(input, dependencies = defaultDependencies$8) {
19125
19125
  const days = reportDays(input.days);
19126
19126
  const generatedAt = dependencies.now().toISOString();
19127
19127
  const range = dateRange(days, new Date(generatedAt));
@@ -19968,7 +19968,7 @@ function emptyQuickWins(input) {
19968
19968
  warnings: []
19969
19969
  };
19970
19970
  }
19971
- const defaultDependencies$6 = {
19971
+ const defaultDependencies$7 = {
19972
19972
  trafficAnomaly,
19973
19973
  updateCorrelation,
19974
19974
  segmentImpact,
@@ -19979,7 +19979,7 @@ const defaultDependencies$6 = {
19979
19979
  };
19980
19980
  async function diagnoseProperty(input, dependencies = {}) {
19981
19981
  const providers = {
19982
- ...defaultDependencies$6,
19982
+ ...defaultDependencies$7,
19983
19983
  ...dependencies
19984
19984
  };
19985
19985
  const limit = input.limit ?? 10;
@@ -20843,6 +20843,42 @@ async function providerRequestJson(input) {
20843
20843
  return parsed.data;
20844
20844
  }
20845
20845
  //#endregion
20846
+ //#region packages/core/dist/providers/dataforseo/account-pricing.js
20847
+ function unitPrice(components) {
20848
+ const prices = components ?? [];
20849
+ const total = (type) => {
20850
+ const matching = prices.filter((item) => item.cost_type === type);
20851
+ return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
20852
+ };
20853
+ return {
20854
+ perRequestMicros: total("per_request"),
20855
+ perResultMicros: total("per_result")
20856
+ };
20857
+ }
20858
+ function dataForSeoAccountPrices(account) {
20859
+ return {
20860
+ keywordOverviewPrice: unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal),
20861
+ keywordDiscoveryPrices: {
20862
+ ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
20863
+ related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
20864
+ suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
20865
+ },
20866
+ domainResearchPrices: {
20867
+ domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
20868
+ rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
20869
+ rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
20870
+ serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
20871
+ },
20872
+ linkPrices: {
20873
+ summary: unitPrice(account.price?.backlinks?.summary?.live?.priority_normal),
20874
+ backlinks: unitPrice(account.price?.backlinks?.backlinks?.live?.priority_normal),
20875
+ referringDomains: unitPrice(account.price?.backlinks?.referring_domains?.live?.priority_normal)
20876
+ },
20877
+ serpLiveAdvancedPrice: unitPrice(account.price?.serp?.live?.advanced?.priority_normal),
20878
+ serpTaskPostPrice: unitPrice(account.price?.serp?.task_post?.priority_normal)
20879
+ };
20880
+ }
20881
+ //#endregion
20846
20882
  //#region packages/core/dist/providers/dataforseo/account-schema.js
20847
20883
  const userDataMoneySchema = z.object({
20848
20884
  total: z.number().finite().nonnegative().optional(),
@@ -20860,6 +20896,11 @@ const priceComponentSchema = z.object({
20860
20896
  const normalPriorityPriceSchema = z.object({ priority_normal: z.array(priceComponentSchema).max(20).optional() }).passthrough();
20861
20897
  const livePriceSchema = z.object({ live: normalPriorityPriceSchema.optional() }).passthrough();
20862
20898
  const userDataPriceSchema = z.object({
20899
+ backlinks: z.object({
20900
+ summary: livePriceSchema.optional(),
20901
+ backlinks: livePriceSchema.optional(),
20902
+ referring_domains: livePriceSchema.optional()
20903
+ }).passthrough().optional(),
20863
20904
  dataforseo_labs: z.object({
20864
20905
  keyword_overview: livePriceSchema.optional(),
20865
20906
  keyword_ideas: livePriceSchema.optional(),
@@ -20917,7 +20958,7 @@ function discoveryNextCursor(response) {
20917
20958
  //#region packages/core/dist/providers/dataforseo/schema.js
20918
20959
  const MAX_PROVIDER_MONTHLY_SEARCH_ROWS = 120;
20919
20960
  const nullableNumber$1 = z.number().finite().nullable().optional();
20920
- const nullableCount$1 = z.union([z.number().finite(), z.string().trim().min(1)]).nullable().optional();
20961
+ const nullableCount$2 = z.union([z.number().finite(), z.string().trim().min(1)]).nullable().optional();
20921
20962
  const monthlySearchSchema$1 = z.object({
20922
20963
  year: z.number().int(),
20923
20964
  month: z.number().int(),
@@ -20933,7 +20974,7 @@ const keywordOverviewItemSchema = z.object({
20933
20974
  last_updated_time: z.string().trim().min(1).nullable().optional()
20934
20975
  }).nullish(),
20935
20976
  keyword_properties: z.object({ keyword_difficulty: nullableNumber$1 }).nullish(),
20936
- serp_info: z.object({ se_results_count: nullableCount$1 }).nullish(),
20977
+ serp_info: z.object({ se_results_count: nullableCount$2 }).nullish(),
20937
20978
  search_intent_info: z.object({ main_intent: z.enum([
20938
20979
  "informational",
20939
20980
  "navigational",
@@ -21006,37 +21047,37 @@ const dataForSeoDiscoveryResponseSchema = z.object({
21006
21047
  //#region packages/core/dist/providers/dataforseo/domain-schema.js
21007
21048
  const nullableNumber = z.number().finite().nullable().optional();
21008
21049
  const nullableNonnegativeNumber = z.number().finite().nonnegative().nullable().optional();
21009
- const nullableCount = z.number().int().nonnegative().nullable().optional();
21050
+ const nullableCount$1 = z.number().int().nonnegative().nullable().optional();
21010
21051
  const rankingMetricsSchema = z.object({
21011
- pos_1: nullableCount,
21012
- pos_2_3: nullableCount,
21013
- pos_4_10: nullableCount,
21014
- pos_11_20: nullableCount,
21015
- pos_21_30: nullableCount,
21016
- pos_31_40: nullableCount,
21017
- pos_41_50: nullableCount,
21018
- pos_51_60: nullableCount,
21019
- pos_61_70: nullableCount,
21020
- pos_71_80: nullableCount,
21021
- pos_81_90: nullableCount,
21022
- pos_91_100: nullableCount,
21052
+ pos_1: nullableCount$1,
21053
+ pos_2_3: nullableCount$1,
21054
+ pos_4_10: nullableCount$1,
21055
+ pos_11_20: nullableCount$1,
21056
+ pos_21_30: nullableCount$1,
21057
+ pos_31_40: nullableCount$1,
21058
+ pos_41_50: nullableCount$1,
21059
+ pos_51_60: nullableCount$1,
21060
+ pos_61_70: nullableCount$1,
21061
+ pos_71_80: nullableCount$1,
21062
+ pos_81_90: nullableCount$1,
21063
+ pos_91_100: nullableCount$1,
21023
21064
  etv: nullableNonnegativeNumber,
21024
- count: nullableCount,
21065
+ count: nullableCount$1,
21025
21066
  estimated_paid_traffic_cost: nullableNonnegativeNumber,
21026
- is_new: nullableCount,
21027
- is_up: nullableCount,
21028
- is_down: nullableCount,
21029
- is_lost: nullableCount
21067
+ is_new: nullableCount$1,
21068
+ is_up: nullableCount$1,
21069
+ is_down: nullableCount$1,
21070
+ is_lost: nullableCount$1
21030
21071
  }).passthrough();
21031
21072
  const metricsSchema = z.object({ organic: rankingMetricsSchema.nullable().optional() }).passthrough();
21032
- const paidResponseSchema = z.object({
21073
+ const paidResponseSchema$1 = z.object({
21033
21074
  status_code: z.number().int(),
21034
21075
  status_message: z.string().max(1e3),
21035
21076
  cost: z.number().finite().nonnegative().optional(),
21036
21077
  tasks_count: z.number().int().nonnegative(),
21037
21078
  tasks_error: z.number().int().nonnegative()
21038
21079
  }).passthrough();
21039
- const taskFields = {
21080
+ const taskFields$1 = {
21040
21081
  id: z.string().trim().min(1).max(100).optional(),
21041
21082
  status_code: z.number().int(),
21042
21083
  status_message: z.string().max(1e3),
@@ -21056,8 +21097,8 @@ const overviewResultSchema = z.object({
21056
21097
  items_count: z.number().int().nonnegative().optional(),
21057
21098
  items: z.array(overviewItemSchema).max(1e3).nullable().optional()
21058
21099
  }).passthrough();
21059
- const dataForSeoDomainOverviewResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21060
- ...taskFields,
21100
+ const dataForSeoDomainOverviewResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
21101
+ ...taskFields$1,
21061
21102
  result: z.array(overviewResultSchema).max(10).nullable().optional()
21062
21103
  }).passthrough()).max(10) });
21063
21104
  const rankedSerpItemSchema = z.object({
@@ -21077,8 +21118,8 @@ const rankedKeywordsResultSchema = z.object({
21077
21118
  items_count: z.number().int().nonnegative().optional(),
21078
21119
  items: z.array(rankedKeywordItemSchema).max(1e3).nullable().optional()
21079
21120
  }).passthrough();
21080
- const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21081
- ...taskFields,
21121
+ const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
21122
+ ...taskFields$1,
21082
21123
  result: z.array(rankedKeywordsResultSchema).max(10).nullable().optional()
21083
21124
  }).passthrough()).max(10) });
21084
21125
  const rankingPageItemSchema = z.object({
@@ -21091,8 +21132,8 @@ const rankingPagesResultSchema = z.object({
21091
21132
  items_count: z.number().int().nonnegative().optional(),
21092
21133
  items: z.array(rankingPageItemSchema).max(1e3).nullable().optional()
21093
21134
  }).passthrough();
21094
- const dataForSeoRankingPagesResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21095
- ...taskFields,
21135
+ const dataForSeoRankingPagesResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
21136
+ ...taskFields$1,
21096
21137
  result: z.array(rankingPagesResultSchema).max(10).nullable().optional()
21097
21138
  }).passthrough()).max(10) });
21098
21139
  const competitorItemSchema = z.object({
@@ -21100,9 +21141,9 @@ const competitorItemSchema = z.object({
21100
21141
  avg_position: nullableNumber,
21101
21142
  median_position: nullableNumber,
21102
21143
  etv: nullableNonnegativeNumber,
21103
- keywords_count: nullableCount,
21144
+ keywords_count: nullableCount$1,
21104
21145
  visibility: nullableNonnegativeNumber,
21105
- relevant_serp_items: nullableCount,
21146
+ relevant_serp_items: nullableCount$1,
21106
21147
  keywords_positions: z.record(z.string().trim().min(1).max(80), z.array(z.number().int().positive()).max(100)).nullable().optional()
21107
21148
  }).passthrough();
21108
21149
  const competitorsResultSchema = z.object({
@@ -21111,8 +21152,8 @@ const competitorsResultSchema = z.object({
21111
21152
  items_count: z.number().int().nonnegative().optional(),
21112
21153
  items: z.array(competitorItemSchema).max(1e3).nullable().optional()
21113
21154
  }).passthrough();
21114
- const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21115
- ...taskFields,
21155
+ const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
21156
+ ...taskFields$1,
21116
21157
  result: z.array(competitorsResultSchema).max(10).nullable().optional()
21117
21158
  }).passthrough()).max(10) });
21118
21159
  const DOMAIN_OVERVIEW_PATH = "v3/dataforseo_labs/google/domain_rank_overview/live";
@@ -21302,6 +21343,209 @@ function serpCompetitorsPaidRequest(input, ttlMs) {
21302
21343
  };
21303
21344
  }
21304
21345
  //#endregion
21346
+ //#region packages/core/dist/providers/dataforseo/link-schema.js
21347
+ const nullableCount = z.number().int().nonnegative().nullable().optional();
21348
+ const nullableScore = z.number().finite().nonnegative().nullable().optional();
21349
+ const nullableText = z.string().max(2e4).nullable().optional();
21350
+ const paidResponseSchema = z.object({
21351
+ status_code: z.number().int(),
21352
+ status_message: z.string().max(1e3),
21353
+ cost: z.number().finite().nonnegative().optional(),
21354
+ tasks_count: z.number().int().nonnegative(),
21355
+ tasks_error: z.number().int().nonnegative()
21356
+ }).passthrough();
21357
+ const taskFields = {
21358
+ id: z.string().trim().min(1).max(100).optional(),
21359
+ status_code: z.number().int(),
21360
+ status_message: z.string().max(1e3),
21361
+ cost: z.number().finite().nonnegative().optional(),
21362
+ result_count: z.number().int().nonnegative().optional()
21363
+ };
21364
+ const dataForSeoLinkSummaryItemSchema = z.object({
21365
+ target: z.string().trim().min(1).max(2048).optional(),
21366
+ rank: nullableScore,
21367
+ backlinks: nullableCount,
21368
+ referring_pages: nullableCount,
21369
+ referring_domains: nullableCount,
21370
+ broken_backlinks: nullableCount,
21371
+ broken_pages: nullableCount,
21372
+ backlinks_spam_score: nullableScore,
21373
+ info: z.object({ target_spam_score: nullableScore }).passthrough().nullable().optional()
21374
+ }).passthrough();
21375
+ const dataForSeoLinkSummaryResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21376
+ ...taskFields,
21377
+ result: z.array(dataForSeoLinkSummaryItemSchema).max(10).nullable().optional()
21378
+ }).passthrough()).max(10) });
21379
+ const dataForSeoBacklinkItemSchema = z.object({
21380
+ domain_from: nullableText,
21381
+ url_from: nullableText,
21382
+ url_to: nullableText,
21383
+ anchor: nullableText,
21384
+ item_type: z.string().max(100).nullable().optional(),
21385
+ dofollow: z.boolean().nullable().optional(),
21386
+ rank: nullableScore,
21387
+ domain_from_rank: nullableScore,
21388
+ page_from_rank: nullableScore,
21389
+ backlink_spam_score: nullableScore,
21390
+ backlinks_spam_score: nullableScore,
21391
+ first_seen: z.string().max(100).nullable().optional(),
21392
+ last_visited: z.string().max(100).nullable().optional(),
21393
+ lost_date: z.string().max(100).nullable().optional(),
21394
+ is_lost: z.boolean().nullable().optional(),
21395
+ is_indirect_link: z.boolean().nullable().optional(),
21396
+ links_count: nullableCount,
21397
+ group_count: nullableCount,
21398
+ rel_attributes: z.array(z.string().trim().min(1).max(100)).max(100).nullable().optional(),
21399
+ attributes: z.array(z.string().trim().min(1).max(100)).max(100).nullable().optional()
21400
+ }).passthrough();
21401
+ const backlinkResultSchema = z.object({
21402
+ target: z.string().trim().min(1).max(2048).optional(),
21403
+ total_count: nullableCount,
21404
+ items_count: nullableCount,
21405
+ search_after_token: z.string().max(2e4).nullable().optional(),
21406
+ items: z.array(dataForSeoBacklinkItemSchema).max(1e3).nullable().optional()
21407
+ }).passthrough();
21408
+ const dataForSeoBacklinksResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21409
+ ...taskFields,
21410
+ result: z.array(backlinkResultSchema).max(10).nullable().optional()
21411
+ }).passthrough()).max(10) });
21412
+ const dataForSeoReferringDomainItemSchema = z.object({
21413
+ domain: z.string().trim().min(1).max(2048).nullable().optional(),
21414
+ backlinks: nullableCount,
21415
+ referring_pages: nullableCount,
21416
+ broken_backlinks: nullableCount,
21417
+ broken_pages: nullableCount,
21418
+ rank: nullableScore,
21419
+ backlinks_spam_score: nullableScore,
21420
+ first_seen: z.string().max(100).nullable().optional()
21421
+ }).passthrough();
21422
+ const referringDomainsResultSchema = z.object({
21423
+ target: z.string().trim().min(1).max(2048).optional(),
21424
+ total_count: nullableCount,
21425
+ items_count: nullableCount,
21426
+ items: z.array(dataForSeoReferringDomainItemSchema).max(1e3).nullable().optional()
21427
+ }).passthrough();
21428
+ const dataForSeoReferringDomainsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21429
+ ...taskFields,
21430
+ result: z.array(referringDomainsResultSchema).max(10).nullable().optional()
21431
+ }).passthrough()).max(10) });
21432
+ const MAX_LINK_ROWS$1 = 1e3;
21433
+ const MAX_LINK_OFFSET = 2e4;
21434
+ const LINK_ENDPOINTS = {
21435
+ summary: "v3/backlinks/summary/live",
21436
+ backlinks: "v3/backlinks/backlinks/live",
21437
+ referringDomains: "v3/backlinks/referring_domains/live"
21438
+ };
21439
+ function resultRows(response) {
21440
+ return response.tasks.reduce((taskTotal, task) => taskTotal + (task.result ?? []).reduce((resultTotal, result) => resultTotal + (result.items?.length ?? 0), 0), 0);
21441
+ }
21442
+ function summaryRows(response) {
21443
+ return response.tasks.reduce((total, task) => total + (task.result?.length ?? 0), 0);
21444
+ }
21445
+ function commonRequest(input) {
21446
+ return {
21447
+ target: input.target,
21448
+ include_subdomains: input.scope === "domain" ? input.includeSubdomains : false,
21449
+ include_indirect_links: true,
21450
+ exclude_internal_backlinks: true,
21451
+ backlinks_status_type: "live",
21452
+ rank_scale: "one_hundred"
21453
+ };
21454
+ }
21455
+ function validatePage(input) {
21456
+ if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 1e3) throw new ProviderError({
21457
+ provider: "dataforseo",
21458
+ operation: input.operation,
21459
+ code: "configuration",
21460
+ message: `Link row limit must be from 1 to ${MAX_LINK_ROWS$1}.`
21461
+ });
21462
+ if (!Number.isSafeInteger(input.offset) || input.offset < 0 || input.offset > 2e4) throw new ProviderError({
21463
+ provider: "dataforseo",
21464
+ operation: input.operation,
21465
+ code: "configuration",
21466
+ message: `Link row offset must be from 0 to ${MAX_LINK_OFFSET}.`
21467
+ });
21468
+ }
21469
+ function linkSummaryPaidRequest(input, ttlMs) {
21470
+ return {
21471
+ operation: "link-summary",
21472
+ capability: "link-summary",
21473
+ endpoint: LINK_ENDPOINTS.summary,
21474
+ request: commonRequest(input),
21475
+ schema: dataForSeoLinkSummaryResponseSchema,
21476
+ requestedRows: 1,
21477
+ price: (account) => account.linkPrices.summary,
21478
+ context: input.context,
21479
+ ttlMs,
21480
+ refresh: input.refresh,
21481
+ rowCount: summaryRows
21482
+ };
21483
+ }
21484
+ function backlinksPaidRequest(input, ttlMs) {
21485
+ validatePage({
21486
+ limit: input.limit,
21487
+ offset: input.offset,
21488
+ operation: "backlinks"
21489
+ });
21490
+ if (input.orderBy.length < 1 || input.orderBy.length > 3) throw new ProviderError({
21491
+ provider: "dataforseo",
21492
+ operation: "backlinks",
21493
+ code: "configuration",
21494
+ message: "Backlink rows need from 1 to 3 sort rules."
21495
+ });
21496
+ return {
21497
+ operation: "backlinks",
21498
+ capability: "backlinks",
21499
+ endpoint: LINK_ENDPOINTS.backlinks,
21500
+ request: {
21501
+ ...commonRequest(input),
21502
+ mode: input.mode,
21503
+ backlinks_status_type: input.status,
21504
+ limit: input.limit,
21505
+ offset: input.offset,
21506
+ order_by: input.orderBy
21507
+ },
21508
+ schema: dataForSeoBacklinksResponseSchema,
21509
+ requestedRows: input.limit,
21510
+ price: (account) => account.linkPrices.backlinks,
21511
+ context: input.context,
21512
+ ttlMs,
21513
+ refresh: input.refresh,
21514
+ rowCount: resultRows
21515
+ };
21516
+ }
21517
+ function referringDomainsPaidRequest(input, ttlMs) {
21518
+ validatePage({
21519
+ limit: input.limit,
21520
+ offset: input.offset,
21521
+ operation: "referring-domains"
21522
+ });
21523
+ if (input.orderBy.length < 1 || input.orderBy.length > 3) throw new ProviderError({
21524
+ provider: "dataforseo",
21525
+ operation: "referring-domains",
21526
+ code: "configuration",
21527
+ message: "Referring-domain rows need from 1 to 3 sort rules."
21528
+ });
21529
+ return {
21530
+ operation: "referring-domains",
21531
+ capability: "referring-domains",
21532
+ endpoint: LINK_ENDPOINTS.referringDomains,
21533
+ request: {
21534
+ ...commonRequest(input),
21535
+ limit: input.limit,
21536
+ offset: input.offset,
21537
+ order_by: input.orderBy
21538
+ },
21539
+ schema: dataForSeoReferringDomainsResponseSchema,
21540
+ requestedRows: input.limit,
21541
+ price: (account) => account.linkPrices.referringDomains,
21542
+ context: input.context,
21543
+ ttlMs,
21544
+ refresh: input.refresh,
21545
+ rowCount: resultRows
21546
+ };
21547
+ }
21548
+ //#endregion
21305
21549
  //#region packages/core/dist/providers/dataforseo/paid-request.js
21306
21550
  function usdToMicros(value) {
21307
21551
  if (value === void 0) return null;
@@ -21825,41 +22069,6 @@ const MAX_KEYWORD_CHARACTERS = 80;
21825
22069
  const MAX_KEYWORD_WORDS = 10;
21826
22070
  const MAX_DISCOVERY_SEEDS$1 = 5;
21827
22071
  const MAX_DISCOVERY_ROWS$1 = 100;
21828
- function unitPrice(components) {
21829
- const prices = components ?? [];
21830
- const total = (type) => {
21831
- const matching = prices.filter((item) => item.cost_type === type);
21832
- return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
21833
- };
21834
- return {
21835
- perRequestMicros: total("per_request"),
21836
- perResultMicros: total("per_result")
21837
- };
21838
- }
21839
- function keywordOverviewPrice(account) {
21840
- return unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal);
21841
- }
21842
- function keywordDiscoveryPrices(account) {
21843
- return {
21844
- ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
21845
- related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
21846
- suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
21847
- };
21848
- }
21849
- function domainResearchPrices(account) {
21850
- return {
21851
- domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
21852
- rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
21853
- rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
21854
- serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
21855
- };
21856
- }
21857
- function serpLiveAdvancedPrice(account) {
21858
- return unitPrice(account.price?.serp?.live?.advanced?.priority_normal);
21859
- }
21860
- function serpTaskPostPrice(account) {
21861
- return unitPrice(account.price?.serp?.task_post?.priority_normal);
21862
- }
21863
22072
  function responseTaskIds(response) {
21864
22073
  return [...new Set(response.tasks.flatMap((task) => task.id ? [task.id] : []))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
21865
22074
  }
@@ -21879,6 +22088,7 @@ var DataForSeoClient = class {
21879
22088
  serpTtlMs;
21880
22089
  accountPricingTtlMs;
21881
22090
  domainResearchTtlMs;
22091
+ linkTtlMs;
21882
22092
  spendLimits;
21883
22093
  accountPricing;
21884
22094
  accountPricingRequest;
@@ -21895,6 +22105,7 @@ var DataForSeoClient = class {
21895
22105
  this.serpTtlMs = options.serpTtlMs ?? DEFAULT_SERP_TTL_MS;
21896
22106
  this.accountPricingTtlMs = options.accountPricingTtlMs ?? DEFAULT_ACCOUNT_PRICING_TTL_MS;
21897
22107
  this.domainResearchTtlMs = options.domainResearchTtlMs ?? 6048e5;
22108
+ this.linkTtlMs = options.linkTtlMs ?? 864e5;
21898
22109
  this.spendLimits = options.spendLimits;
21899
22110
  }
21900
22111
  async getCredentials(operation) {
@@ -21977,11 +22188,7 @@ var DataForSeoClient = class {
21977
22188
  accountDailySpendMicros: dataForSeoUsdToMicros(account.money?.statistics?.day?.total),
21978
22189
  accountDailySpendPeriod: account.money?.statistics?.day?.value ?? null,
21979
22190
  accountDailyLimitMicros: dataForSeoUsdToMicros(account.money?.limits?.day?.total),
21980
- keywordOverviewPrice: keywordOverviewPrice(account),
21981
- keywordDiscoveryPrices: keywordDiscoveryPrices(account),
21982
- domainResearchPrices: domainResearchPrices(account),
21983
- serpLiveAdvancedPrice: serpLiveAdvancedPrice(account),
21984
- serpTaskPostPrice: serpTaskPostPrice(account),
22191
+ ...dataForSeoAccountPrices(account),
21985
22192
  backlinksSubscriptionExpiresAt: account.backlinks_subscription_expiry_date ?? null,
21986
22193
  aiMentionsSubscriptionExpiresAt: account.llm_mentions_subscription_expiry_date ?? null,
21987
22194
  apiVersion: response.version ?? null,
@@ -22044,6 +22251,15 @@ var DataForSeoClient = class {
22044
22251
  async serpCompetitors(input) {
22045
22252
  return this.paidPost(serpCompetitorsPaidRequest(input, this.domainResearchTtlMs));
22046
22253
  }
22254
+ async linkSummary(input) {
22255
+ return this.paidPost(linkSummaryPaidRequest(input, this.linkTtlMs));
22256
+ }
22257
+ async backlinks(input) {
22258
+ return this.paidPost(backlinksPaidRequest(input, this.linkTtlMs));
22259
+ }
22260
+ async referringDomains(input) {
22261
+ return this.paidPost(referringDomainsPaidRequest(input, this.linkTtlMs));
22262
+ }
22047
22263
  async keywordOverview(input) {
22048
22264
  const keywords = input.keywords.map((keyword) => keyword.trim());
22049
22265
  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({
@@ -22387,20 +22603,20 @@ function compareCodepoints$3(left, right) {
22387
22603
  function normalizedKeyword$2(keyword) {
22388
22604
  return keyword.trim().replace(/\s+/gu, " ").toLowerCase();
22389
22605
  }
22390
- function missing$1(field) {
22606
+ function missing$2(field) {
22391
22607
  return unavailableValue("missing", `DataForSEO omitted ${field}.`);
22392
22608
  }
22393
22609
  function numericValue(values, field, valid) {
22394
22610
  const present = values.filter((value) => value !== null && value !== void 0);
22395
- if (present.length === 0) return missing$1(field);
22611
+ if (present.length === 0) return missing$2(field);
22396
22612
  const numbers = present.filter((value) => typeof value === "number" && valid(value));
22397
22613
  if (numbers.length !== present.length) return unavailableValue("invalid", `DataForSEO returned an invalid ${field}.`);
22398
22614
  const unique = [...new Set(numbers)];
22399
22615
  return unique.length === 1 ? observedValue(unique[0]) : unavailableValue("invalid", `DataForSEO returned conflicting ${field} values.`);
22400
22616
  }
22401
- function stringValue(values, field, normalize) {
22617
+ function stringValue$1(values, field, normalize) {
22402
22618
  const present = values.filter((value) => value !== null && value !== void 0);
22403
- if (present.length === 0) return missing$1(field);
22619
+ if (present.length === 0) return missing$2(field);
22404
22620
  const strings = present.flatMap((value) => {
22405
22621
  if (typeof value !== "string") return [];
22406
22622
  const normalized = normalize(value);
@@ -22412,7 +22628,7 @@ function stringValue(values, field, normalize) {
22412
22628
  }
22413
22629
  function monthlySearchesValue(rows) {
22414
22630
  const histories = rows.map((row) => row.keyword_info?.monthly_searches).filter((value) => value !== null && value !== void 0);
22415
- if (histories.length === 0) return missing$1("monthlySearches");
22631
+ if (histories.length === 0) return missing$2("monthlySearches");
22416
22632
  const byMonth = /* @__PURE__ */ new Map();
22417
22633
  let invalid = false;
22418
22634
  for (const history of histories) for (const item of history) {
@@ -22445,27 +22661,27 @@ function resultCountValues(rows) {
22445
22661
  function metricForKeyword(keyword, rows) {
22446
22662
  if (rows.length === 0) return {
22447
22663
  keyword,
22448
- monthlySearchVolume: missing$1("monthlySearchVolume"),
22449
- monthlySearches: missing$1("monthlySearches"),
22450
- searchVolumeUpdatedAt: missing$1("searchVolumeUpdatedAt"),
22451
- cpcUsd: missing$1("cpcUsd"),
22452
- paidCompetition: missing$1("paidCompetition"),
22453
- keywordDifficulty: missing$1("keywordDifficulty"),
22454
- intent: missing$1("intent"),
22455
- resultCount: missing$1("resultCount")
22664
+ monthlySearchVolume: missing$2("monthlySearchVolume"),
22665
+ monthlySearches: missing$2("monthlySearches"),
22666
+ searchVolumeUpdatedAt: missing$2("searchVolumeUpdatedAt"),
22667
+ cpcUsd: missing$2("cpcUsd"),
22668
+ paidCompetition: missing$2("paidCompetition"),
22669
+ keywordDifficulty: missing$2("keywordDifficulty"),
22670
+ intent: missing$2("intent"),
22671
+ resultCount: missing$2("resultCount")
22456
22672
  };
22457
22673
  return {
22458
22674
  keyword,
22459
22675
  monthlySearchVolume: numericValue(rows.map((row) => row.keyword_info?.search_volume), "monthlySearchVolume", (value) => Number.isSafeInteger(value) && value >= 0),
22460
22676
  monthlySearches: monthlySearchesValue(rows),
22461
- searchVolumeUpdatedAt: stringValue(rows.map((row) => row.keyword_info?.last_updated_time), "searchVolumeUpdatedAt", (value) => {
22677
+ searchVolumeUpdatedAt: stringValue$1(rows.map((row) => row.keyword_info?.last_updated_time), "searchVolumeUpdatedAt", (value) => {
22462
22678
  const timestamp = Date.parse(value);
22463
22679
  return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
22464
22680
  }),
22465
22681
  cpcUsd: numericValue(rows.map((row) => row.keyword_info?.cpc), "cpcUsd", (value) => Number.isFinite(value) && value >= 0),
22466
22682
  paidCompetition: numericValue(rows.map((row) => row.keyword_info?.competition), "paidCompetition", (value) => Number.isFinite(value) && value >= 0 && value <= 1),
22467
22683
  keywordDifficulty: numericValue(rows.map((row) => row.keyword_properties?.keyword_difficulty), "keywordDifficulty", (value) => Number.isFinite(value) && value >= 0 && value <= 100),
22468
- intent: stringValue(rows.map((row) => row.search_intent_info?.main_intent), "intent", (value) => value.trim().toLowerCase() || null),
22684
+ intent: stringValue$1(rows.map((row) => row.search_intent_info?.main_intent), "intent", (value) => value.trim().toLowerCase() || null),
22469
22685
  resultCount: numericValue(resultCountValues(rows), "resultCount", (value) => Number.isSafeInteger(value) && value >= 0)
22470
22686
  };
22471
22687
  }
@@ -22518,21 +22734,21 @@ const COMPETITOR_RESULT_TYPES = /* @__PURE__ */ new Set([
22518
22734
  "featured_snippet",
22519
22735
  "local_pack"
22520
22736
  ]);
22521
- function missing(field) {
22737
+ function missing$1(field) {
22522
22738
  return unavailableValue("missing", `DataForSEO omitted ${field}.`);
22523
22739
  }
22524
- function numberValue(value, field) {
22525
- return value === null || value === void 0 ? missing(field) : observedValue(value);
22740
+ function numberValue$1(value, field) {
22741
+ return value === null || value === void 0 ? missing$1(field) : observedValue(value);
22526
22742
  }
22527
22743
  function countValue(value, field) {
22528
- return numberValue(value, field);
22744
+ return numberValue$1(value, field);
22529
22745
  }
22530
22746
  function sumBuckets(metrics, fields, label) {
22531
22747
  const values = fields.map((field) => metrics[field]);
22532
- return values.some((value) => value === null || value === void 0) ? missing(label) : observedValue(values.reduce((sum, value) => sum + (typeof value === "number" ? value : 0), 0));
22748
+ return values.some((value) => value === null || value === void 0) ? missing$1(label) : observedValue(values.reduce((sum, value) => sum + (typeof value === "number" ? value : 0), 0));
22533
22749
  }
22534
22750
  function rankingDistribution(metrics) {
22535
- if (!metrics) return missing("organic ranking distribution");
22751
+ if (!metrics) return missing$1("organic ranking distribution");
22536
22752
  const buckets = {
22537
22753
  first: sumBuckets(metrics, ["pos_1"], "position 1 rankings"),
22538
22754
  top3: sumBuckets(metrics, ["pos_1", "pos_2_3"], "top 3 rankings"),
@@ -22583,9 +22799,9 @@ function rankingDistribution(metrics) {
22583
22799
  }
22584
22800
  function organicFootprint(metrics) {
22585
22801
  return {
22586
- estimatedMonthlyTraffic: numberValue(metrics?.etv, "estimated organic monthly traffic"),
22802
+ estimatedMonthlyTraffic: numberValue$1(metrics?.etv, "estimated organic monthly traffic"),
22587
22803
  rankedKeywords: countValue(metrics?.count, "ranked organic keywords"),
22588
- estimatedMonthlyTrafficCostUsd: numberValue(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
22804
+ estimatedMonthlyTrafficCostUsd: numberValue$1(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
22589
22805
  rankings: rankingDistribution(metrics),
22590
22806
  newRankings: countValue(metrics?.is_new, "new rankings"),
22591
22807
  improvedRankings: countValue(metrics?.is_up, "improved rankings"),
@@ -22640,7 +22856,7 @@ function researchMarket(input, operation) {
22640
22856
  });
22641
22857
  return parsed;
22642
22858
  }
22643
- function rowLimit(limit, offset = 0) {
22859
+ function rowLimit$1(limit, offset = 0) {
22644
22860
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
22645
22861
  provider: "dataforseo",
22646
22862
  operation: "domain-research",
@@ -22664,7 +22880,7 @@ function resultTypes(input, allowed) {
22664
22880
  });
22665
22881
  return normalized;
22666
22882
  }
22667
- function safeUrl$1(value) {
22883
+ function safeUrl$2(value) {
22668
22884
  if (!value) return null;
22669
22885
  try {
22670
22886
  const url = new URL(value);
@@ -22684,7 +22900,7 @@ function dedupeBy(rows, key) {
22684
22900
  }
22685
22901
  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]);
22686
22902
  }
22687
- function observedNumber(value) {
22903
+ function observedNumber$1(value) {
22688
22904
  return value.state === "observed" ? value.value : -1;
22689
22905
  }
22690
22906
  function duplicateWarning(available, retained, label) {
@@ -22695,7 +22911,7 @@ function duplicateWarning(available, retained, label) {
22695
22911
  message: `${duplicates} duplicate ${label} row${duplicates === 1 ? "" : "s"} were collapsed deterministically.`
22696
22912
  }] : [];
22697
22913
  }
22698
- function totalRows(values) {
22914
+ function totalRows$1(values) {
22699
22915
  const present = values.filter((value) => value !== null && value !== void 0);
22700
22916
  return present.length ? Math.max(...present) : null;
22701
22917
  }
@@ -22711,7 +22927,7 @@ function coverage(input) {
22711
22927
  nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
22712
22928
  };
22713
22929
  }
22714
- function requestContext(reportId, supplied) {
22930
+ function requestContext$1(reportId, supplied) {
22715
22931
  return supplied ?? {
22716
22932
  reportId,
22717
22933
  reportRunId: randomUUID()
@@ -22760,7 +22976,7 @@ async function dataForSeoDomainOverview(client, input) {
22760
22976
  languageCode: market.languageCode.split("-")[0],
22761
22977
  ...location,
22762
22978
  refresh: input.refresh,
22763
- context: requestContext("domain-overview", input.context)
22979
+ context: requestContext$1("domain-overview", input.context)
22764
22980
  });
22765
22981
  const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
22766
22982
  const matchingResult = results.find((row) => !row.target || domain(row.target) === target);
@@ -22852,7 +23068,7 @@ async function dataForSeoRankedKeywords(client, input) {
22852
23068
  const market = researchMarket(input.market, "ranked-keywords");
22853
23069
  const target = rankedTarget(input.target);
22854
23070
  const offset = input.offset ?? 0;
22855
- rowLimit(input.limit, offset);
23071
+ rowLimit$1(input.limit, offset);
22856
23072
  const types = resultTypes(input.resultTypes, RANKED_RESULT_TYPES);
22857
23073
  const excluded = excludedTerms(input);
22858
23074
  const requestedFilters = filters$1(input, excluded);
@@ -22872,7 +23088,7 @@ async function dataForSeoRankedKeywords(client, input) {
22872
23088
  limit: input.limit,
22873
23089
  offset,
22874
23090
  refresh: input.refresh,
22875
- context: requestContext("ranked-keywords", input.context)
23091
+ context: requestContext$1("ranked-keywords", input.context)
22876
23092
  });
22877
23093
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
22878
23094
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
@@ -22881,7 +23097,7 @@ async function dataForSeoRankedKeywords(client, input) {
22881
23097
  const keywordData = row.keyword_data;
22882
23098
  const serp = row.ranked_serp_element?.serp_item;
22883
23099
  const keyword = keywordData?.keyword ? normalizedKeyword$2(keywordData.keyword) : "";
22884
- const url = safeUrl$1(serp?.url);
23100
+ const url = safeUrl$2(serp?.url);
22885
23101
  if (!keywordData || !keyword || !url || !serp?.rank_group || !serp.rank_absolute || !serp.type) {
22886
23102
  invalidRows += 1;
22887
23103
  return [];
@@ -22892,11 +23108,11 @@ async function dataForSeoRankedKeywords(client, input) {
22892
23108
  rankGroup: serp.rank_group,
22893
23109
  rankAbsolute: serp.rank_absolute,
22894
23110
  resultType: serp.type,
22895
- estimatedMonthlyTraffic: numberValue(serp.etv, "estimated keyword traffic")
23111
+ estimatedMonthlyTraffic: numberValue$1(serp.etv, "estimated keyword traffic")
22896
23112
  }];
22897
23113
  });
22898
- 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));
22899
- const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
23114
+ 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));
23115
+ const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
22900
23116
  return domainEvidence({
22901
23117
  capability: "ranked-keywords",
22902
23118
  data: {
@@ -22969,7 +23185,7 @@ async function dataForSeoRankingPages(client, input) {
22969
23185
  const market = researchMarket(input.market, "ranking-pages");
22970
23186
  const target = domain(input.domain);
22971
23187
  const offset = input.offset ?? 0;
22972
- rowLimit(input.limit, offset);
23188
+ rowLimit$1(input.limit, offset);
22973
23189
  const requestedFilters = filters(input);
22974
23190
  const orderBy = ["metrics.organic.etv,desc", "page_address,asc"];
22975
23191
  const snapshot = await client.rankingPages({
@@ -22981,13 +23197,13 @@ async function dataForSeoRankingPages(client, input) {
22981
23197
  limit: input.limit,
22982
23198
  offset,
22983
23199
  refresh: input.refresh,
22984
- context: requestContext("ranking-pages", input.context)
23200
+ context: requestContext$1("ranking-pages", input.context)
22985
23201
  });
22986
23202
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
22987
23203
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
22988
23204
  let invalidRows = 0;
22989
23205
  const mappedRows = rawRows.flatMap((row) => {
22990
- const url = safeUrl$1(row.page_address);
23206
+ const url = safeUrl$2(row.page_address);
22991
23207
  if (!url) {
22992
23208
  invalidRows += 1;
22993
23209
  return [];
@@ -22997,8 +23213,8 @@ async function dataForSeoRankingPages(client, input) {
22997
23213
  organic: organicFootprint(row.metrics?.organic)
22998
23214
  }];
22999
23215
  });
23000
- const rows = dedupeBy(mappedRows, (row) => row.url).sort((left, right) => observedNumber(right.organic.estimatedMonthlyTraffic) - observedNumber(left.organic.estimatedMonthlyTraffic) || compareCodepoints$3(left.url, right.url));
23001
- const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
23216
+ 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));
23217
+ const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
23002
23218
  return domainEvidence({
23003
23219
  capability: "relevant-pages",
23004
23220
  data: {
@@ -23043,7 +23259,7 @@ function keywords(input) {
23043
23259
  async function dataForSeoSerpCompetitors(client, input) {
23044
23260
  const market = researchMarket(input.market, "serp-competitors");
23045
23261
  const offset = input.offset ?? 0;
23046
- rowLimit(input.limit, offset);
23262
+ rowLimit$1(input.limit, offset);
23047
23263
  const requestedKeywords = keywords(input.keywords);
23048
23264
  const types = resultTypes(input.resultTypes, COMPETITOR_RESULT_TYPES);
23049
23265
  const orderBy = [
@@ -23061,7 +23277,7 @@ async function dataForSeoSerpCompetitors(client, input) {
23061
23277
  limit: input.limit,
23062
23278
  offset,
23063
23279
  refresh: input.refresh,
23064
- context: requestContext("serp-competitors", input.context)
23280
+ context: requestContext$1("serp-competitors", input.context)
23065
23281
  });
23066
23282
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
23067
23283
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
@@ -23084,16 +23300,16 @@ async function dataForSeoSerpCompetitors(client, input) {
23084
23300
  return [{
23085
23301
  domain: normalizedDomain,
23086
23302
  matchedKeywords: row.keywords_count,
23087
- averagePosition: numberValue(row.avg_position, "average position"),
23088
- medianPosition: numberValue(row.median_position, "median position"),
23089
- visibility: numberValue(row.visibility, "visibility"),
23090
- estimatedMonthlyTraffic: numberValue(row.etv, "estimated monthly traffic"),
23303
+ averagePosition: numberValue$1(row.avg_position, "average position"),
23304
+ medianPosition: numberValue$1(row.median_position, "median position"),
23305
+ visibility: numberValue$1(row.visibility, "visibility"),
23306
+ estimatedMonthlyTraffic: numberValue$1(row.etv, "estimated monthly traffic"),
23091
23307
  relevantResults: countValue(row.relevant_serp_items, "relevant result count"),
23092
23308
  keywordPositions
23093
23309
  }];
23094
23310
  });
23095
- 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));
23096
- const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
23311
+ 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));
23312
+ const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
23097
23313
  return domainEvidence({
23098
23314
  capability: "serp-competitors",
23099
23315
  data: {
@@ -24008,7 +24224,7 @@ function analyzeCompetitorGap(acquisition) {
24008
24224
  }
24009
24225
  //#endregion
24010
24226
  //#region packages/core/dist/analyze/domain-research/gap-report.js
24011
- const MAX_FINDINGS = 10;
24227
+ const MAX_FINDINGS$2 = 10;
24012
24228
  const MAX_SOURCE_BRIEFS = 5;
24013
24229
  function count(candidates, classification) {
24014
24230
  return candidates.filter((item) => item.classification === classification).length;
@@ -24023,7 +24239,7 @@ function dataStatus(input) {
24023
24239
  function findings$2(input) {
24024
24240
  const result = [];
24025
24241
  for (const [index, candidate] of input.candidates.entries()) {
24026
- if (result.length >= MAX_FINDINGS) break;
24242
+ if (result.length >= MAX_FINDINGS$2) break;
24027
24243
  if (candidate.classification === "relevant-gap-candidate") result.push({
24028
24244
  code: "relevant-competitor-gap",
24029
24245
  evidenceRefs: [`candidates[${index}]`, ...candidate.evidenceRefs],
@@ -24038,7 +24254,7 @@ function findings$2(input) {
24038
24254
  });
24039
24255
  }
24040
24256
  for (const [index, pattern] of input.patterns.entries()) {
24041
- if (result.length >= MAX_FINDINGS) break;
24257
+ if (result.length >= MAX_FINDINGS$2) break;
24042
24258
  result.push({
24043
24259
  code: "competitor-template-pattern",
24044
24260
  evidenceRefs: [`repeatedCompetitorPatterns[${index}]`, ...pattern.evidenceRefs],
@@ -25085,7 +25301,7 @@ function measurementWindow(input) {
25085
25301
  }
25086
25302
  //#endregion
25087
25303
  //#region packages/core/dist/analyze/experiments/measure-change.js
25088
- const defaultDependencies$5 = {
25304
+ const defaultDependencies$6 = {
25089
25305
  searchAnalytics: querySearchAnalytics,
25090
25306
  googleAnalyticsReport: runGa4Report,
25091
25307
  contentGroup: getContentGroup,
@@ -25103,7 +25319,7 @@ function counterfactualDelta(input) {
25103
25319
  const expectedAfter = input.treatmentBefore * (input.controlAfter / input.controlBefore);
25104
25320
  return fixed(input.treatmentAfter - expectedAfter);
25105
25321
  }
25106
- async function measureChange(input, dependencies = defaultDependencies$5) {
25322
+ async function measureChange(input, dependencies = defaultDependencies$6) {
25107
25323
  const now = dependencies.now();
25108
25324
  const change = (input.id ? getChange(input.id) : void 0) ?? {
25109
25325
  id: "adhoc",
@@ -25826,14 +26042,14 @@ async function verifyInternalLinkCandidate(input) {
25826
26042
  }
25827
26043
  //#endregion
25828
26044
  //#region packages/core/dist/analyze/internal-links.js
25829
- const MAX_GSC_ROWS$1 = 1e5;
25830
- const defaultDependencies$4 = {
26045
+ const MAX_GSC_ROWS$2 = 1e5;
26046
+ const defaultDependencies$5 = {
25831
26047
  searchAnalytics: querySearchAnalytics,
25832
26048
  fetch: fetchPage,
25833
26049
  extract: extractPage,
25834
26050
  now: () => /* @__PURE__ */ new Date()
25835
26051
  };
25836
- async function internalLinksReport(input, dependencies = defaultDependencies$4) {
26052
+ async function internalLinksReport(input, dependencies = defaultDependencies$5) {
25837
26053
  const targetPage = new URL(assertUrlMatchesGscProperty(input.site, input.targetUrl));
25838
26054
  targetPage.hash = "";
25839
26055
  const targetUrl = targetPage.toString();
@@ -25882,7 +26098,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
25882
26098
  dimensions: ["query", "page"],
25883
26099
  type: "web",
25884
26100
  dataState: "final",
25885
- maxRows: MAX_GSC_ROWS$1,
26101
+ maxRows: MAX_GSC_ROWS$2,
25886
26102
  dimensionFilterGroups: [{
25887
26103
  groupType: "and",
25888
26104
  filters: [{
@@ -25914,7 +26130,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
25914
26130
  dimensions: ["query", "page"],
25915
26131
  type: "web",
25916
26132
  dataState: "final",
25917
- maxRows: MAX_GSC_ROWS$1
26133
+ maxRows: MAX_GSC_ROWS$2
25918
26134
  }, { refresh: input.refresh }) : {
25919
26135
  rows: [],
25920
26136
  calls: 0,
@@ -25959,8 +26175,8 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
25959
26175
  else checkedSources += 1;
25960
26176
  }
25961
26177
  const uncheckedCandidates = Math.max(0, analysis.candidates.length - attemptedSources);
25962
- const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$1);
25963
- const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS$1;
26178
+ const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$2);
26179
+ const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS$2;
25964
26180
  const invalidRows = analysis.selection.targetInvalidRows + analysis.selection.sourceInvalidRows;
25965
26181
  const structuredWarnings = uniqueInternalLinksWarnings(warnings);
25966
26182
  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";
@@ -26001,18 +26217,18 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
26001
26217
  pageFilter: alias,
26002
26218
  rowsFetched: result.rowsFetched,
26003
26219
  calls: result.calls,
26004
- possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$1
26220
+ possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$2
26005
26221
  })),
26006
26222
  rowsFetched: targetRequests.reduce((sum, { result }) => sum + result.rowsFetched, 0),
26007
26223
  calls: targetRequests.reduce((sum, { result }) => sum + result.calls, 0),
26008
- maxRowsPerRequest: MAX_GSC_ROWS$1,
26224
+ maxRowsPerRequest: MAX_GSC_ROWS$2,
26009
26225
  possiblyTruncated: targetTruncated
26010
26226
  },
26011
26227
  candidates: {
26012
26228
  queried: queryCandidates,
26013
26229
  rowsFetched: sourceResult.rowsFetched,
26014
26230
  calls: sourceResult.calls,
26015
- maxRows: MAX_GSC_ROWS$1,
26231
+ maxRows: MAX_GSC_ROWS$2,
26016
26232
  possiblyTruncated: sourceTruncated
26017
26233
  },
26018
26234
  completeness: !queryCandidates ? "not-queried" : targetTruncated || sourceTruncated ? "possibly-truncated" : "retained-rows-only"
@@ -26031,7 +26247,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
26031
26247
  minImpressions,
26032
26248
  resultLimit: limit,
26033
26249
  checkLimit,
26034
- maxGscRowsPerRequest: MAX_GSC_ROWS$1
26250
+ maxGscRowsPerRequest: MAX_GSC_ROWS$2
26035
26251
  },
26036
26252
  target: {
26037
26253
  requestedUrl: target.requestedUrl,
@@ -26968,8 +27184,8 @@ function analyzeSecondPageRows(input) {
26968
27184
  }
26969
27185
  //#endregion
26970
27186
  //#region packages/core/dist/analyze/keyword-opportunities.js
26971
- const DEFAULT_DAYS = 28;
26972
- const MAX_DAYS = 548;
27187
+ const DEFAULT_DAYS$1 = 28;
27188
+ const MAX_DAYS$1 = 548;
26973
27189
  const DEFAULT_REPORT_LIMIT = 10;
26974
27190
  const MAX_REPORT_LIMIT = 25;
26975
27191
  const DEFAULT_KEYWORD_LIMIT = 30;
@@ -27016,9 +27232,9 @@ function validateInput$2(input) {
27016
27232
  site,
27017
27233
  days: integerOption$1({
27018
27234
  value: input.days,
27019
- fallback: DEFAULT_DAYS,
27235
+ fallback: DEFAULT_DAYS$1,
27020
27236
  minimum: 1,
27021
- maximum: MAX_DAYS,
27237
+ maximum: MAX_DAYS$1,
27022
27238
  label: "Days"
27023
27239
  }),
27024
27240
  limit: integerOption$1({
@@ -28553,7 +28769,7 @@ function analyzeIndexCoverageSignals(input) {
28553
28769
  const MAX_SITEMAP_DOCUMENTS = 20;
28554
28770
  const MAX_SEARCH_ROWS = 25e4;
28555
28771
  const MAX_SITEMAP_URLS = 25e4;
28556
- const defaultDependencies$3 = {
28772
+ const defaultDependencies$4 = {
28557
28773
  loadCrawl: loadCrawlReport,
28558
28774
  latestCrawl: latestCrawlReport,
28559
28775
  searchAnalytics: querySearchAnalytics,
@@ -28585,7 +28801,7 @@ function sitemapCompleteness(input) {
28585
28801
  if (input.inventoryTruncated) return "truncated";
28586
28802
  return input.results.every((result) => result.dataStatus === "complete") ? "complete" : "partial";
28587
28803
  }
28588
- async function indexCoverageSignals(input, dependencies = defaultDependencies$3) {
28804
+ async function indexCoverageSignals(input, dependencies = defaultDependencies$4) {
28589
28805
  const days = integerOption$2({
28590
28806
  value: input.days,
28591
28807
  fallback: 90,
@@ -29068,7 +29284,7 @@ function pruneIndexWatchSnapshots(rootSite, url, database = getDb()) {
29068
29284
  WHERE attempts.attempt_rank > ? AND latest_success.id IS NULL
29069
29285
  )`).run(rootSite, url, rootSite, url, 20).changes;
29070
29286
  }
29071
- const defaultDependencies$2 = {
29287
+ const defaultDependencies$3 = {
29072
29288
  inspectUrl,
29073
29289
  now: () => /* @__PURE__ */ new Date(),
29074
29290
  store: {
@@ -29113,7 +29329,7 @@ function isFatalBatchError(error) {
29113
29329
  "AUTH_REQUIRED"
29114
29330
  ].includes(error.code);
29115
29331
  }
29116
- async function indexWatch(input, dependencies = defaultDependencies$2) {
29332
+ async function indexWatch(input, dependencies = defaultDependencies$3) {
29117
29333
  const languageCode = validateLanguageCode(input.languageCode);
29118
29334
  const dailyLimit = integerOption$2({
29119
29335
  value: input.dailyLimit,
@@ -31208,9 +31424,9 @@ function pseoSampleUrls(report) {
31208
31424
  }
31209
31425
  //#endregion
31210
31426
  //#region packages/core/dist/analyze/pseo/audit.js
31211
- const MAX_GSC_ROWS = 5e4;
31427
+ const MAX_GSC_ROWS$1 = 5e4;
31212
31428
  const DEFAULT_SITEMAP_URLS = 5e4;
31213
- const defaultDependencies$1 = {
31429
+ const defaultDependencies$2 = {
31214
31430
  searchAnalytics: querySearchAnalytics,
31215
31431
  fetchPage,
31216
31432
  inspectUrl,
@@ -31412,7 +31628,7 @@ async function inspectSample(input) {
31412
31628
  };
31413
31629
  }
31414
31630
  }
31415
- async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$1) {
31631
+ async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$2) {
31416
31632
  const options = pseoAuditOptions(input);
31417
31633
  const now = dependencies.now();
31418
31634
  const generatedAt = now.toISOString();
@@ -31431,7 +31647,7 @@ async function acquirePseoAuditEvidence(input, dependencies = defaultDependencie
31431
31647
  type: "web",
31432
31648
  dataState: "final",
31433
31649
  aggregationType: "auto",
31434
- maxRows: MAX_GSC_ROWS
31650
+ maxRows: MAX_GSC_ROWS$1
31435
31651
  };
31436
31652
  input.progress?.(`Fetching ${options.days} days of GSC page and query/page rows`);
31437
31653
  const [pageResponse, queryPageResponse] = await Promise.all([dependencies.searchAnalytics(input.site, {
@@ -31482,7 +31698,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
31482
31698
  minimumTemplateImpressions: options.minimumTemplateImpressions,
31483
31699
  crawlSamplesPerTemplate: options.crawlSamples,
31484
31700
  inspectionSamplesPerTemplate: options.inspectSamples,
31485
- maxRowsPerRequest: MAX_GSC_ROWS,
31701
+ maxRowsPerRequest: MAX_GSC_ROWS$1,
31486
31702
  pageRowsFetched: acquisition.pageRowsFetched,
31487
31703
  queryPageRowsFetched: acquisition.queryPageRowsFetched,
31488
31704
  sitemapsRequested: input.sitemaps?.length ?? 0,
@@ -31493,7 +31709,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
31493
31709
  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."]
31494
31710
  });
31495
31711
  }
31496
- async function pseoAuditReport(input, dependencies = defaultDependencies$1) {
31712
+ async function pseoAuditReport(input, dependencies = defaultDependencies$2) {
31497
31713
  const acquisition = await acquirePseoAuditEvidence(input, dependencies);
31498
31714
  const sampleUrls = pseoSampleUrls(buildPseoAuditFromAcquisition(input, acquisition));
31499
31715
  const topQueries = topQueryByUrl(acquisition.queryPageRows, input);
@@ -32335,7 +32551,7 @@ function pseoOpportunityVerdict(input) {
32335
32551
  //#region packages/core/dist/providers/dataforseo/serp-snapshot.js
32336
32552
  const MAX_SERP_DEPTH = 100;
32337
32553
  const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
32338
- function safeUrl(value) {
32554
+ function safeUrl$1(value) {
32339
32555
  if (!value) return null;
32340
32556
  try {
32341
32557
  const parsed = new URL(value);
@@ -32348,11 +32564,11 @@ function safeUrl(value) {
32348
32564
  }
32349
32565
  }
32350
32566
  function validOrganic(item) {
32351
- 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;
32567
+ 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;
32352
32568
  }
32353
32569
  function organicResult(item) {
32354
32570
  if (!validOrganic(item)) return null;
32355
- const url = safeUrl(item.url);
32571
+ const url = safeUrl$1(item.url);
32356
32572
  if (!url) return null;
32357
32573
  return {
32358
32574
  rankGroup: item.rank_group,
@@ -32401,7 +32617,7 @@ function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
32401
32617
  field: "checkedAt",
32402
32618
  message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
32403
32619
  });
32404
- const checkUrl = safeUrl(result.check_url);
32620
+ const checkUrl = safeUrl$1(result.check_url);
32405
32621
  if (result.check_url && !checkUrl) warnings.push({
32406
32622
  code: "invalid-serp-check-url",
32407
32623
  field: "checkUrl",
@@ -34544,7 +34760,7 @@ function groupedMappings(evidence) {
34544
34760
  //#endregion
34545
34761
  //#region packages/core/dist/analyze/second-page.js
34546
34762
  const MAX_SOURCE_ROWS = 1e5;
34547
- const defaultDependencies = {
34763
+ const defaultDependencies$1 = {
34548
34764
  searchAnalytics: querySearchAnalytics,
34549
34765
  fetch: fetchPage,
34550
34766
  extract: extractPage,
@@ -34621,7 +34837,7 @@ function reportRecommendations(items) {
34621
34837
  "Treat the priority score as a triage heuristic, not an estimated traffic lift."
34622
34838
  ]);
34623
34839
  }
34624
- async function secondPage(input, dependencies = defaultDependencies) {
34840
+ async function secondPage(input, dependencies = defaultDependencies$1) {
34625
34841
  const days = integerOption({
34626
34842
  value: input.range,
34627
34843
  fallback: 28,
@@ -36812,10 +37028,10 @@ const DEFAULT_TARGET_LIMIT = 20;
36812
37028
  const MAX_TARGET_LIMIT = 50;
36813
37029
  const DEFAULT_DETAIL_PAGES = 1;
36814
37030
  const MAX_DETAIL_PAGES = 3;
36815
- const DEFAULT_ROW_LIMIT$2 = 500;
36816
- const MAX_ROW_LIMIT$2 = 1e3;
37031
+ const DEFAULT_ROW_LIMIT$3 = 500;
37032
+ const MAX_ROW_LIMIT$3 = 1e3;
36817
37033
  const MAX_CONCURRENCY = 3;
36818
- function integer(value, fallback, max, name) {
37034
+ function integer$1(value, fallback, max, name) {
36819
37035
  const resolved = value ?? fallback;
36820
37036
  if (!Number.isInteger(resolved) || resolved < 1 || resolved > max) throw new SeoError("INVALID_INPUT", `${name} must be between 1 and ${max}.`);
36821
37037
  return resolved;
@@ -36876,9 +37092,9 @@ async function targetDetails(input) {
36876
37092
  }
36877
37093
  async function collectBingLinkEvidence(input) {
36878
37094
  if (!input.site.trim()) throw new SeoError("INVALID_INPUT", "Pass a Bing Webmaster site URL.");
36879
- const targetLimit = integer(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
36880
- const detailPages = integer(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
36881
- const rowLimit = integer(input.rowLimit, DEFAULT_ROW_LIMIT$2, MAX_ROW_LIMIT$2, "Bing link row limit");
37095
+ const targetLimit = integer$1(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
37096
+ const detailPages = integer$1(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
37097
+ const rowLimit = integer$1(input.rowLimit, DEFAULT_ROW_LIMIT$3, MAX_ROW_LIMIT$3, "Bing link row limit");
36882
37098
  const client = input.client ?? (await createBingWebmasterClient()).client;
36883
37099
  const countRows = [];
36884
37100
  let countPagesRequested = 0;
@@ -36999,6 +37215,884 @@ async function collectBingLinkEvidence(input) {
36999
37215
  };
37000
37216
  }
37001
37217
  //#endregion
37218
+ //#region packages/core/dist/links/context-sources.js
37219
+ function urlKey(value) {
37220
+ try {
37221
+ const url = new URL(value);
37222
+ if (!["http:", "https:"].includes(url.protocol)) return null;
37223
+ url.hash = "";
37224
+ return url.toString();
37225
+ } catch {
37226
+ return null;
37227
+ }
37228
+ }
37229
+ function compareTargets(left, right) {
37230
+ return right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0);
37231
+ }
37232
+ function linkTargetCounts(evidence) {
37233
+ if (evidence.targetCounts.length) return [...evidence.targetCounts].sort(compareTargets);
37234
+ const counts = /* @__PURE__ */ new Map();
37235
+ for (const row of evidence.rows) counts.set(row.targetUrl, (counts.get(row.targetUrl) ?? 0) + 1);
37236
+ return [...counts].map(([targetUrl, observedLinks]) => ({
37237
+ targetUrl,
37238
+ observedLinks
37239
+ })).sort(compareTargets);
37240
+ }
37241
+ function crawlForLinkTargets(input) {
37242
+ const values = /* @__PURE__ */ new Map();
37243
+ if (!input.report) {
37244
+ for (const target of input.targets) values.set(target.targetUrl, {
37245
+ state: "unavailable",
37246
+ reason: "No matching saved crawl was available."
37247
+ });
37248
+ return {
37249
+ values,
37250
+ matched: 0
37251
+ };
37252
+ }
37253
+ const pages = /* @__PURE__ */ new Map();
37254
+ for (const page of input.report.pages) for (const value of [page.url, page.finalUrl]) {
37255
+ const key = urlKey(value);
37256
+ if (key && !pages.has(key)) pages.set(key, page);
37257
+ }
37258
+ const issues = /* @__PURE__ */ new Map();
37259
+ for (const issue of input.report.issues) {
37260
+ const key = urlKey(issue.url);
37261
+ if (!key) continue;
37262
+ const ids = issues.get(key) ?? /* @__PURE__ */ new Set();
37263
+ ids.add(issue.ruleId);
37264
+ issues.set(key, ids);
37265
+ }
37266
+ let matched = 0;
37267
+ for (const target of input.targets) {
37268
+ const key = urlKey(target.targetUrl);
37269
+ const page = key ? pages.get(key) : void 0;
37270
+ if (!page) {
37271
+ values.set(target.targetUrl, {
37272
+ state: "not-observed",
37273
+ reason: "The target URL was not present in the retained crawl pages."
37274
+ });
37275
+ continue;
37276
+ }
37277
+ matched += 1;
37278
+ values.set(target.targetUrl, {
37279
+ state: "observed",
37280
+ reportId: input.report.id,
37281
+ observedAt: input.report.generatedAt,
37282
+ status: page.status,
37283
+ finalUrl: page.finalUrl,
37284
+ indexable: page.indexable,
37285
+ canonical: page.canonical ?? null,
37286
+ issueIds: [...issues.get(key) ?? []].sort()
37287
+ });
37288
+ }
37289
+ return {
37290
+ values,
37291
+ matched
37292
+ };
37293
+ }
37294
+ function aggregateLinkSearchRows(rows) {
37295
+ const values = /* @__PURE__ */ new Map();
37296
+ for (const row of rows) {
37297
+ const page = row.keys[0];
37298
+ const key = page ? urlKey(page) : null;
37299
+ if (!key) continue;
37300
+ const current = values.get(key) ?? {
37301
+ clicks: 0,
37302
+ impressions: 0,
37303
+ positionWeight: 0
37304
+ };
37305
+ current.clicks += row.clicks;
37306
+ current.impressions += row.impressions;
37307
+ current.positionWeight += row.position * row.impressions;
37308
+ values.set(key, current);
37309
+ }
37310
+ return values;
37311
+ }
37312
+ //#endregion
37313
+ //#region packages/core/dist/links/context-findings.js
37314
+ const MAX_FINDINGS$1 = 50;
37315
+ function findingPriority(input) {
37316
+ const searchValue = input.search.state === "observed" && (input.search.clicks > 0 || input.search.impressions >= 100);
37317
+ if (input.code === "linked-broken-target" || searchValue) return "high";
37318
+ if (input.observedLinks >= 5) return "medium";
37319
+ return "low";
37320
+ }
37321
+ function finding(input) {
37322
+ const crawl = input.row.crawl;
37323
+ if (crawl.state !== "observed") throw new Error("A link finding requires observed crawl evidence.");
37324
+ 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.` : "";
37325
+ const common = {
37326
+ priority: findingPriority({
37327
+ code: input.code,
37328
+ observedLinks: input.row.observedLinks,
37329
+ crawl,
37330
+ search: input.row.searchConsole
37331
+ }),
37332
+ heuristic: true,
37333
+ targetUrl: input.row.targetUrl,
37334
+ observedLinks: input.row.observedLinks,
37335
+ evidenceRefs: [input.row.targetUrl, crawl.reportId]
37336
+ };
37337
+ if (input.code === "linked-broken-target") return {
37338
+ ...common,
37339
+ code: input.code,
37340
+ principle: "Linked target pages should resolve intentionally.",
37341
+ evidence: `The saved crawl observed status ${crawl.status} for a target with ${input.row.observedLinks} retained representative links.${searchEvidence}`,
37342
+ 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.",
37343
+ verify: "Run redirect-trace for the target, then recrawl it and confirm a stable intended response."
37344
+ };
37345
+ if (input.code === "linked-redirect-target") return {
37346
+ ...common,
37347
+ code: input.code,
37348
+ principle: "External links should reach a stable final destination.",
37349
+ evidence: `The saved crawl observed ${input.row.targetUrl} resolving to ${crawl.finalUrl}.${searchEvidence}`,
37350
+ 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.",
37351
+ verify: "Run redirect-trace and confirm one stable hop to the intended indexable page."
37352
+ };
37353
+ if (input.code === "linked-canonical-conflict") return {
37354
+ ...common,
37355
+ code: input.code,
37356
+ principle: "Canonical hints should agree with the intended linked page.",
37357
+ evidence: `The saved crawl observed canonical ${crawl.canonical} on a linked target that resolved to ${crawl.finalUrl}.${searchEvidence}`,
37358
+ action: "Confirm the preferred URL. Align the canonical and redirect only if the current disagreement is unintended.",
37359
+ verify: "Audit the final page and inspect the declared and Google-selected canonical before changing it."
37360
+ };
37361
+ return {
37362
+ ...common,
37363
+ code: input.code,
37364
+ principle: "A linked page should be indexable when search visibility is intended.",
37365
+ evidence: `The saved crawl marked a linked target as non-indexable.${searchEvidence}`,
37366
+ 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.",
37367
+ verify: "Audit the page, then use URL Inspection when the intended state is indexable."
37368
+ };
37369
+ }
37370
+ function linkTargetFindings(rows) {
37371
+ return rows.flatMap((row) => {
37372
+ const crawl = row.crawl;
37373
+ if (crawl.state !== "observed") return [];
37374
+ if (crawl.status >= 400) return [finding({
37375
+ row,
37376
+ code: "linked-broken-target"
37377
+ })];
37378
+ if (urlKey(crawl.finalUrl) !== urlKey(row.targetUrl)) return [finding({
37379
+ row,
37380
+ code: "linked-redirect-target"
37381
+ })];
37382
+ if (crawl.canonical && urlKey(crawl.canonical) !== urlKey(crawl.finalUrl)) return [finding({
37383
+ row,
37384
+ code: "linked-canonical-conflict"
37385
+ })];
37386
+ if (!crawl.indexable) return [finding({
37387
+ row,
37388
+ code: "linked-non-indexable-target"
37389
+ })];
37390
+ return [];
37391
+ }).sort((left, right) => ({
37392
+ high: 0,
37393
+ medium: 1,
37394
+ low: 2
37395
+ })[left.priority] - {
37396
+ high: 0,
37397
+ medium: 1,
37398
+ low: 2
37399
+ }[right.priority] || right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0)).slice(0, MAX_FINDINGS$1);
37400
+ }
37401
+ //#endregion
37402
+ //#region packages/core/dist/links/context.js
37403
+ const DEFAULT_DAYS = 90;
37404
+ const MAX_DAYS = 548;
37405
+ const DEFAULT_CONTEXT_LIMIT = 100;
37406
+ const MAX_CONTEXT_LIMIT = 100;
37407
+ const MAX_GSC_ROWS = 25e3;
37408
+ const defaultDependencies = {
37409
+ latestCrawl: latestCrawlReport,
37410
+ searchAnalytics: querySearchAnalytics,
37411
+ now: () => /* @__PURE__ */ new Date()
37412
+ };
37413
+ function integer(input) {
37414
+ const value = input.value ?? input.fallback;
37415
+ if (!Number.isSafeInteger(value) || value < 1 || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be between 1 and ${input.maximum}.`);
37416
+ return value;
37417
+ }
37418
+ function searchEvidence(input) {
37419
+ if (!input.site) return {
37420
+ state: "unavailable",
37421
+ reason: "No Search Console property was selected for this report."
37422
+ };
37423
+ if (input.error) return {
37424
+ state: "unavailable",
37425
+ reason: input.error
37426
+ };
37427
+ if (!input.aggregate) return {
37428
+ state: "not-retained",
37429
+ 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."
37430
+ };
37431
+ return {
37432
+ state: "observed",
37433
+ clicks: input.aggregate.clicks,
37434
+ impressions: input.aggregate.impressions,
37435
+ ctr: input.aggregate.impressions > 0 ? input.aggregate.clicks / input.aggregate.impressions : 0,
37436
+ position: input.aggregate.impressions > 0 ? input.aggregate.positionWeight / input.aggregate.impressions : 0
37437
+ };
37438
+ }
37439
+ async function linkTargetContext(input, dependencies = defaultDependencies) {
37440
+ const days = integer({
37441
+ value: input.days,
37442
+ fallback: DEFAULT_DAYS,
37443
+ maximum: MAX_DAYS,
37444
+ label: "Link context days"
37445
+ });
37446
+ const limit = integer({
37447
+ value: input.limit,
37448
+ fallback: DEFAULT_CONTEXT_LIMIT,
37449
+ maximum: MAX_CONTEXT_LIMIT,
37450
+ label: "Link context limit"
37451
+ });
37452
+ const available = linkTargetCounts(input.evidence);
37453
+ const selected = available.slice(0, limit);
37454
+ const crawlLookup = input.crawlSite ?? input.searchConsoleSite;
37455
+ const crawl = crawlLookup ? dependencies.latestCrawl(crawlLookup) : void 0;
37456
+ const crawlValues = crawlForLinkTargets({
37457
+ report: crawl,
37458
+ targets: selected
37459
+ });
37460
+ const warnings = [];
37461
+ const range = finalGscDateRange(days, dependencies.now());
37462
+ let searchRows = /* @__PURE__ */ new Map();
37463
+ let searchCalls = 0;
37464
+ let searchRowsFetched = 0;
37465
+ let searchError = null;
37466
+ if (input.searchConsoleSite) try {
37467
+ const result = await dependencies.searchAnalytics(input.searchConsoleSite, {
37468
+ ...range,
37469
+ dimensions: ["page"],
37470
+ type: "web",
37471
+ dataState: "final",
37472
+ rowLimit: MAX_GSC_ROWS,
37473
+ maxRows: MAX_GSC_ROWS
37474
+ }, { refresh: input.refresh });
37475
+ searchRows = aggregateLinkSearchRows(result.rows);
37476
+ searchCalls = result.calls;
37477
+ searchRowsFetched = result.rowsFetched;
37478
+ } catch (error) {
37479
+ searchError = error instanceof Error ? error.message : "Search Console evidence could not be loaded.";
37480
+ warnings.push(`Search Console context was unavailable. ${searchError}`);
37481
+ }
37482
+ let searchMatches = 0;
37483
+ const retainedLimitReached = searchRowsFetched >= MAX_GSC_ROWS;
37484
+ const rows = selected.map((target) => {
37485
+ const aggregate = searchRows.get(urlKey(target.targetUrl) ?? "");
37486
+ if (aggregate) searchMatches += 1;
37487
+ return {
37488
+ ...target,
37489
+ crawl: crawlValues.values.get(target.targetUrl) ?? {
37490
+ state: "not-observed",
37491
+ reason: "The target URL was not present in the retained crawl pages."
37492
+ },
37493
+ searchConsole: searchEvidence({
37494
+ site: input.searchConsoleSite,
37495
+ error: searchError,
37496
+ retainedLimitReached,
37497
+ aggregate
37498
+ })
37499
+ };
37500
+ });
37501
+ if (!crawl) warnings.push("No matching saved crawl was available, so target response and indexability checks were skipped.");
37502
+ if (retainedLimitReached) warnings.push(`Search Console reached the ${MAX_GSC_ROWS}-row acquisition limit. Missing target rows remain not retained rather than zero.`);
37503
+ const unavailableSources = Number(!crawl) + Number(!input.searchConsoleSite);
37504
+ return {
37505
+ schemaVersion: 1,
37506
+ dataStatus: unavailableSources === 2 ? "unavailable" : unavailableSources > 0 || searchError || retainedLimitReached ? "partial" : "complete",
37507
+ selection: {
37508
+ availableTargets: available.length,
37509
+ returnedTargets: rows.length,
37510
+ omittedTargets: Math.max(0, available.length - rows.length),
37511
+ limit
37512
+ },
37513
+ provenance: {
37514
+ crawl: {
37515
+ status: crawl ? "joined" : "unavailable",
37516
+ reportId: crawl?.id ?? null,
37517
+ observedAt: crawl?.generatedAt ?? null,
37518
+ availablePages: crawl?.pages.length ?? 0,
37519
+ matchedTargets: crawlValues.matched
37520
+ },
37521
+ searchConsole: {
37522
+ status: !input.searchConsoleSite || searchError ? "unavailable" : retainedLimitReached ? "partial" : "joined",
37523
+ site: input.searchConsoleSite ?? null,
37524
+ range: input.searchConsoleSite ? {
37525
+ ...range,
37526
+ days
37527
+ } : null,
37528
+ calls: searchCalls,
37529
+ rowsFetched: searchRowsFetched,
37530
+ retainedRowLimit: MAX_GSC_ROWS,
37531
+ retainedRowLimitReached: retainedLimitReached,
37532
+ matchedTargets: searchMatches
37533
+ }
37534
+ },
37535
+ rows,
37536
+ findings: linkTargetFindings(rows),
37537
+ warnings
37538
+ };
37539
+ }
37540
+ //#endregion
37541
+ //#region packages/core/dist/providers/dataforseo/link-research-shared.js
37542
+ function linkTarget(value, requestedScope = "domain") {
37543
+ const raw = value.trim();
37544
+ if (!raw || raw.length > 2048) throw invalidTarget();
37545
+ if (requestedScope === "page") try {
37546
+ const url = new URL(raw);
37547
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error();
37548
+ url.hash = "";
37549
+ return {
37550
+ target: url.toString(),
37551
+ scope: "page"
37552
+ };
37553
+ } catch {
37554
+ throw invalidTarget();
37555
+ }
37556
+ try {
37557
+ const hostname = new URL(raw.includes("://") ? raw : `https://${raw}`).hostname.toLowerCase().replace(/^www\./u, "").replace(/\.$/u, "");
37558
+ if (!hostname || hostname.length > 253 || hostname.includes("..") || !hostname.includes(".") || !/^[a-z0-9.-]+$/u.test(hostname)) throw new Error();
37559
+ return {
37560
+ target: hostname,
37561
+ scope: "domain"
37562
+ };
37563
+ } catch {
37564
+ throw invalidTarget();
37565
+ }
37566
+ }
37567
+ function invalidTarget() {
37568
+ return new ProviderError({
37569
+ provider: "dataforseo",
37570
+ operation: "link-evidence",
37571
+ code: "configuration",
37572
+ message: "Use a valid domain, or choose page scope and pass an absolute URL."
37573
+ });
37574
+ }
37575
+ function linkRowLimit(limit, offset = 0) {
37576
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
37577
+ provider: "dataforseo",
37578
+ operation: "link-evidence",
37579
+ code: "configuration",
37580
+ message: `Link row limit must be from 1 to ${MAX_LINK_ROWS$1}.`
37581
+ });
37582
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > 2e4) throw new ProviderError({
37583
+ provider: "dataforseo",
37584
+ operation: "link-evidence",
37585
+ code: "configuration",
37586
+ message: `Link row offset must be from 0 to ${MAX_LINK_OFFSET}.`
37587
+ });
37588
+ }
37589
+ function requestContext(reportId, supplied) {
37590
+ return supplied ?? {
37591
+ reportId,
37592
+ reportRunId: randomUUID()
37593
+ };
37594
+ }
37595
+ function missing(field) {
37596
+ return unavailableValue("missing", `DataForSEO omitted ${field}.`);
37597
+ }
37598
+ function numberValue(value, field) {
37599
+ return value === null || value === void 0 ? missing(field) : observedValue(value);
37600
+ }
37601
+ function stringValue(value, field) {
37602
+ return value ? observedValue(value) : missing(field);
37603
+ }
37604
+ function metric(id, label, value) {
37605
+ return value === null || value === void 0 ? [] : [{
37606
+ provider: "dataforseo",
37607
+ id,
37608
+ label,
37609
+ value,
37610
+ scale: {
37611
+ minimum: 0,
37612
+ maximum: 100
37613
+ }
37614
+ }];
37615
+ }
37616
+ function safeUrl(value) {
37617
+ if (!value) return null;
37618
+ try {
37619
+ const url = new URL(value);
37620
+ if (!["http:", "https:"].includes(url.protocol)) return null;
37621
+ url.username = "";
37622
+ url.password = "";
37623
+ url.hash = "";
37624
+ return url.toString();
37625
+ } catch {
37626
+ return null;
37627
+ }
37628
+ }
37629
+ function normalizedDate(value) {
37630
+ if (!value) return null;
37631
+ const parsed = Date.parse(value);
37632
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
37633
+ }
37634
+ function uniqueSorted(values = []) {
37635
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort(compareCodepoints$3);
37636
+ }
37637
+ function totalRows(values) {
37638
+ const present = values.filter((value) => value !== null && value !== void 0);
37639
+ return present.length ? Math.max(...present) : null;
37640
+ }
37641
+ function linkCoverage(input) {
37642
+ const hasMore = input.providerTotalComparable !== false && input.providerTotalRows !== null ? input.offset + input.returnedRows < input.providerTotalRows : input.returnedRows >= input.requestedRows;
37643
+ return {
37644
+ requestedRows: input.requestedRows,
37645
+ returnedRows: input.returnedRows,
37646
+ retainedRows: input.retainedRows,
37647
+ invalidRows: input.invalidRows,
37648
+ providerTotalRows: input.providerTotalRows,
37649
+ completeness: input.invalidRows > 0 ? "partial" : hasMore ? "capped" : input.filtered ? "filtered" : "complete",
37650
+ nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
37651
+ };
37652
+ }
37653
+ function linkEvidence(input) {
37654
+ return {
37655
+ schemaVersion: 1,
37656
+ provider: "dataforseo",
37657
+ capability: input.capability,
37658
+ data: input.data,
37659
+ observedAt: input.snapshot.observedAt,
37660
+ market: null,
37661
+ coverage: input.coverage,
37662
+ cache: input.snapshot.cache,
37663
+ cost: input.snapshot.cost,
37664
+ request: {
37665
+ operation: input.capability,
37666
+ endpoint: input.endpoint,
37667
+ limit: input.limit,
37668
+ filters: input.filters,
37669
+ sort: input.sort
37670
+ },
37671
+ warnings: input.warnings
37672
+ };
37673
+ }
37674
+ function mappingWarnings(input) {
37675
+ return [
37676
+ ...input.snapshotWarnings,
37677
+ ...input.invalidRows ? [{
37678
+ code: `invalid-${input.label}-rows`,
37679
+ field: "data.rows",
37680
+ message: `${input.invalidRows} ${input.label} row${input.invalidRows === 1 ? "" : "s"} lacked required fields and were omitted.`
37681
+ }] : [],
37682
+ ...input.duplicateRows ? [{
37683
+ code: `duplicate-${input.label}-rows`,
37684
+ field: "data.rows",
37685
+ message: `${input.duplicateRows} duplicate ${input.label} row${input.duplicateRows === 1 ? "" : "s"} were collapsed deterministically.`
37686
+ }] : []
37687
+ ];
37688
+ }
37689
+ //#endregion
37690
+ //#region packages/core/dist/providers/dataforseo/backlinks.js
37691
+ function metricValue(row, id) {
37692
+ return row.metrics.find((item) => item.id === id)?.value ?? -1;
37693
+ }
37694
+ function compareRows(left, right) {
37695
+ 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 ?? "");
37696
+ }
37697
+ function dedupe(rows) {
37698
+ const sorted = [...rows].sort(compareRows);
37699
+ const seen = /* @__PURE__ */ new Set();
37700
+ return sorted.filter((row) => {
37701
+ const key = `${row.sourceUrl}\0${row.targetUrl}\0${row.anchorText ?? ""}`;
37702
+ if (seen.has(key)) return false;
37703
+ seen.add(key);
37704
+ return true;
37705
+ });
37706
+ }
37707
+ async function dataForSeoBacklinks(client, input) {
37708
+ const normalized = linkTarget(input.target, input.scope);
37709
+ const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
37710
+ const offset = input.offset ?? 0;
37711
+ linkRowLimit(input.limit, offset);
37712
+ const mode = input.mode ?? "representative";
37713
+ const status = input.status ?? "live";
37714
+ const providerMode = mode === "representative" ? "one_per_domain" : "as_is";
37715
+ const orderBy = [
37716
+ "rank,desc",
37717
+ "domain_from_rank,desc",
37718
+ "url_from,asc"
37719
+ ];
37720
+ const snapshot = await client.backlinks({
37721
+ ...normalized,
37722
+ includeSubdomains,
37723
+ mode: providerMode,
37724
+ status,
37725
+ limit: input.limit,
37726
+ offset,
37727
+ orderBy,
37728
+ refresh: input.refresh,
37729
+ context: requestContext("link-evidence", input.context)
37730
+ });
37731
+ const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
37732
+ const rawRows = results.flatMap((result) => result.items ?? []);
37733
+ let invalidRows = 0;
37734
+ const mapped = rawRows.flatMap((row) => {
37735
+ const sourceUrl = safeUrl(row.url_from);
37736
+ const targetUrl = safeUrl(row.url_to);
37737
+ if (!sourceUrl || !targetUrl) {
37738
+ invalidRows += 1;
37739
+ return [];
37740
+ }
37741
+ return [{
37742
+ sourceUrl,
37743
+ sourceDomain: new URL(sourceUrl).hostname.toLowerCase(),
37744
+ targetUrl,
37745
+ anchorText: row.anchor ?? null,
37746
+ linkType: row.item_type ?? null,
37747
+ dofollow: row.dofollow ?? null,
37748
+ attributes: uniqueSorted(row.rel_attributes ?? row.attributes ?? []),
37749
+ firstSeenAt: normalizedDate(row.first_seen),
37750
+ lastSeenAt: normalizedDate(row.lost_date ?? row.last_visited),
37751
+ state: row.is_lost || row.lost_date ? "lost" : "live",
37752
+ indirect: row.is_indirect_link ?? null,
37753
+ linksFromPage: row.links_count ?? null,
37754
+ linksFromDomain: row.group_count ?? null,
37755
+ metrics: [
37756
+ ...metric("backlink-rank", "DataForSEO backlink rank", row.rank),
37757
+ ...metric("source-domain-rank", "DataForSEO source domain rank", row.domain_from_rank),
37758
+ ...metric("source-page-rank", "DataForSEO source page rank", row.page_from_rank),
37759
+ ...metric("backlink-spam-score", "DataForSEO backlink spam score", row.backlink_spam_score ?? row.backlinks_spam_score)
37760
+ ]
37761
+ }];
37762
+ });
37763
+ const rows = dedupe(mapped);
37764
+ const duplicateRows = mapped.length - rows.length;
37765
+ const providerTotalRows = totalRows(results.map((result) => result.total_count));
37766
+ return linkEvidence({
37767
+ capability: "backlinks",
37768
+ data: {
37769
+ target: normalized.target,
37770
+ mode,
37771
+ rows,
37772
+ totalRows: providerTotalRows
37773
+ },
37774
+ snapshot,
37775
+ coverage: linkCoverage({
37776
+ requestedRows: input.limit,
37777
+ returnedRows: rawRows.length,
37778
+ retainedRows: rows.length,
37779
+ invalidRows,
37780
+ providerTotalRows,
37781
+ offset,
37782
+ filtered: mode === "representative" || status !== "all",
37783
+ providerTotalComparable: mode !== "representative"
37784
+ }),
37785
+ endpoint: LINK_ENDPOINTS.backlinks,
37786
+ limit: input.limit,
37787
+ filters: {
37788
+ scope: normalized.scope,
37789
+ includeSubdomains,
37790
+ mode,
37791
+ status,
37792
+ includeIndirectLinks: true,
37793
+ excludeInternalBacklinks: true,
37794
+ offset
37795
+ },
37796
+ sort: orderBy,
37797
+ warnings: mappingWarnings({
37798
+ snapshotWarnings: snapshot.warnings,
37799
+ invalidRows,
37800
+ duplicateRows,
37801
+ label: "backlink"
37802
+ })
37803
+ });
37804
+ }
37805
+ //#endregion
37806
+ //#region packages/core/dist/providers/dataforseo/link-summary.js
37807
+ async function dataForSeoLinkSummary(client, input) {
37808
+ const normalized = linkTarget(input.target, input.scope);
37809
+ const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
37810
+ const snapshot = await client.linkSummary({
37811
+ ...normalized,
37812
+ includeSubdomains,
37813
+ refresh: input.refresh,
37814
+ context: requestContext("link-evidence", input.context)
37815
+ });
37816
+ const rows = snapshot.response.tasks.flatMap((task) => task.result ?? []);
37817
+ const row = [...rows].sort((left, right) => compareCodepoints$3(left.target ?? "", right.target ?? ""))[0];
37818
+ const data = {
37819
+ target: normalized.target,
37820
+ scope: normalized.scope,
37821
+ backlinks: numberValue(row?.backlinks, "total backlinks"),
37822
+ referringDomains: numberValue(row?.referring_domains, "referring domains"),
37823
+ referringPages: numberValue(row?.referring_pages, "referring pages"),
37824
+ brokenBacklinks: numberValue(row?.broken_backlinks, "broken backlinks"),
37825
+ brokenPages: numberValue(row?.broken_pages, "broken referring pages"),
37826
+ metrics: [
37827
+ ...metric("rank", "DataForSEO rank", row?.rank),
37828
+ ...metric("backlinks-spam-score", "DataForSEO backlink spam score", row?.backlinks_spam_score),
37829
+ ...metric("target-spam-score", "DataForSEO target spam score", row?.info?.target_spam_score)
37830
+ ]
37831
+ };
37832
+ const retainedRows = row ? 1 : 0;
37833
+ return linkEvidence({
37834
+ capability: "link-summary",
37835
+ data,
37836
+ snapshot,
37837
+ coverage: {
37838
+ requestedRows: 1,
37839
+ returnedRows: rows.length,
37840
+ retainedRows,
37841
+ invalidRows: 0,
37842
+ providerTotalRows: rows.length,
37843
+ completeness: row ? "complete" : "unavailable",
37844
+ nextCursor: null
37845
+ },
37846
+ endpoint: LINK_ENDPOINTS.summary,
37847
+ limit: 1,
37848
+ filters: {
37849
+ scope: normalized.scope,
37850
+ includeSubdomains,
37851
+ status: "live",
37852
+ includeIndirectLinks: true,
37853
+ excludeInternalBacklinks: true
37854
+ },
37855
+ sort: [],
37856
+ warnings: [
37857
+ ...snapshot.warnings,
37858
+ ...rows.length > 1 ? [{
37859
+ code: "multiple-link-summary-rows",
37860
+ field: "data",
37861
+ message: "DataForSEO returned more than one summary row. One row was retained deterministically."
37862
+ }] : [],
37863
+ ...!row ? [{
37864
+ code: "missing-link-summary-row",
37865
+ field: "data",
37866
+ message: "DataForSEO returned no summary row. Counts remain unavailable rather than zero."
37867
+ }] : []
37868
+ ]
37869
+ });
37870
+ }
37871
+ //#endregion
37872
+ //#region packages/core/dist/providers/dataforseo/referring-domains.js
37873
+ function observedNumber(row) {
37874
+ return row.backlinks.state === "observed" ? row.backlinks.value : -1;
37875
+ }
37876
+ async function dataForSeoReferringDomains(client, input) {
37877
+ const normalized = linkTarget(input.target, input.scope);
37878
+ const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
37879
+ const offset = input.offset ?? 0;
37880
+ linkRowLimit(input.limit, offset);
37881
+ const orderBy = [
37882
+ "backlinks,desc",
37883
+ "rank,desc",
37884
+ "domain,asc"
37885
+ ];
37886
+ const snapshot = await client.referringDomains({
37887
+ ...normalized,
37888
+ includeSubdomains,
37889
+ limit: input.limit,
37890
+ offset,
37891
+ orderBy,
37892
+ refresh: input.refresh,
37893
+ context: requestContext("link-evidence", input.context)
37894
+ });
37895
+ const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
37896
+ const rawRows = results.flatMap((result) => result.items ?? []);
37897
+ let invalidRows = 0;
37898
+ const mapped = rawRows.flatMap((row) => {
37899
+ const domain = row.domain?.trim().toLowerCase();
37900
+ if (!domain) {
37901
+ invalidRows += 1;
37902
+ return [];
37903
+ }
37904
+ const firstSeenAt = normalizedDate(row.first_seen);
37905
+ return [{
37906
+ domain,
37907
+ backlinks: numberValue(row.backlinks, "domain backlinks"),
37908
+ referringPages: numberValue(row.referring_pages, "domain referring pages"),
37909
+ brokenBacklinks: numberValue(row.broken_backlinks, "domain broken backlinks"),
37910
+ brokenPages: numberValue(row.broken_pages, "domain broken pages"),
37911
+ firstSeenAt: stringValue(firstSeenAt, "domain first seen date"),
37912
+ 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)]
37913
+ }];
37914
+ });
37915
+ const byDomain = /* @__PURE__ */ new Map();
37916
+ 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);
37917
+ const rows = [...byDomain.values()];
37918
+ const duplicateRows = mapped.length - rows.length;
37919
+ const providerTotalRows = totalRows(results.map((result) => result.total_count));
37920
+ return linkEvidence({
37921
+ capability: "referring-domains",
37922
+ data: {
37923
+ target: normalized.target,
37924
+ rows,
37925
+ totalRows: providerTotalRows
37926
+ },
37927
+ snapshot,
37928
+ coverage: linkCoverage({
37929
+ requestedRows: input.limit,
37930
+ returnedRows: rawRows.length,
37931
+ retainedRows: rows.length,
37932
+ invalidRows,
37933
+ providerTotalRows,
37934
+ offset,
37935
+ filtered: true
37936
+ }),
37937
+ endpoint: LINK_ENDPOINTS.referringDomains,
37938
+ limit: input.limit,
37939
+ filters: {
37940
+ scope: normalized.scope,
37941
+ includeSubdomains,
37942
+ status: "live",
37943
+ includeIndirectLinks: true,
37944
+ excludeInternalBacklinks: true,
37945
+ offset
37946
+ },
37947
+ sort: orderBy,
37948
+ warnings: mappingWarnings({
37949
+ snapshotWarnings: snapshot.warnings,
37950
+ invalidRows,
37951
+ duplicateRows,
37952
+ label: "referring-domain"
37953
+ })
37954
+ });
37955
+ }
37956
+ //#endregion
37957
+ //#region packages/core/dist/providers/dataforseo/link-research.js
37958
+ var DataForSeoLinkProvider = class {
37959
+ provider = "dataforseo";
37960
+ capabilitySupport = [
37961
+ "link-summary",
37962
+ "referring-domains",
37963
+ "backlinks"
37964
+ ].map((capability) => ({
37965
+ capability,
37966
+ status: "available",
37967
+ markets: "all"
37968
+ }));
37969
+ client;
37970
+ constructor(options = {}) {
37971
+ this.client = options.client ?? new DataForSeoClient(options);
37972
+ }
37973
+ linkSummary(input) {
37974
+ return dataForSeoLinkSummary(this.client, input);
37975
+ }
37976
+ backlinks(input) {
37977
+ return dataForSeoBacklinks(this.client, input);
37978
+ }
37979
+ referringDomains(input) {
37980
+ return dataForSeoReferringDomains(this.client, input);
37981
+ }
37982
+ };
37983
+ //#endregion
37984
+ //#region packages/core/dist/links/dataforseo.js
37985
+ const DEFAULT_ROW_LIMIT$2 = 100;
37986
+ const MAX_ROW_LIMIT$2 = 500;
37987
+ function rowLimit(value) {
37988
+ const limit = value ?? DEFAULT_ROW_LIMIT$2;
37989
+ 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}.`);
37990
+ return limit;
37991
+ }
37992
+ async function collectDataForSeoLinkEvidence(input) {
37993
+ const limit = rowLimit(input.rowLimit);
37994
+ const provider = input.provider ?? new DataForSeoLinkProvider();
37995
+ const context = input.context ?? {
37996
+ reportId: "link-evidence",
37997
+ reportRunId: randomUUID()
37998
+ };
37999
+ const summary = await provider.linkSummary({
38000
+ target: input.target,
38001
+ scope: input.scope,
38002
+ includeSubdomains: input.includeSubdomains,
38003
+ refresh: input.refresh,
38004
+ context
38005
+ });
38006
+ const backlinks = await provider.backlinks({
38007
+ target: summary.data.target,
38008
+ scope: summary.data.scope,
38009
+ includeSubdomains: input.includeSubdomains,
38010
+ mode: "representative",
38011
+ status: "live",
38012
+ limit,
38013
+ refresh: input.refresh,
38014
+ context
38015
+ });
38016
+ const rows = backlinks.data.rows.map((row) => ({
38017
+ sourceUrl: row.sourceUrl,
38018
+ sourceDomain: row.sourceDomain,
38019
+ targetUrl: row.targetUrl,
38020
+ anchorText: row.anchorText ?? void 0,
38021
+ firstSeenAt: row.firstSeenAt ?? void 0,
38022
+ lastSeenAt: row.lastSeenAt ?? void 0,
38023
+ nofollow: row.dofollow === null ? void 0 : !row.dofollow,
38024
+ linkType: row.linkType ?? void 0,
38025
+ attributes: row.attributes,
38026
+ state: row.state,
38027
+ indirect: row.indirect ?? void 0,
38028
+ linksFromPage: row.linksFromPage ?? void 0,
38029
+ linksFromDomain: row.linksFromDomain ?? void 0,
38030
+ providerMetrics: row.metrics
38031
+ }));
38032
+ const counts = /* @__PURE__ */ new Map();
38033
+ for (const row of rows) counts.set(row.targetUrl, (counts.get(row.targetUrl) ?? 0) + 1);
38034
+ const targetCounts = [...counts].map(([targetUrl, observedLinks]) => ({
38035
+ targetUrl,
38036
+ observedLinks
38037
+ })).sort((left, right) => right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0));
38038
+ const completeness = backlinks.coverage.completeness;
38039
+ const representative = backlinks.data.mode === "representative";
38040
+ const partial = completeness !== "complete" || representative;
38041
+ return {
38042
+ rows,
38043
+ targetCounts,
38044
+ provenance: {
38045
+ provider: "dataforseo",
38046
+ observedAt: backlinks.observedAt > summary.observedAt ? backlinks.observedAt : summary.observedAt,
38047
+ cached: summary.cache.status === "hit" && backlinks.cache.status === "hit",
38048
+ suppliedRows: backlinks.coverage.returnedRows ?? rows.length,
38049
+ validRows: rows.length,
38050
+ invalidRows: backlinks.coverage.invalidRows,
38051
+ duplicateRows: Math.max(0, (backlinks.coverage.returnedRows ?? rows.length) - backlinks.coverage.invalidRows - rows.length),
38052
+ capped: ["capped", "partial"].includes(completeness),
38053
+ rowLimit: limit,
38054
+ completeness: partial ? "partial" : "complete",
38055
+ providerRequests: {
38056
+ methods: [summary.request.endpoint, backlinks.request.endpoint],
38057
+ maxConcurrentRequests: 1
38058
+ },
38059
+ providerCoverage: {
38060
+ targetCountRows: {
38061
+ returnedRows: targetCounts.length,
38062
+ retainedRows: targetCounts.length,
38063
+ invalidRows: 0
38064
+ },
38065
+ detailRows: {
38066
+ returnedRows: backlinks.coverage.returnedRows ?? rows.length,
38067
+ retainedRows: rows.length,
38068
+ invalidRows: backlinks.coverage.invalidRows
38069
+ },
38070
+ summaryRows: {
38071
+ returnedRows: summary.coverage.returnedRows ?? 0,
38072
+ retainedRows: summary.coverage.retainedRows ?? 0,
38073
+ invalidRows: summary.coverage.invalidRows
38074
+ },
38075
+ backlinkRows: {
38076
+ returnedRows: backlinks.coverage.returnedRows ?? rows.length,
38077
+ retainedRows: rows.length,
38078
+ invalidRows: backlinks.coverage.invalidRows,
38079
+ providerTotalRows: backlinks.coverage.providerTotalRows
38080
+ }
38081
+ }
38082
+ },
38083
+ externalProvider: {
38084
+ summary,
38085
+ backlinks
38086
+ },
38087
+ warnings: [
38088
+ ...summary.warnings.map((warning) => warning.message),
38089
+ ...backlinks.warnings.map((warning) => warning.message),
38090
+ ...representative ? ["The link list retains one representative backlink per referring domain. Provider summary counts remain separate from retained rows."] : [],
38091
+ ...!representative && partial ? ["The link list is bounded or partial. Provider summary counts remain separate from retained rows."] : []
38092
+ ]
38093
+ };
38094
+ }
38095
+ //#endregion
37002
38096
  //#region packages/core/dist/links/import.js
37003
38097
  const DEFAULT_ROW_LIMIT$1 = 1e4;
37004
38098
  const MAX_ROW_LIMIT$1 = 1e5;
@@ -37187,6 +38281,10 @@ async function importLinkEvidence(input) {
37187
38281
  //#region packages/core/dist/links/report.js
37188
38282
  const DEFAULT_OUTPUT_LIMIT = 100;
37189
38283
  const MAX_OUTPUT_LIMIT = 500;
38284
+ const MAX_TARGET_COUNTS = 100;
38285
+ const MAX_CONTEXT_ROWS = 100;
38286
+ const MAX_FINDINGS = 50;
38287
+ const MAX_STRUCTURED_DETAIL_ROWS = 750;
37190
38288
  function boundedLimit(value) {
37191
38289
  const limit = value ?? DEFAULT_OUTPUT_LIMIT;
37192
38290
  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}.`);
@@ -37198,7 +38296,7 @@ function targetCounts(rows) {
37198
38296
  return [...counts].map(([targetUrl, observedLinks]) => ({
37199
38297
  targetUrl,
37200
38298
  observedLinks
37201
- })).sort((a, b) => b.observedLinks - a.observedLinks || a.targetUrl.localeCompare(b.targetUrl, "en"));
38299
+ })).sort((a, b) => b.observedLinks - a.observedLinks || (a.targetUrl < b.targetUrl ? -1 : a.targetUrl > b.targetUrl ? 1 : 0));
37202
38300
  }
37203
38301
  function linkEvidenceReport(input) {
37204
38302
  const limit = boundedLimit(input.limit);
@@ -37206,8 +38304,24 @@ function linkEvidenceReport(input) {
37206
38304
  const domains = new Set(input.evidence.rows.map((row) => row.sourceDomain));
37207
38305
  const observedTargets = targetCounts(input.evidence.rows);
37208
38306
  const counts = input.evidence.targetCounts.length ? input.evidence.targetCounts : observedTargets;
38307
+ const providerEvidence = input.evidence.externalProvider ? {
38308
+ summary: input.evidence.externalProvider.summary,
38309
+ backlinks: {
38310
+ ...input.evidence.externalProvider.backlinks,
38311
+ data: {
38312
+ target: input.evidence.externalProvider.backlinks.data.target,
38313
+ mode: input.evidence.externalProvider.backlinks.data.mode,
38314
+ totalRows: input.evidence.externalProvider.backlinks.data.totalRows,
38315
+ retainedRows: input.evidence.externalProvider.backlinks.data.rows.length
38316
+ }
38317
+ }
38318
+ } : null;
38319
+ const targetCountsOutput = counts.slice(0, MAX_TARGET_COUNTS);
38320
+ const contextRows = input.targetContext?.rows.slice(0, MAX_CONTEXT_ROWS) ?? [];
38321
+ const findings = input.targetContext?.findings.slice(0, MAX_FINDINGS) ?? [];
38322
+ const returnedDetailRows = rows.length + targetCountsOutput.length + contextRows.length + findings.length;
37209
38323
  return {
37210
- schemaVersion: 1,
38324
+ schemaVersion: 2,
37211
38325
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
37212
38326
  dataStatus: input.evidence.provenance.completeness === "complete" ? "complete" : "partial",
37213
38327
  summary: {
@@ -37216,6 +38330,8 @@ function linkEvidenceReport(input) {
37216
38330
  targetPages: new Set(input.evidence.rows.map((row) => row.targetUrl)).size,
37217
38331
  providerTargetPages: counts.length
37218
38332
  },
38333
+ providerSummary: providerEvidence?.summary.data ?? null,
38334
+ providerEvidence,
37219
38335
  provenance: input.evidence.provenance,
37220
38336
  selection: {
37221
38337
  availableRows: input.evidence.rows.length,
@@ -37223,13 +38339,45 @@ function linkEvidenceReport(input) {
37223
38339
  omittedRows: Math.max(0, input.evidence.rows.length - rows.length),
37224
38340
  limit
37225
38341
  },
37226
- targetCounts: counts.slice(0, 100),
38342
+ outputBudget: {
38343
+ unit: "structured-detail-rows",
38344
+ limit: MAX_STRUCTURED_DETAIL_ROWS,
38345
+ returned: returnedDetailRows,
38346
+ 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)),
38347
+ sections: {
38348
+ links: {
38349
+ limit: MAX_OUTPUT_LIMIT,
38350
+ returned: rows.length
38351
+ },
38352
+ targetCounts: {
38353
+ limit: MAX_TARGET_COUNTS,
38354
+ returned: targetCountsOutput.length
38355
+ },
38356
+ targetContext: {
38357
+ limit: MAX_CONTEXT_ROWS,
38358
+ returned: contextRows.length
38359
+ },
38360
+ findings: {
38361
+ limit: MAX_FINDINGS,
38362
+ returned: findings.length
38363
+ }
38364
+ }
38365
+ },
38366
+ targetCounts: targetCountsOutput,
37227
38367
  links: rows,
37228
- warnings: input.evidence.warnings,
38368
+ targetContext: input.targetContext ? {
38369
+ ...input.targetContext,
38370
+ rows: contextRows,
38371
+ findings
38372
+ } : null,
38373
+ findings,
38374
+ warnings: [...input.evidence.warnings, ...input.targetContext?.warnings ?? []],
37229
38375
  caveats: [
37230
38376
  "This is bounded provider or imported evidence, not a complete backlink index.",
37231
38377
  "A missing link is not proof that no link exists, and an observed link is not a quality or ranking verdict.",
37232
- "Referring-domain counts describe the retained rows only."
38378
+ "Referring-domain counts describe the retained rows only.",
38379
+ ...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."] : [],
38380
+ ...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."] : []
37233
38381
  ]
37234
38382
  };
37235
38383
  }
@@ -40651,9 +41799,14 @@ function registerProviderTools(server) {
40651
41799
  }
40652
41800
  });
40653
41801
  server.registerTool("seo_link_evidence", {
40654
- description: "Review bounded referring-link evidence from Bing Webmaster or a local export",
41802
+ description: "Review bounded referring-link evidence from DataForSEO, Bing Webmaster or a local export",
40655
41803
  inputSchema: {
40656
41804
  site: z$1.string().url().max(2e3).optional(),
41805
+ provider: z$1.enum(["dataforseo", "bing"]).optional(),
41806
+ target: z$1.string().trim().min(1).max(2048).optional(),
41807
+ scope: z$1.enum(["domain", "page"]).optional(),
41808
+ includeSubdomains: z$1.boolean().optional(),
41809
+ searchConsoleSite: z$1.string().trim().min(1).max(2048).optional(),
40657
41810
  file: z$1.string().min(1).max(4096).optional(),
40658
41811
  format: z$1.enum([
40659
41812
  "csv",
@@ -40663,23 +41816,43 @@ function registerProviderTools(server) {
40663
41816
  rowLimit: z$1.number().int().min(1).max(1e5).optional(),
40664
41817
  targetLimit: z$1.number().int().min(1).max(50).optional(),
40665
41818
  detailPagesPerTarget: z$1.number().int().min(1).max(3).optional(),
40666
- limit: z$1.number().int().min(1).max(500).optional()
41819
+ limit: z$1.number().int().min(1).max(500).optional(),
41820
+ days: z$1.number().int().min(1).max(548).optional(),
41821
+ refresh: z$1.boolean().optional()
40667
41822
  }
40668
- }, async ({ site, file, format, rowLimit, targetLimit, detailPagesPerTarget, limit }) => {
41823
+ }, async ({ site, provider, target, scope, includeSubdomains, searchConsoleSite, file, format, rowLimit, targetLimit, detailPagesPerTarget, limit, days, refresh }) => {
40669
41824
  try {
40670
- if (Boolean(site) === Boolean(file)) throw new SeoError("INVALID_INPUT", "Pass exactly one of site for Bing or file for a local import.");
41825
+ const liveProvider = provider ?? (target ? "dataforseo" : "bing");
41826
+ if (Number(Boolean(file)) + Number(Boolean(site)) + Number(Boolean(target)) !== 1) throw new SeoError("INVALID_INPUT", "Pass one link source: file, site for Bing, or target for DataForSEO.");
41827
+ if (file && provider) throw new SeoError("INVALID_INPUT", "Do not pass provider with a local link file.");
41828
+ if (liveProvider === "dataforseo" && !target) throw new SeoError("INVALID_INPUT", "Pass target for DataForSEO link evidence.");
41829
+ if (liveProvider === "bing" && !site && !file) throw new SeoError("INVALID_INPUT", "Pass site for Bing link evidence.");
41830
+ const evidence = file ? await importLinkEvidence({
41831
+ file,
41832
+ format,
41833
+ rowLimit
41834
+ }) : liveProvider === "dataforseo" ? await collectDataForSeoLinkEvidence({
41835
+ target: target ?? "",
41836
+ scope,
41837
+ includeSubdomains,
41838
+ rowLimit,
41839
+ refresh
41840
+ }) : await collectBingLinkEvidence({
41841
+ site: site ?? "",
41842
+ rowLimit,
41843
+ targetLimit,
41844
+ detailPagesPerTarget
41845
+ });
40671
41846
  const report = linkEvidenceReport({
40672
- evidence: file ? await importLinkEvidence({
40673
- file,
40674
- format,
40675
- rowLimit
40676
- }) : await collectBingLinkEvidence({
40677
- site: site ?? "",
40678
- rowLimit,
40679
- targetLimit,
40680
- detailPagesPerTarget
40681
- }),
40682
- limit
41847
+ evidence,
41848
+ limit,
41849
+ targetContext: searchConsoleSite || evidence.provenance.provider === "dataforseo" ? await linkTargetContext({
41850
+ evidence,
41851
+ searchConsoleSite,
41852
+ crawlSite: searchConsoleSite ?? target,
41853
+ days,
41854
+ refresh
41855
+ }) : void 0
40683
41856
  });
40684
41857
  return toolSuccess(`${report.summary.observedLinks} referring links were retained from ${report.provenance.provider}.`, report);
40685
41858
  } catch (error) {
@@ -41094,15 +42267,18 @@ const REPORT_DEPTH = {
41094
42267
  "link-evidence": {
41095
42268
  readOrder: [
41096
42269
  "dataStatus, provenance, warnings, and caveats",
41097
- "selection and summary coverage counts",
41098
- "targetCounts, then the bounded links list"
42270
+ "providerSummary and providerEvidence coverage, cache, cost, filters, and observation time when present",
42271
+ "selection, outputBudget, and retained summary counts",
42272
+ "findings and targetContext with crawl and Search Console evidence states",
42273
+ "targetCounts, then the bounded representative links list and provider-native metrics"
41099
42274
  ],
41100
42275
  doNotClaim: [
41101
- "The retained rows are not a complete backlink index.",
41102
- "Missing rows do not prove a link does not exist.",
41103
- "Observed links do not establish quality, authority, value, or ranking impact."
42276
+ "The retained rows are not a complete backlink index, and missing rows do not prove a link does not exist.",
42277
+ "Observed links do not establish quality, authority, value, or ranking impact.",
42278
+ "Provider rank and spam metrics keep their provider-specific scales. They are not search-engine ranking factors or interchangeable authority scores.",
42279
+ "A missing Search Console row is not zero, and a saved crawl finding does not prove the target still has that state."
41104
42280
  ],
41105
- verify: "Open a selected referring URL and confirm the link is still present before acting on it.",
42281
+ verify: "Open a selected referring URL to confirm the link, then check any flagged target live with redirect-trace or audit-page before changing it.",
41106
42282
  related: [
41107
42283
  {
41108
42284
  id: "link-recovery",
@@ -41115,6 +42291,10 @@ const REPORT_DEPTH = {
41115
42291
  {
41116
42292
  id: "site-crawl",
41117
42293
  reason: "Checks internal links and live pages."
42294
+ },
42295
+ {
42296
+ id: "search-performance-overview",
42297
+ reason: "Adds broader first-party search context."
41118
42298
  }
41119
42299
  ]
41120
42300
  },
@@ -42843,10 +44023,14 @@ const REPORT_GUIDANCE = {
42843
44023
  },
42844
44024
  "link-evidence": {
42845
44025
  name: "Referring link evidence",
42846
- description: "Review a bounded set of pages that link to the site from Bing Webmaster or a local export.",
42847
- useWhen: ["You want concrete referring URLs and anchor text where available.", "You need one normalized view of Bing or an exported CSV, JSON, or JSONL file."],
42848
- avoidWhen: ["You need a complete backlink index or a domain authority score.", "You want to find broken internal links during a crawl."],
42849
- outcome: "A bounded, deduplicated link list with target-page counts and explicit source coverage."
44026
+ description: "Review bounded referring-link evidence from a live provider, Bing Webmaster or a local export.",
44027
+ useWhen: [
44028
+ "You want concrete referring URLs and anchor text where available.",
44029
+ "You want to compare a live provider summary with saved crawl and Search Console evidence for linked target pages.",
44030
+ "You need one normalized view of DataForSEO, Bing or an exported CSV, JSON, or JSONL file."
44031
+ ],
44032
+ avoidWhen: ["You need a complete backlink index or a universal authority score.", "You want to find broken internal links during a crawl."],
44033
+ outcome: "Provider summary counts, representative links, target-page search and crawl context, and bounded technical findings with explicit source coverage."
42850
44034
  },
42851
44035
  "server-log-analysis": {
42852
44036
  name: "Server log crawler analysis",