{"record":{"id":"ad3524a4c6e580de","repo":"santifer/career-ops","slug":"generalist-world-cards-gw-job-card-anchor-s-matched-but-none","errorCode":null,"errorMessage":"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","messagePattern":"generalist-world: (.+?) gw-job-card anchor\\(s\\) matched but none carried a usable title, employer and /jobs/(.+?)/ link; the card markup likely changed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/generalist-world.mjs","lineNumber":292,"sourceCode":"  if (typeof html !== 'string' || !html.trim()) return [];\n  const rendered = stripNonRendered(html);\n  /** @type {Job[]} */\n  const jobs = [];\n  const seen = new Set();\n  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]) {","sourceCodeStart":274,"sourceCodeEnd":310,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/generalist-world.mjs#L274-L310","documentation":"parseGeneralistWorldJobs throws when gw-job-card anchors were found on the Generalist World board page but every one failed to yield a usable title, employer and /jobs/{slug}/ link. Anchors matching yet all failing means an inner field (title, company or href) moved inside the card — a card redesign — not an empty board. Individual malformed cards among usable ones are simply skipped; only an all-fail page throws.","triggerScenarios":"Calling parseGeneralistWorldJobs (or the provider's fetch) on a page where CARDS_RE matches cards > 0 but cardToJob returns null for every anchor — e.g. the title, employer, or /jobs/{slug}/ href selector inside the card changed.","commonSituations":"Generalist World redesigns the card inner layout; a field like the company label is renamed or moved into a nested element; testing against a stale HTML fixture whose card internals no longer match the regexes; a challenge/partial page renders card shells without real content.","solutions":["Fetch the live board page and update TITLE_RE, COMPANY_RE or HREF_RE in providers/generalist-world.mjs to match the new card inner markup","Diff against a known-good fixture to see which inner field moved, then fix that one regex","Confirm the page is a real board render and not a bot challenge or partial hydration shell","Update the test fixtures to the current markup so cardToJob yields jobs again"],"exampleFix":"// before: company selector misses moved field\nconst COMPANY_RE = /<span class=\"gw-card__company\">([\\s\\S]*?)<\\/span>/;\n// after: accept the redesigned wrapper\nconst COMPANY_RE = /<div class=\"gw-card__employer[^\"]*\"[^>]*>([\\s\\S]*?)<\\/div>/;","handlingStrategy":"try-catch","validationCode":"function willParseGwCards(html) {\n  const cards = [...String(html).matchAll(/<a class=\"gw-job-card\"[\\s\\S]*?<\\/a>/g)];\n  return cards.length === 0 || cards.some(c => /\\/jobs\\/[\\w-]+\\//.test(c[0]) && /gw-card__title|<h[1-4]/.test(c[0]));\n}","typeGuard":"function isGwBoardHtml(html) {\n  return typeof html === 'string' && /gw-job-card/.test(html);\n}","tryCatchPattern":"try {\n  const jobs = await gwProvider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('gw-job-card') && e.message.includes('markup likely changed')) {\n    alertScrapeRegression('generalist-world'); // card internals moved; refresh regexes/fixtures\n  } else throw e;\n}","preventionTips":["Keep a live-captured fixture of a populated board page and assert parseGeneralistWorldJobs returns > 0 jobs from it in CI","When updating any inner-card regex (title/company/href), update the fixture in the same change","Distinguish all-cards-fail (throw) from some-cards-fail (skip) when interpreting logs — a few skips are normal","Run board audits periodically so a card redesign is caught within days, not months"],"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"}