seo 0.2.19 → 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 load, t as version } from "./package-Dl66IhTH.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";
@@ -115,6 +115,86 @@ function seoErrorEnvelope(error) {
115
115
  };
116
116
  }
117
117
  //#endregion
118
+ //#region packages/core/dist/version.js
119
+ const SEO_VERSION = version;
120
+ //#endregion
121
+ //#region packages/core/dist/fetch/crawler-identity.js
122
+ const SEO_CRAWLER_TOKEN = "SEO-Skill";
123
+ const SEO_CRAWLER_USER_AGENT = `${SEO_CRAWLER_TOKEN}/${SEO_VERSION} (+https://seoskill.dev)`;
124
+ const SEO_CRAWLER_IDENTITY = {
125
+ name: "SEO Skill",
126
+ robotsToken: SEO_CRAWLER_TOKEN,
127
+ version: SEO_VERSION,
128
+ userAgent: SEO_CRAWLER_USER_AGENT,
129
+ documentationUrl: "https://seoskill.dev/docs/crawler/"
130
+ };
131
+ //#endregion
132
+ //#region packages/core/dist/fetch/http-client.js
133
+ var ResponseSizeLimitError = class extends Error {
134
+ maxBytes;
135
+ constructor(maxBytes, label = "Response") {
136
+ super(`${label} exceeds the ${maxBytes}-byte response limit.`);
137
+ this.maxBytes = maxBytes;
138
+ this.name = "ResponseSizeLimitError";
139
+ }
140
+ };
141
+ function requestHeaders(profile) {
142
+ if (profile === "bot") return new Headers({
143
+ accept: "text/plain,*/*;q=0.8",
144
+ "accept-language": "en-US,en;q=0.9",
145
+ connection: "close",
146
+ "user-agent": SEO_CRAWLER_USER_AGENT
147
+ });
148
+ return new Headers({
149
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
150
+ "accept-language": "en-US,en;q=0.9",
151
+ connection: "close",
152
+ "sec-ch-ua-mobile": "?0",
153
+ "sec-ch-ua-platform": "\"macOS\"",
154
+ "upgrade-insecure-requests": "1",
155
+ "user-agent": SEO_CRAWLER_USER_AGENT
156
+ });
157
+ }
158
+ function publicHttpFetch(url, input = {}) {
159
+ const { profile = "browser", headers, ...init } = input;
160
+ const base = requestHeaders(profile);
161
+ if (headers instanceof Headers) for (const [key, value] of headers.entries()) base.set(key, value);
162
+ else if (Array.isArray(headers)) for (const [key, value] of headers) base.set(key, value);
163
+ else if (headers) {
164
+ for (const [key, value] of Object.entries(headers)) if (value !== void 0) base.set(key, value);
165
+ }
166
+ return fetch$1(url, {
167
+ ...init,
168
+ headers: base
169
+ });
170
+ }
171
+ async function readBoundedResponseText(response, maxBytes, label = "Response") {
172
+ const declaredLength = Number(response.headers.get("content-length"));
173
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
174
+ await response.body?.cancel().catch(() => void 0);
175
+ throw new ResponseSizeLimitError(maxBytes, label);
176
+ }
177
+ const reader = response.body?.getReader();
178
+ if (!reader) return "";
179
+ const chunks = [];
180
+ let total = 0;
181
+ try {
182
+ while (true) {
183
+ const { done, value } = await reader.read();
184
+ if (done) break;
185
+ total += value.byteLength;
186
+ if (total > maxBytes) {
187
+ await reader.cancel().catch(() => void 0);
188
+ throw new ResponseSizeLimitError(maxBytes, label);
189
+ }
190
+ chunks.push(value);
191
+ }
192
+ } finally {
193
+ reader.releaseLock();
194
+ }
195
+ return Buffer.concat(chunks, total).toString("utf8");
196
+ }
197
+ //#endregion
118
198
  //#region packages/core/dist/paths.js
119
199
  let memoizedPaths;
