{"record":{"id":"16294a0686dccd1f","repo":"santifer/career-ops","slug":"generalist-world-no-gw-job-card-anchors-and-no-listing","errorCode":null,"errorMessage":"generalist-world: no gw-job-card anchors and no listing container in the response; the page structure likely changed","messagePattern":"generalist-world: no gw-job-card anchors and no listing container in the response; the page structure likely changed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/generalist-world.mjs","lineNumber":297,"sourceCode":"  let cards = 0;\n  for (const m of rendered.matchAll(CARDS_RE)) {\n    cards++;\n    const job = cardToJob(m[1], m[2]);\n    if (!job || seen.has(job.url)) continue;\n    seen.add(job.url);\n    jobs.push(job);\n  }\n  // Anchors that all fail to yield a job mean an inner field moved, not an\n  // empty board: the listing container is still there, so without this check\n  // a card redesign would read as zero postings forever. A malformed card\n  // among usable ones is still just skipped.\n  if (jobs.length === 0 && cards > 0) {\n    throw new Error(\n      `generalist-world: ${cards} gw-job-card anchor(s) matched but none carried a usable title, employer and /jobs/{slug}/ link; the card markup likely changed`,\n    );\n  }\n  if (jobs.length === 0 && !BOARD_MARKER_RE.test(rendered)) {\n    throw new Error(\n      'generalist-world: no gw-job-card anchors and no listing container in the response; the page structure likely changed',\n    );\n  }\n  return jobs;\n}\n\n/** @param {import('./_types.js').PortalEntry | null | undefined} entry */\nfunction detectGeneralistWorldEntry(entry) {\n  if (!entry || typeof entry !== 'object') return null;\n  if (entry.provider === 'generalist-world') return { url: LIST_URL };\n  if (entry.provider) return null;\n\n  for (const value of [entry.api, entry.careers_url]) {\n    if (typeof value !== 'string') continue;\n    try {\n      const parsed = new URL(value);\n      if (parsed.protocol === 'https:' && TRUSTED_HOSTS.has(parsed.hostname)) return { url: LIST_URL };\n    } catch {","sourceCodeStart":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/generalist-world.mjs#L279-L315","documentation":"parseGeneralistWorldJobs throws when the rendered page contains neither gw-job-card anchors nor the board listing container (BOARD_MARKER_RE). A page with the container but no cards is an alive-but-empty board (returns []); a page with neither means the page structure changed or the response is not the board at all. This prevents a redesign or a challenge page from quietly reading as zero postings forever.","triggerScenarios":"Calling parseGeneralistWorldJobs on HTML with no gw-job-card anchors and no listing-container marker — e.g. the list page moved to a new template, a CDN/bot-challenge page was served, or an empty/whitespace body (which returns [] earlier) was not the case.","commonSituations":"Generalist World restructures its board page so the marker and card classes disappear; fetchText follows to an error or login page; an interposed proxy returns an HTML error page; tests feed a fixture from an unrelated page.","solutions":["Inspect the HTML actually fetched from LIST_URL; identify whether it is a challenge/error page or a redesigned board","If redesigned, update BOARD_MARKER_RE and CARDS_RE in providers/generalist-world.mjs to the new container and card selectors","Retry the request — a transient challenge may resolve with different headers or after a delay","If the board permanently moved URL/template, update LIST_URL or the provider wiring"],"exampleFix":"// before: stale marker misses new template\nconst BOARD_MARKER_RE = /id=\"job-listing\"/;\n// after: match the redesigned container\nconst BOARD_MARKER_RE = /<div class=\"board[^\"]*jobs?[^\"]*\"/i;","handlingStrategy":"fallback","validationCode":"function looksLikeGwBoardOrEmpty(html) {\n  const s = String(html ?? '');\n  return /gw-job-card/.test(s) || /job-listing|board/.test(s) || !s.trim();\n}","typeGuard":"function isNonBoardHtml(html) {\n  const s = String(html ?? '');\n  return s.trim().length > 0 && !/gw-job-card/.test(s) && !/job-listing|board/.test(s);\n}","tryCatchPattern":"try {\n  const jobs = await gwProvider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('no listing container')) {\n    // retry once (transient challenge/error page), then alert on structure change\n    const jobs2 = await gwProvider.fetch(entry, ctx).catch(err => { alertScrapeRegression('generalist-world'); throw err; });\n    return jobs2;\n  }\n  throw e;\n}","preventionTips":["Add a single retry with backoff before treating the throw as a permanent structure change — challenge pages are transient","Log a trimmed HTML snippet with the error so responders can immediately tell challenge page vs redesign","Keep BOARD_MARKER_RE covering the container's stable attributes (id/class) and update fixtures when the site redesigns","Alert on this error distinctly from error 1: no cards at all vs cards-that-fail point to different fixes"],"tags":["scraping","html-parsing","markup-change","generalist-world"],"backgroundTag":"unexpected-response-shape","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-22T13:19:01.448Z","contentChangedAt":"2026-09-22T13:19:01.448Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}