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
- Open templates/states.yml and ensure a top-level `states:` key exists with a list of state entries each having an `id`
- Restore the canonical file: git checkout -- templates/states.yml (or re-run node update-system.mjs apply)
- 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
- 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
- Never hand-edit templates/states.yml without re-validating its schema
- Keep the `states` top-level key intact; add entries, don't restructure
- Run node verify-pipeline.mjs after editing template files
- Restore from git when unsure: git checkout -- templates/states.yml
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.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- apify: entry has invalid field_map. Each of title, url…
- Invalid YAML in profile file
- Malformed benchmarks file at
- portals.yml: an enabled entry has no name
- apify: entry missing 'actor' (e.g. misceres/indeed-scraper)
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)