santifer/career-ops · critical · Error
Malformed states file at ${statesPath}: expected a top-level
Error message
Malformed states file at ${statesPath}: expected a top-level "states" list What it means
loadCanonicalStates() in tracker-utils.mjs reads templates/states.yml and, like loadLifecycle (error 355), requires a top-level `states` array. It throws an Error if the document is null or doc.states is not an array. This is the entry point used by set-status.mjs and the canonical state resolver, so a malformed file blocks all status updates.
Source
Thrown at tracker-utils.mjs:540
rmSync(tmpPath, { force: true });
throw err;
}
}
/**
* Load the canonical tracker states from `templates/states.yml`.
*
* states.yml is the single source of truth for the 8 canonical states and
* their aliases. Parsing it here (instead of hardcoding the list) means a new
* state or alias lands in one file and every consumer follows.
*
* @param {string} statesPath - Path to templates/states.yml.
* @returns {{id:string,label:string,aliases:string[]}[]} Parsed state entries.
*/
export function loadCanonicalStates(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`);
}
return doc.states.map(s => ({
id: String(s.id ?? ''),
label: String(s.label ?? ''),
aliases: Array.isArray(s.aliases) ? s.aliases.map(String) : [],
}));
}
/**
* Resolve user input to a canonical state label, strictly.
*
* Case-insensitive match against each state's label, id, and aliases, after
* stripping markdown bold. Unlike merge-tracker's lenient batch normalization
* (which defaults unknowns to "Evaluated" so a whole merge isn't lost), this
* is the strict variant for interactive/CLI use: unknown input returns null so
* the caller can reject it before anything touches the tracker.
*
* @param {string} input - Raw state text from the user or a script.View on GitHub (pinned to 9b17a8ac97)
Solutions
- Restore the canonical file: git checkout HEAD -- templates/states.yml.
- Re-run node update-system.mjs apply.
- Validate structure: node -e "const y=require('js-yaml').load(require('fs').readFileSync('templates/states.yml','utf8')); console.assert(Array.isArray(y.states))".
- Run node verify-pipeline.mjs to catch related integrity issues.
Example fix
# restore the single source of truth git checkout HEAD -- templates/states.yml node verify-pipeline.mjs
Defensive patterns
Strategy: validation
Validate before calling
function validateStatesFile(statesPath) {
const doc = yaml.load(readFileSync(statesPath, 'utf-8'));
if (!doc || !Array.isArray(doc.states)) {
throw new Error(`states.yml missing 'states' list — restore: git checkout HEAD -- templates/states.yml`);
}
} Try / catch
try {
const states = loadCanonicalStates(STATES_FILE);
} catch (err) {
if (err.message.includes('Malformed states file')) {
console.error(err.message);
console.error('Restoring system file and retrying...');
execSync('git checkout HEAD -- templates/states.yml');
// retry after restore
} else throw err;
} Prevention
- Treat templates/states.yml as read-only system data; never hand-edit.
- Include a states.yml integrity check in verify-pipeline.mjs / CI.
- Run node update-system.mjs apply after pulling repo updates.
When it happens
Trigger: templates/states.yml is empty, has wrong top-level key, contains non-object YAML, or is truncated. Any state-writing operation (set-status.mjs, merge-tracker.mjs) that calls resolveCanonicalState -> loadCanonicalStates will surface this.
Common situations: Hand-editing states.yml and breaking the schema; a botched update-system.mjs run; a git merge conflict; an editor that auto-converted tabs/spaces breaking YAML parsing.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Malformed states file at ${statesPath}: expected a top-level
- ROW_NOT_FOUND
- Cannot read benchmarks at ${path}: ${err.message}
- Malformed benchmarks file at ${path}: expected a top-level "
- arbeitsagentur: entry "${entry.name || '(unnamed)'}" has no
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/6e34df61c8cd9489.
Report an issue: GitHub.