seo 0.2.21 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
2
- import { n as version, t as load } from "./esm-CSE-hgHf.js";
2
+ import { n as version, t as load } from "./esm-insIkRuo.js";
3
3
  import { Headers, fetch as fetch$1 } from "undici";
4
4
  import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
5
  import { arch, homedir, platform, totalmem } from "node:os";
@@ -2249,7 +2249,7 @@ function aiReferralSourceForValue(value) {
2249
2249
  }
2250
2250
  //#endregion
2251
2251
  //#region packages/core/src/analyze/ai-referrals-analysis.ts
2252
- function compareText$18(left, right) {
2252
+ function compareText$19(left, right) {
2253
2253
  return left < right ? -1 : left > right ? 1 : 0;
2254
2254
  }
2255
2255
  function metric$1(row, name) {
@@ -2338,7 +2338,7 @@ function analyzeAiReferralRows(input) {
2338
2338
  const generalCaveat = "Google Analytics only attributes visits that arrive with a recognized session source; some AI visits appear as direct, unassigned, or another source.";
2339
2339
  const possiblyTruncated = input.sourceDiscovery.truncated || input.detail.truncated || input.totalUsersEvidence.truncated;
2340
2340
  const allLandingPages = [...pageTotals.entries()].map(([landingPage, metrics]) => {
2341
- const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$18(left[0], right[0]))[0]?.[0];
2341
+ const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$19(left[0], right[0]))[0]?.[0];
2342
2342
  const topSource = topSourceId ? sourceTotals.get(topSourceId)?.definition : void 0;
2343
2343
  if (!topSource) throw new SeoError("PROVIDER_UNAVAILABLE", "Could not resolve the top AI referral source.");
2344
2344
  return {
@@ -2352,7 +2352,7 @@ function analyzeAiReferralRows(input) {
2352
2352
  label: topSource.label
2353
2353
  }
2354
2354
  };
2355
- }).sort((left, right) => right.sessions - left.sessions || compareText$18(left.landingPage, right.landingPage));
2355
+ }).sort((left, right) => right.sessions - left.sessions || compareText$19(left.landingPage, right.landingPage));
2356
2356
  const landingPages = allLandingPages.slice(0, input.resultLimit);
2357
2357
  const selection = {
2358
2358
  limit: input.resultLimit,
@@ -2397,19 +2397,19 @@ function analyzeAiReferralRows(input) {
2397
2397
  id: source.definition.id,
2398
2398
  label: source.definition.label,
2399
2399
  source: source.definition.label,
2400
- observedSessionSources: [...source.observedSessionSources].sort(compareText$18),
2400
+ observedSessionSources: [...source.observedSessionSources].sort(compareText$19),
2401
2401
  sessions: source.sessions,
2402
2402
  eventCount: source.eventCount,
2403
2403
  totalUsers: null,
2404
2404
  shareOfAiSessions: total.sessions ? source.sessions / total.sessions : 0,
2405
2405
  share: total.sessions ? source.sessions / total.sessions : 0
2406
- })).sort((left, right) => right.sessions - left.sessions || compareText$18(left.id, right.id)),
2406
+ })).sort((left, right) => right.sessions - left.sessions || compareText$19(left.id, right.id)),
2407
2407
  landingPages,
