{"record":{"id":"70b90d632be0c9f9","repo":"santifer/career-ops","slug":"portals-yml-not-found","errorCode":null,"errorMessage":"portals.yml not found","messagePattern":"portals\\.yml not found","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"openrouter-runner.mjs","lineNumber":454,"sourceCode":"  }\n}\n\n// ---------------------------------------------------------------------------\n// portals.yml parser — reads the canonical schema with js-yaml (same library and\n// field names as scan.mjs: `title_filter.positive/negative` + `tracked_companies`),\n// so it never drifts from the main scanner. The runner's no-CLI scan path covers\n// companies that expose a direct JSON `api:`; careers_url-only / Playwright /\n// search-query companies are handled by the full /career-ops scan pipeline.\n// `rawOverride` lets tests feed YAML text directly (see test-all.mjs drift guard).\n// ---------------------------------------------------------------------------\nfunction normKeywords(v) {\n  if (!Array.isArray(v)) return [];\n  return v.map(x => String(x ?? '').toLowerCase().trim()).filter(Boolean);\n}\n\nexport function parsePortals(rawOverride) {\n  const raw = rawOverride ?? readFile('portals.yml');\n  if (!raw) throw new Error('portals.yml not found');\n  const config = yaml.load(raw) || {};\n\n  const tf = config.title_filter || {};\n  const positive = normKeywords(tf.positive);\n  const negative = normKeywords(tf.negative);\n  function titleMatches(title) {\n    const t = String(title ?? '').toLowerCase();\n    return positive.some(k => t.includes(k)) && !negative.some(k => t.includes(k));\n  }\n\n  // Companies with a direct JSON `api:` endpoint (the no-CLI scan path).\n  const tracked = Array.isArray(config.tracked_companies) ? config.tracked_companies : [];\n  const companies = tracked\n    .filter(c => c && c.api && c.enabled !== false)\n    .map(c => ({ name: String(c.name ?? c.company ?? 'Unknown'), api: String(c.api).trim() }));\n\n  return { companies, titleMatches };\n}","sourceCodeStart":436,"sourceCodeEnd":472,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/openrouter-runner.mjs#L436-L472","documentation":"Thrown by parsePortals() when readFile('portals.yml') returns empty/falsy (file missing, empty, or unreadable as text). parsePortals is the runner's no-CLI scanner config source, mirroring scan.mjs's schema; without it the runner cannot know which companies to scan.","triggerScenarios":"First run before onboarding completed; portals.yml deleted or never created; the file exists but is empty; running the runner from a working directory that isn't the repo root so the relative path resolves to nothing.","commonSituations":"New clone without running onboarding; portals.yml was gitignored locally and not regenerated; user ran `node openrouter-runner.mjs` from a subdirectory; the file was truncated to 0 bytes by a bad editor save.","solutions":["Run onboarding (node doctor.mjs) to scaffold portals.yml from templates/portals.example.yml.","If the file exists but is empty, copy templates/portals.example.yml over it and edit.","Run the runner from the repo root so the relative 'portals.yml' path resolves.","Pass a rawOverride string to parsePortals() for tests instead of relying on the file."],"exampleFix":"// before: portals.yml missing -> throws\nconst cfg = parsePortals();\n// after: copy the template first, then parse\nfs.copyFileSync('templates/portals.example.yml', 'portals.yml');\nconst cfg = parsePortals();","handlingStrategy":"validation","validationCode":"import { existsSync, statSync } from 'fs';\nfunction portalsReadable() {\n  if (!existsSync('portals.yml')) return false;\n  return statSync('portals.yml').size > 0;\n}\nif (!portalsReadable()) {\n  // scaffold from template before calling parsePortals()\n}","typeGuard":"null","tryCatchPattern":"try {\n  const cfg = parsePortals();\n} catch (e) {\n  if (/portals\\.yml not found/.test(e.message)) {\n    // run onboarding / copy template\n  } else throw e;\n}","preventionTips":["Run onboarding (node doctor.mjs) before any scan/eval on a fresh clone.","Always run the runner from the repo root.","For tests, call parsePortals(rawOverride) with YAML text instead of relying on the file."],"tags":["config","onboarding","file-not-found","portals","openrouter-runner"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}