{"record":{"id":"69f1113260db9ae6","repo":"santifer/career-ops","slug":"arbeitsagentur-entry-entry-name-unnamed","errorCode":null,"errorMessage":"arbeitsagentur: entry \"${entry.name || '(unnamed)'}\" has no arbeitsagentur.keywords[]","messagePattern":"arbeitsagentur: entry \"(.+?)\" has no arbeitsagentur\\.keywords\\[\\]","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/arbeitsagentur.mjs","lineNumber":160,"sourceCode":"// candidate whose title makes no remote claim keeps its real city, which is the\n// fail-closed behaviour an unverifiable lookup had in v4: the `Deutschlandweit\n// (Homeoffice)` marker exempts a job from the commute location_filter, so\n// tagging on nv_true alone would smuggle every hybrid past it.\n\n/** @type {Provider} */\nexport default {\n  id: 'arbeitsagentur',\n\n  /**\n   * Fetches and normalizes postings from the Arbeitsagentur Jobsuche API.\n   * @param {{ name?: string, arbeitsagentur?: any }} entry\n   * @param {{ fetchJson: (url: string, opts?: object) => Promise<any> }} ctx\n   * @returns {Promise<Array<{title: string, url: string, company: string, location: string}>>}\n   */\n  async fetch(entry, ctx) {\n    const { keywords, wo, umkreis, days, size, remoteNationwide, remoteMatch, remoteMaxPages } = parseArbeitsagenturConfig(entry);\n    if (!keywords.length) {\n      throw new Error(`arbeitsagentur: entry \"${entry.name || '(unnamed)'}\" has no arbeitsagentur.keywords[]`);\n    }\n\n    /** @param {string} was @param {Record<string,string>} [extra] */\n    const fetchKeyword = async (was, extra = {}) => {\n      const params = new URLSearchParams({\n        was,\n        size: String(size),\n        page: '1',\n        angebotsart: '1', // 1 = ARBEIT (employment; excludes Ausbildung/Selbständigkeit)\n        veroeffentlichtseit: String(days),\n        ...extra,\n      });\n      // redirect:'error' prevents SSRF via server-side redirects.\n      const json = await ctx.fetchJson(`${API_URL}?${params.toString()}`, {\n        headers: { 'X-API-Key': API_KEY, accept: 'application/json' },\n        redirect: 'error',\n        timeoutMs: 12_000,\n      });","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/arbeitsagentur.mjs#L142-L178","documentation":"The arbeitsagentur provider requires each portals.yml entry using it to define arbeitsagentur.keywords[] — the list of search terms driving the 'was' query parameter. An entry with no keywords cannot produce any requests, so fetch throws immediately naming the entry.","triggerScenarios":"A job_boards entry with provider: arbeitsagentur lacks the arbeitsagentur block, has keywords: [] (or an empty/whitespace-only list after parseArbeitsagenturConfig filters), or misspells the key (keyword: instead of keywords:) so it parses to an empty array.","commonSituations":"Copy-pasting an entry from another provider (arbeitnow/agentic-jobs don't need keywords) without adding the arbeitsagentur block; a YAML indentation mistake nesting keywords under the wrong parent; renaming the key during cleanup; adding a new board entry and forgetting the required field.","solutions":["Add arbeitsagentur.keywords to the entry named in the message, e.g. arbeitsagentur: { keywords: ['Softwareentwickler'], umkreis: 50 }.","Check YAML indentation — keywords must sit under the arbeitsagentur key of that entry, not a sibling level.","Verify the key spelling is exactly `keywords` (plural, array) and that entries are non-empty strings.","Run node audit-portals.mjs or verify-pipeline.mjs to catch misconfigured entries before a scan run."],"exampleFix":"# before\n- name: bundesinnenministerium\n  provider: arbeitsagentur\n  location: Berlin\n# after\n- name: bundesinnenministerium\n  provider: arbeitsagentur\n  arbeitsagentur:\n    keywords: [\"IT-Projektmanagement\", \"Softwareentwickler\"]\n    wo: Berlin\n    umkreis: 50","handlingStrategy":"validation","validationCode":"for (const entry of config.job_boards) {\n  if (entry.provider === 'arbeitsagentur') {\n    const kws = entry.arbeitsagentur?.keywords;\n    if (!Array.isArray(kws) || kws.length === 0 || !kws.every(k => typeof k === 'string' && k.trim())) {\n      throw new Error(`entry \"${entry.name}\" needs a non-empty arbeitsagentur.keywords[] array`);\n    }\n  }\n}","typeGuard":"function hasArbeitsagenturKeywords(entry) {\n  const kws = entry?.arbeitsagentur?.keywords;\n  return Array.isArray(kws) && kws.length > 0 && kws.every(k => typeof k === 'string' && k.trim().length > 0);\n}","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).includes('has no arbeitsagentur.keywords')) {\n    console.error(`Fix portals.yml: entry \"${entry.name}\" is missing arbeitsagentur.keywords[] (check indentation and plural key)`);\n  }\n  throw err;\n}","preventionTips":["Run node verify-pipeline.mjs / audit-portals.mjs after editing portals.yml — they catch missing provider-specific fields before a scan.","Copy arbeitsagentur entries from a working example so the keywords block and indentation carry over.","Remember other providers don't need keywords; each provider has its own required block — check the provider header comment."],"tags":["config","missing-field","yaml","provider"],"backgroundTag":"missing-required-config-field","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}