mcp-scraper 0.35.1 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +9 -9
  2. package/dist/bin/api-server.cjs +25071 -19082
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.cjs +51 -7
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +48 -5
  8. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs +2 -2
  10. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-install.js +2 -2
  12. package/dist/bin/mcp-stdio-server.cjs +995 -222
  13. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-stdio-server.js +8 -8
  15. package/dist/bin/paa-harvest.cjs +125 -70
  16. package/dist/bin/paa-harvest.cjs.map +1 -1
  17. package/dist/bin/paa-harvest.js +4 -4
  18. package/dist/chunk-345BQXZH.js +712 -0
  19. package/dist/chunk-345BQXZH.js.map +1 -0
  20. package/dist/chunk-3LWYPAU5.js +7 -0
  21. package/dist/chunk-3LWYPAU5.js.map +1 -0
  22. package/dist/{chunk-M2S27J6Z.js → chunk-44HZLHDV.js} +10 -1
  23. package/dist/chunk-44HZLHDV.js.map +1 -0
  24. package/dist/{chunk-62DQAWPF.js → chunk-CSCD2HNS.js} +498 -43
  25. package/dist/chunk-CSCD2HNS.js.map +1 -0
  26. package/dist/{chunk-ZID3WQID.js → chunk-FQI5PFE7.js} +9 -71
  27. package/dist/chunk-FQI5PFE7.js.map +1 -0
  28. package/dist/{chunk-3HBPKR5G.js → chunk-FSAXLDB3.js} +3 -3
  29. package/dist/chunk-G3P3ZDB4.js +69 -0
  30. package/dist/chunk-G3P3ZDB4.js.map +1 -0
  31. package/dist/{chunk-YRGSEY5L.js → chunk-G7KAVJ3F.js} +2 -2
  32. package/dist/{chunk-YRGSEY5L.js.map → chunk-G7KAVJ3F.js.map} +1 -1
  33. package/dist/{chunk-NPMW5HUS.js → chunk-JK2FRDAP.js} +930 -244
  34. package/dist/chunk-JK2FRDAP.js.map +1 -0
  35. package/dist/{chunk-XVVNKASZ.js → chunk-LOPKN3YL.js} +118 -73
  36. package/dist/chunk-LOPKN3YL.js.map +1 -0
  37. package/dist/{chunk-4ZB3X6BQ.js → chunk-MA5JBAUZ.js} +16 -2
  38. package/dist/{chunk-4ZB3X6BQ.js.map → chunk-MA5JBAUZ.js.map} +1 -1
  39. package/dist/chunk-PUJFYJXB.js +684 -0
  40. package/dist/chunk-PUJFYJXB.js.map +1 -0
  41. package/dist/{chunk-BWXLTWF7.js → chunk-PWPUKR5U.js} +9 -5
  42. package/dist/chunk-PWPUKR5U.js.map +1 -0
  43. package/dist/chunk-Q35WZJJK.js +499 -0
  44. package/dist/chunk-Q35WZJJK.js.map +1 -0
  45. package/dist/chunk-QZXKQB7Y.js +414 -0
  46. package/dist/chunk-QZXKQB7Y.js.map +1 -0
  47. package/dist/{db-YAI5AQOI.js → db-N3YECFWR.js} +12 -2
  48. package/dist/{extract-bundle-ONWZVV55.js → extract-bundle-346R6MXD.js} +284 -98
  49. package/dist/extract-bundle-346R6MXD.js.map +1 -0
  50. package/dist/index.cjs +129 -70
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +11 -0
  53. package/dist/index.d.ts +11 -0
  54. package/dist/index.js +4 -4
  55. package/dist/location-data-repository-O2VII3ON.js +35 -0
  56. package/dist/{server-GKUTC73B.js → server-QKDDEBVQ.js} +7325 -3762
  57. package/dist/server-QKDDEBVQ.js.map +1 -0
  58. package/dist/site-extract-repository-I3VM6WXN.js +62 -0
  59. package/dist/site-extract-repository-I3VM6WXN.js.map +1 -0
  60. package/dist/{worker-645BZPEK.js → worker-TTFXPPDK.js} +7 -7
  61. package/docs/hosted-location-data.md +108 -0
  62. package/docs/mcp-tool-craft-lint.generated.md +6 -3
  63. package/docs/mcp-tool-manifest.generated.json +1447 -240
  64. package/docs/mcp-tool-quality-spec.md +1 -1
  65. package/docs/specs/connected-services-control-plane-decoupling-spec.md +1044 -0
  66. package/docs/specs/kernel-stealth-captcha-test-matrix.md +278 -0
  67. package/docs/specs/multimodal-image-memory-architecture-spec.md +1022 -0
  68. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +36 -27
  69. package/package.json +7 -6
  70. package/dist/chunk-62DQAWPF.js.map +0 -1
  71. package/dist/chunk-BWXLTWF7.js.map +0 -1
  72. package/dist/chunk-M2S27J6Z.js.map +0 -1
  73. package/dist/chunk-NPMW5HUS.js.map +0 -1
  74. package/dist/chunk-R7EETU7Z.js +0 -419
  75. package/dist/chunk-R7EETU7Z.js.map +0 -1
  76. package/dist/chunk-U44TPRST.js +0 -130
  77. package/dist/chunk-U44TPRST.js.map +0 -1
  78. package/dist/chunk-XVVNKASZ.js.map +0 -1
  79. package/dist/chunk-YR4LJ6AQ.js +0 -7
  80. package/dist/chunk-YR4LJ6AQ.js.map +0 -1
  81. package/dist/chunk-YV2FUEBX.js +0 -851
  82. package/dist/chunk-YV2FUEBX.js.map +0 -1
  83. package/dist/chunk-ZID3WQID.js.map +0 -1
  84. package/dist/extract-bundle-ONWZVV55.js.map +0 -1
  85. package/dist/server-GKUTC73B.js.map +0 -1
  86. package/dist/site-extract-repository-L6BHWVDU.js +0 -30
  87. /package/dist/{chunk-3HBPKR5G.js.map → chunk-FSAXLDB3.js.map} +0 -0
  88. /package/dist/{db-YAI5AQOI.js.map → db-N3YECFWR.js.map} +0 -0
  89. /package/dist/{site-extract-repository-L6BHWVDU.js.map → location-data-repository-O2VII3ON.js.map} +0 -0
  90. /package/dist/{worker-645BZPEK.js.map → worker-TTFXPPDK.js.map} +0 -0
