{"record":{"id":"b0ff341f32145217","repo":"santifer/career-ops","slug":"agentic-jobs-parsed-0-jobs-from-the-api-the-res","errorCode":null,"errorMessage":"agentic-jobs: parsed 0 jobs from the API — the response shape likely changed","messagePattern":"agentic-jobs: parsed 0 jobs from the API — the response shape likely changed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/agentic-jobs.mjs","lineNumber":193,"sourceCode":"      // Trust the API's own reported page size over our constant, in case it\n      // ever differs from the documented default.\n      const effectivePageSize = typeof json.meta?.per_page === 'number' && json.meta.per_page > 0 ? json.meta.per_page : PAGE_SIZE;\n\n      for (const record of records) {\n        const job = normalizeAgenticJob(record);\n        if (job && !seen.has(job.url)) {\n          seen.add(job.url);\n          jobs.push(job);\n        }\n      }\n\n      if (jobs.length >= MAX_JOBS) break;\n      if (records.length < effectivePageSize) break; // short page — last one\n      if (total !== null && page * effectivePageSize >= total) break;\n    }\n\n    if (jobs.length === 0) {\n      throw new Error('agentic-jobs: parsed 0 jobs from the API — the response shape likely changed');\n    }\n    return jobs.slice(0, MAX_JOBS);\n  },\n};\n","sourceCodeStart":175,"sourceCodeEnd":198,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/agentic-jobs.mjs#L175-L198","documentation":"After paginating through all agentic-jobs pages (respecting the MAX_JOBS=2000 and MAX_PAGES=40 caps), if zero jobs were collected the provider throws. The reasoning: a genuinely empty board is implausible for this feed, so parsing 0 jobs almost certainly means the per-record normalizer (normalizeAgenticJob) no longer matches the record shape — every record got normalized to null and was skipped. This is a 'parsed nothing' sentinel that distinguishes 'feed empty' (shouldn't happen) from 'we silently lost every job to a shape change'.","triggerScenarios":"The API returned well-formed `{data:[...]}` pages (so error 138 did not fire), but normalizeAgenticJob returned null for every record — typically because each record is missing required fields the normalizer checks (url, title, etc.), or the record's field layout changed so the normalizer's destructuring yields garbage. The total reached 0 across all pages.","commonSituations":"Upstream added a nesting level (e.g. job fields moved under a `attributes` sub-object, common in JSON:API); the `url` or `title` field was renamed, so the normalizer's required-field guard rejects every record; a localized/censored response dropped fields the normalizer requires.","solutions":["Inspect normalizeAgenticJob in providers/agentic-jobs.mjs to see which fields it requires and how it returns null when they're missing.","Reproduce with `curl '<API_BASE>/jobs?page=1'` and compare a real record's keys against the normalizer's expected fields.","If the record layout changed (e.g. fields nested under attributes), update normalizeAgenticJob to read from the new shape.","If a required field was renamed, update the field access in the normalizer.","Run the provider's tests after the change."],"exampleFix":"// Example: upstream wrapped job fields under `attributes` (JSON:API style)\n// before\nfunction normalizeAgenticJob(record) {\n  const url = record?.url;\n  const title = record?.title;\n  if (!url || !title) return null;\n  ...\n}\n\n// after\nfunction normalizeAgenticJob(record) {\n  const a = record?.attributes ?? record; // tolerate both shapes\n  const url = a?.url;\n  const title = a?.title;\n  if (!url || !title) return null;\n  ...\n}","handlingStrategy":"validation","validationCode":"// Sanity-check the normalizer against a sample record before a full sweep.\nconst sample = await ctx.fetchJson(`${API_BASE}/jobs?page=1`);\nconst first = sample?.data?.[0];\nconst normalized = normalizeAgenticJob(first);\nif (!normalized) {\n  throw new Error('agentic-jobs normalizer rejects sample record — field shape likely changed; aborting sweep.');\n","typeGuard":"/** @param {unknown} j @returns {j is { url: string, title: string, company?: string, location?: string } | null} */\nfunction isNormalizedJob(j) {\n  if (!j || typeof j !== 'object') return false;\n  const o = /** @type {any} */ (j);\n  return typeof o.url === 'string' && typeof o.title === 'string';\n}","tryCatchPattern":"try {\n  const jobs = await provider.fetch(entry, ctx);\n} catch (err) {\n  if (/parsed 0 jobs/.test(String(err?.message))) {\n    console.error('agentic-jobs: normalizer rejected every record. curl the API, compare record keys to normalizeAgenticJob, update it.');\n  }\n  throw err;\n}","preventionTips":["Keep normalizeAgenticJob defensive but explicit about required fields, and log (sample) a rejected record's keys when it returns null — silent null-loss is exactly what this guard exists to catch.","Pin a fixture record in CI so a nesting change (e.g. JSON:API attributes) fails the test, not production.","If the API moves fields under `attributes`, tolerate both shapes (`record?.attributes ?? record`) to ease migrations."],"tags":["agentic-jobs","api","response-shape","provider","normalization","validation"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}