{"record":{"id":"dfa19fb30a737fa5","repo":"santifer/career-ops","slug":"phenom-cannot-resolve-origin-for-entry-name","errorCode":null,"errorMessage":"phenom: cannot resolve origin for ${entry.name}","messagePattern":"phenom: cannot resolve origin for (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/phenom.mjs","lineNumber":169,"sourceCode":"    });\n  }\n  return { total, rows };\n}\n\n/** Resolve the page cap: a positive integer `max_pages` on the entry, capped. */\nexport function resolveMaxPages(entry) {\n  const v = entry?.max_pages;\n  if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES_CAP);\n  return DEFAULT_MAX_PAGES;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'phenom',\n\n  async fetch(entry, ctx) {\n    const cfg = resolveConfig(entry);\n    if (!cfg) throw new Error(`phenom: cannot resolve origin for ${entry.name}`);\n\n    const wait = (ms) => (ctx.sleep ? ctx.sleep(ms) : new Promise((r) => setTimeout(r, ms)));\n    const maxPages = resolveMaxPages(entry);\n    // Honor a context page cap — verify-portals' liveness probe sets\n    // `ctx.maxPages: 1` so it only needs to know a board is live, not its\n    // full count (mirrors providers/workday.mjs). Kept separate from\n    // maxPages so the entry-cap warning below (page === maxPages) doesn't\n    // misfire when it was really the probe's cap that stopped pagination.\n    // No effect on real scans, which don't set ctx.maxPages.\n    const ctxCap = Number.isInteger(ctx?.maxPages) && ctx.maxPages > 0 ? ctx.maxPages : Infinity;\n    const pagesToFetch = Math.min(maxPages, ctxCap);\n    const jobs = [];\n    const seen = new Set();\n    let total = null;\n\n    // Why pagination stopped — drives the truncation warning below. Only a\n    // 'cap' stop is worth surfacing: 'complete' means the widget ran out of\n    // fresh rows on its own, and a fetch failure already keeps whatever was","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/phenom.mjs#L151-L187","documentation":"The Phenom provider needs an explicit origin configuration (base URL of the Phenom-powered careers site) to paginate its API; resolveConfig(entry) returning null means the entry carries no usable origin. fetch() throws rather than guessing an origin, since Phenom boards are per-tenant and there is no global default. This is a configuration-resolution failure before any request is made.","triggerScenarios":"Calling providers/phenom.mjs fetch(entry, ctx) when entry lacks the expected config keys (careers_url or the provider-specific origin field), the value is not a string, or it cannot be parsed into a valid origin.","commonSituations":"portals.yml entry for a Phenom company missing its origin/careers_url; wrong provider id (company actually on Workday/Greenhouse); YAML indentation making the field land under the wrong entry; Phenom tenant reached only via vanity domain not captured in config.","solutions":["Add the Phenom board origin (e.g. https://acme.wd3.myworkday... no — the Phenom host such as https://acme.phenompeople.com or the company's phenom careers host) to the entry per resolveConfig's expected key.","Check the entry's provider id is phenom and the field is not misspelled or misplaced in the YAML.","Inspect providers/phenom.mjs resolveConfig to see exactly which keys it accepts and supply one.","Use audit-portals.mjs to confirm the careers_url belongs to a Phenom board; reassign providers otherwise."],"exampleFix":"// before (portals.yml)\n- name: Acme\n  provider: phenom\n// after\n- name: Acme\n  provider: phenom\n  careers_url: https://acme.phenompeople.com","handlingStrategy":"validation","validationCode":"function hasPhenomOrigin(entry) {\n  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  if (!raw) return false;\n  try {\n    const { protocol, hostname } = new URL(raw);\n    return protocol === 'https:' && hostname.length > 0;\n  } catch { return false; }\n}\nif (!hasPhenomOrigin(entry)) throw new Error(`phenom: entry ${entry.name} is missing a usable origin`);","typeGuard":"function hasPhenomCareersUrl(entry) {\n  return entry != null && typeof entry === 'object' &&\n    typeof entry.careers_url === 'string' && entry.careers_url.length > 0 &&\n    (() => { try { return new URL(entry.careers_url).protocol === 'https:'; } catch { return false; } })();\n}","tryCatchPattern":"try {\n  await phenomProvider.fetch(entry, ctx);\n} catch (e) {\n  if (String(e.message).startsWith('phenom: cannot resolve origin')) {\n    logger.warn({ entry: entry.name }, 'missing Phenom origin in config — skipping entry');\n    return null;\n  }\n  throw e;\n}","preventionTips":["Add the origin/careers_url key exactly as resolveConfig in providers/phenom.mjs expects it.","Schema-validate portals.yml entries per provider before running scans.","Confirm the company is really on Phenom (audit-portals.mjs) before assigning provider: phenom.","Check YAML indentation so the field belongs to the intended entry."],"tags":["config","missing-config","phenom","url-derivation"],"backgroundTag":"missing-config-value","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"}