120
200
  function defaultConfigDir(input) {
@@ -1558,6 +1638,22 @@ function hashKey(parts) {
1558
1638
  return Buffer.from(json).toString("base64url");
1559
1639
  }
1560
1640
  //#endregion
1641
+ //#region packages/core/dist/ga4/account-summaries.js
1642
+ const propertySummarySchema = z.object({
1643
+ property: z.string().trim().min(1),
1644
+ displayName: z.string().optional(),
1645
+ propertyType: z.string().optional(),
1646
+ parent: z.string().optional()
1647
+ }).strip();
1648
+ z.object({
1649
+ accountSummaries: z.array(z.object({
1650
+ account: z.string().trim().min(1),
1651
+ displayName: z.string().optional(),
1652
+ propertySummaries: z.array(propertySummarySchema).optional()
1653
+ }).strip()).max(200).optional(),
1654
+ nextPageToken: z.string().optional()
1655
+ }).strip();
1656
+ //#endregion
1561
1657
  //#region packages/core/dist/ga4/client.js
1562
1658
  function ga4RequestCanUseCache(body) {
1563
1659
  return body.dateRanges.every((range) => /^\d{4}-\d{2}-\d{2}$/.test(range.startDate) && /^\d{4}-\d{2}-\d{2}$/.test(range.endDate));
@@ -1718,7 +1814,7 @@ function aiReferralSourceForValue(value) {
1718
1814
  function compareText$18(left, right) {
1719
1815
  return left < right ? -1 : left > right ? 1 : 0;
1720
1816
  }
1721
- function metric(row, name) {
1817
+ function metric$1(row, name) {
1722
1818
  const value = row[name];
1723
1819
  const parsed = Number(value);
1724
1820
  if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `Google Analytics returned an invalid ${name} value.`);
@@ -1753,8 +1849,8 @@ function analyzeAiReferralRows(input) {
1753
1849
  ...emptyMetrics()
1754
1850
  };
1755
1851
  addMetrics(total, {
1756
- sessions: metric(row, "sessions"),
1757
- eventCount: metric(row, "eventCount")
1852
+ sessions: metric$1(row, "sessions"),
1853
+ eventCount: metric$1(row, "eventCount")
1758
1854
  });
1759
1855
  total.observedSessionSources.add(row.sessionSource ?? "");
1760
1856
  sourceTotals.set(definition.id, total);
@@ -1766,8 +1862,8 @@ function analyzeAiReferralRows(input) {
1766
1862
  const definition = aiReferralSourceForValue(row.sessionSource ?? "");
1767
1863
  if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics detail rows contained an unexpected session source.");
1768
1864
  const metrics = {
1769
- sessions: metric(row, "sessions"),
1770
- eventCount: metric(row, "eventCount")
1865
+ sessions: metric$1(row, "sessions"),
1866
+ eventCount: metric$1(row, "eventCount")
1771
1867
  };
1772
1868
  addMetrics(detailTotal, metrics);
1773
1869
  const landingPage = row.landingPagePlusQueryString || "(not set)";
@@ -3410,20 +3506,6 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
3410
3506
  };
3411
3507
  }
3412
3508
  //#endregion
3413
- //#region packages/core/dist/version.js
3414
- const SEO_VERSION = version;
3415
- //#endregion
3416
- //#region packages/core/dist/fetch/crawler-identity.js
3417
- const SEO_CRAWLER_TOKEN = "SEO-Skill";
3418
- const SEO_CRAWLER_USER_AGENT = `${SEO_CRAWLER_TOKEN}/${SEO_VERSION} (+https://seoskill.dev)`;
3419
- const SEO_CRAWLER_IDENTITY = {
3420
- name: "SEO Skill",
3421
- robotsToken: SEO_CRAWLER_TOKEN,
3422
- version: SEO_VERSION,
3423
- userAgent: SEO_CRAWLER_USER_AGENT,
3424
- documentationUrl: "https://seoskill.dev/docs/crawler/"
3425
- };
3426
- //#endregion
3427
3509
  //#region packages/core/dist/fetch/access-block.js
3428
3510
  const CLOUDFLARE_SKIP_DOCS = "https://developers.cloudflare.com/waf/custom-rules/skip/";
3429
3511
  function isHeadersLike(headers) {
@@ -3495,72 +3577,6 @@ function detectAccessBlock(input) {
3495
3577
  };
3496
3578
  }
3497
3579
  //#endregion
3498
- //#region packages/core/dist/fetch/http-client.js
3499
- var ResponseSizeLimitError = class extends Error {
3500
- maxBytes;
3501
- constructor(maxBytes, label = "Response") {
3502
- super(`${label} exceeds the ${maxBytes}-byte response limit.`);
3503
- this.maxBytes = maxBytes;
3504
- this.name = "ResponseSizeLimitError";
3505
- }
3506
- };
3507
- function requestHeaders(profile) {
3508
- if (profile === "bot") return new Headers({
3509
- accept: "text/plain,*/*;q=0.8",
3510
- "accept-language": "en-US,en;q=0.9",
3511
- connection: "close",
3512
- "user-agent": SEO_CRAWLER_USER_AGENT
3513
- });
3514
- return new Headers({
3515
- accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
3516
- "accept-language": "en-US,en;q=0.9",
3517
- connection: "close",
3518
- "sec-ch-ua-mobile": "?0",
3519
- "sec-ch-ua-platform": "\"macOS\"",
3520
- "upgrade-insecure-requests": "1",
3521
- "user-agent": SEO_CRAWLER_USER_AGENT
3522
- });
3523
- }
3524
- function publicHttpFetch(url, input = {}) {
3525
- const { profile = "browser", headers, ...init } = input;
3526
- const base = requestHeaders(profile);
3527
- if (headers instanceof Headers) for (const [key, value] of headers.entries()) base.set(key, value);
3528
- else if (Array.isArray(headers)) for (const [key, value] of headers) base.set(key, value);
3529
- else if (headers) {
3530
- for (const [key, value] of Object.entries(headers)) if (value !== void 0) base.set(key, value);
3531
- }
3532
- return fetch$1(url, {
3533
- ...init,
3534
- headers: base
3535
- });
3536
- }
3537
- async function readBoundedResponseText(response, maxBytes, label = "Response") {
3538
- const declaredLength = Number(response.headers.get("content-length"));
3539
- if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
3540
- await response.body?.cancel().catch(() => void 0);
3541
- throw new ResponseSizeLimitError(maxBytes, label);
3542
- }
3543
- const reader = response.body?.getReader();
3544
- if (!reader) return "";
3545
- const chunks = [];
3546
- let total = 0;
3547
- try {
3548
- while (true) {
3549
- const { done, value } = await reader.read();
3550
- if (done) break;
3551
- total += value.byteLength;
3552
- if (total > maxBytes) {
3553
- await reader.cancel().catch(() => void 0);
3554
- throw new ResponseSizeLimitError(maxBytes, label);
3555
- }
3556
- chunks.push(value);
3557
- }
3558
- } finally {
3559
- reader.releaseLock();
3560
- }
3561
- return Buffer.concat(chunks, total).toString("utf8");
3562
- }
3563
- //#endregion
3564
3580
  //#region packages/core/dist/fetch/page-fetcher/rate-controls.js
3565
3581
  const HOST_QUEUES = /* @__PURE__ */ new Map();
3566
3582
  const HOST_HEALTH = /* @__PURE__ */ new Map();
@@ -5307,7 +5323,7 @@ function estimateSerpTitleWidth(title) {
5307
5323
  }
5308
5324
  //#endregion
5309
5325
  //#region packages/core/dist/analyze/audit-page.js
5310
- const defaultDependencies$15 = {
5326
+ const defaultDependencies$16 = {
5311
5327
  fetchPage,
5312
5328
  queryPageMetrics,
5313
5329
  now: () => /* @__PURE__ */ new Date()
@@ -5338,7 +5354,7 @@ function buildRecommendations(report) {
5338
5354
  });
5339
5355
  return recommendations;
5340
5356
  }
5341
- async function auditPage(input, dependencies = defaultDependencies$15) {
5357
+ async function auditPage(input, dependencies = defaultDependencies$16) {
5342
5358
  const fetched = await dependencies.fetchPage(input.url, {
5343
5359
  js: input.js,
5344
5360
  refresh: input.refresh
@@ -6912,7 +6928,7 @@ function pageOpportunityRecommendations(analysis) {
6912
6928
  //#endregion
6913
6929
  //#region packages/core/dist/analyze/page-opportunities.js
6914
6930
  const MAX_BENCHMARK_ROWS = 1e5;
6915
- const defaultDependencies$14 = {
6931
+ const defaultDependencies$15 = {
6916
6932
  searchAnalytics: querySearchAnalytics,
6917
6933
  fetch: fetchPage,
6918
6934
  extract: extractPage,
@@ -6984,7 +7000,7 @@ function reportCaveats$1(input) {
6984
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.` : ""
6985
7001
  ].filter(Boolean);
6986
7002
  }
6987
- async function pageOpportunitiesReport(input, dependencies = defaultDependencies$14) {
7003
+ async function pageOpportunitiesReport(input, dependencies = defaultDependencies$15) {
6988
7004
  const url = assertUrlMatchesGscProperty(input.site, input.url);
6989
7005
  const days = reportDays$1(input.days);
6990
7006
  const range = defaultDateRange$1(days);
@@ -10771,7 +10787,7 @@ function absoluteHttpUrl(value) {
10771
10787
  return false;
10772
10788
  }
10773
10789
  }
10774
- function urlKey(value) {
10790
+ function urlKey$1(value) {
10775
10791
  if (!value) return void 0;
10776
10792
  try {
10777
10793
  const url = new URL(value);
@@ -10970,7 +10986,7 @@ function auditCrawlPages(pages, opts = {}) {
10970
10986
  for (const page of pages) {
10971
10987
  if (!isAuditableHtmlPage(page)) continue;
10972
10988
  for (const value of [page.url, page.finalUrl]) {
10973
- const key = urlKey(value);
10989
+ const key = urlKey$1(value);
10974
10990
  if (key) pageByUrl.set(key, page);
10975
10991
  }
10976
10992
  }
@@ -11112,7 +11128,7 @@ function auditCrawlPages(pages, opts = {}) {
11112
11128
  canonicalRaw: page.canonicalRaw,
11113
11129
  canonical: page.canonical
11114
11130
  }));
11115
- const canonicalTarget = pageByUrl.get(urlKey(page.canonical) ?? "");
11131
+ const canonicalTarget = pageByUrl.get(urlKey$1(page.canonical) ?? "");
11116
11132
  if (canonicalTarget?.canonical && !sameUrl(canonicalTarget.canonical, page.canonical)) issues.push(issue("canonical_chain", page, page.canonical, {
11117
11133
  canonical: page.canonical,
11118
11134
  nextCanonical: canonicalTarget.canonical,
@@ -11286,7 +11302,7 @@ const SENSITIVE_FIELD_NAMES = /* @__PURE__ */ new Set([
11286
11302
  "refresh-token"
11287
11303
  ]);
11288
11304
  const SENSITIVE_PARAM_PATTERN = /^(access_?token|auth|api_?key|client_?secret|code|jwt|key|password|refresh_?token|secret|session|sig|signature|token)$/i;
11289
- function uniqueSorted(values = []) {
11305
+ function uniqueSorted$1(values = []) {
11290
11306
  return [...new Set(values.filter(Boolean))].sort();
11291
11307
  }
11292
11308
  function normalizeFetchRate(input, strategy) {
@@ -11306,13 +11322,13 @@ function normalizeCrawlConfig(input) {
11306
11322
  mode: input.mode ?? (strategy === "health" ? "sitemap" : "site"),
11307
11323
  strategy,
11308
11324
  ...input.sitemapUrl ? { sitemapUrl: new URL(input.sitemapUrl).toString() } : {},
11309
- urls: uniqueSorted(input.urls ?? []),
11325
+ urls: uniqueSorted$1(input.urls ?? []),
11310
11326
  maxPages: input.maxPages ?? 500,
11311
11327
  maxDepth: input.maxDepth ?? 16,
11312
11328
  concurrency,
11313
11329
  timeoutMs: input.timeoutMs ?? 2e4,
11314
- include: uniqueSorted(input.include ?? []),
11315
- exclude: uniqueSorted(input.exclude ?? []),
11330
+ include: uniqueSorted$1(input.include ?? []),
11331
+ exclude: uniqueSorted$1(input.exclude ?? []),
11316
11332
  respectRobots: input.respectRobots ?? true,
11317
11333
  useSitemap: input.useSitemap ?? true,
11318
11334
  checkExternal: strategy === "health" ? false : input.checkExternal ?? true,
@@ -13557,7 +13573,7 @@ async function fetchSitemapUrls(input) {
13557
13573
  }
13558
13574
  //#endregion
13559
13575
  //#region packages/core/dist/analyze/workflows/analytics-value.js
13560
- const defaultDependencies$13 = { runGa4Report };
13576
+ const defaultDependencies$14 = { runGa4Report };
13561
13577
  function normalizePath(value) {
13562
13578
  if (!value || value === "(not set)") return "";
13563
13579
  const [path = ""] = value.split("?");
@@ -13617,7 +13633,7 @@ function landingPageRankingPolicy(input) {
13617
13633
  warnings
13618
13634
  };
13619
13635
  }
13620
- async function fetchLandingPageValues(input, dependencies = defaultDependencies$13) {
13636
+ async function fetchLandingPageValues(input, dependencies = defaultDependencies$14) {
13621
13637
  const retainedRowLimit = input.limit ?? 5e3;
13622
13638
  if (!input.propertyId) return {
13623
13639
  values: /* @__PURE__ */ new Map(),
@@ -16585,15 +16601,15 @@ function analyzeCtrUnderperformersFromRows(input) {
16585
16601
  }
16586
16602
  //#endregion
16587
16603
  //#region packages/core/dist/analyze/ctr-underperformers.js
16588
- const MAX_GSC_ROWS$4 = 1e5;
16604
+ const MAX_GSC_ROWS$5 = 1e5;
16589
16605
  function plural$3(count, singular, pluralLabel = `${singular}s`) {
16590
16606
  return count === 1 ? singular : pluralLabel;
16591
16607
  }
16592
- const defaultDependencies$12 = {
16608
+ const defaultDependencies$13 = {
16593
16609
  searchAnalytics: querySearchAnalytics,
16594
16610
  now: () => /* @__PURE__ */ new Date()
16595
16611
  };
16596
- async function ctrUnderperformersReport(input, dependencies = defaultDependencies$12) {
16612
+ async function ctrUnderperformersReport(input, dependencies = defaultDependencies$13) {
16597
16613
  const minImpressions = integerOption$2({
16598
16614
  value: input.minImpressions,
16599
16615
  fallback: 200,
@@ -16615,7 +16631,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
16615
16631
  dimensions: ["query", "page"],
16616
16632
  type: "web",
16617
16633
  dataState: "final",
16618
- maxRows: MAX_GSC_ROWS$4
16634
+ maxRows: MAX_GSC_ROWS$5
16619
16635
  }, { refresh: input.refresh });
16620
16636
  const { items, totalClickShortfall, returnedClickShortfall, selection } = analyzeCtrUnderperformersFromRows({
16621
16637
  rows: source.rows,
@@ -16626,7 +16642,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
16626
16642
  includeBrand: input.includeBrand
16627
16643
  });
16628
16644
  const top = items[0];
16629
- const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$4;
16645
+ const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$5;
16630
16646
  const partialValidation = selection.invalidRows > 0;
16631
16647
  const dataStatus = selection.sourceRows === 0 ? "empty" : possiblyTruncated || partialValidation ? "partial" : selection.eligibleUnderperformers === 0 ? "filtered" : "complete";
16632
16648
  const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
@@ -16643,7 +16659,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
16643
16659
  dataState: "final",
16644
16660
  rowsFetched: source.rowsFetched,
16645
16661
  calls: source.calls,
16646
- maxRows: MAX_GSC_ROWS$4,
16662
+ maxRows: MAX_GSC_ROWS$5,
16647
16663
  possiblyTruncated,
16648
16664
  completeness,
16649
16665
  validation: {
@@ -16673,7 +16689,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
16673
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.",
16674
16690
  ...selection.invalidRows ? [`${selection.invalidRows} invalid provider ${plural$3(selection.invalidRows, "row")} ${selection.invalidRows === 1 ? "was" : "were"} excluded, so this report is partial.`] : [],
16675
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.`] : [],
16676
- ...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.`] : [],
16677
16693
  ...selection.limitedUnderperformers ? [`${selection.limitedUnderperformers} eligible ${plural$3(selection.limitedUnderperformers, "underperformer")} ${selection.limitedUnderperformers === 1 ? "was" : "were"} omitted by the output limit.`] : []
16678
16694
  ],
16679
16695
  recommendations: top ? [
@@ -17186,11 +17202,11 @@ function unavailableSegmentImpactReport(input) {
17186
17202
  //#endregion
17187
17203
  //#region packages/core/dist/analyze/segment-impact.js
17188
17204
  const DEFAULT_MAX_ROWS = 1e5;
17189
- const defaultDependencies$11 = {
17205
+ const defaultDependencies$12 = {
17190
17206
  searchAnalytics: querySearchAnalytics,
17191
17207
  now: () => /* @__PURE__ */ new Date()
17192
17208
  };
17193
- async function segmentImpact(input, dependencies = defaultDependencies$11) {
17209
+ async function segmentImpact(input, dependencies = defaultDependencies$12) {
17194
17210
  validateSegmentSite(input.site);
17195
17211
  const now = dependencies.now();
17196
17212
  const ranges = resolveSegmentRanges({
@@ -17743,8 +17759,8 @@ function analyzeCannibalRows(input) {
17743
17759
  }
17744
17760
  //#endregion
17745
17761
  //#region packages/core/dist/analyze/site-diagnostics/cannibal.js
17746
- const MAX_GSC_ROWS$3 = 1e5;
17747
- const defaultDependencies$10 = {
17762
+ const MAX_GSC_ROWS$4 = 1e5;
17763
+ const defaultDependencies$11 = {
17748
17764
  searchAnalytics: querySearchAnalytics,
17749
17765
  now: () => /* @__PURE__ */ new Date()
17750
17766
  };
@@ -17757,7 +17773,7 @@ function verdict$4(input) {
17757
17773
  const result = `${input.returned} of ${input.eligible} multi-URL query candidate${input.eligible === 1 ? "" : "s"} returned for intent and technical review.`;
17758
17774
  return input.status === "partial" ? `${result} Provider evidence is partial.` : result;
17759
17775
  }
17760
- async function cannibalReport(input, dependencies = defaultDependencies$10) {
17776
+ async function cannibalReport(input, dependencies = defaultDependencies$11) {
17761
17777
  const days = integerOption$2({
17762
17778
  value: input.days,
17763
17779
  fallback: 28,
@@ -17787,7 +17803,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
17787
17803
  ...range,
17788
17804
  type: "web",
17789
17805
  dataState: "final",
17790
- maxRows: MAX_GSC_ROWS$3
17806
+ maxRows: MAX_GSC_ROWS$4
17791
17807
  };
17792
17808
  const [pageExposure, propertyDemand] = await Promise.all([dependencies.searchAnalytics(input.site, {
17793
17809
  ...requestBase,
@@ -17810,8 +17826,8 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
17810
17826
  brandTerms: input.brandTerms,
17811
17827
  includeBrand: input.includeBrand
17812
17828
  });
17813
- const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$3;
17814
- 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;
17815
17831
  const possiblyTruncated = pageTruncated || propertyTruncated;
17816
17832
  const partialValidation = analysis.selection.invalidRows + analysis.selection.propertyInvalidRows > 0;
17817
17833
  const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
@@ -17836,7 +17852,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
17836
17852
  invalidRows: analysis.selection.invalidRows
17837
17853
  },
17838
17854
  calls: pageExposure.calls,
17839
- maxRows: MAX_GSC_ROWS$3,
17855
+ maxRows: MAX_GSC_ROWS$4,
17840
17856
  possiblyTruncated: pageTruncated
17841
17857
  },
17842
17858
  propertyDemand: {
@@ -17848,7 +17864,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
17848
17864
  invalidRows: analysis.selection.propertyInvalidRows
17849
17865
  },
17850
17866
  calls: propertyDemand.calls,
17851
- maxRows: MAX_GSC_ROWS$3,
17867
+ maxRows: MAX_GSC_ROWS$4,
17852
17868
  possiblyTruncated: propertyTruncated
17853
17869
  },
17854
17870
  completeness
@@ -18156,8 +18172,8 @@ function analyzeDecay(input) {
18156
18172
  }
18157
18173
  //#endregion
18158
18174
  //#region packages/core/dist/analyze/site-diagnostics/decaying.js
18159
- const MAX_GSC_ROWS$2 = 1e5;
18160
- const defaultDependencies$9 = {
18175
+ const MAX_GSC_ROWS$3 = 1e5;
18176
+ const defaultDependencies$10 = {
18161
18177
  searchAnalytics: querySearchAnalytics,
18162
18178
  now: () => /* @__PURE__ */ new Date()
18163
18179
  };
@@ -18168,7 +18184,7 @@ function verdict$3(report) {
18168
18184
  if (!report.selection.eligibleRows) return "No observed retained query/page declines matched the report filters.";
18169
18185
  return `${report.selection.returnedRows} of ${report.selection.eligibleRows} observed retained query/page declines returned for review.`;
18170
18186
  }
18171
- async function decayingReport(input, dependencies = defaultDependencies$9) {
18187
+ async function decayingReport(input, dependencies = defaultDependencies$10) {
18172
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.");
18173
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.");
18174
18190
  const days = integerOption$2({
@@ -18221,7 +18237,7 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
18221
18237
  aggregationType: "auto",
18222
18238
  type: "web",
18223
18239
  dataState: "final",
18224
- maxRows: MAX_GSC_ROWS$2
18240
+ maxRows: MAX_GSC_ROWS$3
18225
18241
  };
18226
18242
  const [current, previous] = await Promise.all([dependencies.searchAnalytics(input.site, {
18227
18243
  ...request,
@@ -18244,8 +18260,8 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
18244
18260
  brandTerms: input.brandTerms,
18245
18261
  includeBrand: input.includeBrand
18246
18262
  });
18247
- const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$2;
18248
- 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;
18249
18265
  const possiblyTruncated = currentTruncated || previousTruncated;
18250
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";
18251
18267
  const reportBase = {
@@ -18272,13 +18288,13 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
18272
18288
  current: {
18273
18289
  rowsFetched: current.rowsFetched,
18274
18290
  calls: current.calls,
18275
- maxRows: MAX_GSC_ROWS$2,
18291
+ maxRows: MAX_GSC_ROWS$3,
18276
18292
  possiblyTruncated: currentTruncated
18277
18293
  },
18278
18294
  previous: {
18279
18295
  rowsFetched: previous.rowsFetched,
18280
18296
  calls: previous.calls,
18281
- maxRows: MAX_GSC_ROWS$2,
18297
+ maxRows: MAX_GSC_ROWS$3,
18282
18298
  possiblyTruncated: previousTruncated
18283
18299
  },
18284
18300
  completeness: possiblyTruncated ? "possibly-truncated" : "retained-query-rows-only"
@@ -18654,12 +18670,12 @@ function quickWinTemplateSummaries(items) {
18654
18670
  }
18655
18671
  //#endregion
18656
18672
  //#region packages/core/dist/analyze/site-diagnostics/quick-wins.js
18657
- const DEFAULT_DAYS$2 = 28;
18658
- const MAX_DAYS$2 = 548;
18673
+ const DEFAULT_DAYS$3 = 28;
18674
+ const MAX_DAYS$3 = 548;
18659
18675
  const DEFAULT_VERIFY_LIMIT$1 = 5;
18660
18676
  const MAX_VERIFY_LIMIT = 100;
18661
18677
  const MAX_SOURCE_ROWS$3 = 1e5;
18662
- const defaultDependencies$8 = {
18678
+ const defaultDependencies$9 = {
18663
18679
  searchAnalytics: querySearchAnalytics,
18664
18680
  fetch: fetchPage,
18665
18681
  extract: extractPage,
@@ -18678,13 +18694,13 @@ function applyVerification$1(item, coverage) {
18678
18694
  evidenceRef: `${item.recommendation.evidenceRef} ${coverage.summary}`
18679
18695
  };
18680
18696
  }
18681
- async function quickWinsReport(input, dependencies = defaultDependencies$8) {
18682
- const explicitRange = explicitDateRange(input, MAX_DAYS$2);
18697
+ async function quickWinsReport(input, dependencies = defaultDependencies$9) {
18698
+ const explicitRange = explicitDateRange(input, MAX_DAYS$3);
18683
18699
  const days = explicitRange?.days ?? integerOption$2({
18684
18700
  value: input.days,
18685
- fallback: DEFAULT_DAYS$2,
18701
+ fallback: DEFAULT_DAYS$3,
18686
18702
  minimum: 1,
18687
- maximum: MAX_DAYS$2,
18703
+ maximum: MAX_DAYS$3,
18688
18704
  label: "Days"
18689
18705
  });
18690
18706
  const verifyLimit = integerOption$2({
@@ -19051,11 +19067,11 @@ function analyzeStrikingDistanceRows(input) {
19051
19067
  }
19052
19068
  //#endregion
19053
19069
  //#region packages/core/dist/analyze/striking-distance.js
19054
- const DEFAULT_DAYS$1 = 28;
19055
- const MAX_DAYS$1 = 548;
19070
+ const DEFAULT_DAYS$2 = 28;
19071
+ const MAX_DAYS$2 = 548;
19056
19072
  const DEFAULT_VERIFY_LIMIT = 5;
19057
19073
  const MAX_SOURCE_ROWS$2 = 1e5;
19058
- const defaultDependencies$7 = {
19074
+ const defaultDependencies$8 = {
19059
19075
  searchAnalytics: querySearchAnalytics,
19060
19076
  verifyContent: verifyQueryContent,
19061
19077
  now: () => /* @__PURE__ */ new Date()
@@ -19065,8 +19081,8 @@ function boundedInteger$3(value, fallback, minimum, maximum) {
19065
19081
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
19066
19082
  }
19067
19083
  function reportDays(value) {
19068
- if (value === void 0) return DEFAULT_DAYS$1;
19069
- 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}.`);
19070
19086
  return value;
19071
19087
  }
19072
19088
  function dateRange(days, now) {
@@ -19105,7 +19121,7 @@ function verdict$2(analysis) {
19105
19121
  if (analysis.dataStatus === "filtered") return "No retained query/page rows met the report filters.";
19106
19122
  return `${countLabel(analysis.summary.eligibleRows, "eligible query/page row")} found; ${countLabel(analysis.summary.returnedRows, "row")} returned for review.`;
19107
19123
  }
19108
- async function strikingDistance(input, dependencies = defaultDependencies$7) {
19124
+ async function strikingDistance(input, dependencies = defaultDependencies$8) {
19109
19125
  const days = reportDays(input.days);
19110
19126
  const generatedAt = dependencies.now().toISOString();
19111
19127
  const range = dateRange(days, new Date(generatedAt));
@@ -19952,7 +19968,7 @@ function emptyQuickWins(input) {
19952
19968
  warnings: []
19953
19969
  };
19954
19970
  }
19955
- const defaultDependencies$6 = {
19971
+ const defaultDependencies$7 = {
19956
19972
  trafficAnomaly,
19957
19973
  updateCorrelation,
19958
19974
  segmentImpact,
@@ -19963,7 +19979,7 @@ const defaultDependencies$6 = {
19963
19979
  };
19964
19980
  async function diagnoseProperty(input, dependencies = {}) {
19965
19981
  const providers = {
19966
- ...defaultDependencies$6,
19982
+ ...defaultDependencies$7,
19967
19983
  ...dependencies
19968
19984
  };
19969
19985
  const limit = input.limit ?? 10;
@@ -20827,6 +20843,42 @@ async function providerRequestJson(input) {
20827
20843
  return parsed.data;
20828
20844
  }
20829
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
20830
20882
  //#region packages/core/dist/providers/dataforseo/account-schema.js
20831
20883
  const userDataMoneySchema = z.object({
20832
20884
  total: z.number().finite().nonnegative().optional(),
@@ -20844,6 +20896,11 @@ const priceComponentSchema = z.object({
20844
20896
  const normalPriorityPriceSchema = z.object({ priority_normal: z.array(priceComponentSchema).max(20).optional() }).passthrough();
20845
20897
  const livePriceSchema = z.object({ live: normalPriorityPriceSchema.optional() }).passthrough();
20846
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(),
20847
20904
  dataforseo_labs: z.object({
20848
20905
  keyword_overview: livePriceSchema.optional(),
20849
20906
  keyword_ideas: livePriceSchema.optional(),
@@ -20901,7 +20958,7 @@ function discoveryNextCursor(response) {
20901
20958
  //#region packages/core/dist/providers/dataforseo/schema.js
20902
20959
  const MAX_PROVIDER_MONTHLY_SEARCH_ROWS = 120;
20903
20960
  const nullableNumber$1 = z.number().finite().nullable().optional();
20904
- 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();
20905
20962
  const monthlySearchSchema$1 = z.object({
20906
20963
  year: z.number().int(),
20907
20964
  month: z.number().int(),
@@ -20917,7 +20974,7 @@ const keywordOverviewItemSchema = z.object({
20917
20974
  last_updated_time: z.string().trim().min(1).nullable().optional()
20918
20975
  }).nullish(),
20919
20976
  keyword_properties: z.object({ keyword_difficulty: nullableNumber$1 }).nullish(),
20920
- serp_info: z.object({ se_results_count: nullableCount$1 }).nullish(),
20977
+ serp_info: z.object({ se_results_count: nullableCount$2 }).nullish(),
20921
20978
  search_intent_info: z.object({ main_intent: z.enum([
20922
20979
  "informational",
20923
20980
  "navigational",
@@ -20990,37 +21047,37 @@ const dataForSeoDiscoveryResponseSchema = z.object({
20990
21047
  //#region packages/core/dist/providers/dataforseo/domain-schema.js
20991
21048
  const nullableNumber = z.number().finite().nullable().optional();
20992
21049
  const nullableNonnegativeNumber = z.number().finite().nonnegative().nullable().optional();
20993
- const nullableCount = z.number().int().nonnegative().nullable().optional();
21050
+ const nullableCount$1 = z.number().int().nonnegative().nullable().optional();
20994
21051
  const rankingMetricsSchema = z.object({
20995
- pos_1: nullableCount,
20996
- pos_2_3: nullableCount,
20997
- pos_4_10: nullableCount,
20998
- pos_11_20: nullableCount,
20999
- pos_21_30: nullableCount,
21000
- pos_31_40: nullableCount,
21001
- pos_41_50: nullableCount,
21002
- pos_51_60: nullableCount,
21003
- pos_61_70: nullableCount,
21004
- pos_71_80: nullableCount,
21005
- pos_81_90: nullableCount,
21006
- 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,
21007
21064
  etv: nullableNonnegativeNumber,
21008
- count: nullableCount,
21065
+ count: nullableCount$1,
21009
21066
  estimated_paid_traffic_cost: nullableNonnegativeNumber,
21010
- is_new: nullableCount,
21011
- is_up: nullableCount,
21012
- is_down: nullableCount,
21013
- is_lost: nullableCount
21067
+ is_new: nullableCount$1,
21068
+ is_up: nullableCount$1,
21069
+ is_down: nullableCount$1,
21070
+ is_lost: nullableCount$1
21014
21071
  }).passthrough();
21015
21072
  const metricsSchema = z.object({ organic: rankingMetricsSchema.nullable().optional() }).passthrough();
21016
- const paidResponseSchema = z.object({
21073
+ const paidResponseSchema$1 = z.object({
21017
21074
  status_code: z.number().int(),
21018
21075
  status_message: z.string().max(1e3),
21019
21076
  cost: z.number().finite().nonnegative().optional(),
21020
21077
  tasks_count: z.number().int().nonnegative(),
21021
21078
  tasks_error: z.number().int().nonnegative()
21022
21079
  }).passthrough();
21023
- const taskFields = {
21080
+ const taskFields$1 = {
21024
21081
  id: z.string().trim().min(1).max(100).optional(),
21025
21082
  status_code: z.number().int(),
21026
21083
  status_message: z.string().max(1e3),
@@ -21040,8 +21097,8 @@ const overviewResultSchema = z.object({
21040
21097
  items_count: z.number().int().nonnegative().optional(),
21041
21098
  items: z.array(overviewItemSchema).max(1e3).nullable().optional()
21042
21099
  }).passthrough();
21043
- const dataForSeoDomainOverviewResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21044
- ...taskFields,
21100
+ const dataForSeoDomainOverviewResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
21101
+ ...taskFields$1,
21045
21102
  result: z.array(overviewResultSchema).max(10).nullable().optional()
21046
21103
  }).passthrough()).max(10) });
21047
21104
  const rankedSerpItemSchema = z.object({
@@ -21061,8 +21118,8 @@ const rankedKeywordsResultSchema = z.object({
21061
21118
  items_count: z.number().int().nonnegative().optional(),
21062
21119
  items: z.array(rankedKeywordItemSchema).max(1e3).nullable().optional()
21063
21120
  }).passthrough();
21064
- const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21065
- ...taskFields,
21121
+ const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
21122
+ ...taskFields$1,
21066
21123
  result: z.array(rankedKeywordsResultSchema).max(10).nullable().optional()
21067
21124
  }).passthrough()).max(10) });
21068
21125
  const rankingPageItemSchema = z.object({
@@ -21075,8 +21132,8 @@ const rankingPagesResultSchema = z.object({
21075
21132
  items_count: z.number().int().nonnegative().optional(),
21076
21133
  items: z.array(rankingPageItemSchema).max(1e3).nullable().optional()
21077
21134
  }).passthrough();
21078
- const dataForSeoRankingPagesResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21079
- ...taskFields,
21135
+ const dataForSeoRankingPagesResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
21136
+ ...taskFields$1,
21080
21137
  result: z.array(rankingPagesResultSchema).max(10).nullable().optional()
21081
21138
  }).passthrough()).max(10) });
21082
21139
  const competitorItemSchema = z.object({
@@ -21084,9 +21141,9 @@ const competitorItemSchema = z.object({
21084
21141
  avg_position: nullableNumber,
21085
21142
  median_position: nullableNumber,
21086
21143
  etv: nullableNonnegativeNumber,
21087
- keywords_count: nullableCount,
21144
+ keywords_count: nullableCount$1,
21088
21145
  visibility: nullableNonnegativeNumber,
21089
- relevant_serp_items: nullableCount,
21146
+ relevant_serp_items: nullableCount$1,
21090
21147
  keywords_positions: z.record(z.string().trim().min(1).max(80), z.array(z.number().int().positive()).max(100)).nullable().optional()
21091
21148
  }).passthrough();
21092
21149
  const competitorsResultSchema = z.object({
@@ -21095,8 +21152,8 @@ const competitorsResultSchema = z.object({
21095
21152
  items_count: z.number().int().nonnegative().optional(),
21096
21153
  items: z.array(competitorItemSchema).max(1e3).nullable().optional()
21097
21154
  }).passthrough();
21098
- const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
21099
- ...taskFields,
21155
+ const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
21156
+ ...taskFields$1,
21100
21157
  result: z.array(competitorsResultSchema).max(10).nullable().optional()
21101
21158
  }).passthrough()).max(10) });
21102
21159
  const DOMAIN_OVERVIEW_PATH = "v3/dataforseo_labs/google/domain_rank_overview/live";
@@ -21286,6 +21343,209 @@ function serpCompetitorsPaidRequest(input, ttlMs) {
21286
21343
  };
21287
21344
  }
21288
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
21289
21549
  //#region packages/core/dist/providers/dataforseo/paid-request.js
21290
21550
  function usdToMicros(value) {
21291
21551
  if (value === void 0) return null;
@@ -21809,41 +22069,6 @@ const MAX_KEYWORD_CHARACTERS = 80;
21809
22069
  const MAX_KEYWORD_WORDS = 10;
21810
22070
  const MAX_DISCOVERY_SEEDS$1 = 5;
21811
22071
  const MAX_DISCOVERY_ROWS$1 = 100;
21812
- function unitPrice(components) {
21813
- const prices = components ?? [];
21814
- const total = (type) => {
21815
- const matching = prices.filter((item) => item.cost_type === type);
21816
- return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
21817
- };
21818
- return {
21819
- perRequestMicros: total("per_request"),
21820
- perResultMicros: total("per_result")
21821
- };
21822
- }
21823
- function keywordOverviewPrice(account) {
21824
- return unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal);
21825
- }
21826
- function keywordDiscoveryPrices(account) {
21827
- return {
21828
- ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
21829
- related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
21830
- suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
21831
- };
21832
- }
21833
- function domainResearchPrices(account) {
21834
- return {
21835
- domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
21836
- rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
21837
- rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
21838
- serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
21839
- };
21840
- }
21841
- function serpLiveAdvancedPrice(account) {
21842
- return unitPrice(account.price?.serp?.live?.advanced?.priority_normal);
21843
- }
21844
- function serpTaskPostPrice(account) {
21845
- return unitPrice(account.price?.serp?.task_post?.priority_normal);
21846
- }
21847
22072
  function responseTaskIds(response) {
21848
22073
  return [...new Set(response.tasks.flatMap((task) => task.id ? [task.id] : []))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
21849
22074
  }
@@ -21863,6 +22088,7 @@ var DataForSeoClient = class {
21863
22088
  serpTtlMs;
21864
22089
  accountPricingTtlMs;
21865
22090
  domainResearchTtlMs;
22091
+ linkTtlMs;
21866
22092
  spendLimits;
21867
22093
  accountPricing;
21868
22094
  accountPricingRequest;
@@ -21879,6 +22105,7 @@ var DataForSeoClient = class {
21879
22105
  this.serpTtlMs = options.serpTtlMs ?? DEFAULT_SERP_TTL_MS;
21880
22106
  this.accountPricingTtlMs = options.accountPricingTtlMs ?? DEFAULT_ACCOUNT_PRICING_TTL_MS;
21881
22107
  this.domainResearchTtlMs = options.domainResearchTtlMs ?? 6048e5;
22108
+ this.linkTtlMs = options.linkTtlMs ?? 864e5;
21882
22109
  this.spendLimits = options.spendLimits;
21883
22110
  }
21884
22111
  async getCredentials(operation) {
@@ -21961,11 +22188,7 @@ var DataForSeoClient = class {
21961
22188
  accountDailySpendMicros: dataForSeoUsdToMicros(account.money?.statistics?.day?.total),
21962
22189
  accountDailySpendPeriod: account.money?.statistics?.day?.value ?? null,
21963
22190
  accountDailyLimitMicros: dataForSeoUsdToMicros(account.money?.limits?.day?.total),
21964
- keywordOverviewPrice: keywordOverviewPrice(account),
21965
- keywordDiscoveryPrices: keywordDiscoveryPrices(account),
21966
- domainResearchPrices: domainResearchPrices(account),
21967
- serpLiveAdvancedPrice: serpLiveAdvancedPrice(account),
21968
- serpTaskPostPrice: serpTaskPostPrice(account),
22191
+ ...dataForSeoAccountPrices(account),
21969
22192
  backlinksSubscriptionExpiresAt: account.backlinks_subscription_expiry_date ?? null,
21970
22193
  aiMentionsSubscriptionExpiresAt: account.llm_mentions_subscription_expiry_date ?? null,
21971
22194
  apiVersion: response.version ?? null,
@@ -22028,6 +22251,15 @@ var DataForSeoClient = class {
22028
22251
  async serpCompetitors(input) {
22029
22252
  return this.paidPost(serpCompetitorsPaidRequest(input, this.domainResearchTtlMs));
22030
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
+ }
22031
22263
  async keywordOverview(input) {
22032
22264
  const keywords = input.keywords.map((keyword) => keyword.trim());
22033
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({
@@ -22371,20 +22603,20 @@ function compareCodepoints$3(left, right) {
22371
22603
  function normalizedKeyword$2(keyword) {
22372
22604
  return keyword.trim().replace(/\s+/gu, " ").toLowerCase();
22373
22605
  }
22374
- function missing$1(field) {
22606
+ function missing$2(field) {
22375
22607
  return unavailableValue("missing", `DataForSEO omitted ${field}.`);
22376
22608
  }
22377
22609
  function numericValue(values, field, valid) {
22378
22610
  const present = values.filter((value) => value !== null && value !== void 0);
22379
- if (present.length === 0) return missing$1(field);
22611
+ if (present.length === 0) return missing$2(field);
22380
22612
  const numbers = present.filter((value) => typeof value === "number" && valid(value));
22381
22613
  if (numbers.length !== present.length) return unavailableValue("invalid", `DataForSEO returned an invalid ${field}.`);
22382
22614
  const unique = [...new Set(numbers)];
22383
22615
  return unique.length === 1 ? observedValue(unique[0]) : unavailableValue("invalid", `DataForSEO returned conflicting ${field} values.`);
22384
22616
  }
22385
- function stringValue(values, field, normalize) {
22617
+ function stringValue$1(values, field, normalize) {
22386
22618
  const present = values.filter((value) => value !== null && value !== void 0);
22387
- if (present.length === 0) return missing$1(field);
22619
+ if (present.length === 0) return missing$2(field);
22388
22620
  const strings = present.flatMap((value) => {
22389
22621
  if (typeof value !== "string") return [];
22390
22622
  const normalized = normalize(value);
@@ -22396,7 +22628,7 @@ function stringValue(values, field, normalize) {
22396
22628
  }
22397
22629
  function monthlySearchesValue(rows) {
22398
22630
  const histories = rows.map((row) => row.keyword_info?.monthly_searches).filter((value) => value !== null && value !== void 0);
22399
- if (histories.length === 0) return missing$1("monthlySearches");
22631
+ if (histories.length === 0) return missing$2("monthlySearches");
22400
22632
  const byMonth = /* @__PURE__ */ new Map();
22401
22633
  let invalid = false;
22402
22634
  for (const history of histories) for (const item of history) {
@@ -22429,27 +22661,27 @@ function resultCountValues(rows) {
22429
22661
  function metricForKeyword(keyword, rows) {
22430
22662
  if (rows.length === 0) return {
22431
22663
  keyword,
22432
- monthlySearchVolume: missing$1("monthlySearchVolume"),
22433
- monthlySearches: missing$1("monthlySearches"),
22434
- searchVolumeUpdatedAt: missing$1("searchVolumeUpdatedAt"),
22435
- cpcUsd: missing$1("cpcUsd"),
22436
- paidCompetition: missing$1("paidCompetition"),
22437
- keywordDifficulty: missing$1("keywordDifficulty"),
22438
- intent: missing$1("intent"),
22439
- 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")
22440
22672
  };
22441
22673
  return {
22442
22674
  keyword,
22443
22675
  monthlySearchVolume: numericValue(rows.map((row) => row.keyword_info?.search_volume), "monthlySearchVolume", (value) => Number.isSafeInteger(value) && value >= 0),
22444
22676
  monthlySearches: monthlySearchesValue(rows),
22445
- 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) => {
22446
22678
  const timestamp = Date.parse(value);
22447
22679
  return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
22448
22680
  }),
22449
22681
  cpcUsd: numericValue(rows.map((row) => row.keyword_info?.cpc), "cpcUsd", (value) => Number.isFinite(value) && value >= 0),
22450
22682
  paidCompetition: numericValue(rows.map((row) => row.keyword_info?.competition), "paidCompetition", (value) => Number.isFinite(value) && value >= 0 && value <= 1),
22451
22683
  keywordDifficulty: numericValue(rows.map((row) => row.keyword_properties?.keyword_difficulty), "keywordDifficulty", (value) => Number.isFinite(value) && value >= 0 && value <= 100),
22452
- 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),
22453
22685
  resultCount: numericValue(resultCountValues(rows), "resultCount", (value) => Number.isSafeInteger(value) && value >= 0)
22454
22686
  };
22455
22687
  }
@@ -22502,21 +22734,21 @@ const COMPETITOR_RESULT_TYPES = /* @__PURE__ */ new Set([
22502
22734
  "featured_snippet",
22503
22735
  "local_pack"
22504
22736
  ]);
22505
- function missing(field) {
22737
+ function missing$1(field) {
22506
22738
  return unavailableValue("missing", `DataForSEO omitted ${field}.`);
22507
22739
  }
22508
- function numberValue(value, field) {
22509
- 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);
22510
22742
  }
22511
22743
  function countValue(value, field) {
22512
- return numberValue(value, field);
22744
+ return numberValue$1(value, field);
22513
22745
  }
22514
22746
  function sumBuckets(metrics, fields, label) {
22515
22747
  const values = fields.map((field) => metrics[field]);
22516
- 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));
22517
22749
  }
22518
22750
  function rankingDistribution(metrics) {
22519
- if (!metrics) return missing("organic ranking distribution");
22751
+ if (!metrics) return missing$1("organic ranking distribution");
22520
22752
  const buckets = {
22521
22753
  first: sumBuckets(metrics, ["pos_1"], "position 1 rankings"),
22522
22754
  top3: sumBuckets(metrics, ["pos_1", "pos_2_3"], "top 3 rankings"),
@@ -22567,9 +22799,9 @@ function rankingDistribution(metrics) {
22567
22799
  }
22568
22800
  function organicFootprint(metrics) {
22569
22801
  return {
22570
- estimatedMonthlyTraffic: numberValue(metrics?.etv, "estimated organic monthly traffic"),
22802
+ estimatedMonthlyTraffic: numberValue$1(metrics?.etv, "estimated organic monthly traffic"),
22571
22803
  rankedKeywords: countValue(metrics?.count, "ranked organic keywords"),
22572
- estimatedMonthlyTrafficCostUsd: numberValue(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
22804
+ estimatedMonthlyTrafficCostUsd: numberValue$1(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
22573
22805
  rankings: rankingDistribution(metrics),
22574
22806
  newRankings: countValue(metrics?.is_new, "new rankings"),
22575
22807
  improvedRankings: countValue(metrics?.is_up, "improved rankings"),
@@ -22624,7 +22856,7 @@ function researchMarket(input, operation) {
22624
22856
  });
22625
22857
  return parsed;
22626
22858
  }
22627
- function rowLimit(limit, offset = 0) {
22859
+ function rowLimit$1(limit, offset = 0) {
22628
22860
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
22629
22861
  provider: "dataforseo",
22630
22862
  operation: "domain-research",
@@ -22648,7 +22880,7 @@ function resultTypes(input, allowed) {
22648
22880
  });
22649
22881
  return normalized;
22650
22882
  }
22651
- function safeUrl$1(value) {
22883
+ function safeUrl$2(value) {
22652
22884
  if (!value) return null;
22653
22885
  try {
22654
22886
  const url = new URL(value);
@@ -22668,7 +22900,7 @@ function dedupeBy(rows, key) {
22668
22900
  }
22669
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]);
22670
22902
  }
22671
- function observedNumber(value) {
22903
+ function observedNumber$1(value) {
22672
22904
  return value.state === "observed" ? value.value : -1;
22673
22905
  }
22674
22906
  function duplicateWarning(available, retained, label) {
@@ -22679,7 +22911,7 @@ function duplicateWarning(available, retained, label) {
22679
22911
  message: `${duplicates} duplicate ${label} row${duplicates === 1 ? "" : "s"} were collapsed deterministically.`
22680
22912
  }] : [];
22681
22913
  }
22682
- function totalRows(values) {
22914
+ function totalRows$1(values) {
22683
22915
  const present = values.filter((value) => value !== null && value !== void 0);
22684
22916
  return present.length ? Math.max(...present) : null;
22685
22917
  }
@@ -22695,7 +22927,7 @@ function coverage(input) {
22695
22927
  nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
22696
22928
  };
22697
22929
  }
22698
- function requestContext(reportId, supplied) {
22930
+ function requestContext$1(reportId, supplied) {
22699
22931
  return supplied ?? {
22700
22932
  reportId,
22701
22933
  reportRunId: randomUUID()
@@ -22744,7 +22976,7 @@ async function dataForSeoDomainOverview(client, input) {
22744
22976
  languageCode: market.languageCode.split("-")[0],
22745
22977
  ...location,
22746
22978
  refresh: input.refresh,
22747
- context: requestContext("domain-overview", input.context)
22979
+ context: requestContext$1("domain-overview", input.context)
22748
22980
  });
22749
22981
  const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
22750
22982
  const matchingResult = results.find((row) => !row.target || domain(row.target) === target);
@@ -22836,7 +23068,7 @@ async function dataForSeoRankedKeywords(client, input) {
22836
23068
  const market = researchMarket(input.market, "ranked-keywords");
22837
23069
  const target = rankedTarget(input.target);
22838
23070
  const offset = input.offset ?? 0;
22839
- rowLimit(input.limit, offset);
23071
+ rowLimit$1(input.limit, offset);
22840
23072
  const types = resultTypes(input.resultTypes, RANKED_RESULT_TYPES);
22841
23073
  const excluded = excludedTerms(input);
22842
23074
  const requestedFilters = filters$1(input, excluded);
@@ -22856,7 +23088,7 @@ async function dataForSeoRankedKeywords(client, input) {
22856
23088
  limit: input.limit,
22857
23089
  offset,
22858
23090
  refresh: input.refresh,
22859
- context: requestContext("ranked-keywords", input.context)
23091
+ context: requestContext$1("ranked-keywords", input.context)
22860
23092
  });
22861
23093
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
22862
23094
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
@@ -22865,7 +23097,7 @@ async function dataForSeoRankedKeywords(client, input) {
22865
23097
  const keywordData = row.keyword_data;
22866
23098
  const serp = row.ranked_serp_element?.serp_item;
22867
23099
  const keyword = keywordData?.keyword ? normalizedKeyword$2(keywordData.keyword) : "";
22868
- const url = safeUrl$1(serp?.url);
23100
+ const url = safeUrl$2(serp?.url);
22869
23101
  if (!keywordData || !keyword || !url || !serp?.rank_group || !serp.rank_absolute || !serp.type) {
22870
23102
  invalidRows += 1;
22871
23103
  return [];
@@ -22876,11 +23108,11 @@ async function dataForSeoRankedKeywords(client, input) {
22876
23108
  rankGroup: serp.rank_group,
22877
23109
  rankAbsolute: serp.rank_absolute,
22878
23110
  resultType: serp.type,
22879
- estimatedMonthlyTraffic: numberValue(serp.etv, "estimated keyword traffic")
23111
+ estimatedMonthlyTraffic: numberValue$1(serp.etv, "estimated keyword traffic")
22880
23112
  }];
22881
23113
  });
22882
- 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));
22883
- 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));
22884
23116
  return domainEvidence({
22885
23117
  capability: "ranked-keywords",
22886
23118
  data: {
@@ -22953,7 +23185,7 @@ async function dataForSeoRankingPages(client, input) {
22953
23185
  const market = researchMarket(input.market, "ranking-pages");
22954
23186
  const target = domain(input.domain);
22955
23187
  const offset = input.offset ?? 0;
22956
- rowLimit(input.limit, offset);
23188
+ rowLimit$1(input.limit, offset);
22957
23189
  const requestedFilters = filters(input);
22958
23190
  const orderBy = ["metrics.organic.etv,desc", "page_address,asc"];
22959
23191
  const snapshot = await client.rankingPages({
@@ -22965,13 +23197,13 @@ async function dataForSeoRankingPages(client, input) {
22965
23197
  limit: input.limit,
22966
23198
  offset,
22967
23199
  refresh: input.refresh,
22968
- context: requestContext("ranking-pages", input.context)
23200
+ context: requestContext$1("ranking-pages", input.context)
22969
23201
  });
22970
23202
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
22971
23203
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
22972
23204
  let invalidRows = 0;
22973
23205
  const mappedRows = rawRows.flatMap((row) => {
22974
- const url = safeUrl$1(row.page_address);
23206
+ const url = safeUrl$2(row.page_address);
22975
23207
  if (!url) {
22976
23208
  invalidRows += 1;
22977
23209
  return [];
@@ -22981,8 +23213,8 @@ async function dataForSeoRankingPages(client, input) {
22981
23213
  organic: organicFootprint(row.metrics?.organic)
22982
23214
  }];
22983
23215
  });
22984
- const rows = dedupeBy(mappedRows, (row) => row.url).sort((left, right) => observedNumber(right.organic.estimatedMonthlyTraffic) - observedNumber(left.organic.estimatedMonthlyTraffic) || compareCodepoints$3(left.url, right.url));
22985
- 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));
22986
23218
  return domainEvidence({
22987
23219
  capability: "relevant-pages",
22988
23220
  data: {
@@ -23027,7 +23259,7 @@ function keywords(input) {
23027
23259
  async function dataForSeoSerpCompetitors(client, input) {
23028
23260
  const market = researchMarket(input.market, "serp-competitors");
23029
23261
  const offset = input.offset ?? 0;
23030
- rowLimit(input.limit, offset);
23262
+ rowLimit$1(input.limit, offset);
23031
23263
  const requestedKeywords = keywords(input.keywords);
23032
23264
  const types = resultTypes(input.resultTypes, COMPETITOR_RESULT_TYPES);
23033
23265
  const orderBy = [
@@ -23045,7 +23277,7 @@ async function dataForSeoSerpCompetitors(client, input) {
23045
23277
  limit: input.limit,
23046
23278
  offset,
23047
23279
  refresh: input.refresh,
23048
- context: requestContext("serp-competitors", input.context)
23280
+ context: requestContext$1("serp-competitors", input.context)
23049
23281
  });
23050
23282
  const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
23051
23283
  const rawRows = responseResults.flatMap((result) => result.items ?? []);
@@ -23068,16 +23300,16 @@ async function dataForSeoSerpCompetitors(client, input) {
23068
23300
  return [{
23069
23301
  domain: normalizedDomain,
23070
23302
  matchedKeywords: row.keywords_count,
23071
- averagePosition: numberValue(row.avg_position, "average position"),
23072
- medianPosition: numberValue(row.median_position, "median position"),
23073
- visibility: numberValue(row.visibility, "visibility"),
23074
- 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"),
23075
23307
  relevantResults: countValue(row.relevant_serp_items, "relevant result count"),
23076
23308
  keywordPositions
23077
23309
  }];
23078
23310
  });
23079
- 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));
23080
- 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));
23081
23313
  return domainEvidence({
23082
23314
  capability: "serp-competitors",
23083
23315
  data: {
@@ -23992,7 +24224,7 @@ function analyzeCompetitorGap(acquisition) {
23992
24224
  }
23993
24225
  //#endregion
23994
24226
  //#region packages/core/dist/analyze/domain-research/gap-report.js
23995
- const MAX_FINDINGS = 10;
24227
+ const MAX_FINDINGS$2 = 10;
23996
24228
  const MAX_SOURCE_BRIEFS = 5;
23997
24229
  function count(candidates, classification) {
23998
24230
  return candidates.filter((item) => item.classification === classification).length;
@@ -24007,7 +24239,7 @@ function dataStatus(input) {
24007
24239
  function findings$2(input) {
24008
24240
  const result = [];
24009
24241
  for (const [index, candidate] of input.candidates.entries()) {
24010
- if (result.length >= MAX_FINDINGS) break;
24242
+ if (result.length >= MAX_FINDINGS$2) break;
24011
24243
  if (candidate.classification === "relevant-gap-candidate") result.push({
24012
24244
  code: "relevant-competitor-gap",
24013
24245
  evidenceRefs: [`candidates[${index}]`, ...candidate.evidenceRefs],
@@ -24022,7 +24254,7 @@ function findings$2(input) {
24022
24254
  });
24023
24255
  }
24024
24256
  for (const [index, pattern] of input.patterns.entries()) {
24025
- if (result.length >= MAX_FINDINGS) break;
24257
+ if (result.length >= MAX_FINDINGS$2) break;
24026
24258
  result.push({
24027
24259
  code: "competitor-template-pattern",
24028
24260
  evidenceRefs: [`repeatedCompetitorPatterns[${index}]`, ...pattern.evidenceRefs],
@@ -25069,7 +25301,7 @@ function measurementWindow(input) {
25069
25301
  }
25070
25302
  //#endregion
25071
25303
  //#region packages/core/dist/analyze/experiments/measure-change.js
25072
- const defaultDependencies$5 = {
25304
+ const defaultDependencies$6 = {
25073
25305
  searchAnalytics: querySearchAnalytics,
25074
25306
  googleAnalyticsReport: runGa4Report,
25075
25307
  contentGroup: getContentGroup,
@@ -25087,7 +25319,7 @@ function counterfactualDelta(input) {
25087
25319
  const expectedAfter = input.treatmentBefore * (input.controlAfter / input.controlBefore);
25088
25320
  return fixed(input.treatmentAfter - expectedAfter);
25089
25321
  }
25090
- async function measureChange(input, dependencies = defaultDependencies$5) {
25322
+ async function measureChange(input, dependencies = defaultDependencies$6) {
25091
25323
  const now = dependencies.now();
25092
25324
  const change = (input.id ? getChange(input.id) : void 0) ?? {
25093
25325
  id: "adhoc",
@@ -25810,14 +26042,14 @@ async function verifyInternalLinkCandidate(input) {
25810
26042
  }
25811
26043
  //#endregion
25812
26044
  //#region packages/core/dist/analyze/internal-links.js
25813
- const MAX_GSC_ROWS$1 = 1e5;
25814
- const defaultDependencies$4 = {
26045
+ const MAX_GSC_ROWS$2 = 1e5;
26046
+ const defaultDependencies$5 = {
25815
26047
  searchAnalytics: querySearchAnalytics,
25816
26048
  fetch: fetchPage,
25817
26049
  extract: extractPage,
25818
26050
  now: () => /* @__PURE__ */ new Date()
25819
26051
  };
25820
- async function internalLinksReport(input, dependencies = defaultDependencies$4) {
26052
+ async function internalLinksReport(input, dependencies = defaultDependencies$5) {
25821
26053
  const targetPage = new URL(assertUrlMatchesGscProperty(input.site, input.targetUrl));
25822
26054
  targetPage.hash = "";
25823
26055
  const targetUrl = targetPage.toString();
@@ -25866,7 +26098,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
25866
26098
  dimensions: ["query", "page"],
25867
26099
  type: "web",
25868
26100
  dataState: "final",
25869
- maxRows: MAX_GSC_ROWS$1,
26101
+ maxRows: MAX_GSC_ROWS$2,
25870
26102
  dimensionFilterGroups: [{
25871
26103
  groupType: "and",
25872
26104
  filters: [{
@@ -25898,7 +26130,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
25898
26130
  dimensions: ["query", "page"],
25899
26131
  type: "web",
25900
26132
  dataState: "final",
25901
- maxRows: MAX_GSC_ROWS$1
26133
+ maxRows: MAX_GSC_ROWS$2
25902
26134
  }, { refresh: input.refresh }) : {
25903
26135
  rows: [],
25904
26136
  calls: 0,
@@ -25943,8 +26175,8 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
25943
26175
  else checkedSources += 1;
25944
26176
  }
25945
26177
  const uncheckedCandidates = Math.max(0, analysis.candidates.length - attemptedSources);
25946
- const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$1);
25947
- 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;
25948
26180
  const invalidRows = analysis.selection.targetInvalidRows + analysis.selection.sourceInvalidRows;
25949
26181
  const structuredWarnings = uniqueInternalLinksWarnings(warnings);
25950
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";
@@ -25985,18 +26217,18 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
25985
26217
  pageFilter: alias,
25986
26218
  rowsFetched: result.rowsFetched,
25987
26219
  calls: result.calls,
25988
- possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$1
26220
+ possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$2
25989
26221
  })),
25990
26222
  rowsFetched: targetRequests.reduce((sum, { result }) => sum + result.rowsFetched, 0),
25991
26223
  calls: targetRequests.reduce((sum, { result }) => sum + result.calls, 0),
25992
- maxRowsPerRequest: MAX_GSC_ROWS$1,
26224
+ maxRowsPerRequest: MAX_GSC_ROWS$2,
25993
26225
  possiblyTruncated: targetTruncated
25994
26226
  },
25995
26227
  candidates: {
25996
26228
  queried: queryCandidates,
25997
26229
  rowsFetched: sourceResult.rowsFetched,
25998
26230
  calls: sourceResult.calls,
25999
- maxRows: MAX_GSC_ROWS$1,
26231
+ maxRows: MAX_GSC_ROWS$2,
26000
26232
  possiblyTruncated: sourceTruncated
26001
26233
  },
26002
26234
  completeness: !queryCandidates ? "not-queried" : targetTruncated || sourceTruncated ? "possibly-truncated" : "retained-rows-only"
@@ -26015,7 +26247,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
26015
26247
  minImpressions,
26016
26248
  resultLimit: limit,
26017
26249
  checkLimit,
26018
- maxGscRowsPerRequest: MAX_GSC_ROWS$1
26250
+ maxGscRowsPerRequest: MAX_GSC_ROWS$2
26019
26251
  },
26020
26252
  target: {
26021
26253
  requestedUrl: target.requestedUrl,
@@ -26952,8 +27184,8 @@ function analyzeSecondPageRows(input) {
26952
27184
  }
26953
27185
  //#endregion
26954
27186
  //#region packages/core/dist/analyze/keyword-opportunities.js
26955
- const DEFAULT_DAYS = 28;
26956
- const MAX_DAYS = 548;
27187
+ const DEFAULT_DAYS$1 = 28;
27188
+ const MAX_DAYS$1 = 548;
26957
27189
  const DEFAULT_REPORT_LIMIT = 10;
26958
27190
  const MAX_REPORT_LIMIT = 25;
26959
27191
  const DEFAULT_KEYWORD_LIMIT = 30;
@@ -27000,9 +27232,9 @@ function validateInput$2(input) {
27000
27232
  site,
27001
27233
  days: integerOption$1({
27002
27234
  value: input.days,
27003
- fallback: DEFAULT_DAYS,
27235
+ fallback: DEFAULT_DAYS$1,
27004
27236
  minimum: 1,
27005
- maximum: MAX_DAYS,
27237
+ maximum: MAX_DAYS$1,
27006
27238
  label: "Days"
27007
27239
  }),
27008
27240
  limit: integerOption$1({
@@ -28537,7 +28769,7 @@ function analyzeIndexCoverageSignals(input) {
28537
28769
  const MAX_SITEMAP_DOCUMENTS = 20;
28538
28770
  const MAX_SEARCH_ROWS = 25e4;
28539
28771
  const MAX_SITEMAP_URLS = 25e4;
28540
- const defaultDependencies$3 = {
28772
+ const defaultDependencies$4 = {
28541
28773
  loadCrawl: loadCrawlReport,
28542
28774
  latestCrawl: latestCrawlReport,
28543
28775
  searchAnalytics: querySearchAnalytics,
@@ -28569,7 +28801,7 @@ function sitemapCompleteness(input) {
28569
28801
  if (input.inventoryTruncated) return "truncated";
28570
28802
  return input.results.every((result) => result.dataStatus === "complete") ? "complete" : "partial";
28571
28803
  }
28572
- async function indexCoverageSignals(input, dependencies = defaultDependencies$3) {
28804
+ async function indexCoverageSignals(input, dependencies = defaultDependencies$4) {
28573
28805
  const days = integerOption$2({
28574
28806
  value: input.days,
28575
28807
  fallback: 90,
@@ -29052,7 +29284,7 @@ function pruneIndexWatchSnapshots(rootSite, url, database = getDb()) {
29052
29284
  WHERE attempts.attempt_rank > ? AND latest_success.id IS NULL
29053
29285
  )`).run(rootSite, url, rootSite, url, 20).changes;
29054
29286
  }
29055
- const defaultDependencies$2 = {
29287
+ const defaultDependencies$3 = {
29056
29288
  inspectUrl,
29057
29289
  now: () => /* @__PURE__ */ new Date(),
29058
29290
  store: {
@@ -29097,7 +29329,7 @@ function isFatalBatchError(error) {
29097
29329
  "AUTH_REQUIRED"
29098
29330
  ].includes(error.code);
29099
29331
  }
29100
- async function indexWatch(input, dependencies = defaultDependencies$2) {
29332
+ async function indexWatch(input, dependencies = defaultDependencies$3) {
29101
29333
  const languageCode = validateLanguageCode(input.languageCode);
29102
29334
  const dailyLimit = integerOption$2({
29103
29335
  value: input.dailyLimit,
@@ -31192,9 +31424,9 @@ function pseoSampleUrls(report) {
31192
31424
  }
31193
31425
  //#endregion
31194
31426
  //#region packages/core/dist/analyze/pseo/audit.js
31195
- const MAX_GSC_ROWS = 5e4;
31427
+ const MAX_GSC_ROWS$1 = 5e4;
31196
31428
  const DEFAULT_SITEMAP_URLS = 5e4;
31197
- const defaultDependencies$1 = {
31429
+ const defaultDependencies$2 = {
31198
31430
  searchAnalytics: querySearchAnalytics,
31199
31431
  fetchPage,
31200
31432
  inspectUrl,
@@ -31396,7 +31628,7 @@ async function inspectSample(input) {
31396
31628
  };
31397
31629
  }
31398
31630
  }
31399
- async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$1) {
31631
+ async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$2) {
31400
31632
  const options = pseoAuditOptions(input);
31401
31633
  const now = dependencies.now();
31402
31634
  const generatedAt = now.toISOString();
@@ -31415,7 +31647,7 @@ async function acquirePseoAuditEvidence(input, dependencies = defaultDependencie
31415
31647
  type: "web",
31416
31648
  dataState: "final",
31417
31649
  aggregationType: "auto",
31418
- maxRows: MAX_GSC_ROWS
31650
+ maxRows: MAX_GSC_ROWS$1
31419
31651
  };
31420
31652
  input.progress?.(`Fetching ${options.days} days of GSC page and query/page rows`);
31421
31653
  const [pageResponse, queryPageResponse] = await Promise.all([dependencies.searchAnalytics(input.site, {
@@ -31466,7 +31698,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
31466
31698
  minimumTemplateImpressions: options.minimumTemplateImpressions,
31467
31699
  crawlSamplesPerTemplate: options.crawlSamples,
31468
31700
  inspectionSamplesPerTemplate: options.inspectSamples,
31469
- maxRowsPerRequest: MAX_GSC_ROWS,
31701
+ maxRowsPerRequest: MAX_GSC_ROWS$1,
31470
31702
  pageRowsFetched: acquisition.pageRowsFetched,
31471
31703
  queryPageRowsFetched: acquisition.queryPageRowsFetched,
31472
31704
  sitemapsRequested: input.sitemaps?.length ?? 0,
@@ -31477,7 +31709,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
31477
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."]
31478
31710
  });
31479
31711
  }
31480
- async function pseoAuditReport(input, dependencies = defaultDependencies$1) {
31712
+ async function pseoAuditReport(input, dependencies = defaultDependencies$2) {
31481
31713
  const acquisition = await acquirePseoAuditEvidence(input, dependencies);
31482
31714
  const sampleUrls = pseoSampleUrls(buildPseoAuditFromAcquisition(input, acquisition));
31483
31715
  const topQueries = topQueryByUrl(acquisition.queryPageRows, input);
@@ -32319,7 +32551,7 @@ function pseoOpportunityVerdict(input) {
32319
32551
  //#region packages/core/dist/providers/dataforseo/serp-snapshot.js
32320
32552
  const MAX_SERP_DEPTH = 100;
32321
32553
  const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
32322
- function safeUrl(value) {
32554
+ function safeUrl$1(value) {
32323
32555
  if (!value) return null;
32324
32556
  try {
32325
32557
  const parsed = new URL(value);
@@ -32332,11 +32564,11 @@ function safeUrl(value) {
32332
32564
  }
32333
32565
  }
32334
32566
  function validOrganic(item) {
32335
- 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;
32336
32568
  }
32337
32569
  function organicResult(item) {
32338
32570
  if (!validOrganic(item)) return null;
32339
- const url = safeUrl(item.url);
32571
+ const url = safeUrl$1(item.url);
32340
32572
  if (!url) return null;
32341
32573
  return {
32342
32574
  rankGroup: item.rank_group,
@@ -32385,7 +32617,7 @@ function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
32385
32617
  field: "checkedAt",
32386
32618
  message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
32387
32619
  });
32388
- const checkUrl = safeUrl(result.check_url);
32620
+ const checkUrl = safeUrl$1(result.check_url);
32389
32621
  if (result.check_url && !checkUrl) warnings.push({
32390
32622
  code: "invalid-serp-check-url",
32391
32623
  field: "checkUrl",
@@ -34528,7 +34760,7 @@ function groupedMappings(evidence) {
34528
34760
  //#endregion
34529
34761
  //#region packages/core/dist/analyze/second-page.js
34530
34762
  const MAX_SOURCE_ROWS = 1e5;
34531
- const defaultDependencies = {
34763
+ const defaultDependencies$1 = {
34532
34764
  searchAnalytics: querySearchAnalytics,
34533
34765
  fetch: fetchPage,
34534
34766
  extract: extractPage,
@@ -34605,7 +34837,7 @@ function reportRecommendations(items) {
34605
34837
  "Treat the priority score as a triage heuristic, not an estimated traffic lift."
34606
34838
  ]);
34607
34839
  }
34608
- async function secondPage(input, dependencies = defaultDependencies) {
34840
+ async function secondPage(input, dependencies = defaultDependencies$1) {
34609
34841
  const days = integerOption({
34610
34842
  value: input.range,
34611
34843
  fallback: 28,
@@ -36796,10 +37028,10 @@ const DEFAULT_TARGET_LIMIT = 20;
36796
37028
  const MAX_TARGET_LIMIT = 50;
36797
37029
  const DEFAULT_DETAIL_PAGES = 1;
36798
37030
  const MAX_DETAIL_PAGES = 3;
36799
- const DEFAULT_ROW_LIMIT$2 = 500;
36800
- const MAX_ROW_LIMIT$2 = 1e3;
37031
+ const DEFAULT_ROW_LIMIT$3 = 500;
37032
+ const MAX_ROW_LIMIT$3 = 1e3;
36801
37033
  const MAX_CONCURRENCY = 3;
36802
- function integer(value, fallback, max, name) {
37034
+ function integer$1(value, fallback, max, name) {
36803
37035
  const resolved = value ?? fallback;
36804
37036
  if (!Number.isInteger(resolved) || resolved < 1 || resolved > max) throw new SeoError("INVALID_INPUT", `${name} must be between 1 and ${max}.`);
36805
37037
  return resolved;
@@ -36860,9 +37092,9 @@ async function targetDetails(input) {
36860
37092
  }
36861
37093
  async function collectBingLinkEvidence(input) {
36862
37094
  if (!input.site.trim()) throw new SeoError("INVALID_INPUT", "Pass a Bing Webmaster site URL.");
36863
- const targetLimit = integer(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
36864
- const detailPages = integer(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
36865
- 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");
36866
37098
  const client = input.client ?? (await createBingWebmasterClient()).client;
36867
37099
  const countRows = [];
36868
37100
  let countPagesRequested = 0;
@@ -36983,6 +37215,884 @@ async function collectBingLinkEvidence(input) {
36983
37215
  };
36984
37216
  }
36985
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
36986
38096
  //#region packages/core/dist/links/import.js
36987
38097
  const DEFAULT_ROW_LIMIT$1 = 1e4;
36988
38098
  const MAX_ROW_LIMIT$1 = 1e5;
@@ -37171,6 +38281,10 @@ async function importLinkEvidence(input) {
37171
38281
  //#region packages/core/dist/links/report.js
37172
38282
  const DEFAULT_OUTPUT_LIMIT = 100;
37173
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;
37174
38288
  function boundedLimit(value) {
37175
38289
  const limit = value ?? DEFAULT_OUTPUT_LIMIT;
37176
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}.`);
@@ -37182,7 +38296,7 @@ function targetCounts(rows) {
37182
38296
  return [...counts].map(([targetUrl, observedLinks]) => ({
37183
38297
  targetUrl,
37184
38298
  observedLinks
37185
- })).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));
37186
38300
  }
37187
38301
  function linkEvidenceReport(input) {
37188
38302
  const limit = boundedLimit(input.limit);
@@ -37190,8 +38304,24 @@ function linkEvidenceReport(input) {
37190
38304
  const domains = new Set(input.evidence.rows.map((row) => row.sourceDomain));
37191
38305
  const observedTargets = targetCounts(input.evidence.rows);
37192
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;
37193
38323
  return {
37194
- schemaVersion: 1,
38324
+ schemaVersion: 2,
37195
38325
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
37196
38326
  dataStatus: input.evidence.provenance.completeness === "complete" ? "complete" : "partial",
37197
38327
  summary: {
@@ -37200,6 +38330,8 @@ function linkEvidenceReport(input) {
37200
38330
  targetPages: new Set(input.evidence.rows.map((row) => row.targetUrl)).size,
37201
38331
  providerTargetPages: counts.length
37202
38332
  },
38333
+ providerSummary: providerEvidence?.summary.data ?? null,
38334
+ providerEvidence,
37203
38335
  provenance: input.evidence.provenance,
37204
38336
  selection: {
37205
38337
  availableRows: input.evidence.rows.length,
@@ -37207,13 +38339,45 @@ function linkEvidenceReport(input) {
37207
38339
  omittedRows: Math.max(0, input.evidence.rows.length - rows.length),
37208
38340
  limit
37209
38341
  },
37210
- 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,
37211
38367
  links: rows,
37212
- 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 ?? []],
37213
38375
  caveats: [
37214
38376
  "This is bounded provider or imported evidence, not a complete backlink index.",
37215
38377
  "A missing link is not proof that no link exists, and an observed link is not a quality or ranking verdict.",
37216
- "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."] : []
37217
38381
  ]
37218
38382
  };
37219
38383
  }
@@ -40635,9 +41799,14 @@ function registerProviderTools(server) {
40635
41799
  }
40636
41800
  });
40637
41801
  server.registerTool("seo_link_evidence", {
40638
- 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",
40639
41803
  inputSchema: {
40640
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(),
40641
41810
  file: z$1.string().min(1).max(4096).optional(),
40642
41811
  format: z$1.enum([
40643
41812
  "csv",
@@ -40647,23 +41816,43 @@ function registerProviderTools(server) {
40647
41816
  rowLimit: z$1.number().int().min(1).max(1e5).optional(),
40648
41817
  targetLimit: z$1.number().int().min(1).max(50).optional(),
40649
41818
  detailPagesPerTarget: z$1.number().int().min(1).max(3).optional(),
40650
- 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()
40651
41822
  }
40652
- }, async ({ site, file, format, rowLimit, targetLimit, detailPagesPerTarget, limit }) => {
41823
+ }, async ({ site, provider, target, scope, includeSubdomains, searchConsoleSite, file, format, rowLimit, targetLimit, detailPagesPerTarget, limit, days, refresh }) => {
40653
41824
  try {
40654
- 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
+ });
40655
41846
  const report = linkEvidenceReport({
40656
- evidence: file ? await importLinkEvidence({
40657
- file,
40658
- format,
40659
- rowLimit
40660
- }) : await collectBingLinkEvidence({
40661
- site: site ?? "",
40662
- rowLimit,
40663
- targetLimit,
40664
- detailPagesPerTarget
40665
- }),
40666
- 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
40667
41856
  });
40668
41857
  return toolSuccess(`${report.summary.observedLinks} referring links were retained from ${report.provenance.provider}.`, report);
40669
41858
  } catch (error) {
@@ -41078,15 +42267,18 @@ const REPORT_DEPTH = {
41078
42267
  "link-evidence": {
41079
42268
  readOrder: [
41080
42269
  "dataStatus, provenance, warnings, and caveats",
41081
- "selection and summary coverage counts",
41082
- "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"
41083
42274
  ],
41084
42275
  doNotClaim: [
41085
- "The retained rows are not a complete backlink index.",
41086
- "Missing rows do not prove a link does not exist.",
41087
- "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."
41088
42280
  ],
41089
- 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.",
41090
42282
  related: [
41091
42283
  {
41092
42284
  id: "link-recovery",
@@ -41099,6 +42291,10 @@ const REPORT_DEPTH = {
41099
42291
  {
41100
42292
  id: "site-crawl",
41101
42293
  reason: "Checks internal links and live pages."
42294
+ },
42295
+ {
42296
+ id: "search-performance-overview",
42297
+ reason: "Adds broader first-party search context."
41102
42298
  }
41103
42299
  ]
41104
42300
  },
@@ -42827,10 +44023,14 @@ const REPORT_GUIDANCE = {
42827
44023
  },
42828
44024
  "link-evidence": {
42829
44025
  name: "Referring link evidence",
42830
- description: "Review a bounded set of pages that link to the site from Bing Webmaster or a local export.",
42831
- 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."],
42832
- avoidWhen: ["You need a complete backlink index or a domain authority score.", "You want to find broken internal links during a crawl."],
42833
- 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."
42834
44034
  },
42835
44035
  "server-log-analysis": {
42836
44036
  name: "Server log crawler analysis",