{"record":{"id":"6e34df61c8cd9489","repo":"santifer/career-ops","slug":"malformed-states-file-at-statespath-expected-a-6e34df","errorCode":null,"errorMessage":"Malformed states file at ${statesPath}: expected a top-level \"states\" list","messagePattern":"Malformed states file at (.+?): expected a top-level \"states\" list","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tracker-utils.mjs","lineNumber":713,"sourceCode":"\n/**\n * Load the canonical tracker states from `templates/states.yml`.\n *\n * states.yml is the single source of truth for the 8 canonical states and\n * their aliases. Parsing it here (instead of hardcoding the list) means a new\n * state or alias lands in one file and every consumer follows.\n *\n * `description` and `terminal` are passed through for callers that EXPLAIN the\n * states rather than list them (set-status.mjs --help). Both default rather\n * than throw: an entry omitting them is still a usable state.\n *\n * @param {string} statesPath - Path to templates/states.yml.\n * @returns {{id:string,label:string,aliases:string[],description:string,terminal:boolean}[]} Parsed state entries.\n */\nexport function loadCanonicalStates(statesPath) {\n  const doc = yaml.load(readFileSync(statesPath, 'utf-8'));\n  if (!doc || !Array.isArray(doc.states)) {\n    throw new Error(`Malformed states file at ${statesPath}: expected a top-level \"states\" list`);\n  }\n  return doc.states.map(s => ({\n    id: String(s.id ?? ''),\n    label: String(s.label ?? ''),\n    aliases: Array.isArray(s.aliases) ? s.aliases.map(String) : [],\n    description: String(s.description ?? ''),\n    terminal: s.terminal === true,\n  }));\n}\n\n/**\n * Resolve user input to a canonical state label, strictly.\n *\n * Case-insensitive match against each state's label, id, and aliases, after\n * stripping markdown bold. Unlike merge-tracker's lenient batch normalization\n * (which defaults unknowns to \"Evaluated\" so a whole merge isn't lost), this\n * is the strict variant for interactive/CLI use: unknown input returns null so\n * the caller can reject it before anything touches the tracker.","sourceCodeStart":695,"sourceCodeEnd":731,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/tracker-utils.mjs#L695-L731","documentation":"loadCanonicalStates parses templates/states.yml and requires the YAML document to have a top-level \"states\" key holding an array of state entries. If the file is missing, empty, or its root is not an object with a states list (e.g. it was overwritten, truncated, or has the states nested under another key), this error is thrown before any states are returned.","triggerScenarios":"Calling loadCanonicalStates(statesPath) where the YAML file (a) does not exist and readFileSync throws is a different error, but (b) exists yet parses to null/undefined (empty file), (c) parses to a scalar or array instead of an object, or (d) is an object whose `states` property is absent or not an Array — all hit this exact throw.","commonSituations":"A user hand-edited templates/states.yml and renamed or indented the `states:` key; a system update or merge conflict left the file empty or half-written; a custom states file was pointed at via configuration that follows a different schema (states nested under e.g. `canonical_states:`); the wrong path is passed so a different YAML file (valid YAML but not a states schema) is loaded.","solutions":["Open templates/states.yml and confirm it has a top-level `states:` key followed by a YAML list of entries with id, label, and aliases fields.","If the file is empty or corrupted, restore it from the upstream career-ops repository (git checkout -- templates/states.yml or re-clone).","If you passed a custom path, verify you are pointing at the real states schema file, not another YAML config.","Validate the YAML parses to an object: node -e \"console.log(require('js-yaml').load(require('fs').readFileSync('templates/states.yml','utf8')))\" and check .states is an array.","Re-apply or update career-ops system files (node update-system.mjs apply --confirm) to restore shipped templates."],"exampleFix":"# before (malformed templates/states.yml)\nEvaluated: some text\n\n# after\ncanonical_states:\n  - id: evaluated\n\n# correct schema (restored)\nstates:\n  - id: evaluated\n    label: Evaluated\n    aliases: [evaluated]","handlingStrategy":"validation","validationCode":"import { readFileSync } from 'node:fs';\nimport yaml from 'js-yaml';\nconst doc = yaml.load(readFileSync(statesPath, 'utf-8'));\nif (!doc || !Array.isArray(doc.states)) {\n  throw new Error(`${statesPath} has no top-level \"states\" list; restore templates/states.yml`);\n}","typeGuard":"function hasStatesList(doc) {\n  return typeof doc === 'object' && doc !== null && Array.isArray(doc.states);\n}","tryCatchPattern":"try {\n  const states = loadCanonicalStates(statesPath);\n} catch (err) {\n  if (err.message.includes('Malformed states file')) {\n    // fall back to a bundled copy or prompt the user to restore templates/states.yml\n  } else throw err;\n}","preventionTips":["Never hand-edit templates/states.yml without validating the YAML afterwards.","Keep the states schema file in git so corrupt/empty edits are recoverable with git checkout.","Run node doctor.mjs or verify-pipeline.mjs after system updates to catch schema drift early.","When pointing at a custom states file, lint it against the documented schema (states: [{id,label,aliases}])."],"tags":["yaml","config","schema-validation","tracker"],"backgroundTag":"schema-validation-failed","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}