santifer/career-ops · error

Malformed states file at

Error message

Malformed states file at ${statesPath}: expected a top-level "states" list

What it means

loadLifecycle parses templates/states.yml and requires a top-level `states` key holding a list. It throws when the YAML is empty, parses to a non-object, or lacks a `states` array. This guards the lifecycle order/terminal-state tables the tracker sync check depends on.

Solutions

  1. Open templates/states.yml and ensure a top-level `states:` key exists with a list of state entries each having an `id`
  2. Restore the canonical file: git checkout -- templates/states.yml (or re-run node update-system.mjs apply)
  3. Validate the YAML parses as expected (node -e "console.log(require('js-yaml').load(require('fs').readFileSync('templates/states.yml','utf8')))") and inspect doc.states
  4. Confirm the statesPath argument points to templates/states.yml, not another YAML file

Example fix

# before (states.yml)
lifecycle:
  - id: Applied
# after
states:
  - id: Evaluated
  - id: Applied
Defensive patterns

Strategy: validation

Validate before calling

const doc = yaml.load(readFileSync(statesPath, 'utf-8'));
if (!doc || !Array.isArray(doc.states)) {
  throw new Error(`${statesPath} must contain a top-level 'states' list`);
}

Type guard

const hasStatesList = (doc) => !!doc && Array.isArray(doc.states);

Try / catch

try {
  const lc = loadLifecycle('templates/states.yml');
} catch (e) {
  if (String(e.message).includes('Malformed states file')) {
    // restore templates/states.yml from repo before continuing
  }
}

Prevention

When it happens

Trigger: Calling loadLifecycle(statesPath) with a states.yml that is empty, contains only comments, has states nested under another key (e.g. `lifecycle: states:`), or was overwritten with a different schema (YAML that parses to null/string/list without a `states` array).

Common situations: Hand-editing states.yml and accidentally deleting or renaming the `states` key; truncating the file during an editor crash; copying a template from an older career-ops version with a different schema; pointing statesPath at the wrong file (e.g. another YAML config); YAML indentation errors collapsing the structure.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/e4eee8f2aaf7744f. Report an issue: GitHub.

Appendix: source

Thrown at tracker-sync-check.mjs:114

// against every other status) instead of correctly ranking it as terminal.
//
// states.yml has no explicit ordering field, so LIFECYCLE_ORDER is taken from
// the file's own array order among states NOT marked `terminal: true`; the
// terminal set and the id -> display-label map are read directly off each
// state's `terminal` and `label` fields. See the comment above `states:` in
// templates/states.yml for the contract.
const STATES_FILE = join(CAREER_OPS, 'templates/states.yml');

/**
 * Load the canonical lifecycle order, terminal-status set, and id -> label
 * map from templates/states.yml.
 * @param {string} statesPath - Path to templates/states.yml.
 * @returns {{ order: string[], terminal: Set<string>, labels: Record<string,string> }}
 */
export function loadLifecycle(statesPath) {
  const doc = yaml.load(readFileSync(statesPath, 'utf-8'));
  if (!doc || !Array.isArray(doc.states)) {
    throw new Error(`Malformed states file at ${statesPath}: expected a top-level "states" list`);
  }
  const order = [];
  const terminal = new Set();
  const labels = {};
  for (const s of doc.states) {
    const id = String(s?.id ?? '').trim();
    if (!id) continue;
    labels[id] = String(s.label ?? id);
    if (s.terminal) terminal.add(id);
    else order.push(id);
  }
  return { order, terminal, labels };
}

const { order: LIFECYCLE_ORDER, terminal: TERMINAL_STATUSES, labels: CANONICAL_LABELS } = loadLifecycle(STATES_FILE);

// Mirrors the ALIASES map in analyze-patterns.mjs / verify-pipeline.mjs —
// applications.md status cell normalization (bold markers, trailing dates,

View on GitHub (pinned to aac998c7ed)