@@ -5,7 +5,7 @@ import {
5
5
  currentCostContext,
6
6
  recordKernelSession,
7
7
  runWithCostContext
8
- } from "./chunk-4ZB3X6BQ.js";
8
+ } from "./chunk-MA5JBAUZ.js";
9
9
  import {
10
10
  postToMemoryLibrary
11
11
  } from "./chunk-K443GQY5.js";
@@ -22,7 +22,7 @@ import {
22
22
  RECAPTCHA_INSTRUCTIONS,
23
23
  RequestAbortedError,
24
24
  sanitizeVendorName
25
- } from "./chunk-M2S27J6Z.js";
25
+ } from "./chunk-44HZLHDV.js";
26
26
 
27
27
  // src/driver/BrowserDriver.ts
28
28
  import { chromium } from "playwright-extra";
@@ -136,8 +136,6 @@ var MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)
136
136
  var DEFAULT_KERNEL_BROWSER_TIMEOUT_SECONDS = 180;
137
137
  var KERNEL_BROWSER_CLOSE_TIMEOUT_MS = 3e3;
138
138
  var KERNEL_SESSION_DELETE_TIMEOUT_MS = 5e3;
139
- var DEFAULT_SERP_CHALLENGE_SOLVE_WAIT_MS = 2500;
140
- var SERP_CHALLENGE_SOLVE_POLL_MS = 500;
141
139
  function positiveIntFromEnv(name, fallback) {
142
140
  const raw = process.env[name];
143
141
  if (!raw) return fallback;
@@ -509,8 +507,7 @@ var BrowserDriver = class {
509
507
  }
510
508
  if (await this.isSerpChallengeVisible()) {
511
509
  await this.updateSerpNavigationDebug(navDebug, url, { hasPaa: false, captchaDetected: true });
512
- const solved = await this.waitForSerpChallengeToClear(navDebug, url);
513
- if (!solved) throw new CaptchaError(this.captchaMessage());
510
+ throw new CaptchaError(this.captchaMessage());
514
511
  }
515
512
  const fastFound = await this.page.waitForSelector(PAASelectors.item, { timeout: 4e3 }).catch(() => null);
516
513
  if (fastFound) {
@@ -519,8 +516,7 @@ var BrowserDriver = class {
519
516
  }
520
517
  if (await this.isSerpChallengeVisible()) {
521
518
  await this.updateSerpNavigationDebug(navDebug, url, { hasPaa: false, captchaDetected: true });
522
- const solved = await this.waitForSerpChallengeToClear(navDebug, url);
523
- if (!solved) throw new CaptchaError(this.captchaMessage());
519
+ throw new CaptchaError(this.captchaMessage());
524
520
  }
525
521
  for (let i = 1; i <= 6; i++) {
526
522
  await this.page.evaluate((f) => {
@@ -548,23 +544,6 @@ var BrowserDriver = class {
548
544
  }).catch(() => "");
549
545
  return /recaptcha|unusual traffic|are you a robot|about this page|detected unusual traffic/i.test(bodySnippet);
550
546
  }
551
- async waitForSerpChallengeToClear(navDebug, requestedUrl) {
552
- if (!this.kernelClient || !this.page) return false;
553
- const waitMs = positiveIntFromEnv("SERP_CHALLENGE_SOLVE_WAIT_MS", DEFAULT_SERP_CHALLENGE_SOLVE_WAIT_MS);
554
- const deadline = Date.now() + waitMs;
555
- while (Date.now() < deadline) {
556
- await this.page.waitForTimeout(Math.min(SERP_CHALLENGE_SOLVE_POLL_MS, Math.max(250, deadline - Date.now())));
557
- if (!await this.isSerpChallengeVisible()) {
558
- await this.page.waitForTimeout(1e3);
559
- if (!await this.isSerpChallengeVisible()) {
560
- await this.updateSerpNavigationDebug(navDebug, requestedUrl, { hasPaa: null, captchaDetected: false });
561
- return true;
562
- }
563
- }
564
- }
565
- await this.updateSerpNavigationDebug(navDebug, requestedUrl, { hasPaa: false, captchaDetected: true });
566
- return false;
567
- }
568
547
  async updateSerpNavigationDebug(navDebug, requestedUrl, state) {
569
548
  if (!navDebug || !this.page) return;
570
549
  try {
@@ -613,8 +592,7 @@ var BrowserDriver = class {
613
592
  try {
614
593
  await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
615
594
  if (/google\.[^/]+\/search\?/i.test(url) && await this.isSerpChallengeVisible()) {
616
- const solved = await this.waitForSerpChallengeToClear(null, url);
617
- if (!solved) throw new CaptchaError(this.captchaMessage());
595
+ throw new CaptchaError(this.captchaMessage());
618
596
  }
619
597
  } catch (err) {
620
598
  if (err instanceof CaptchaError) throw err;
@@ -844,6 +822,27 @@ function normalizeLocation(input) {
844
822
  return input;
845
823
  }
846
824
 
825
+ // src/serp-location-query.ts
826
+ function normalizedTerms(value) {
827
+ return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
828
+ }
829
+ function includesTermSequence(query, phrase) {
830
+ const queryTerms = normalizedTerms(query);
831
+ const phraseTerms = normalizedTerms(phrase);
832
+ if (phraseTerms.length === 0 || phraseTerms.length > queryTerms.length) return false;
833
+ return queryTerms.some((_, start) => phraseTerms.every((term, index) => queryTerms[start + index] === term));
834
+ }
835
+ function cityFromCanonicalLocation(location, fallback) {
836
+ return location.split(",", 1)[0]?.trim() || fallback.split(",", 1)[0]?.trim() || fallback.trim();
837
+ }
838
+ function buildLocalizedSerpQuery(query, location, canonicalLocation) {
839
+ const normalizedQuery = query.trim();
840
+ const requestedLocation = location?.trim();
841
+ if (!requestedLocation || !canonicalLocation) return normalizedQuery;
842
+ const city = cityFromCanonicalLocation(canonicalLocation, requestedLocation);
843
+ return includesTermSequence(normalizedQuery, city) ? normalizedQuery : `${normalizedQuery} ${requestedLocation}`;
844
+ }
845
+
847
846
  // src/serp-location-debug.ts
848
847
  var STATE_TO_CODE = {
849
848
  alabama: "AL",
@@ -1097,6 +1096,11 @@ async function extractAISurfacesFromDocument(config) {
1097
1096
  if (!el) return "";
1098
1097
  return (el.innerText ?? el.textContent ?? "").trim();
1099
1098
  }
1099
+ function hasSubstantiveContent(text) {
1100
+ if (!text) return false;
1101
+ const normalized = text.replace(/\s+/g, " ").trim();
1102
+ return normalized.length >= 16 && normalized.split(" ").filter(Boolean).length >= 2;
1103
+ }
1100
1104
  function hasAIOverviewLabel(el) {
1101
1105
  const heading = el.querySelector(selectors.aio.heading);
1102
1106
  if (textOf(heading) === "AI Overview") return true;
@@ -1259,19 +1263,22 @@ async function extractAISurfacesFromDocument(config) {
1259
1263
  aioCitations = extractCitations(aioRoot);
1260
1264
  }
1261
1265
  const aimRoot = document.querySelector(selectors.aim.root);
1262
- const aimDetected = surface === "aim" && !!aimRoot;
1266
+ const aimRootDetected = surface === "aim" && !!aimRoot;
1263
1267
  const aimContainer = aimRoot?.closest(selectors.aim.wrapper) ?? aimRoot;
1264
- const aimText = cleanText(aimContainer);
1268
+ const rawAimText = aimRootDetected ? cleanText(aimContainer) : null;
1269
+ const aimDetected = aimRootDetected && hasSubstantiveContent(rawAimText);
1270
+ const aimText = aimDetected ? rawAimText : null;
1265
1271
  const aimCitations = aimDetected ? extractCitations(aimContainer) : [];
1272
+ const aiOverviewDetected = !!aioRoot && hasSubstantiveContent(aioText);
1266
1273
  return {
1267
1274
  surface,
1268
1275
  aiOverview: {
1269
- detected: !!aioRoot && aioText !== null,
1270
- text: aioText,
1271
- citations: aioCitations,
1272
- expanded: aioExpanded,
1273
- fullyExpanded: aioFullyExpanded,
1274
- sections: aioSections
1276
+ detected: aiOverviewDetected,
1277
+ text: aiOverviewDetected ? aioText : null,
1278
+ citations: aiOverviewDetected ? aioCitations : [],
1279
+ expanded: aiOverviewDetected ? aioExpanded : false,
1280
+ fullyExpanded: aiOverviewDetected ? aioFullyExpanded : false,
1281
+ sections: aiOverviewDetected ? aioSections : []
1275
1282
  },
1276
1283
  aiMode: {
1277
1284
  detected: aimDetected && aimText !== null,
@@ -2045,8 +2052,10 @@ var PAAExtractor = class {
2045
2052
  this.throwIfAborted(signal);
2046
2053
  const canonicalLocation = options.location ? normalizeLocation(options.location) : null;
2047
2054
  const uule = canonicalLocation ? encodeUule(canonicalLocation) : null;
2055
+ const executedQuery = buildLocalizedSerpQuery(options.query, options.location, canonicalLocation);
2056
+ const executionOptions = executedQuery === options.query ? options : { ...options, query: executedQuery };
2048
2057
  const { hasPaa } = await this.driver.navigateToSERP(
2049
- options.query,
2058
+ executionOptions.query,
2050
2059
  uule,
2051
2060
  options.gl,
2052
2061
  options.hl,
@@ -2071,8 +2080,7 @@ var PAAExtractor = class {
2071
2080
  let locationEvidence2 = options.debug ? inferSerpLocationEvidence(canonicalLocation, organicResults2, localPack2) : void 0;
2072
2081
  let allOrganic2 = organicResults2;
2073
2082
  if ((options.pages ?? 1) >= 2) {
2074
- const p2params = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2075
- if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2083
+ const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2076
2084
  if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2077
2085
  if (uule) p2params.set("uule", uule);
2078
2086
  await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
@@ -2084,7 +2092,7 @@ var PAAExtractor = class {
2084
2092
  }
2085
2093
  }
2086
2094
  const stats2 = {
2087
- seed: options.query,
2095
+ seed: executionOptions.query,
2088
2096
  totalQuestions: 0,
2089
2097
  maxDepthReached: 0,
2090
2098
  durationMs: Date.now() - startMs,
@@ -2092,14 +2100,14 @@ var PAAExtractor = class {
2092
2100
  };
2093
2101
  this.reporter.onComplete(stats2);
2094
2102
  return {
2095
- seed: options.query,
2103
+ seed: executionOptions.query,
2096
2104
  location: options.location ?? null,
2097
2105
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2098
2106
  diagnostics: {
2099
2107
  completionStatus: "serp_only",
2100
2108
  problem: null,
2101
2109
  completeness: { ...this.completeness },
2102
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(options, canonicalLocation, uule, locationEvidence2) } : {}
2110
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2) } : {}
2103
2111
  },
2104
2112
  totalQuestions: 0,
2105
2113
  surface: aiSurfaces2.surface,
@@ -2132,8 +2140,7 @@ var PAAExtractor = class {
2132
2140
  let noPaaOrganic = organicResults;
2133
2141
  let locationEvidence2 = initialLocationEvidence;
2134
2142
  if ((options.pages ?? 1) >= 2) {
2135
- const p2params = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2136
- if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2143
+ const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2137
2144
  if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2138
2145
  if (uule) p2params.set("uule", uule);
2139
2146
  await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
@@ -2146,7 +2153,7 @@ var PAAExtractor = class {
2146
2153
  }
2147
2154
  const aiSurfaces2 = await this.extractAISurfaces(page, options);
2148
2155
  const stats2 = {
2149
- seed: options.query,
2156
+ seed: executionOptions.query,
2150
2157
  totalQuestions: 0,
2151
2158
  maxDepthReached: 0,
2152
2159
  durationMs: Date.now() - startMs,
@@ -2154,14 +2161,14 @@ var PAAExtractor = class {
2154
2161
  };
2155
2162
  this.reporter.onComplete(stats2);
2156
2163
  return {
2157
- seed: options.query,
2164
+ seed: executionOptions.query,
2158
2165
  location: options.location ?? null,
2159
2166
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2160
2167
  diagnostics: {
2161
2168
  completionStatus: "no_paa",
2162
2169
  problem: null,
2163
2170
  completeness: { ...this.completeness },
2164
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(options, canonicalLocation, uule, locationEvidence2) } : {}
2171
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2) } : {}
2165
2172
  },
2166
2173
  totalQuestions: 0,
2167
2174
  surface: aiSurfaces2.surface,
@@ -2178,10 +2185,10 @@ var PAAExtractor = class {
2178
2185
  stats: stats2
2179
2186
  };
2180
2187
  }
2181
- const flat = await this.runBFS(page, options, signal);
2188
+ const flat = await this.runBFS(page, executionOptions, signal);
2182
2189
  this.throwIfAborted(signal);
2183
2190
  const aiSurfaces = await this.extractAISurfaces(page, options);
2184
- const shortVidsParams = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
2191
+ const shortVidsParams = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
2185
2192
  if (uule) shortVidsParams.set("uule", uule);
2186
2193
  let shortVideos = [];
2187
2194
  try {
@@ -2200,7 +2207,7 @@ var PAAExtractor = class {
2200
2207
  let allOrganic = organicResults;
2201
2208
  let locationEvidence = initialLocationEvidence;
2202
2209
  if ((options.pages ?? 1) >= 2) {
2203
- const p2params = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2210
+ const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2204
2211
  if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2205
2212
  if (uule) p2params.set("uule", uule);
2206
2213
  await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
@@ -2212,9 +2219,9 @@ var PAAExtractor = class {
2212
2219
  }
2213
2220
  }
2214
2221
  const allVideos = [...videos, ...shortVideos];
2215
- const tree = this.buildTree(flat, options.query);
2222
+ const tree = this.buildTree(flat, executionOptions.query);
2216
2223
  const stats = {
2217
- seed: options.query,
2224
+ seed: executionOptions.query,
2218
2225
  totalQuestions: flat.length,
2219
2226
  maxDepthReached: flat.reduce((m, r) => Math.max(m, r.depth), 0),
2220
2227
  durationMs: Date.now() - startMs,
@@ -2222,7 +2229,7 @@ var PAAExtractor = class {
2222
2229
  };
2223
2230
  this.reporter.onComplete(stats);
2224
2231
  return {
2225
- seed: options.query,
2232
+ seed: executionOptions.query,
2226
2233
  location: options.location ?? null,
2227
2234
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2228
2235
  diagnostics: {
@@ -2230,7 +2237,7 @@ var PAAExtractor = class {
2230
2237
  problem: null,
2231
2238
  completeness: { ...this.completeness },
2232
2239
  ...diagnosticWarnings.length > 0 ? { warnings: diagnosticWarnings } : {},
2233
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(options, canonicalLocation, uule, locationEvidence) } : {}
2240
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence) } : {}
2234
2241
  },
2235
2242
  totalQuestions: flat.length,
2236
2243
  surface: aiSurfaces.surface,
@@ -2951,8 +2958,33 @@ function classifyAttemptError(err) {
2951
2958
  return /timeout|timed out|Timeout \d+ms exceeded|deadline/i.test(message) ? "timeout" : "error";
2952
2959
  }
2953
2960
  function classifyAttemptResult(result) {
2961
+ if (result.diagnostics.degradedResult) return "degraded_result";
2954
2962
  return result.diagnostics?.completionStatus ?? (result.totalQuestions > 0 ? "paa_found" : "no_paa");
2955
2963
  }
2964
+ function resultHasUsableContent(result) {
2965
+ return result.flat.length > 0 || result.organicResults.length > 0 || result.localPack.length > 0 || result.videos.length > 0 || result.forums.length > 0 || result.whatPeopleSaying.length > 0 || result.aiOverview.detected || result.aiMode.detected;
2966
+ }
2967
+ function withResultQuality(result) {
2968
+ const hasUsableContent = resultHasUsableContent(result);
2969
+ const hasWarnings = (result.diagnostics.warnings?.length ?? 0) > 0;
2970
+ const resultQuality = hasUsableContent ? hasWarnings ? "partial" : "complete" : "degraded";
2971
+ const degradedResult = resultQuality === "degraded";
2972
+ result.diagnostics = {
2973
+ ...result.diagnostics,
2974
+ resultQuality,
2975
+ degradedResult,
2976
+ degradationReasons: degradedResult ? ["empty_primary_serp"] : [],
2977
+ retryRecommended: degradedResult
2978
+ };
2979
+ return result;
2980
+ }
2981
+ function degradedResultMessage(result) {
2982
+ const reasons = result.diagnostics.degradationReasons ?? [];
2983
+ if (reasons.includes("empty_primary_serp")) {
2984
+ return "No usable primary SERP surface was captured; the result may be incomplete.";
2985
+ }
2986
+ return "The harvest result was marked degraded.";
2987
+ }
2956
2988
  function errorMessage(err) {
2957
2989
  return err instanceof Error ? err.message : String(err);
2958
2990
  }
@@ -3122,60 +3154,60 @@ async function harvest(rawOptions) {
3122
3154
  const attempt = baseCtx ? await runWithCostContext({ ...baseCtx, forceHeadful: true }, () => extractOnce(attemptOptions, signal)) : await extractOnce(attemptOptions, signal);
3123
3155
  if (attempt.error) {
3124
3156
  const err = attempt.error;
3125
- const outcome = classifyAttemptError(err);
3126
- if ((outcome === "captcha" || outcome === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3157
+ const outcome2 = classifyAttemptError(err);
3158
+ if ((outcome2 === "captcha" || outcome2 === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3127
3159
  proxyFailures++;
3128
3160
  if (proxyFailures >= 2) effectiveProxyMode = "none";
3129
3161
  }
3130
- const willRetry = i < maxAttempts - 1 && (outcome === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome));
3131
- if (outcome === "captcha") {
3162
+ const willRetry2 = i < maxAttempts - 1 && (outcome2 === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome2));
3163
+ if (outcome2 === "captcha") {
3132
3164
  console.warn(JSON.stringify({
3133
3165
  event: "harvest_attempt_captcha",
3134
3166
  attempt_number: attemptNumber,
3135
3167
  max_attempts: maxAttempts,
3136
3168
  message: errorMessage(err),
3137
- will_retry: willRetry
3169
+ will_retry: willRetry2
3138
3170
  }));
3139
- } else if (willRetry) {
3171
+ } else if (willRetry2) {
3140
3172
  console.warn(JSON.stringify({
3141
3173
  event: "harvest_attempt_proxy_retry",
3142
3174
  attempt_number: attemptNumber,
3143
3175
  max_attempts: maxAttempts,
3144
- outcome,
3176
+ outcome: outcome2,
3145
3177
  message: errorMessage(err),
3146
- will_retry: willRetry
3178
+ will_retry: willRetry2
3147
3179
  }));
3148
3180
  }
3149
3181
  await emitAttemptEvent(onAttemptEvent, {
3150
3182
  type: "finished",
3151
3183
  attemptNumber,
3152
3184
  maxAttempts,
3153
- outcome,
3185
+ outcome: outcome2,
3154
3186
  kernelSessionId: attempt.cleanup.kernelSessionId,
3155
3187
  questionCount: 0,
3156
3188
  durationMs: Date.now() - startedAtMs,
3157
3189
  error: errorMessage(err),
3158
- willRetry,
3190
+ willRetry: willRetry2,
3159
3191
  cleanup: attempt.cleanup,
3160
3192
  debug: attempt.debug,
3161
3193
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3162
3194
  });
3163
3195
  await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
3164
3196
  lastError = err;
3165
- if (willRetry) continue;
3197
+ if (willRetry2) continue;
3166
3198
  break;
3167
3199
  }
3168
3200
  const result = attempt.result;
3169
3201
  if (!result) throw new Error("Harvest attempt completed without a result");
3170
3202
  if (shouldRetryLocationMismatch(result, proxyMode)) {
3171
3203
  const err = new LocationMismatchError(locationMismatchMessage(result));
3172
- const willRetry = i < maxAttempts - 1;
3204
+ const willRetry2 = i < maxAttempts - 1;
3173
3205
  console.warn(JSON.stringify({
3174
3206
  event: "harvest_attempt_location_mismatch",
3175
3207
  attempt_number: attemptNumber,
3176
3208
  max_attempts: maxAttempts,
3177
3209
  message: err.message,
3178
- will_retry: willRetry
3210
+ will_retry: willRetry2
3179
3211
  }));
3180
3212
  await emitAttemptEvent(onAttemptEvent, {
3181
3213
  type: "finished",
@@ -3186,32 +3218,45 @@ async function harvest(rawOptions) {
3186
3218
  questionCount: result.totalQuestions,
3187
3219
  durationMs: Date.now() - startedAtMs,
3188
3220
  error: err.message,
3189
- willRetry,
3221
+ willRetry: willRetry2,
3190
3222
  cleanup: attempt.cleanup,
3191
3223
  debug: attempt.debug,
3192
3224
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3193
3225
  });
3194
3226
  await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
3195
3227
  lastError = err;
3196
- if (willRetry) continue;
3228
+ if (willRetry2) continue;
3197
3229
  break;
3198
3230
  }
3199
- const finalResult = stripInternalDebug(result, requestedDebug);
3231
+ const finalResult = withResultQuality(stripInternalDebug(result, requestedDebug));
3232
+ const outcome = classifyAttemptResult(finalResult);
3233
+ const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
3234
+ const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
3235
+ if (willRetry) {
3236
+ console.warn(JSON.stringify({
3237
+ event: "harvest_attempt_degraded_result",
3238
+ attempt_number: attemptNumber,
3239
+ max_attempts: maxAttempts,
3240
+ degradation_reasons: finalResult.diagnostics.degradationReasons,
3241
+ will_retry: true
3242
+ }));
3243
+ }
3200
3244
  await emitAttemptEvent(onAttemptEvent, {
3201
3245
  type: "finished",
3202
3246
  attemptNumber,
3203
3247
  maxAttempts,
3204
- outcome: classifyAttemptResult(finalResult),
3248
+ outcome,
3205
3249
  kernelSessionId: attempt.cleanup.kernelSessionId,
3206
3250
  questionCount: finalResult.totalQuestions,
3207
3251
  durationMs: Date.now() - startedAtMs,
3208
- error: null,
3209
- willRetry: false,
3252
+ error: resultError,
3253
+ willRetry,
3210
3254
  cleanup: attempt.cleanup,
3211
3255
  debug: attempt.debug,
3212
3256
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3213
3257
  });
3214
3258
  await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
3259
+ if (willRetry) continue;
3215
3260
  if (attemptOptions.format === "json" || attemptOptions.format === "both") {
3216
3261
  await serializer.writeJSON(finalResult, attemptOptions.outputDir);
3217
3262
  }
@@ -3301,4 +3346,4 @@ export {
3301
3346
  resolveKernelProxyId,
3302
3347
  harvest
3303
3348
  };
3304
- //# sourceMappingURL=chunk-XVVNKASZ.js.map
3349
+ //# sourceMappingURL=chunk-LOPKN3YL.js.map