2408
2408
  daily: [...dailyTotals.entries()].map(([date, metrics]) => ({
2409
2409
  date,
2410
2410
  ...metrics,
2411
2411
  totalUsers: null
2412
- })).sort((left, right) => compareText$18(left.date, right.date)),
2412
+ })).sort((left, right) => compareText$19(left.date, right.date)),
2413
2413
  caveats: [
2414
2414
  generalCaveat,
2415
2415
  "Attribution uses Google Analytics sessionSource only; landing-page text, medium text, and event referrers are never treated as referral sources.",
@@ -2499,7 +2499,7 @@ function sessionSourceInListFilter(values) {
2499
2499
  //#endregion
2500
2500
  //#region packages/core/src/analyze/ai-referrals.ts
2501
2501
  const DEFAULT_MAX_ROWS$1 = 1e5;
2502
- const MAX_ROWS = 1e5;
2502
+ const MAX_ROWS$1 = 1e5;
2503
2503
  const DEFAULT_RESULT_LIMIT = 25;
2504
2504
  const MAX_RESULT_LIMIT = 1e3;
2505
2505
  function validIsoDate$1(value) {
@@ -2525,7 +2525,7 @@ function reportRange(input) {
2525
2525
  function maxRows(input) {
2526
2526
  if (input.maxRows !== void 0 && input.limit !== void 0 && input.maxRows !== input.limit) throw new SeoError("INVALID_INPUT", "Use maxRows or the legacy limit option, not both.");
2527
2527
  const value = input.maxRows ?? input.limit ?? DEFAULT_MAX_ROWS$1;
2528
- if (!Number.isInteger(value) || value < 1 || value > MAX_ROWS) throw new SeoError("INVALID_INPUT", `maxRows must be a whole number between 1 and ${MAX_ROWS}.`);
2528
+ if (!Number.isInteger(value) || value < 1 || value > MAX_ROWS$1) throw new SeoError("INVALID_INPUT", `maxRows must be a whole number between 1 and ${MAX_ROWS$1}.`);
2529
2529
  return value;
2530
2530
  }
2531
2531
  function resultLimit(input) {
@@ -5948,7 +5948,7 @@ function tokenStem(token) {
5948
5948
  function tokensMatch(a, b) {
5949
5949
  return a === b || tokenStem(a) === tokenStem(b);
5950
5950
  }
5951
- function includesTokenSequence(queryTokens, termTokens) {
5951
+ function includesTokenSequence$1(queryTokens, termTokens) {
5952
5952
  if (!termTokens.length || termTokens.length > queryTokens.length) return false;
5953
5953
  for (let index = 0; index <= queryTokens.length - termTokens.length; index++) if (termTokens.every((termToken, offset) => tokensMatch(queryTokens[index + offset] ?? "", termToken))) return true;
5954
5954
  return false;
@@ -5977,7 +5977,7 @@ function isBrandQuery(query, brandTerms) {
5977
5977
  const compactMatches = (compactTermLength >= 6 || /[^\p{ASCII}]/u.test(compactTerm) && compactTermLength >= 2) && compactQuery.includes(compactTerm);
5978
5978
  const termTokens = normalizedTerm.split(" ");
5979
5979
  if (termTokens.length === 1) return queryTokens.some((queryToken) => tokensMatch(queryToken, normalizedTerm)) || compactMatches;
5980
- return includesTokenSequence(queryTokens, termTokens) || compactMatches;
5980
+ return includesTokenSequence$1(queryTokens, termTokens) || compactMatches;
5981
5981
  });
5982
5982
  }
5983
5983
  function shouldExcludeBrandQuery(input) {
@@ -6289,7 +6289,7 @@ function resolveQueryOpportunityInput(input, now) {
6289
6289
  }
6290
6290
  //#endregion
6291
6291
  //#region packages/core/src/analyze/query-opportunity-rows.ts
6292
- function compareText$17(left, right) {
6292
+ function compareText$18(left, right) {
6293
6293
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
6294
6294
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
6295
6295
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
@@ -6299,9 +6299,9 @@ function compareText$17(left, right) {
6299
6299
  return leftPoints.length - rightPoints.length;
6300
6300
  }
6301
6301
  function compareQueryOpportunityRows(left, right) {
6302
- return right.impressions - left.impressions || right.clicks - left.clicks || left.position - right.position || compareText$17(left.query, right.query);
6302
+ return right.impressions - left.impressions || right.clicks - left.clicks || left.position - right.position || compareText$18(left.query, right.query);
6303
6303
  }
6304
- function validRow$3(row, query) {
6304
+ function validRow$4(row, query) {
6305
6305
  return row.keys.length === 1 && Boolean(query) && Number.isFinite(row.clicks) && Number.isFinite(row.impressions) && Number.isFinite(row.ctr) && Number.isFinite(row.position) && row.clicks >= 0 && row.impressions > 0 && row.clicks <= row.impressions && row.ctr >= 0 && row.ctr <= 1 && row.position > 0;
6306
6306
  }
6307
6307
  function signature(row) {
@@ -6311,7 +6311,7 @@ function normalizeQueryOpportunityRows(rows, selection) {
6311
6311
  const grouped = /* @__PURE__ */ new Map();
6312
6312
  for (const row of rows) {
6313
6313
  const query = row.keys[0]?.trim() ?? "";
6314
- if (!validRow$3(row, query)) {
6314
+ if (!validRow$4(row, query)) {
6315
6315
  selection.invalidRows++;
6316
6316
  continue;
6317
6317
  }
@@ -7323,11 +7323,11 @@ function selectBenchmarkRows$1(input) {
7323
7323
  }
7324
7324
  //#endregion
7325
7325
  //#region packages/core/src/analyze/page-opportunities-analysis.ts
7326
- function compareText$16(left, right) {
7326
+ function compareText$17(left, right) {
7327
7327
  return left < right ? -1 : left > right ? 1 : 0;
7328
7328
  }
7329
7329
  function compareItems$2(left, right) {
7330
- return (right.estimatedCtrClickShortfall ?? -1) - (left.estimatedCtrClickShortfall ?? -1) || right.impressions - left.impressions || left.position - right.position || compareText$16(left.query, right.query) || compareText$16(left.url, right.url);
7330
+ return (right.estimatedCtrClickShortfall ?? -1) - (left.estimatedCtrClickShortfall ?? -1) || right.impressions - left.impressions || left.position - right.position || compareText$17(left.query, right.query) || compareText$17(left.url, right.url);
7331
7331
  }
7332
7332
  function analyzeRow(input) {
7333
7333
  const query = input.row.keys[0]?.trim() ?? "";
@@ -12024,19 +12024,19 @@ function sanitizeIssues(issues) {
12024
12024
  function sanitizeMessages(values = []) {
12025
12025
  return values.map((value) => sanitizeTenantString(value));
12026
12026
  }
12027
- function compareText$15(left, right) {
12027
+ function compareText$16(left, right) {
12028
12028
  return left < right ? -1 : left > right ? 1 : 0;
12029
12029
  }
12030
12030
  function sortPages(pages) {
12031
- return [...pages].sort((left, right) => compareText$15(left.url, right.url));
12031
+ return [...pages].sort((left, right) => compareText$16(left.url, right.url));
12032
12032
  }
12033
12033
  function sortRequests(requests) {
12034
12034
  return [...requests].sort((left, right) => {
12035
- const byUrl = compareText$15(left.requestedUrl, right.requestedUrl);
12035
+ const byUrl = compareText$16(left.requestedUrl, right.requestedUrl);
12036
12036
  if (byUrl) return byUrl;
12037
- const byOutcome = compareText$15(left.outcome, right.outcome);
12037
+ const byOutcome = compareText$16(left.outcome, right.outcome);
12038
12038
  if (byOutcome) return byOutcome;
12039
- return compareText$15(left.outcome === "response" ? `${left.status}:${left.finalUrl}` : left.outcome === "skipped" ? left.reason : `${left.failureKind}:${left.error}`, right.outcome === "response" ? `${right.status}:${right.finalUrl}` : right.outcome === "skipped" ? right.reason : `${right.failureKind}:${right.error}`);
12039
+ return compareText$16(left.outcome === "response" ? `${left.status}:${left.finalUrl}` : left.outcome === "skipped" ? left.reason : `${left.failureKind}:${left.error}`, right.outcome === "response" ? `${right.status}:${right.finalUrl}` : right.outcome === "skipped" ? right.reason : `${right.failureKind}:${right.error}`);
12040
12040
  });
12041
12041
  }
12042
12042
  function summarizeCrawlReport(input) {
@@ -12258,8 +12258,8 @@ function createCrawlReport(input) {
12258
12258
  ...input.externalLinkVerification ? { externalLinkVerification: sanitizeTenantValue(input.externalLinkVerification) } : {},
12259
12259
  ...input.siteChecks ? { siteChecks: sanitizeTenantValue(input.siteChecks) } : {},
12260
12260
  ...input.agentDiscovery ? { agentDiscovery: sanitizeTenantValue(input.agentDiscovery) } : {},
12261
- warnings: sanitizeMessages(input.warnings).sort(compareText$15),
12262
- caveats: sanitizeMessages(input.caveats).sort(compareText$15)
12261
+ warnings: sanitizeMessages(input.warnings).sort(compareText$16),
12262
+ caveats: sanitizeMessages(input.caveats).sort(compareText$16)
12263
12263
  };
12264
12264
  }
12265
12265
  function normalizeLoadedCrawlReport(report) {
@@ -12289,8 +12289,8 @@ function normalizeLoadedCrawlReport(report) {
12289
12289
  pages,
12290
12290
  issues,
12291
12291
  issueGroups: groupCrawlIssues(issues),
12292
- warnings: sanitizeMessages(report.warnings).sort(compareText$15),
12293
- caveats: sanitizeMessages(report.caveats).sort(compareText$15)
12292
+ warnings: sanitizeMessages(report.warnings).sort(compareText$16),
12293
+ caveats: sanitizeMessages(report.caveats).sort(compareText$16)
12294
12294
  };
12295
12295
  }
12296
12296
  //#endregion
@@ -14167,12 +14167,12 @@ function urlPath(url) {
14167
14167
  return "";
14168
14168
  }
14169
14169
  }
14170
- function compareText$14(left, right) {
14170
+ function compareText$15(left, right) {
14171
14171
  return left < right ? -1 : left > right ? 1 : 0;
14172
14172
  }
14173
14173
  function landingPageValuesFromRows(rows) {
14174
14174
  const orderedRows = [...rows].sort((left, right) => {
14175
- return compareText$14(normalizePath(left.landingPagePlusQueryString ?? ""), normalizePath(right.landingPagePlusQueryString ?? "")) || compareText$14(left.landingPagePlusQueryString ?? "", right.landingPagePlusQueryString ?? "") || compareText$14(left.sessions ?? "", right.sessions ?? "") || compareText$14(left.totalUsers ?? "", right.totalUsers ?? "") || compareText$14(left.conversions ?? "", right.conversions ?? "");
14175
+ return compareText$15(normalizePath(left.landingPagePlusQueryString ?? ""), normalizePath(right.landingPagePlusQueryString ?? "")) || compareText$15(left.landingPagePlusQueryString ?? "", right.landingPagePlusQueryString ?? "") || compareText$15(left.sessions ?? "", right.sessions ?? "") || compareText$15(left.totalUsers ?? "", right.totalUsers ?? "") || compareText$15(left.conversions ?? "", right.conversions ?? "");
14176
14176
  });
14177
14177
  const values = /* @__PURE__ */ new Map();
14178
14178
  for (const row of orderedRows) {
@@ -16858,7 +16858,7 @@ function countLabel(count, singular, pluralLabel = `${singular}s`) {
16858
16858
  //#region packages/core/src/analyze/crawler/okf.ts
16859
16859
  const OKF_DEFAULT_CONCEPTS = 500;
16860
16860
  const OKF_MAX_CONCEPTS = 5e3;
16861
- function compareText$13(left, right) {
16861
+ function compareText$14(left, right) {
16862
16862
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
16863
16863
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
16864
16864
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
@@ -16948,7 +16948,7 @@ function okfTitle(value, report) {
16948
16948
  }
16949
16949
  function selectedOkfPages(report, requestedLimit) {
16950
16950
  const limit = okfConceptLimit(requestedLimit);
16951
- const eligible = report.pages.filter((page) => page.indexable && page.status >= 200 && page.status < 300 && !page.blocked && !page.error).sort((left, right) => (right.searchMetrics?.clicks ?? 0) - (left.searchMetrics?.clicks ?? 0) || (right.searchMetrics?.impressions ?? 0) - (left.searchMetrics?.impressions ?? 0) || (right.internalLinkAuthorityScore ?? 0) - (left.internalLinkAuthorityScore ?? 0) || (right.internalInlinkCount ?? 0) - (left.internalInlinkCount ?? 0) || compareText$13(left.finalUrl, right.finalUrl));
16951
+ const eligible = report.pages.filter((page) => page.indexable && page.status >= 200 && page.status < 300 && !page.blocked && !page.error).sort((left, right) => (right.searchMetrics?.clicks ?? 0) - (left.searchMetrics?.clicks ?? 0) || (right.searchMetrics?.impressions ?? 0) - (left.searchMetrics?.impressions ?? 0) || (right.internalLinkAuthorityScore ?? 0) - (left.internalLinkAuthorityScore ?? 0) || (right.internalInlinkCount ?? 0) - (left.internalInlinkCount ?? 0) || compareText$14(left.finalUrl, right.finalUrl));
16952
16952
  const unique = /* @__PURE__ */ new Map();
16953
16953
  for (const page of eligible) if (!unique.has(page.finalUrl)) unique.set(page.finalUrl, page);
16954
16954
  const uniquePages = [...unique.values()];
@@ -18446,7 +18446,7 @@ const CTR_DEFAULT_MIN_IMPRESSIONS = 200;
18446
18446
  const CTR_MAX_MIN_IMPRESSIONS = 1e9;
18447
18447
  const CTR_DEFAULT_LIMIT = 25;
18448
18448
  const CTR_MAX_LIMIT = 100;
18449
- function compareText$12(left, right) {
18449
+ function compareText$13(left, right) {
18450
18450
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
18451
18451
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
18452
18452
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
@@ -18455,7 +18455,7 @@ function compareText$12(left, right) {
18455
18455
  }
18456
18456
  return leftPoints.length - rightPoints.length;
18457
18457
  }
18458
- function boundedInteger$6(value, fallback, minimum, maximum) {
18458
+ function boundedInteger$7(value, fallback, minimum, maximum) {
18459
18459
  if (value === void 0 || !Number.isFinite(value)) return fallback;
18460
18460
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
18461
18461
  }
@@ -18494,7 +18494,7 @@ function aggregateCtrRows(rows, selection) {
18494
18494
  weightedPosition: 0,
18495
18495
  sourceRows: 0
18496
18496
  };
18497
- if (compareText$12(query, group.query) < 0) group.query = query;
18497
+ if (compareText$13(query, group.query) < 0) group.query = query;
18498
18498
  group.clicks += row.clicks;
18499
18499
  group.impressions += row.impressions;
18500
18500
  group.weightedPosition += row.position * row.impressions;
@@ -18532,8 +18532,8 @@ function initialSelection(sourceRows) {
18532
18532
  };
18533
18533
  }
18534
18534
  function analyzeCtrUnderperformersFromRows(input) {
18535
- const minImpressions = boundedInteger$6(input.minImpressions, 200, 1, CTR_MAX_MIN_IMPRESSIONS);
18536
- const limit = boundedInteger$6(input.limit, 25, 1, 100);
18535
+ const minImpressions = boundedInteger$7(input.minImpressions, 200, 1, CTR_MAX_MIN_IMPRESSIONS);
18536
+ const limit = boundedInteger$7(input.limit, 25, 1, 100);
18537
18537
  const selection = initialSelection(input.rows.length);
18538
18538
  const benchmarkRows = aggregateCtrRows(input.rows, selection).filter((row) => {
18539
18539
  const query = row.keys[0] ?? "";
@@ -18598,7 +18598,7 @@ function analyzeCtrUnderperformersFromRows(input) {
18598
18598
  confidence: "low"
18599
18599
  }
18600
18600
  };
18601
- }).filter((item) => item.actualCtr < item.expectedCtr * .6).sort((left, right) => right.clickShortfall - left.clickShortfall || compareText$12(left.query, right.query) || compareText$12(left.url, right.url));
18601
+ }).filter((item) => item.actualCtr < item.expectedCtr * .6).sort((left, right) => right.clickShortfall - left.clickShortfall || compareText$13(left.query, right.query) || compareText$13(left.url, right.url));
18602
18602
  const items = eligibleItems.slice(0, limit);
18603
18603
  selection.eligibleUnderperformers = eligibleItems.length;
18604
18604
  selection.returnedUnderperformers = items.length;
@@ -18829,7 +18829,7 @@ function diagnosisDataStatus(input) {
18829
18829
  function fixed$1(value) {
18830
18830
  return Number(value.toFixed(3));
18831
18831
  }
18832
- function compareText$11(left, right) {
18832
+ function compareText$12(left, right) {
18833
18833
  return left < right ? -1 : left > right ? 1 : 0;
18834
18834
  }
18835
18835
  function validMetric$2(value) {
@@ -18909,8 +18909,8 @@ function compareRetainedSegmentRows(input) {
18909
18909
  else unmatched.push(unmatchedRow(row, "before"));
18910
18910
  }
18911
18911
  for (const [key, row] of after.rows) if (!before.rows.has(key)) unmatched.push(unmatchedRow(row, "after"));
18912
- matched.sort((left, right) => Math.abs(right.clickDelta) - Math.abs(left.clickDelta) || compareText$11(left.key, right.key));
18913
- unmatched.sort((left, right) => right.clicks - left.clicks || compareText$11(left.retainedIn, right.retainedIn) || compareText$11(left.key, right.key));
18912
+ matched.sort((left, right) => Math.abs(right.clickDelta) - Math.abs(left.clickDelta) || compareText$12(left.key, right.key));
18913
+ unmatched.sort((left, right) => right.clicks - left.clicks || compareText$12(left.retainedIn, right.retainedIn) || compareText$12(left.key, right.key));
18914
18914
  return {
18915
18915
  before,
18916
18916
  after,
@@ -19667,13 +19667,13 @@ const MINIMUM_PAGE_SHARE = .1;
19667
19667
  const MINIMUM_PAGE_IMPRESSIONS = 10;
19668
19668
  const MAXIMUM_DOMINANT_SHARE = .8;
19669
19669
  const MAX_SUPPRESSIONS = 100;
19670
- function boundedInteger$5(value, fallback, minimum, maximum) {
19670
+ function boundedInteger$6(value, fallback, minimum, maximum) {
19671
19671
  if (value === void 0 || !Number.isFinite(value)) return fallback;
19672
19672
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
19673
19673
  }
19674
19674
  function analyzeCannibalRows(input) {
19675
- const minImpressions = boundedInteger$5(input.minImpressions, 50, 0, 1e9);
19676
- const limit = boundedInteger$5(input.limit, 25, 1, 100);
19675
+ const minImpressions = boundedInteger$6(input.minImpressions, 50, 0, 1e9);
19676
+ const limit = boundedInteger$6(input.limit, 25, 1, 100);
19677
19677
  const selection = {
19678
19678
  sourceRows: input.rows.length,
19679
19679
  invalidRows: 0,
@@ -19999,7 +19999,7 @@ function normalizeUrl(value) {
19999
19999
  return;
20000
20000
  }
20001
20001
  }
20002
- function validRow$2(row, query, url) {
20002
+ function validRow$3(row, query, url) {
20003
20003
  return Boolean(query && url) && Number.isFinite(row.clicks) && row.clicks >= 0 && Number.isFinite(row.impressions) && row.impressions > 0 && row.clicks <= row.impressions && Number.isFinite(row.ctr) && row.ctr >= 0 && row.ctr <= 1 && Number.isFinite(row.position) && row.position > 0;
20004
20004
  }
20005
20005
  function aggregateDecayRows(rows) {
@@ -20008,7 +20008,7 @@ function aggregateDecayRows(rows) {
20008
20008
  for (const row of rows) {
20009
20009
  const query = row.keys[0]?.trim() ?? "";
20010
20010
  const url = normalizeUrl(row.keys[1]?.trim() ?? "");
20011
- if (!validRow$2(row, query, url)) {
20011
+ if (!validRow$3(row, query, url)) {
20012
20012
  invalid++;
20013
20013
  continue;
20014
20014
  }
@@ -20415,7 +20415,7 @@ function normalizedQuery(value) {
20415
20415
  function groupKey$1(item) {
20416
20416
  return `${item.template.id}:${normalizedQuery(item.query)}`;
20417
20417
  }
20418
- function compareText$10(left, right) {
20418
+ function compareText$11(left, right) {
20419
20419
  return left < right ? -1 : left > right ? 1 : 0;
20420
20420
  }
20421
20421
  function groupAction(input) {
@@ -20451,11 +20451,11 @@ function groupQuickWins(items) {
20451
20451
  technicalUrls
20452
20452
  });
20453
20453
  return group;
20454
- }).filter((group) => group !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$10(left.id, right.id));
20454
+ }).filter((group) => group !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$11(left.id, right.id));
20455
20455
  }
20456
20456
  //#endregion
20457
20457
  //#region packages/core/src/analyze/site-diagnostics/quick-wins-analysis-primitives.ts
20458
- function boundedInteger$4(value, fallback, minimum, maximum) {
20458
+ function boundedInteger$5(value, fallback, minimum, maximum) {
20459
20459
  if (value === void 0 || !Number.isFinite(value)) return fallback;
20460
20460
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
20461
20461
  }
@@ -20467,7 +20467,7 @@ function validHttpUrl(value) {
20467
20467
  return false;
20468
20468
  }
20469
20469
  }
20470
- function validRow$1(row, query, url) {
20470
+ function validRow$2(row, query, url) {
20471
20471
  return row.keys.length >= 2 && Boolean(query) && validHttpUrl(url) && Number.isFinite(row.clicks) && row.clicks >= 0 && Number.isFinite(row.impressions) && row.impressions > 0 && row.clicks <= row.impressions && Number.isFinite(row.ctr) && row.ctr >= 0 && row.ctr <= 1 && Number.isFinite(row.position) && row.position >= 1;
20472
20472
  }
20473
20473
  function selectBenchmarkRows(input) {
@@ -20489,7 +20489,7 @@ function selectBenchmarkRows(input) {
20489
20489
  for (const row of input.rows) {
20490
20490
  const query = row.keys[0]?.trim() ?? "";
20491
20491
  const url = row.keys[1]?.trim() ?? "";
20492
- if (!validRow$1(row, query, url)) selection.invalidRows++;
20492
+ if (!validRow$2(row, query, url)) selection.invalidRows++;
20493
20493
  else if (row.position > 10) selection.outsideBenchmarkPositionRows++;
20494
20494
  else if (isLowActionabilityQuery(query)) selection.lowActionabilityRows++;
20495
20495
  else if (shouldExcludeBrandQuery({
@@ -20563,11 +20563,11 @@ function quickWinItem(input) {
20563
20563
  }
20564
20564
  };
20565
20565
  }
20566
- function compareText$9(left, right) {
20566
+ function compareText$10(left, right) {
20567
20567
  return left < right ? -1 : left > right ? 1 : 0;
20568
20568
  }
20569
20569
  function compareQuickWinItems(left, right) {
20570
- return right.estimatedCtrClickShortfall - left.estimatedCtrClickShortfall || right.impressions - left.impressions || compareText$9(left.query, right.query) || compareText$9(left.url, right.url);
20570
+ return right.estimatedCtrClickShortfall - left.estimatedCtrClickShortfall || right.impressions - left.impressions || compareText$10(left.query, right.query) || compareText$10(left.url, right.url);
20571
20571
  }
20572
20572
  function roundedSum(values) {
20573
20573
  return Number(values.reduce((sum, value) => sum + value, 0).toFixed(2));
@@ -20580,8 +20580,8 @@ const DEFAULT_LIMIT$3 = 25;
20580
20580
  const MAX_LIMIT$3 = 100;
20581
20581
  const FALLBACK_SOURCE = "builtin_position_ctr_curve_v1";
20582
20582
  function analyzeQuickWinsFromRows(input) {
20583
- const minImpressions = boundedInteger$4(input.minImpressions, DEFAULT_MIN_IMPRESSIONS$2, 0, MAX_MIN_IMPRESSIONS$2);
20584
- const limit = boundedInteger$4(input.limit, DEFAULT_LIMIT$3, 1, MAX_LIMIT$3);
20583
+ const minImpressions = boundedInteger$5(input.minImpressions, DEFAULT_MIN_IMPRESSIONS$2, 0, MAX_MIN_IMPRESSIONS$2);
20584
+ const limit = boundedInteger$5(input.limit, DEFAULT_LIMIT$3, 1, MAX_LIMIT$3);
20585
20585
  const selected = selectBenchmarkRows(input);
20586
20586
  const benchmarkContext = createCtrBenchmarkContext(selected.rows, {
20587
20587
  samplePopulation: "all_qualified_url_samples",
@@ -20682,7 +20682,7 @@ function analyzeQuickWinsFromRows(input) {
20682
20682
  }
20683
20683
  //#endregion
20684
20684
  //#region packages/core/src/analyze/site-diagnostics/quick-wins-templates.ts
20685
- function compareText$8(left, right) {
20685
+ function compareText$9(left, right) {
20686
20686
  return left < right ? -1 : left > right ? 1 : 0;
20687
20687
  }
20688
20688
  function unique$2(values) {
@@ -20708,7 +20708,7 @@ function quickWinTemplateRecommendations(items) {
20708
20708
  action: technicalUrls > 0 ? `Fix verified technical issues on ${technicalUrls} affected URLs before testing shared template changes.` : `Compare live results and verified page evidence for ${urls.length} affected URLs before testing a shared title, snippet, or heading change.`,
20709
20709
  evidence: `${templateItems.length} eligible rows across ${urls.length} distinct URLs share this recognised template. Example queries: ${queries.join("; ")}.`
20710
20710
  };
20711
- }).filter((item) => item !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$8(left.templateId, right.templateId));
20711
+ }).filter((item) => item !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$9(left.templateId, right.templateId));
20712
20712
  }
20713
20713
  function quickWinTemplateSummaries(items) {
20714
20714
  const templates = /* @__PURE__ */ new Map();
@@ -20727,13 +20727,13 @@ function quickWinTemplateSummaries(items) {
20727
20727
  label: item.label,
20728
20728
  rowCount: item.rows,
20729
20729
  urlCount: item.urls.size,
20730
- sampleUrls: [...item.urls].sort(compareText$8).slice(0, 3)
20731
- })).sort((left, right) => right.urlCount - left.urlCount || right.rowCount - left.rowCount || compareText$8(left.id, right.id)).slice(0, 5);
20730
+ sampleUrls: [...item.urls].sort(compareText$9).slice(0, 3)
20731
+ })).sort((left, right) => right.urlCount - left.urlCount || right.rowCount - left.rowCount || compareText$9(left.id, right.id)).slice(0, 5);
20732
20732
  }
20733
20733
  //#endregion
20734
20734
  //#region packages/core/src/analyze/site-diagnostics/quick-wins.ts
20735
20735
  const DEFAULT_DAYS$3 = 28;
20736
- const MAX_DAYS$3 = 548;
20736
+ const MAX_DAYS$4 = 548;
20737
20737
  const DEFAULT_VERIFY_LIMIT$1 = 5;
20738
20738
  const MAX_VERIFY_LIMIT = 100;
20739
20739
  const MAX_SOURCE_ROWS$3 = 1e5;
@@ -20757,12 +20757,12 @@ function applyVerification$1(item, coverage) {
20757
20757
  };
20758
20758
  }
20759
20759
  async function quickWinsReport(input, dependencies = defaultDependencies$9) {
20760
- const explicitRange = explicitDateRange(input, MAX_DAYS$3);
20760
+ const explicitRange = explicitDateRange(input, MAX_DAYS$4);
20761
20761
  const days = explicitRange?.days ?? integerOption$2({
20762
20762
  value: input.days,
20763
20763
  fallback: DEFAULT_DAYS$3,
20764
20764
  minimum: 1,
20765
- maximum: MAX_DAYS$3,
20765
+ maximum: MAX_DAYS$4,
20766
20766
  label: "Days"
20767
20767
  });
20768
20768
  const verifyLimit = integerOption$2({
@@ -20912,7 +20912,7 @@ async function quickWinsReport(input, dependencies = defaultDependencies$9) {
20912
20912
  }
20913
20913
  //#endregion
20914
20914
  //#region packages/core/src/analyze/striking-distance-analysis-primitives.ts
20915
- function compareText$7(left, right) {
20915
+ function compareText$8(left, right) {
20916
20916
  const leftPoints = [...left].map((character) => character.codePointAt(0) ?? 0);
20917
20917
  const rightPoints = [...right].map((character) => character.codePointAt(0) ?? 0);
20918
20918
  const length = Math.min(leftPoints.length, rightPoints.length);
@@ -20978,7 +20978,7 @@ function strikingDistanceItem(row) {
20978
20978
  };
20979
20979
  }
20980
20980
  function compareStrikingDistanceItems(left, right) {
20981
- return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$7(left.query, right.query) || compareText$7(left.url, right.url);
20981
+ return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$8(left.query, right.query) || compareText$8(left.url, right.url);
20982
20982
  }
20983
20983
  function groupStrikingDistanceItems(items) {
20984
20984
  const grouped = /* @__PURE__ */ new Map();
@@ -21015,7 +21015,7 @@ function groupStrikingDistanceItems(items) {
21015
21015
  action: shared ? `Review the sampled ${first.template.label.toLowerCase()} URLs for a recurring technical, content, or internal-link pattern before changing the shared template.` : `Review these URLs individually; the current evidence does not support a shared-template change.`
21016
21016
  }
21017
21017
  };
21018
- }).filter((group) => group !== void 0).sort((left, right) => right.totalImpressions - left.totalImpressions || right.uniqueUrls - left.uniqueUrls || compareText$7(left.id, right.id));
21018
+ }).filter((group) => group !== void 0).sort((left, right) => right.totalImpressions - left.totalImpressions || right.uniqueUrls - left.uniqueUrls || compareText$8(left.id, right.id));
21019
21019
  }
21020
21020
  //#endregion
21021
21021
  //#region packages/core/src/analyze/striking-distance-analysis.ts
@@ -21130,7 +21130,7 @@ function analyzeStrikingDistanceRows(input) {
21130
21130
  //#endregion
21131
21131
  //#region packages/core/src/analyze/striking-distance.ts
21132
21132
  const DEFAULT_DAYS$2 = 28;
21133
- const MAX_DAYS$2 = 548;
21133
+ const MAX_DAYS$3 = 548;
21134
21134
  const DEFAULT_VERIFY_LIMIT = 5;
21135
21135
  const MAX_SOURCE_ROWS$2 = 1e5;
21136
21136
  const defaultDependencies$8 = {
@@ -21138,13 +21138,13 @@ const defaultDependencies$8 = {
21138
21138
  verifyContent: verifyQueryContent,
21139
21139
  now: () => /* @__PURE__ */ new Date()
21140
21140
  };
21141
- function boundedInteger$3(value, fallback, minimum, maximum) {
21141
+ function boundedInteger$4(value, fallback, minimum, maximum) {
21142
21142
  if (value === void 0 || !Number.isFinite(value)) return fallback;
21143
21143
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
21144
21144
  }
21145
21145
  function reportDays(value) {
21146
21146
  if (value === void 0) return DEFAULT_DAYS$2;
21147
- 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}.`);
21147
+ if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS$3) throw new SeoError("INVALID_INPUT", `Days must be a whole number between 1 and ${MAX_DAYS$3}.`);
21148
21148
  return value;
21149
21149
  }
21150
21150
  function dateRange(days, now) {
@@ -21202,7 +21202,7 @@ async function strikingDistance(input, dependencies = defaultDependencies$8) {
21202
21202
  brandTerms: input.brandTerms,
21203
21203
  includeBrand: input.includeBrand
21204
21204
  });
21205
- const verifyLimit = boundedInteger$3(input.verifyLimit, DEFAULT_VERIFY_LIMIT, 0, analysis.limit);
21205
+ const verifyLimit = boundedInteger$4(input.verifyLimit, DEFAULT_VERIFY_LIMIT, 0, analysis.limit);
21206
21206
  const verificationRequested = input.verifyContent === true || input.verifyContent !== false && input.verifyLimit !== void 0;
21207
21207
  const attempted = verificationRequested ? Math.min(verifyLimit, analysis.items.length) : 0;
21208
21208
  const items = await Promise.all(analysis.items.map(async (item, index) => {
@@ -25579,7 +25579,7 @@ function resolveProvider(input) {
25579
25579
  //#endregion
25580
25580
  //#region packages/core/src/analyze/domain-research/shared.ts
25581
25581
  const MAX_GSC_DOMAIN_ROWS = 1e5;
25582
- function compareText$6(left, right) {
25582
+ function compareText$7(left, right) {
25583
25583
  return left < right ? -1 : left > right ? 1 : 0;
25584
25584
  }
25585
25585
  function normalizedKeyword$1(value) {
@@ -25696,8 +25696,8 @@ function aggregateGscQueries(rows) {
25696
25696
  clicks: item.clicks,
25697
25697
  impressions: item.impressions,
25698
25698
  averagePosition: item.positionWeight ? item.weightedPosition / item.positionWeight : null,
25699
- urls: [...item.urls].sort(compareText$6).slice(0, 5)
25700
- })).sort((left, right) => right.impressions - left.impressions || compareText$6(left.query, right.query));
25699
+ urls: [...item.urls].sort(compareText$7).slice(0, 5)
25700
+ })).sort((left, right) => right.impressions - left.impressions || compareText$7(left.query, right.query));
25701
25701
  }
25702
25702
  async function acquireGscQueries(input) {
25703
25703
  const range = finalGscDateRange(input.days, input.now);
@@ -26063,7 +26063,7 @@ function pseoQueryPatterns(rows) {
26063
26063
  //#region packages/core/src/analyze/pseo/templates.ts
26064
26064
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
26065
26065
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
26066
- function compareText$5(left, right) {
26066
+ function compareText$6(left, right) {
26067
26067
  const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
26068
26068
  const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
26069
26069
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
@@ -26119,7 +26119,7 @@ function valueTokens(values) {
26119
26119
  const tokens = value.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
26120
26120
  for (const token of tokens.filter((item) => item.length > 1 || /[^\p{ASCII}]/u.test(item))) counts.set(token, (counts.get(token) ?? 0) + 1);
26121
26121
  }
26122
- return [...counts.entries()].sort((a, b) => b[1] - a[1] || compareText$5(a[0], b[0])).slice(0, 8).map(([token]) => token);
26122
+ return [...counts.entries()].sort((a, b) => b[1] - a[1] || compareText$6(a[0], b[0])).slice(0, 8).map(([token]) => token);
26123
26123
  }
26124
26124
  function templateShape(signature, urls) {
26125
26125
  const signatureParts = signature.split("/").filter(Boolean);
@@ -26168,7 +26168,7 @@ function collectClusters(urls, signatures) {
26168
26168
  return bySignature;
26169
26169
  }
26170
26170
  function clusterPseoTemplates(urls, opts = {}) {
26171
- const uniqueUrls = [...new Set(urls)].sort(compareText$5);
26171
+ const uniqueUrls = [...new Set(urls)].sort(compareText$6);
26172
26172
  const minUrls = opts.minUrls ?? 3;
26173
26173
  const minShare = opts.minShare ?? .01;
26174
26174
  const sampleSize = opts.sampleSize ?? 5;
@@ -26186,15 +26186,15 @@ function clusterPseoTemplates(urls, opts = {}) {
26186
26186
  shape: templateShape(signature, clusterUrls),
26187
26187
  urlCount: clusterUrls.length,
26188
26188
  share: uniqueUrls.length ? clusterUrls.length / uniqueUrls.length : 0,
26189
- sampleUrls: clusterUrls.sort(compareText$5).slice(0, sampleSize)
26190
- })).filter((cluster) => cluster.shape.variableSegments.length > 0 && cluster.urlCount >= minUrls && cluster.share >= minShare).sort((a, b) => b.urlCount - a.urlCount || compareText$5(a.signature, b.signature)).slice(0, opts.limit ?? 50);
26189
+ sampleUrls: clusterUrls.sort(compareText$6).slice(0, sampleSize)
26190
+ })).filter((cluster) => cluster.shape.variableSegments.length > 0 && cluster.urlCount >= minUrls && cluster.share >= minShare).sort((a, b) => b.urlCount - a.urlCount || compareText$6(a.signature, b.signature)).slice(0, opts.limit ?? 50);
26191
26191
  }
26192
26192
  //#endregion
26193
26193
  //#region packages/core/src/analyze/domain-research/gap-analysis.ts
26194
26194
  const MAX_RELEVANCE_QUERY_REFS = 3;
26195
26195
  const MAX_FIRST_PARTY_PATTERN_URLS = 1e4;
26196
26196
  function terms(value) {
26197
- return [...new Set(pseoQueryTerms(value).map(canonicalPseoTerm).filter(Boolean))].sort(compareText$6);
26197
+ return [...new Set(pseoQueryTerms(value).map(canonicalPseoTerm).filter(Boolean))].sort(compareText$7);
26198
26198
  }
26199
26199
  function observedVolume$1(row) {
26200
26200
  return value(row.monthlySearchVolume) ?? -1;
@@ -26238,7 +26238,7 @@ function relevanceForKeyword(keyword, acquisition, tokenIndex) {
26238
26238
  query: row.query,
26239
26239
  overlap: terms(row.query).filter((token) => keywordTerms.includes(token)).length,
26240
26240
  impressions: row.impressions
26241
- })).filter((row) => row.overlap > 0).sort((left, right) => right.overlap - left.overlap || right.impressions - left.impressions || compareText$6(left.query, right.query)).slice(0, MAX_RELEVANCE_QUERY_REFS).map((row) => row.query);
26241
+ })).filter((row) => row.overlap > 0).sort((left, right) => right.overlap - left.overlap || right.impressions - left.impressions || compareText$7(left.query, right.query)).slice(0, MAX_RELEVANCE_QUERY_REFS).map((row) => row.query);
26242
26242
  return {
26243
26243
  state: sharedTokens.length >= 2 ? "observed-overlap" : sharedTokens.length === 1 ? "weak-overlap" : "unavailable",
26244
26244
  sharedTokens,
@@ -26260,10 +26260,10 @@ function repeatedCompetitorPatterns(rows) {
26260
26260
  urlCount: pattern.urlCount,
26261
26261
  sampleUrls: pattern.sampleUrls,
26262
26262
  evidenceRefs: items.flatMap((item) => pattern.sampleUrls.includes(item.row.url) ? [item.evidenceRef] : [])
26263
- }))).sort((left, right) => right.urlCount - left.urlCount || compareText$6(left.domain, right.domain) || compareText$6(left.signature, right.signature));
26263
+ }))).sort((left, right) => right.urlCount - left.urlCount || compareText$7(left.domain, right.domain) || compareText$7(left.signature, right.signature));
26264
26264
  }
26265
26265
  function ownPatterns(acquisition) {
26266
- return clusterPseoTemplates([...new Set(acquisition.sourceRows.rows.flatMap((row) => row.urls))].sort(compareText$6).slice(0, MAX_FIRST_PARTY_PATTERN_URLS), {
26266
+ return clusterPseoTemplates([...new Set(acquisition.sourceRows.rows.flatMap((row) => row.urls))].sort(compareText$7).slice(0, MAX_FIRST_PARTY_PATTERN_URLS), {
26267
26267
  minUrls: 3,
26268
26268
  minShare: 0,
26269
26269
  limit: 50,
@@ -26304,10 +26304,10 @@ function analyzeCompetitorGap(acquisition) {
26304
26304
  const byDomain = /* @__PURE__ */ new Map();
26305
26305
  for (const item of rows) {
26306
26306
  const current = byDomain.get(item.domain);
26307
- if (!current || item.row.rankGroup < current.row.rankGroup || item.row.rankGroup === current.row.rankGroup && compareText$6(item.row.url, current.row.url) < 0) byDomain.set(item.domain, item);
26307
+ if (!current || item.row.rankGroup < current.row.rankGroup || item.row.rankGroup === current.row.rankGroup && compareText$7(item.row.url, current.row.url) < 0) byDomain.set(item.domain, item);
26308
26308
  }
26309
- const retainedRows = [...byDomain.values()].sort((left, right) => left.row.rankGroup - right.row.rankGroup || compareText$6(left.domain, right.domain));
26310
- const best = [...retainedRows].sort((left, right) => observedVolume$1(right.row) - observedVolume$1(left.row) || left.row.rankGroup - right.row.rankGroup || compareText$6(left.domain, right.domain))[0];
26309
+ const retainedRows = [...byDomain.values()].sort((left, right) => left.row.rankGroup - right.row.rankGroup || compareText$7(left.domain, right.domain));
26310
+ const best = [...retainedRows].sort((left, right) => observedVolume$1(right.row) - observedVolume$1(left.row) || left.row.rankGroup - right.row.rankGroup || compareText$7(left.domain, right.domain))[0];
26311
26311
  const firstParty = firstPartyByQuery.get(keyword);
26312
26312
  const relevance = relevanceForKeyword(keyword, acquisition, tokenIndex.index);
26313
26313
  const ownRank = ownRanks.get(keyword) ?? null;
@@ -26351,7 +26351,7 @@ function analyzeCompetitorGap(acquisition) {
26351
26351
  },
26352
26352
  evidenceRefs: retainedRows.map((item) => item.evidenceRef)
26353
26353
  };
26354
- }).sort((left, right) => classificationOrder(left.classification) - classificationOrder(right.classification) || right.competitorCount - left.competitorCount || (value(right.monthlySearchVolume) ?? -1) - (value(left.monthlySearchVolume) ?? -1) || Math.min(...left.competitors.map((item) => item.rank)) - Math.min(...right.competitors.map((item) => item.rank)) || compareText$6(left.keyword, right.keyword));
26354
+ }).sort((left, right) => classificationOrder(left.classification) - classificationOrder(right.classification) || right.competitorCount - left.competitorCount || (value(right.monthlySearchVolume) ?? -1) - (value(left.monthlySearchVolume) ?? -1) || Math.min(...left.competitors.map((item) => item.rank)) - Math.min(...right.competitors.map((item) => item.rank)) || compareText$7(left.keyword, right.keyword));
26355
26355
  return {
26356
26356
  rawRows,
26357
26357
  candidates,
@@ -26823,7 +26823,7 @@ async function rankingPagesReport(input, dependencies = {}) {
26823
26823
  impressions: row.impressions,
26824
26824
  averagePosition: row.position
26825
26825
  }] : [];
26826
- }).sort((left, right) => right.impressions - left.impressions || compareText$6(left.url, right.url));
26826
+ }).sort((left, right) => right.impressions - left.impressions || compareText$7(left.url, right.url));
26827
26827
  const firstPartyStatus = !input.site ? "not-requested" : (firstPartyResponse?.rowsFetched ?? 0) >= MAX_GSC_PAGE_ROWS ? "partial" : firstPartyResponse?.rows.length === 0 ? "empty" : "complete";
26828
26828
  const findings = patterns.slice(0, 5).map((pattern, index) => ({
26829
26829
  code: "repeated-ranking-page-pattern",
@@ -26904,7 +26904,7 @@ async function serpCompetitorsReport(input, dependencies = {}) {
26904
26904
  const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
26905
26905
  const market = validatedMarket(input.market);
26906
26906
  const providerId = validatedProvider(input.provider);
26907
- const keywords = [...new Set(input.keywords.map(normalizedKeyword$1))].filter(Boolean).sort(compareText$6);
26907
+ const keywords = [...new Set(input.keywords.map(normalizedKeyword$1))].filter(Boolean).sort(compareText$7);
26908
26908
  if (keywords.length < 2 || keywords.length > 200 || keywords.some((keyword) => keyword.length > 80 || keyword.split(/\s+/u).length > 10)) throw new SeoError("INVALID_INPUT", "SERP competitor research requires 2 to 200 keywords of at most 80 characters and 10 words.");
26909
26909
  const target = input.targetDomain ? normalizeDomain$1(input.targetDomain) : null;
26910
26910
  const declared = declaredCompetitors(input.declaredCompetitors);
@@ -28685,7 +28685,7 @@ function providerReportError(error) {
28685
28685
  if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
28686
28686
  throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
28687
28687
  }
28688
- function validateInput$3(input) {
28688
+ function validateInput$4(input) {
28689
28689
  if (input.keywords.length < 1 || input.keywords.length > MAX_REPORT_KEYWORDS || input.keywords.some((keyword) => {
28690
28690
  const value = keyword.trim();
28691
28691
  return value.length < 1 || value.length > 80 || value.split(/\s+/u).length > 10;
@@ -28719,7 +28719,7 @@ function findings$1(analysis) {
28719
28719
  }).sort((left, right) => Number(right.code === "recent-demand-increase") - Number(left.code === "recent-demand-increase") || right.recentAverage - left.recentAverage || compareCodepoints$2(left.keyword, right.keyword)).slice(0, 10).map(({ recentAverage: _recentAverage, ...finding }) => finding);
28720
28720
  }
28721
28721
  async function keywordMetricsReport(input, dependencies = {}) {
28722
- const validated = validateInput$3(input);
28722
+ const validated = validateInput$4(input);
28723
28723
  let candidates;
28724
28724
  try {
28725
28725
  candidates = dependencies.candidates ?? await defaultCandidates$3();
@@ -29017,16 +29017,16 @@ function benchmarkDetails(benchmark) {
29017
29017
  positiveUrlSamples: benchmark.positiveUrlSamples
29018
29018
  };
29019
29019
  }
29020
- function boundedInteger$2(input) {
29020
+ function boundedInteger$3(input) {
29021
29021
  const value = Number.isFinite(input.value) ? input.value ?? 0 : input.fallback;
29022
29022
  return Math.min(input.maximum ?? Number.POSITIVE_INFINITY, Math.max(1, Math.floor(value)));
29023
29023
  }
29024
29024
  function analyzeQueryClustersFromRows(input) {
29025
- const minImpressions = boundedInteger$2({
29025
+ const minImpressions = boundedInteger$3({
29026
29026
  value: input.minImpressions,
29027
29027
  fallback: 25
29028
29028
  });
29029
- const limit = boundedInteger$2({
29029
+ const limit = boundedInteger$3({
29030
29030
  value: input.limit,
29031
29031
  fallback: 25,
29032
29032
  maximum: 100
@@ -29145,7 +29145,7 @@ function integerInRange(value, fallback, minimum, maximum) {
29145
29145
  if (value === void 0 || !Number.isFinite(value)) return fallback;
29146
29146
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
29147
29147
  }
29148
- function compareText$4(left, right) {
29148
+ function compareText$5(left, right) {
29149
29149
  const leftPoints = [...left].map((character) => character.codePointAt(0) ?? 0);
29150
29150
  const rightPoints = [...right].map((character) => character.codePointAt(0) ?? 0);
29151
29151
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
@@ -29164,7 +29164,7 @@ function normalizedHttpUrl(value) {
29164
29164
  return;
29165
29165
  }
29166
29166
  }
29167
- function validRow(row, query, url) {
29167
+ function validRow$1(row, query, url) {
29168
29168
  return Boolean(query && url) && Number.isFinite(row.clicks) && Number.isFinite(row.impressions) && Number.isFinite(row.ctr) && Number.isFinite(row.position) && row.clicks >= 0 && row.impressions > 0 && row.clicks <= row.impressions && row.ctr >= 0 && row.ctr <= 1 && row.position > 0;
29169
29169
  }
29170
29170
  function queryFromRow(row) {
@@ -29177,7 +29177,7 @@ function queryFromRow(row) {
29177
29177
  };
29178
29178
  }
29179
29179
  function compareQueries(left, right) {
29180
- return right.impressions - left.impressions || left.position - right.position || right.clicks - left.clicks || compareText$4(left.query, right.query);
29180
+ return right.impressions - left.impressions || left.position - right.position || right.clicks - left.clicks || compareText$5(left.query, right.query);
29181
29181
  }
29182
29182
  function recommendation(input) {
29183
29183
  return {
@@ -29223,7 +29223,7 @@ function pageItem(url, rows) {
29223
29223
  };
29224
29224
  }
29225
29225
  function compareItems$1(left, right) {
29226
- return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$4(left.url, right.url);
29226
+ return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$5(left.url, right.url);
29227
29227
  }
29228
29228
  function analyzeSecondPageRows(input) {
29229
29229
  const minImpressions = integerInRange(input.minImpressions, DEFAULT_MIN_IMPRESSIONS, 0, MAX_MIN_IMPRESSIONS);
@@ -29244,7 +29244,7 @@ function analyzeSecondPageRows(input) {
29244
29244
  for (const row of input.rows) {
29245
29245
  const query = row.keys[0]?.trim() ?? "";
29246
29246
  const url = normalizedHttpUrl(row.keys[1]?.trim() ?? "");
29247
- if (!validRow(row, query, url)) selection.invalidRows++;
29247
+ if (!validRow$1(row, query, url)) selection.invalidRows++;
29248
29248
  else if (row.position <= 10 || row.position > 20) selection.outsidePositionRows++;
29249
29249
  else if (shouldExcludeBrandQuery({
29250
29250
  query,
@@ -29336,7 +29336,7 @@ function analyzeSecondPageRows(input) {
29336
29336
  //#endregion
29337
29337
  //#region packages/core/src/analyze/keyword-opportunities.ts
29338
29338
  const DEFAULT_DAYS$1 = 28;
29339
- const MAX_DAYS$1 = 548;
29339
+ const MAX_DAYS$2 = 548;
29340
29340
  const DEFAULT_REPORT_LIMIT = 10;
29341
29341
  const MAX_REPORT_LIMIT = 25;
29342
29342
  const DEFAULT_KEYWORD_LIMIT = 30;
@@ -29353,7 +29353,7 @@ const SOURCE_ORDER = [
29353
29353
  "second-page",
29354
29354
  "striking-distance"
29355
29355
  ];
29356
- function compareText$3(left, right) {
29356
+ function compareText$4(left, right) {
29357
29357
  return left < right ? -1 : left > right ? 1 : 0;
29358
29358
  }
29359
29359
  function rounded(value, precision = 3) {
@@ -29365,7 +29365,7 @@ function integerOption$1(input) {
29365
29365
  if (!Number.isInteger(input.value) || input.value < input.minimum || input.value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be a whole number between ${input.minimum} and ${input.maximum}.`);
29366
29366
  return input.value;
29367
29367
  }
29368
- function validateInput$2(input) {
29368
+ function validateInput$3(input) {
29369
29369
  const site = input.site.trim();
29370
29370
  if (!site || site.length > 2048) throw new SeoError("INVALID_INPUT", "Use a valid Search Console property.");
29371
29371
  if (input.minImpressions !== void 0 && (!Number.isInteger(input.minImpressions) || input.minImpressions < 0 || input.minImpressions > 1e9)) throw new SeoError("INVALID_INPUT", "Minimum impressions must be a whole number between 0 and 1000000000.");
@@ -29385,7 +29385,7 @@ function validateInput$2(input) {
29385
29385
  value: input.days,
29386
29386
  fallback: DEFAULT_DAYS$1,
29387
29387
  minimum: 1,
29388
- maximum: MAX_DAYS$1,
29388
+ maximum: MAX_DAYS$2,
29389
29389
  label: "Days"
29390
29390
  }),
29391
29391
  limit: integerOption$1({
@@ -29497,7 +29497,7 @@ function aggregateCandidates(rows) {
29497
29497
  impressions: rounded(row.impressions),
29498
29498
  ctr: rounded(row.ctr, 4),
29499
29499
  averagePosition: rounded(row.position, 2)
29500
- })).sort((left, right) => right.impressions - left.impressions || left.averagePosition - right.averagePosition || compareText$3(left.url, right.url));
29500
+ })).sort((left, right) => right.impressions - left.impressions || left.averagePosition - right.averagePosition || compareText$4(left.url, right.url));
29501
29501
  return [keyword, {
29502
29502
  keyword,
29503
29503
  clicks: rounded(clicks),
@@ -29731,7 +29731,7 @@ function nextSteps(input) {
29731
29731
  return steps;
29732
29732
  }
29733
29733
  async function keywordOpportunitiesReport(input, dependencies = {}) {
29734
- const validated = validateInput$2(input);
29734
+ const validated = validateInput$3(input);
29735
29735
  const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
29736
29736
  const generatedAt = now.toISOString();
29737
29737
  const range = finalGscDateRange(validated.days, now);
@@ -30099,7 +30099,7 @@ function providerError$3(error) {
30099
30099
  if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
30100
30100
  throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
30101
30101
  }
30102
- function validateInput$1(input) {
30102
+ function validateInput$2(input) {
30103
30103
  if (input.seeds.length < 1 || input.seeds.length > MAX_RESEARCH_SEEDS || input.seeds.some((seed) => {
30104
30104
  const value = seed.trim();
30105
30105
  return value.length < 1 || value.length > 80 || value.split(/\s+/u).length > 10;
@@ -30152,7 +30152,7 @@ async function keywordResearchReport(input, dependencies = {}) {
30152
30152
  "suggestions"
30153
30153
  ];
30154
30154
  const limit = input.limit ?? 50;
30155
- const validated = validateInput$1({
30155
+ const validated = validateInput$2({
30156
30156
  ...input,
30157
30157
  sources,
30158
30158
  limit
@@ -30235,6 +30235,864 @@ async function keywordResearchReport(input, dependencies = {}) {
30235
30235
  };
30236
30236
  }
30237
30237
  //#endregion
30238
+ //#region packages/core/src/analyze/local-search/intent.ts
30239
+ const NEARBY_PHRASES = [
30240
+ "closest to me",
30241
+ "in my area",
30242
+ "near me",
30243
+ "nearby"
30244
+ ];
30245
+ const UK_POSTCODE = /\b[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}\b/iu;
30246
+ const US_ZIP_CODE = /\b\d{5}(?:-\d{4})?\b/u;
30247
+ const US_ZIP_CONTEXT = /\b(?:around|near|nearby|postal(?:\s+code)?|zip(?:\s+code)?)\b/iu;
30248
+ function normalizeLocalSearchText(value) {
30249
+ return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLowerCase();
30250
+ }
30251
+ function tokens(value) {
30252
+ return normalizeLocalSearchText(value).match(/[\p{L}\p{N}]+/gu) ?? [];
30253
+ }
30254
+ function includesTokenSequence(source, candidate) {
30255
+ if (candidate.length === 0 || candidate.length > source.length) return false;
30256
+ return source.some((_, start) => candidate.every((token, offset) => source[start + offset] === token));
30257
+ }
30258
+ function termMatcher(terms) {
30259
+ const root = {
30260
+ children: /* @__PURE__ */ new Map(),
30261
+ terms: []
30262
+ };
30263
+ for (const term of terms) {
30264
+ let node = root;
30265
+ for (const token of tokens(term)) {
30266
+ const child = node.children.get(token) ?? {
30267
+ children: /* @__PURE__ */ new Map(),
30268
+ terms: []
30269
+ };
30270
+ node.children.set(token, child);
30271
+ node = child;
30272
+ }
30273
+ node.terms.push(term);
30274
+ }
30275
+ return (source) => {
30276
+ const matches = /* @__PURE__ */ new Set();
30277
+ for (let start = 0; start < source.length; start++) {
30278
+ let node = root;
30279
+ for (let index = start; index < source.length; index++) {
30280
+ node = node.children.get(source[index]);
30281
+ if (!node) break;
30282
+ for (const term of node.terms) matches.add(term);
30283
+ }
30284
+ }
30285
+ return [...matches].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
30286
+ };
30287
+ }
30288
+ function normalizeLocationTerms(values = []) {
30289
+ return [...new Set(values.map(normalizeLocalSearchText).filter(Boolean))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
30290
+ }
30291
+ function classifyLocalIntent(query, locationTerms) {
30292
+ return createLocalIntentClassifier(locationTerms)(query);
30293
+ }
30294
+ function createLocalIntentClassifier(locationTerms) {
30295
+ const matchLocations = termMatcher(locationTerms);
30296
+ return (query) => {
30297
+ const queryTokens = tokens(normalizeLocalSearchText(query));
30298
+ const matchedLocations = matchLocations(queryTokens);
30299
+ const matchedNearby = NEARBY_PHRASES.filter((phrase) => includesTokenSequence(queryTokens, tokens(phrase)));
30300
+ const ukPostcode = UK_POSTCODE.exec(query)?.[0] ?? null;
30301
+ const usZipCode = US_ZIP_CONTEXT.test(query) && US_ZIP_CODE.test(query) ? US_ZIP_CODE.exec(query)?.[0] ?? null : null;
30302
+ const postalCode = ukPostcode ?? usZipCode;
30303
+ const classes = [];
30304
+ if (matchedLocations.length > 0) classes.push("named-location");
30305
+ if (matchedNearby.length > 0) classes.push("nearby");
30306
+ if (postalCode) classes.push("postal-code");
30307
+ if (classes.length === 0) return null;
30308
+ return {
30309
+ heuristic: true,
30310
+ method: "explicit-local-intent-v1",
30311
+ classes,
30312
+ matchedTerms: [
30313
+ ...matchedLocations,
30314
+ ...matchedNearby,
30315
+ ...postalCode ? [normalizeLocalSearchText(postalCode)] : []
30316
+ ]
30317
+ };
30318
+ };
30319
+ }
30320
+ //#endregion
30321
+ //#region packages/core/src/analyze/local-search/analysis.ts
30322
+ function compareText$3(left, right) {
30323
+ return left < right ? -1 : left > right ? 1 : 0;
30324
+ }
30325
+ function validRow(row) {
30326
+ const query = row.keys[0]?.trim() ?? "";
30327
+ const page = row.keys[1]?.trim() ?? "";
30328
+ let validPage = false;
30329
+ try {
30330
+ validPage = ["http:", "https:"].includes(new URL(page).protocol);
30331
+ } catch {
30332
+ validPage = false;
30333
+ }
30334
+ if (row.keys.length !== 2 || !query || !validPage || !Number.isFinite(row.clicks) || !Number.isFinite(row.impressions) || !Number.isFinite(row.ctr) || !Number.isFinite(row.position) || row.clicks < 0 || row.impressions <= 0 || row.clicks > row.impressions || row.ctr < 0 || row.ctr > 1 || row.position <= 0) return null;
30335
+ return {
30336
+ query,
30337
+ page,
30338
+ clicks: row.clicks,
30339
+ impressions: row.impressions,
30340
+ ctr: row.ctr,
30341
+ position: row.position
30342
+ };
30343
+ }
30344
+ function rowSignature(row) {
30345
+ return `${row.clicks}\u0000${row.impressions}\u0000${row.ctr}\u0000${row.position}`;
30346
+ }
30347
+ function normalizeRows(rows, selection) {
30348
+ const grouped = /* @__PURE__ */ new Map();
30349
+ for (const source of rows) {
30350
+ const row = validRow(source);
30351
+ if (!row) {
30352
+ selection.invalidRows++;
30353
+ continue;
30354
+ }
30355
+ const key = `${row.query}\u0000${row.page}`;
30356
+ grouped.set(key, [...grouped.get(key) ?? [], row]);
30357
+ }
30358
+ const result = [];
30359
+ for (const group of grouped.values()) {
30360
+ if (new Set(group.map(rowSignature)).size > 1) {
30361
+ selection.conflictingRows += group.length;
30362
+ continue;
30363
+ }
30364
+ selection.exactDuplicateRows += group.length - 1;
30365
+ const row = group[0];
30366
+ if (row) result.push(row);
30367
+ }
30368
+ return result;
30369
+ }
30370
+ function opportunityAction(position, pageCount) {
30371
+ if (pageCount > 1) return "review-page-overlap";
30372
+ if (position <= 3) return "protect-visibility";
30373
+ if (position <= 20) return "improve-existing-page";
30374
+ return "investigate-relevance";
30375
+ }
30376
+ function aggregateQueries(input) {
30377
+ const grouped = /* @__PURE__ */ new Map();
30378
+ for (const row of input.rows) {
30379
+ if (isLowActionabilityQuery(row.query)) {
30380
+ input.selection.lowActionabilityRows++;
30381
+ continue;
30382
+ }
30383
+ if (shouldExcludeBrandQuery({
30384
+ query: row.query,
30385
+ siteUrl: input.site,
30386
+ brandTerms: input.brandTerms,
30387
+ includeBrand: input.includeBrand
30388
+ })) {
30389
+ input.selection.brandRows++;
30390
+ continue;
30391
+ }
30392
+ grouped.set(row.query, [...grouped.get(row.query) ?? [], row]);
30393
+ }
30394
+ const opportunities = [];
30395
+ for (const [query, rows] of grouped) {
30396
+ const intent = input.classify(query);
30397
+ if (!intent) {
30398
+ input.selection.nonLocalRows += rows.length;
30399
+ continue;
30400
+ }
30401
+ const impressions = rows.reduce((sum, row) => sum + row.impressions, 0);
30402
+ if (impressions < input.minImpressions) {
30403
+ input.selection.belowMinimumRows += rows.length;
30404
+ continue;
30405
+ }
30406
+ const clicks = rows.reduce((sum, row) => sum + row.clicks, 0);
30407
+ const averagePosition = rows.reduce((sum, row) => sum + row.position * row.impressions, 0) / impressions;
30408
+ const pages = rows.map((row) => ({
30409
+ url: row.page,
30410
+ clicks: row.clicks,
30411
+ impressions: row.impressions,
30412
+ ctr: row.impressions ? row.clicks / row.impressions : 0,
30413
+ averagePosition: row.position
30414
+ })).sort((left, right) => right.impressions - left.impressions || right.clicks - left.clicks || left.averagePosition - right.averagePosition || compareText$3(left.url, right.url));
30415
+ opportunities.push({
30416
+ query,
30417
+ clicks,
30418
+ impressions,
30419
+ ctr: impressions ? clicks / impressions : 0,
30420
+ averagePosition,
30421
+ intent,
30422
+ action: opportunityAction(averagePosition, pages.length),
30423
+ pages,
30424
+ pageCoverage: {
30425
+ available: pages.length,
30426
+ returned: Math.min(3, pages.length),
30427
+ omitted: Math.max(0, pages.length - 3)
30428
+ }
30429
+ });
30430
+ }
30431
+ return opportunities.sort((left, right) => right.impressions - left.impressions || right.clicks - left.clicks || left.averagePosition - right.averagePosition || compareText$3(left.query, right.query));
30432
+ }
30433
+ function localTemplates(opportunities) {
30434
+ const clusters = clusterPseoTemplates(opportunities.flatMap((item) => item.pages.map((page) => page.url)), {
30435
+ minUrls: 2,
30436
+ minShare: 0,
30437
+ limit: 10,
30438
+ sampleSize: 5
30439
+ });
30440
+ return clusters.map((cluster) => {
30441
+ const matched = opportunities.flatMap((item) => item.pages.filter((page) => templateForUrl(page.url, clusters) === cluster.signature).map((page) => ({
30442
+ query: item.query,
30443
+ page
30444
+ })));
30445
+ return {
30446
+ heuristic: true,
30447
+ signature: cluster.signature,
30448
+ urlCount: cluster.urlCount,
30449
+ sampleUrls: cluster.sampleUrls,
30450
+ queryCount: new Set(matched.map((item) => item.query)).size,
30451
+ clicks: matched.reduce((sum, item) => sum + item.page.clicks, 0),
30452
+ impressions: matched.reduce((sum, item) => sum + item.page.impressions, 0)
30453
+ };
30454
+ });
30455
+ }
30456
+ function analyzeLocalSearchRows(input) {
30457
+ const selection = {
30458
+ sourceRows: input.rows.length,
30459
+ invalidRows: 0,
30460
+ exactDuplicateRows: 0,
30461
+ conflictingRows: 0,
30462
+ lowActionabilityRows: 0,
30463
+ brandRows: 0,
30464
+ nonLocalRows: 0,
30465
+ belowMinimumRows: 0,
30466
+ eligibleQueries: 0,
30467
+ returnedQueries: 0,
30468
+ omittedQueries: 0,
30469
+ limit: input.limit,
30470
+ minImpressions: input.minImpressions
30471
+ };
30472
+ const normalized = normalizeRows(input.rows, selection);
30473
+ const eligible = aggregateQueries({
30474
+ ...input,
30475
+ rows: normalized,
30476
+ selection,
30477
+ classify: createLocalIntentClassifier(input.locationTerms)
30478
+ });
30479
+ selection.eligibleQueries = eligible.length;
30480
+ selection.returnedQueries = Math.min(input.limit, eligible.length);
30481
+ selection.omittedQueries = Math.max(0, eligible.length - input.limit);
30482
+ return {
30483
+ selection,
30484
+ eligibleSummary: {
30485
+ clicks: eligible.reduce((sum, item) => sum + item.clicks, 0),
30486
+ impressions: eligible.reduce((sum, item) => sum + item.impressions, 0),
30487
+ namedLocationQueries: eligible.filter((item) => item.intent.classes.includes("named-location")).length,
30488
+ nearbyQueries: eligible.filter((item) => item.intent.classes.includes("nearby")).length,
30489
+ postalCodeQueries: eligible.filter((item) => item.intent.classes.includes("postal-code")).length,
30490
+ pageOverlapQueries: eligible.filter((item) => item.action === "review-page-overlap").length
30491
+ },
30492
+ eligiblePageCount: new Set(eligible.flatMap((item) => item.pages.map((page) => page.url))).size,
30493
+ opportunities: eligible.slice(0, input.limit).map((item) => ({
30494
+ ...item,
30495
+ pages: item.pages.slice(0, 3)
30496
+ })),
30497
+ templates: localTemplates(eligible)
30498
+ };
30499
+ }
30500
+ //#endregion
30501
+ //#region packages/core/src/providers/dataforseo/serp-snapshot.ts
30502
+ const MAX_SERP_DEPTH = 100;
30503
+ const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
30504
+ function safeUrl$1(value) {
30505
+ if (!value) return null;
30506
+ try {
30507
+ const parsed = new URL(value);
30508
+ if (!["http:", "https:"].includes(parsed.protocol)) return null;
30509
+ parsed.username = "";
30510
+ parsed.password = "";
30511
+ return parsed.toString();
30512
+ } catch {
30513
+ return null;
30514
+ }
30515
+ }
30516
+ function validOrganic(item) {
30517
+ return item.type === "organic" && Number.isSafeInteger(item.rank_group) && (item.rank_group ?? 0) > 0 && Number.isSafeInteger(item.rank_absolute) && (item.rank_absolute ?? 0) > 0 && Number.isSafeInteger(item.page) && (item.page ?? 0) > 0 && Boolean(item.domain?.trim()) && safeUrl$1(item.url) !== null;
30518
+ }
30519
+ function organicResult(item) {
30520
+ if (!validOrganic(item)) return null;
30521
+ const url = safeUrl$1(item.url);
30522
+ if (!url) return null;
30523
+ return {
30524
+ rankGroup: item.rank_group,
30525
+ rankAbsolute: item.rank_absolute,
30526
+ page: item.page,
30527
+ domain: item.domain.trim().toLowerCase(),
30528
+ url,
30529
+ title: item.title ?? null,
30530
+ description: item.description ?? null,
30531
+ isFeaturedSnippet: item.is_featured_snippet ?? null
30532
+ };
30533
+ }
30534
+ function checkedAt(value, fallback) {
30535
+ const timestamp = Date.parse(value);
30536
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
30537
+ }
30538
+ function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
30539
+ const market = searchMarketSchema.parse(input.market);
30540
+ const keyword = normalizedKeyword$2(input.keyword);
30541
+ const location = locationRequest(market, "serp-snapshot");
30542
+ const result = snapshot.response.tasks.flatMap((task) => task.result ?? []).find((item) => normalizedKeyword$2(item.keyword) === keyword);
30543
+ if (!result) throw new ProviderError({
30544
+ provider: "dataforseo",
30545
+ operation: "serp-snapshot",
30546
+ code: "invalid-response",
30547
+ message: "DataForSEO returned no matching SERP result."
30548
+ });
30549
+ const warnings = [...snapshot.warnings];
30550
+ const items = result.items ?? [];
30551
+ const organicItems = items.filter((item) => item.type === "organic");
30552
+ const mappedOrganicResults = organicItems.flatMap((item) => {
30553
+ const mapped = organicResult(item);
30554
+ return mapped ? [mapped] : [];
30555
+ }).sort((left, right) => left.rankAbsolute - right.rankAbsolute || left.rankGroup - right.rankGroup || compareCodepoints$3(left.url, right.url));
30556
+ const organicResults = mappedOrganicResults.slice(0, input.depth);
30557
+ const invalidRows = organicItems.length - mappedOrganicResults.length;
30558
+ const resultCapped = mappedOrganicResults.length > input.depth;
30559
+ const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
30560
+ if (invalidRows > 0) warnings.push({
30561
+ code: "invalid-organic-results",
30562
+ field: "organicResults",
30563
+ message: `DataForSEO returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank, domain, or URL.`
30564
+ });
30565
+ if (!Number.isFinite(Date.parse(result.datetime))) warnings.push({
30566
+ code: "invalid-serp-observation-time",
30567
+ field: "checkedAt",
30568
+ message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
30569
+ });
30570
+ const checkUrl = safeUrl$1(result.check_url);
30571
+ if (result.check_url && !checkUrl) warnings.push({
30572
+ code: "invalid-serp-check-url",
30573
+ field: "checkUrl",
30574
+ message: "DataForSEO returned an invalid SERP check URL."
30575
+ });
30576
+ if (invalidResultCount) warnings.push({
30577
+ code: "invalid-serp-result-count",
30578
+ field: "resultCount",
30579
+ message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
30580
+ });
30581
+ const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
30582
+ return {
30583
+ schemaVersion: 1,
30584
+ provider: "dataforseo",
30585
+ capability: "serp-snapshot",
30586
+ data: {
30587
+ keyword,
30588
+ effectiveKeyword: normalizedKeyword$2(result.spell?.keyword ?? result.keyword),
30589
+ searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
30590
+ checkedAt: checkedAt(result.datetime, snapshot.observedAt),
30591
+ checkUrl,
30592
+ resultCount: invalidResultCount ? null : result.se_results_count ?? null,
30593
+ pagesCount: result.pages_count === void 0 ? null : result.pages_count,
30594
+ features,
30595
+ organicResults
30596
+ },
30597
+ observedAt: snapshot.observedAt,
30598
+ market: {
30599
+ ...market,
30600
+ device: market.device ?? "desktop"
30601
+ },
30602
+ coverage: {
30603
+ requestedRows: input.depth,
30604
+ returnedRows: snapshot.returnedRows,
30605
+ retainedRows: organicResults.length,
30606
+ invalidRows,
30607
+ providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
30608
+ completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
30609
+ nextCursor: null
30610
+ },
30611
+ cache: snapshot.cache,
30612
+ cost: snapshot.cost,
30613
+ request: {
30614
+ operation: "serp-snapshot",
30615
+ endpoint,
30616
+ limit: input.depth,
30617
+ filters: {
30618
+ countryCode: market.countryCode,
30619
+ languageCode: market.languageCode,
30620
+ device: market.device ?? "desktop",
30621
+ ...location.locationCode !== void 0 ? { locationCode: location.locationCode } : { locationName: location.locationName }
30622
+ },
30623
+ sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
30624
+ },
30625
+ warnings
30626
+ };
30627
+ }
30628
+ var DataForSeoSerpSnapshotProvider = class {
30629
+ provider = "dataforseo";
30630
+ capabilitySupport = [{
30631
+ capability: "serp-snapshot",
30632
+ status: "available",
30633
+ markets: [{
30634
+ searchEngines: ["google"],
30635
+ devices: ["desktop", "mobile"],
30636
+ location: "any"
30637
+ }]
30638
+ }];
30639
+ client;
30640
+ constructor(options = {}) {
30641
+ this.client = options.client ?? new DataForSeoClient(options);
30642
+ }
30643
+ async serpSnapshot(input) {
30644
+ const market = searchMarketSchema.parse(input.market);
30645
+ if (market.searchEngine !== "google") throw new ProviderError({
30646
+ provider: "dataforseo",
30647
+ operation: "serp-snapshot",
30648
+ code: "configuration",
30649
+ message: "DataForSEO SERP snapshots currently support Google."
30650
+ });
30651
+ if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_SERP_DEPTH) throw new ProviderError({
30652
+ provider: "dataforseo",
30653
+ operation: "serp-snapshot",
30654
+ code: "configuration",
30655
+ message: "SERP depth must be from 1 to 100."
30656
+ });
30657
+ const keyword = normalizedKeyword$2(input.keyword);
30658
+ if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new ProviderError({
30659
+ provider: "dataforseo",
30660
+ operation: "serp-snapshot",
30661
+ code: "configuration",
30662
+ message: "SERP snapshots require a keyword of at most 80 characters and 10 words."
30663
+ });
30664
+ const context = input.context ?? {
30665
+ reportId: "serp-results",
30666
+ reportRunId: randomUUID()
30667
+ };
30668
+ const location = locationRequest(market, "serp-snapshot");
30669
+ return mapDataForSeoSerpSnapshot(input, await this.client.serpLive({
30670
+ keyword,
30671
+ languageCode: market.languageCode.split("-")[0],
30672
+ ...location,
30673
+ device: market.device ?? "desktop",
30674
+ depth: input.depth,
30675
+ refresh: input.refresh,
30676
+ context
30677
+ }));
30678
+ }
30679
+ };
30680
+ //#endregion
30681
+ //#region packages/core/src/analyze/serp-results.ts
30682
+ const MAX_REPORT_DEPTH = 100;
30683
+ function compareCodepoints(left, right) {
30684
+ return left < right ? -1 : left > right ? 1 : 0;
30685
+ }
30686
+ function snapshotProvider(candidate) {
30687
+ return "serpSnapshot" in candidate && typeof candidate.serpSnapshot === "function" ? candidate : null;
30688
+ }
30689
+ async function defaultCandidates$1() {
30690
+ return [{
30691
+ adapter: new DataForSeoSerpSnapshotProvider(),
30692
+ connected: Boolean(await readDataForSeoCredentials()),
30693
+ priority: 10
30694
+ }];
30695
+ }
30696
+ function providerError$2(error) {
30697
+ if (!(error instanceof ProviderError)) throw error;
30698
+ if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
30699
+ if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
30700
+ throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
30701
+ }
30702
+ function validateInput$1(input) {
30703
+ const keyword = input.keyword.trim().replace(/\s+/gu, " ");
30704
+ if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new SeoError("INVALID_INPUT", "SERP results requires a keyword of at most 80 characters and 10 words.");
30705
+ if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_REPORT_DEPTH) throw new SeoError("INVALID_INPUT", `SERP result depth must be from 1 to ${MAX_REPORT_DEPTH}.`);
30706
+ const market = searchMarketSchema.safeParse(input.market);
30707
+ if (!market.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
30708
+ const provider = input.provider ? providerIdSchema.safeParse(input.provider) : void 0;
30709
+ if (provider && !provider.success) throw new SeoError("INVALID_INPUT", "Use a supported SERP provider.");
30710
+ return {
30711
+ keyword,
30712
+ market: market.data,
30713
+ provider: provider?.data
30714
+ };
30715
+ }
30716
+ function reportStatus$1(evidence) {
30717
+ if (evidence.coverage.completeness === "unavailable" || evidence.coverage.completeness === "invalid") return "unavailable";
30718
+ return evidence.coverage.completeness === "complete" ? "complete" : "partial";
30719
+ }
30720
+ function domainSummaries(snapshot) {
30721
+ const byDomain = /* @__PURE__ */ new Map();
30722
+ for (const result of snapshot.organicResults) byDomain.set(result.domain, [...byDomain.get(result.domain) ?? [], result.rankAbsolute]);
30723
+ return [...byDomain.entries()].map(([domain, ranks]) => ({
30724
+ domain,
30725
+ resultCount: ranks.length,
30726
+ ranks: [...ranks].sort((left, right) => left - right)
30727
+ })).sort((left, right) => right.resultCount - left.resultCount || (left.ranks[0] ?? Number.MAX_SAFE_INTEGER) - (right.ranks[0] ?? Number.MAX_SAFE_INTEGER) || compareCodepoints(left.domain, right.domain));
30728
+ }
30729
+ function reportFindings(snapshot, domains) {
30730
+ const findings = [];
30731
+ if (snapshot.effectiveKeyword !== snapshot.keyword) findings.push({
30732
+ code: "query-correction",
30733
+ evidenceRef: "evidence.data.effectiveKeyword",
30734
+ principle: "A corrected query changes the observed result context and does not prove the original wording has the same intent.",
30735
+ detail: `The provider observed results for ${snapshot.effectiveKeyword} after the requested query ${snapshot.keyword}.`
30736
+ });
30737
+ for (const [index, domain] of domains.entries()) {
30738
+ if (domain.resultCount <= 1) continue;
30739
+ findings.push({
30740
+ code: "repeated-domain",
30741
+ evidenceRef: `domains[${index}]`,
30742
+ principle: "Repeated results are descriptive snapshot evidence, not a domain strength or quality score.",
30743
+ detail: `${domain.domain} appears ${domain.resultCount} times at absolute ranks ${domain.ranks.join(", ")}.`
30744
+ });
30745
+ }
30746
+ return findings.slice(0, 10);
30747
+ }
30748
+ async function serpResultsReport(input, dependencies = {}) {
30749
+ const depth = input.depth ?? 10;
30750
+ const validated = validateInput$1({
30751
+ ...input,
30752
+ depth
30753
+ });
30754
+ let candidates;
30755
+ try {
30756
+ candidates = dependencies.candidates ?? await defaultCandidates$1();
30757
+ } catch (error) {
30758
+ return providerError$2(error);
30759
+ }
30760
+ const resolution = resolveProvider({
30761
+ capability: "serp-snapshot",
30762
+ market: validated.market,
30763
+ candidates,
30764
+ provider: validated.provider
30765
+ });
30766
+ if (resolution.status === "unavailable") throw new SeoError("PROVIDER_UNAVAILABLE", resolution.reason === "provider-not-connected" ? "No connected provider can return live SERP results. Run `seo providers dataforseo connect` first." : validated.provider ? `${validated.provider} cannot return live SERP results for this market.` : "No configured provider can return live SERP results for this market.");
30767
+ const provider = snapshotProvider(resolution.provider);
30768
+ if (!provider) throw new SeoError("PROVIDER_UNAVAILABLE", "The selected provider has no live SERP implementation.");
30769
+ let evidence;
30770
+ try {
30771
+ evidence = await provider.serpSnapshot({
30772
+ keyword: validated.keyword,
30773
+ market: validated.market,
30774
+ depth,
30775
+ refresh: input.refresh,
30776
+ context: {
30777
+ projectId: input.context?.projectId ?? input.projectId,
30778
+ reportId: input.context?.reportId ?? "serp-results",
30779
+ reportRunId: input.context?.reportRunId ?? randomUUID()
30780
+ }
30781
+ });
30782
+ } catch (error) {
30783
+ return providerError$2(error);
30784
+ }
30785
+ const domains = domainSummaries(evidence.data);
30786
+ const dataStatus = reportStatus$1(evidence);
30787
+ const generatedAt = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
30788
+ const correctedQuery = evidence.data.effectiveKeyword !== evidence.data.keyword;
30789
+ return {
30790
+ schemaVersion: 1,
30791
+ generatedAt,
30792
+ dataStatus,
30793
+ market: evidence.market,
30794
+ summary: {
30795
+ keyword: evidence.data.keyword,
30796
+ effectiveKeyword: evidence.data.effectiveKeyword,
30797
+ requestedDepth: depth,
30798
+ organicResults: evidence.data.organicResults.length,
30799
+ uniqueDomains: domains.length,
30800
+ observedFeatures: evidence.data.features.length,
30801
+ correctedQuery,
30802
+ verdict: `${evidence.data.organicResults.length} organic results across ${domains.length} domains were retained from the ${evidence.market.device ?? "desktop"} snapshot.`
30803
+ },
30804
+ evidence,
30805
+ domains,
30806
+ findings: reportFindings(evidence.data, domains),
30807
+ caveats: [
30808
+ "This is one market, location, language, and device-specific result snapshot; it is not rank history.",
30809
+ "Search results can change between checks and may differ from a person's signed-in or personalized results.",
30810
+ "The provider result count is an estimate and may be missing; retained organic rows are bounded by the requested depth.",
30811
+ "Repeated domains, result features, titles, and snippets do not establish content quality, authority, or ranking feasibility."
30812
+ ],
30813
+ nextSteps: [
30814
+ "Review the retained pages and result types before deciding whether the query fits an existing page, a new page, or no page.",
30815
+ "Keep Search Console average position separate from these exact snapshot ranks when comparing first-party performance.",
30816
+ "Repeat the same market and device check later if the decision depends on result stability."
30817
+ ]
30818
+ };
30819
+ }
30820
+ //#endregion
30821
+ //#region packages/core/src/analyze/local-search/report.ts
30822
+ const MAX_DAYS$1 = 548;
30823
+ const MAX_ROWS = 5e4;
30824
+ function boundedInteger$2(input) {
30825
+ const value = input.value ?? input.fallback;
30826
+ if (!Number.isSafeInteger(value) || value < input.minimum || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be a whole number between ${input.minimum} and ${input.maximum}.`);
30827
+ return value;
30828
+ }
30829
+ function validateInput(input) {
30830
+ const site = input.site.trim();
30831
+ if (!site) throw new SeoError("INVALID_INPUT", "site must not be empty.");
30832
+ if (input.locationTerms && (input.locationTerms.length > 100 || input.locationTerms.some((term) => !term.trim() || term.length > 100))) throw new SeoError("INVALID_INPUT", "locationTerms must contain at most 100 non-empty terms of 100 characters or fewer.");
30833
+ if (input.brandTerms && (input.brandTerms.length > 20 || input.brandTerms.some((term) => !term.trim() || term.length > 200))) throw new SeoError("INVALID_INPUT", "brandTerms must contain at most 20 non-empty terms of 200 characters or fewer.");
30834
+ const includeSerps = input.includeSerps ?? false;
30835
+ let market;
30836
+ if (input.market) {
30837
+ const parsed = searchMarketSchema.safeParse(input.market);
30838
+ if (!parsed.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
30839
+ market = parsed.data;
30840
+ }
30841
+ if (includeSerps && !market?.location) throw new SeoError("INVALID_INPUT", "Local SERP evidence requires a country, language, and canonical location.");
30842
+ if (!includeSerps && (input.market || input.provider || input.serpLimit !== void 0 || input.serpDepth !== void 0)) throw new SeoError("INVALID_INPUT", "Set includeSerps to true before passing market, provider, or SERP options.");
30843
+ return {
30844
+ site,
30845
+ days: boundedInteger$2({
30846
+ value: input.days,
30847
+ fallback: 28,
30848
+ minimum: 1,
30849
+ maximum: MAX_DAYS$1,
30850
+ label: "days"
30851
+ }),
30852
+ locationTerms: normalizeLocationTerms(input.locationTerms),
30853
+ minImpressions: boundedInteger$2({
30854
+ value: input.minImpressions,
30855
+ fallback: 1,
30856
+ minimum: 0,
30857
+ maximum: 1e9,
30858
+ label: "minImpressions"
30859
+ }),
30860
+ limit: boundedInteger$2({
30861
+ value: input.limit,
30862
+ fallback: 25,
30863
+ minimum: 1,
30864
+ maximum: 100,
30865
+ label: "limit"
30866
+ }),
30867
+ maxRows: boundedInteger$2({
30868
+ value: input.maxRows,
30869
+ fallback: MAX_ROWS,
30870
+ minimum: 1,
30871
+ maximum: MAX_ROWS,
30872
+ label: "maxRows"
30873
+ }),
30874
+ includeSerps,
30875
+ market,
30876
+ serpLimit: boundedInteger$2({
30877
+ value: input.serpLimit,
30878
+ fallback: 3,
30879
+ minimum: 1,
30880
+ maximum: 3,
30881
+ label: "serpLimit"
30882
+ }),
30883
+ serpDepth: boundedInteger$2({
30884
+ value: input.serpDepth,
30885
+ fallback: 10,
30886
+ minimum: 1,
30887
+ maximum: 20,
30888
+ label: "serpDepth"
30889
+ })
30890
+ };
30891
+ }
30892
+ function emptyCost() {
30893
+ return {
30894
+ currency: "USD",
30895
+ estimatedMicros: 0,
30896
+ actualMicros: 0,
30897
+ taskIds: []
30898
+ };
30899
+ }
30900
+ function aggregateCosts(costs) {
30901
+ const estimated = costs.map((cost) => cost.estimatedMicros);
30902
+ const actual = costs.map((cost) => cost.actualMicros);
30903
+ return {
30904
+ currency: "USD",
30905
+ estimatedMicros: estimated.some((value) => value === null) ? null : estimated.reduce((sum, value) => sum + (value ?? 0), 0),
30906
+ actualMicros: actual.some((value) => value === null) ? null : actual.reduce((sum, value) => sum + (value ?? 0), 0),
30907
+ taskIds: [...new Set(costs.flatMap((cost) => cost.taskIds))].sort()
30908
+ };
30909
+ }
30910
+ function costsWithUnknownRequest(costs) {
30911
+ return {
30912
+ currency: "USD",
30913
+ estimatedMicros: null,
30914
+ actualMicros: null,
30915
+ taskIds: [...new Set(costs.flatMap((cost) => cost.taskIds))].sort()
30916
+ };
30917
+ }
30918
+ async function acquireSerps(input) {
30919
+ const selected = input.queries.slice(0, input.limit);
30920
+ const selection = {
30921
+ availableQueries: input.queries.length,
30922
+ requestedQueries: selected.length,
30923
+ omittedQueries: Math.max(0, input.queries.length - selected.length),
30924
+ limit: input.limit,
30925
+ depth: input.depth,
30926
+ method: "highest-impression-local-queries-v1"
30927
+ };
30928
+ if (!input.requested) return {
30929
+ requested: false,
30930
+ status: "not-requested",
30931
+ selection,
30932
+ market: null,
30933
+ reports: [],
30934
+ cost: emptyCost(),
30935
+ reason: "Local SERP snapshots were not requested, so no paid provider work was attempted."
30936
+ };
30937
+ if (selected.length === 0) return {
30938
+ requested: true,
30939
+ status: "skipped",
30940
+ selection,
30941
+ market: input.market ?? null,
30942
+ reports: [],
30943
+ cost: emptyCost(),
30944
+ reason: "No retained local-intent query was available for a SERP snapshot."
30945
+ };
30946
+ if (!input.market) throw new SeoError("INVALID_INPUT", "Local SERP evidence requires a market.");
30947
+ const reports = [];
30948
+ try {
30949
+ for (const query of selected) reports.push(await input.run({
30950
+ keyword: query,
30951
+ market: input.market,
30952
+ depth: input.depth,
30953
+ provider: input.provider,
30954
+ projectId: input.projectId,
30955
+ context: {
30956
+ reportId: "local-search-demand",
30957
+ reportRunId: input.reportRunId
30958
+ },
30959
+ refresh: input.refresh
30960
+ }));
30961
+ } catch (error) {
30962
+ if (error instanceof SeoError && (error.code === "PROVIDER_UNAVAILABLE" || error.code === "RATE_LIMITED")) return {
30963
+ requested: true,
30964
+ status: reports.length > 0 ? "partial" : "unavailable",
30965
+ selection,
30966
+ market: input.market,
30967
+ reports,
30968
+ cost: costsWithUnknownRequest(reports.map((report) => report.evidence.cost)),
30969
+ reason: "First-party local demand remains available, but not every requested SERP snapshot could be acquired.",
30970
+ error: {
30971
+ code: error.code,
30972
+ message: error.message,
30973
+ retryable: error.retryable
30974
+ }
30975
+ };
30976
+ throw error;
30977
+ }
30978
+ return {
30979
+ requested: true,
30980
+ status: reports.every((report) => report.dataStatus === "complete") ? "complete" : "partial",
30981
+ selection,
30982
+ market: input.market,
30983
+ reports,
30984
+ cost: aggregateCosts(reports.map((report) => report.evidence.cost))
30985
+ };
30986
+ }
30987
+ async function localSearchReport(input, dependencies = {}) {
30988
+ const options = validateInput(input);
30989
+ const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
30990
+ const generatedAt = now.toISOString();
30991
+ const range = finalGscDateRange(options.days, now);
30992
+ const source = await (dependencies.searchAnalytics ?? querySearchAnalytics)(options.site, {
30993
+ ...range,
30994
+ dimensions: ["query", "page"],
30995
+ type: "web",
30996
+ dataState: "final",
30997
+ maxRows: options.maxRows
30998
+ }, { refresh: input.refresh });
30999
+ const analysis = analyzeLocalSearchRows({
31000
+ rows: source.rows,
31001
+ site: options.site,
31002
+ locationTerms: options.locationTerms,
31003
+ brandTerms: input.brandTerms,
31004
+ includeBrand: input.includeBrand,
31005
+ minImpressions: options.minImpressions,
31006
+ limit: options.limit
31007
+ });
31008
+ const serpEvidence = await acquireSerps({
31009
+ requested: options.includeSerps,
31010
+ queries: analysis.opportunities.map((item) => item.query),
31011
+ market: options.market,
31012
+ provider: input.provider,
31013
+ projectId: input.projectId,
31014
+ refresh: input.refresh,
31015
+ limit: options.serpLimit,
31016
+ depth: options.serpDepth,
31017
+ reportRunId: randomUUID(),
31018
+ run: dependencies.serpResults ?? serpResultsReport
31019
+ });
31020
+ const possiblyTruncated = source.rowsFetched >= options.maxRows;
31021
+ const sourceEmpty = source.rows.length === 0;
31022
+ const filtered = !sourceEmpty && analysis.selection.eligibleQueries === 0;
31023
+ const partial = possiblyTruncated || serpEvidence.status === "partial" || serpEvidence.status === "unavailable";
31024
+ const opportunities = analysis.opportunities;
31025
+ const localPackSnapshots = serpEvidence.reports.filter((report) => report.evidence.data.features.includes("local_pack")).length;
31026
+ const dataStatus = sourceEmpty ? "empty" : partial ? "partial" : filtered ? "filtered" : "complete";
31027
+ const warnings = [];
31028
+ if (possiblyTruncated) warnings.push(`Search Console reached the ${options.maxRows}-row retention cap. Lower-volume local queries may be missing.`);
31029
+ if (analysis.selection.invalidRows > 0) warnings.push(`${analysis.selection.invalidRows} malformed Search Console rows were excluded.`);
31030
+ if (analysis.selection.conflictingRows > 0) warnings.push(`${analysis.selection.conflictingRows} conflicting duplicate Search Console rows were excluded.`);
31031
+ if (serpEvidence.error) warnings.push(serpEvidence.error.message);
31032
+ const nextSteps = [];
31033
+ if (!options.includeSerps) nextSteps.push("If the shortlisted queries justify paid verification, rerun with includeSerps true and an exact location and device to inspect current local result features and organic competitors.");
31034
+ else if (serpEvidence.status === "unavailable") nextSteps.push("Check the provider connection, market support, and local spend limits before retrying the missing SERP snapshots.");
31035
+ if (analysis.templates.length > 0) nextSteps.push("Run pseo-audit on the strongest repeated local page pattern before expanding it; verify source fields, page uniqueness, indexability, and representative output.");
31036
+ if (opportunities.some((item) => item.action === "review-page-overlap")) nextSteps.push("Review overlapping pages query by query before consolidating anything; multiple retained pages do not by themselves prove harmful cannibalisation.");
31037
+ nextSteps.push("Use location terms for the actual service area and repeat the report for materially different markets or devices instead of treating one area as representative.");
31038
+ return {
31039
+ schemaVersion: 1,
31040
+ methodology: "local-search-demand-v1",
31041
+ site: options.site,
31042
+ generatedAt,
31043
+ range,
31044
+ rangeDays: options.days,
31045
+ dataStatus,
31046
+ source: {
31047
+ provider: "google-search-console",
31048
+ dimensions: ["query", "page"],
31049
+ searchType: "web",
31050
+ dataState: "final",
31051
+ rowsFetched: source.rowsFetched,
31052
+ calls: source.calls,
31053
+ maxRows: options.maxRows,
31054
+ possiblyTruncated,
31055
+ completeness: possiblyTruncated ? "possibly-truncated" : "retained-query-page-rows-only"
31056
+ },
31057
+ methodologyDetails: {
31058
+ intentMethod: "explicit-local-intent-v1",
31059
+ suppliedLocationTerms: options.locationTerms,
31060
+ automaticPatterns: [
31061
+ "nearby-phrases",
31062
+ "uk-postcodes",
31063
+ "contextual-us-zip-codes"
31064
+ ],
31065
+ opportunityOrder: "impressions-clicks-position-query-v1",
31066
+ templateMethod: "pseo-url-template-clustering-v1"
31067
+ },
31068
+ selection: analysis.selection,
31069
+ summary: {
31070
+ localQueries: analysis.selection.eligibleQueries,
31071
+ returnedQueries: opportunities.length,
31072
+ pages: analysis.eligiblePageCount,
31073
+ ...analysis.eligibleSummary,
31074
+ templates: analysis.templates.length,
31075
+ serpSnapshots: serpEvidence.reports.length,
31076
+ localPackSnapshots,
31077
+ verdict: sourceEmpty ? "Search Console returned no retained query/page rows for this date window." : filtered ? "No retained query/page rows matched the supplied terms or automatic local-intent patterns." : `${countLabel(analysis.selection.eligibleQueries, "local-intent query")} found; ${countLabel(opportunities.length, "query")} returned for review.`
31078
+ },
31079
+ opportunities,
31080
+ templates: analysis.templates,
31081
+ serpEvidence,
31082
+ warnings,
31083
+ caveats: [
31084
+ "Search Console exposes retained top query/page rows and does not guarantee every query. A row cap means lower-volume local demand may be missing.",
31085
+ "Named-place, proximity, and postal-code matching are explicit heuristics. They can miss implicit local intent and postal-looking non-location terms.",
31086
+ "A place in a query describes query wording, not the searcher's physical location. Search Console average position is not an exact local rank.",
31087
+ "A Google Analytics city or region describes measured users under Analytics rules; it is not joined here because it cannot prove which search query brought a user.",
31088
+ "A local_pack feature in a live snapshot proves only that the result type appeared in that market, device, and observation. It does not expose or validate every business listing.",
31089
+ "Repeated URL patterns and page overlap are review signals, not proof that more pages should exist or that consolidation is required.",
31090
+ input.includeBrand ? "Brand queries were included when they matched the local-intent rules." : "Brand-like queries were excluded using the site and supplied brand terms."
31091
+ ],
31092
+ nextSteps
31093
+ };
31094
+ }
31095
+ //#endregion
30238
31096
  //#region packages/core/src/analyze/monitoring/crawl-recommendations.ts
30239
31097
  function statusMovedToError(item) {
30240
31098
  return item.kind !== "removed" && (item.after?.status ?? 0) >= 400 && (item.before?.status ?? 200) < 400;
@@ -34790,326 +35648,6 @@ function pseoOpportunityVerdict(input) {
34790
35648
  return `${expansions} template-expansion candidate${expansions === 1 ? "" : "s"} and ${input.competitors.length} observed competitor domain${input.competitors.length === 1 ? "" : "s"} were retained for review.`;
34791
35649
  }
34792
35650
  //#endregion
34793
- //#region packages/core/src/providers/dataforseo/serp-snapshot.ts
34794
- const MAX_SERP_DEPTH = 100;
34795
- const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
34796
- function safeUrl$1(value) {
34797
- if (!value) return null;
34798
- try {
34799
- const parsed = new URL(value);
34800
- if (!["http:", "https:"].includes(parsed.protocol)) return null;
34801
- parsed.username = "";
34802
- parsed.password = "";
34803
- return parsed.toString();
34804
- } catch {
34805
- return null;
34806
- }
34807
- }
34808
- function validOrganic(item) {
34809
- 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;
34810
- }
34811
- function organicResult(item) {
34812
- if (!validOrganic(item)) return null;
34813
- const url = safeUrl$1(item.url);
34814
- if (!url) return null;
34815
- return {
34816
- rankGroup: item.rank_group,
34817
- rankAbsolute: item.rank_absolute,
34818
- page: item.page,
34819
- domain: item.domain.trim().toLowerCase(),
34820
- url,
34821
- title: item.title ?? null,
34822
- description: item.description ?? null,
34823
- isFeaturedSnippet: item.is_featured_snippet ?? null
34824
- };
34825
- }
34826
- function checkedAt(value, fallback) {
34827
- const timestamp = Date.parse(value);
34828
- return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
34829
- }
34830
- function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
34831
- const market = searchMarketSchema.parse(input.market);
34832
- const keyword = normalizedKeyword$2(input.keyword);
34833
- const location = locationRequest(market, "serp-snapshot");
34834
- const result = snapshot.response.tasks.flatMap((task) => task.result ?? []).find((item) => normalizedKeyword$2(item.keyword) === keyword);
34835
- if (!result) throw new ProviderError({
34836
- provider: "dataforseo",
34837
- operation: "serp-snapshot",
34838
- code: "invalid-response",
34839
- message: "DataForSEO returned no matching SERP result."
34840
- });
34841
- const warnings = [...snapshot.warnings];
34842
- const items = result.items ?? [];
34843
- const organicItems = items.filter((item) => item.type === "organic");
34844
- const mappedOrganicResults = organicItems.flatMap((item) => {
34845
- const mapped = organicResult(item);
34846
- return mapped ? [mapped] : [];
34847
- }).sort((left, right) => left.rankAbsolute - right.rankAbsolute || left.rankGroup - right.rankGroup || compareCodepoints$3(left.url, right.url));
34848
- const organicResults = mappedOrganicResults.slice(0, input.depth);
34849
- const invalidRows = organicItems.length - mappedOrganicResults.length;
34850
- const resultCapped = mappedOrganicResults.length > input.depth;
34851
- const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
34852
- if (invalidRows > 0) warnings.push({
34853
- code: "invalid-organic-results",
34854
- field: "organicResults",
34855
- message: `DataForSEO returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank, domain, or URL.`
34856
- });
34857
- if (!Number.isFinite(Date.parse(result.datetime))) warnings.push({
34858
- code: "invalid-serp-observation-time",
34859
- field: "checkedAt",
34860
- message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
34861
- });
34862
- const checkUrl = safeUrl$1(result.check_url);
34863
- if (result.check_url && !checkUrl) warnings.push({
34864
- code: "invalid-serp-check-url",
34865
- field: "checkUrl",
34866
- message: "DataForSEO returned an invalid SERP check URL."
34867
- });
34868
- if (invalidResultCount) warnings.push({
34869
- code: "invalid-serp-result-count",
34870
- field: "resultCount",
34871
- message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
34872
- });
34873
- const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
34874
- return {
34875
- schemaVersion: 1,
34876
- provider: "dataforseo",
34877
- capability: "serp-snapshot",
34878
- data: {
34879
- keyword,
34880
- effectiveKeyword: normalizedKeyword$2(result.spell?.keyword ?? result.keyword),
34881
- searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
34882
- checkedAt: checkedAt(result.datetime, snapshot.observedAt),
34883
- checkUrl,
34884
- resultCount: invalidResultCount ? null : result.se_results_count ?? null,
34885
- pagesCount: result.pages_count === void 0 ? null : result.pages_count,
34886
- features,
34887
- organicResults
34888
- },
34889
- observedAt: snapshot.observedAt,
34890
- market: {
34891
- ...market,
34892
- device: market.device ?? "desktop"
34893
- },
34894
- coverage: {
34895
- requestedRows: input.depth,
34896
- returnedRows: snapshot.returnedRows,
34897
- retainedRows: organicResults.length,
34898
- invalidRows,
34899
- providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
34900
- completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
34901
- nextCursor: null
34902
- },
34903
- cache: snapshot.cache,
34904
- cost: snapshot.cost,
34905
- request: {
34906
- operation: "serp-snapshot",
34907
- endpoint,
34908
- limit: input.depth,
34909
- filters: {
34910
- countryCode: market.countryCode,
34911
- languageCode: market.languageCode,
34912
- device: market.device ?? "desktop",
34913
- ...location.locationCode !== void 0 ? { locationCode: location.locationCode } : { locationName: location.locationName }
34914
- },
34915
- sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
34916
- },
34917
- warnings
34918
- };
34919
- }
34920
- var DataForSeoSerpSnapshotProvider = class {
34921
- provider = "dataforseo";
34922
- capabilitySupport = [{
34923
- capability: "serp-snapshot",
34924
- status: "available",
34925
- markets: [{
34926
- searchEngines: ["google"],
34927
- devices: ["desktop", "mobile"],
34928
- location: "any"
34929
- }]
34930
- }];
34931
- client;
34932
- constructor(options = {}) {
34933
- this.client = options.client ?? new DataForSeoClient(options);
34934
- }
34935
- async serpSnapshot(input) {
34936
- const market = searchMarketSchema.parse(input.market);
34937
- if (market.searchEngine !== "google") throw new ProviderError({
34938
- provider: "dataforseo",
34939
- operation: "serp-snapshot",
34940
- code: "configuration",
34941
- message: "DataForSEO SERP snapshots currently support Google."
34942
- });
34943
- if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_SERP_DEPTH) throw new ProviderError({
34944
- provider: "dataforseo",
34945
- operation: "serp-snapshot",
34946
- code: "configuration",
34947
- message: "SERP depth must be from 1 to 100."
34948
- });
34949
- const keyword = normalizedKeyword$2(input.keyword);
34950
- if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new ProviderError({
34951
- provider: "dataforseo",
34952
- operation: "serp-snapshot",
34953
- code: "configuration",
34954
- message: "SERP snapshots require a keyword of at most 80 characters and 10 words."
34955
- });
34956
- const context = input.context ?? {
34957
- reportId: "serp-results",
34958
- reportRunId: randomUUID()
34959
- };
34960
- const location = locationRequest(market, "serp-snapshot");
34961
- return mapDataForSeoSerpSnapshot(input, await this.client.serpLive({
34962
- keyword,
34963
- languageCode: market.languageCode.split("-")[0],
34964
- ...location,
34965
- device: market.device ?? "desktop",
34966
- depth: input.depth,
34967
- refresh: input.refresh,
34968
- context
34969
- }));
34970
- }
34971
- };
34972
- //#endregion
34973
- //#region packages/core/src/analyze/serp-results.ts
34974
- const MAX_REPORT_DEPTH = 100;
34975
- function compareCodepoints(left, right) {
34976
- return left < right ? -1 : left > right ? 1 : 0;
34977
- }
34978
- function snapshotProvider(candidate) {
34979
- return "serpSnapshot" in candidate && typeof candidate.serpSnapshot === "function" ? candidate : null;
34980
- }
34981
- async function defaultCandidates$1() {
34982
- return [{
34983
- adapter: new DataForSeoSerpSnapshotProvider(),
34984
- connected: Boolean(await readDataForSeoCredentials()),
34985
- priority: 10
34986
- }];
34987
- }
34988
- function providerError$2(error) {
34989
- if (!(error instanceof ProviderError)) throw error;
34990
- if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
34991
- if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
34992
- throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
34993
- }
34994
- function validateInput(input) {
34995
- const keyword = input.keyword.trim().replace(/\s+/gu, " ");
34996
- if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new SeoError("INVALID_INPUT", "SERP results requires a keyword of at most 80 characters and 10 words.");
34997
- if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_REPORT_DEPTH) throw new SeoError("INVALID_INPUT", `SERP result depth must be from 1 to ${MAX_REPORT_DEPTH}.`);
34998
- const market = searchMarketSchema.safeParse(input.market);
34999
- if (!market.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
35000
- const provider = input.provider ? providerIdSchema.safeParse(input.provider) : void 0;
35001
- if (provider && !provider.success) throw new SeoError("INVALID_INPUT", "Use a supported SERP provider.");
35002
- return {
35003
- keyword,
35004
- market: market.data,
35005
- provider: provider?.data
35006
- };
35007
- }
35008
- function reportStatus$1(evidence) {
35009
- if (evidence.coverage.completeness === "unavailable" || evidence.coverage.completeness === "invalid") return "unavailable";
35010
- return evidence.coverage.completeness === "complete" ? "complete" : "partial";
35011
- }
35012
- function domainSummaries(snapshot) {
35013
- const byDomain = /* @__PURE__ */ new Map();
35014
- for (const result of snapshot.organicResults) byDomain.set(result.domain, [...byDomain.get(result.domain) ?? [], result.rankAbsolute]);
35015
- return [...byDomain.entries()].map(([domain, ranks]) => ({
35016
- domain,
35017
- resultCount: ranks.length,
35018
- ranks: [...ranks].sort((left, right) => left - right)
35019
- })).sort((left, right) => right.resultCount - left.resultCount || (left.ranks[0] ?? Number.MAX_SAFE_INTEGER) - (right.ranks[0] ?? Number.MAX_SAFE_INTEGER) || compareCodepoints(left.domain, right.domain));
35020
- }
35021
- function reportFindings(snapshot, domains) {
35022
- const findings = [];
35023
- if (snapshot.effectiveKeyword !== snapshot.keyword) findings.push({
35024
- code: "query-correction",
35025
- evidenceRef: "evidence.data.effectiveKeyword",
35026
- principle: "A corrected query changes the observed result context and does not prove the original wording has the same intent.",
35027
- detail: `The provider observed results for ${snapshot.effectiveKeyword} after the requested query ${snapshot.keyword}.`
35028
- });
35029
- for (const [index, domain] of domains.entries()) {
35030
- if (domain.resultCount <= 1) continue;
35031
- findings.push({
35032
- code: "repeated-domain",
35033
- evidenceRef: `domains[${index}]`,
35034
- principle: "Repeated results are descriptive snapshot evidence, not a domain strength or quality score.",
35035
- detail: `${domain.domain} appears ${domain.resultCount} times at absolute ranks ${domain.ranks.join(", ")}.`
35036
- });
35037
- }
35038
- return findings.slice(0, 10);
35039
- }
35040
- async function serpResultsReport(input, dependencies = {}) {
35041
- const depth = input.depth ?? 10;
35042
- const validated = validateInput({
35043
- ...input,
35044
- depth
35045
- });
35046
- let candidates;
35047
- try {
35048
- candidates = dependencies.candidates ?? await defaultCandidates$1();
35049
- } catch (error) {
35050
- return providerError$2(error);
35051
- }
35052
- const resolution = resolveProvider({
35053
- capability: "serp-snapshot",
35054
- market: validated.market,
35055
- candidates,
35056
- provider: validated.provider
35057
- });
35058
- if (resolution.status === "unavailable") throw new SeoError("PROVIDER_UNAVAILABLE", resolution.reason === "provider-not-connected" ? "No connected provider can return live SERP results. Run `seo providers dataforseo connect` first." : validated.provider ? `${validated.provider} cannot return live SERP results for this market.` : "No configured provider can return live SERP results for this market.");
35059
- const provider = snapshotProvider(resolution.provider);
35060
- if (!provider) throw new SeoError("PROVIDER_UNAVAILABLE", "The selected provider has no live SERP implementation.");
35061
- let evidence;
35062
- try {
35063
- evidence = await provider.serpSnapshot({
35064
- keyword: validated.keyword,
35065
- market: validated.market,
35066
- depth,
35067
- refresh: input.refresh,
35068
- context: {
35069
- projectId: input.context?.projectId ?? input.projectId,
35070
- reportId: input.context?.reportId ?? "serp-results",
35071
- reportRunId: input.context?.reportRunId ?? randomUUID()
35072
- }
35073
- });
35074
- } catch (error) {
35075
- return providerError$2(error);
35076
- }
35077
- const domains = domainSummaries(evidence.data);
35078
- const dataStatus = reportStatus$1(evidence);
35079
- const generatedAt = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
35080
- const correctedQuery = evidence.data.effectiveKeyword !== evidence.data.keyword;
35081
- return {
35082
- schemaVersion: 1,
35083
- generatedAt,
35084
- dataStatus,
35085
- market: evidence.market,
35086
- summary: {
35087
- keyword: evidence.data.keyword,
35088
- effectiveKeyword: evidence.data.effectiveKeyword,
35089
- requestedDepth: depth,
35090
- organicResults: evidence.data.organicResults.length,
35091
- uniqueDomains: domains.length,
35092
- observedFeatures: evidence.data.features.length,
35093
- correctedQuery,
35094
- verdict: `${evidence.data.organicResults.length} organic results across ${domains.length} domains were retained from the ${evidence.market.device ?? "desktop"} snapshot.`
35095
- },
35096
- evidence,
35097
- domains,
35098
- findings: reportFindings(evidence.data, domains),
35099
- caveats: [
35100
- "This is one market, location, language, and device-specific result snapshot; it is not rank history.",
35101
- "Search results can change between checks and may differ from a person's signed-in or personalized results.",
35102
- "The provider result count is an estimate and may be missing; retained organic rows are bounded by the requested depth.",
35103
- "Repeated domains, result features, titles, and snippets do not establish content quality, authority, or ranking feasibility."
35104
- ],
35105
- nextSteps: [
35106
- "Review the retained pages and result types before deciding whether the query fits an existing page, a new page, or no page.",
35107
- "Keep Search Console average position separate from these exact snapshot ranks when comparing first-party performance.",
35108
- "Repeat the same market and device check later if the decision depends on result stability."
35109
- ]
35110
- };
35111
- }
35112
- //#endregion
35113
35651
  //#region packages/core/src/analyze/pseo-opportunities/serps.ts
35114
35652
  function selectSerpKeywords(candidates, limit) {
35115
35653
  const seedRefs = /* @__PURE__ */ new Set();
@@ -42988,6 +43526,7 @@ const TELEMETRY_REPORTS = [
42988
43526
  "crawl-history",
42989
43527
  "crawler-rules",
42990
43528
  "llms-txt-audit",
43529
+ "local-search-demand",
42991
43530
  "generate-llms-txt",
42992
43531
  "measure-change",
42993
43532
  "monthly-report",
@@ -43331,6 +43870,6 @@ function isoDate(daysAgo = 0) {
43331
43870
  return date.toISOString().slice(0, 10);
43332
43871
  }
43333
43872
  //#endregion
43334
- export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoLinkProvider, DataForSeoProvider, DataForSeoRankTrackingCollector, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, RANK_TRACKING_LIMITS, RANK_TRACKING_SCHEMA_SQL, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, activeRankTrackingRun, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, collectDataForSeoLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, competitorKeywordGapReport, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, domainOverviewReport, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, enforceRankTrackingLogicalBytes, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, executeRankTracking, expectedCtrForPosition, explainOkfValidation, explainRule, failRankTrackingTask, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getOrCreateRankTrackingConfiguration, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRankTrackingRun, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, latestRankTrackingRun, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, linkTargetContext, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, mapDataForSeoSerpSnapshot, markRankTasksPosted, markRankTasksPosting, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, priorComparableRankTrackingRun, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, rankObservations, rankTrackingLogicalBytes, rankTrackingReport, rankTrackingTasks, rankedKeywordsReport, rankingPagesReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, recoverRankTaskReceipt, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, refreshRankTrackingRun, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, saveRankObservation, saveRankObservations, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpCompetitorsReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, startRankTrackingRun, strikingDistance, submitIndexNow, summarizeCrawlReport, targetMatchesDomain, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
43873
+ export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoLinkProvider, DataForSeoProvider, DataForSeoRankTrackingCollector, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, RANK_TRACKING_LIMITS, RANK_TRACKING_SCHEMA_SQL, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, activeRankTrackingRun, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzeLocalSearchRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, classifyLocalIntent, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, collectDataForSeoLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, competitorKeywordGapReport, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLocalIntentClassifier, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, domainOverviewReport, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, enforceRankTrackingLogicalBytes, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, executeRankTracking, expectedCtrForPosition, explainOkfValidation, explainRule, failRankTrackingTask, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getOrCreateRankTrackingConfiguration, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRankTrackingRun, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, latestRankTrackingRun, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, linkTargetContext, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, localSearchReport, loginWithLoopback, makeRunId, mapDataForSeoSerpSnapshot, markRankTasksPosted, markRankTasksPosting, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeLocalSearchText, normalizeLocationTerms, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, priorComparableRankTrackingRun, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, rankObservations, rankTrackingLogicalBytes, rankTrackingReport, rankTrackingTasks, rankedKeywordsReport, rankingPagesReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, recoverRankTaskReceipt, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, refreshRankTrackingRun, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, saveRankObservation, saveRankObservations, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpCompetitorsReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, startRankTrackingRun, strikingDistance, submitIndexNow, summarizeCrawlReport, targetMatchesDomain, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
43335
43874
 
43336
43875
  //# sourceMappingURL=index.js.map