santifer/career-ops · error · Error

Existing candidates file at

Error message

Existing candidates file at ${candidatesPath} is not a JSON array

What it means

appendCandidate expects data/reply-candidates.json to be a JSON array of candidate entries. If the file parses as valid JSON but holds any other type (object, string, number, null), it throws this error instead of overwriting the unexpected shape.

Solutions

  1. Inspect the file and unwrap the array (e.g. replace {"candidates":[...]} with the inner [...])
  2. Convert the top-level value to a plain JSON array before re-running paste-reply
  3. Restore from backup/git if the file was clobbered by another tool
  4. If the wrapper format is intentional, migrate the entries into the flat-array format paste-reply expects

Example fix

// before
{"candidates": [{"from": "recruiter@acme.com"}]}
// after
[{"from": "recruiter@acme.com"}]
Defensive patterns

Strategy: validation

Validate before calling

const v = JSON.parse(readFileSync(p, 'utf-8')); if (!Array.isArray(v)) throw new Error('reply-candidates.json must be a top-level array');

Type guard

const isCandidateFile = (v) => Array.isArray(v) && v.every((e) => e && typeof e === 'object');

Try / catch

try { appendCandidate(c); } catch (e) { if (e.message.includes('is not a JSON array')) { console.error('Unwrap the file to a top-level JSON array, then retry'); } else throw e; }

Prevention

When it happens

Trigger: appendCandidate(candidate) called when data/reply-candidates.json contains a JSON object (e.g. {"candidates": [...]}) or any non-array JSON value.

Common situations: Another tool or script rewrote the file as a wrapper object; user hand-wrapped the array in an object; file was seeded from a different project's format; null literal written by a failed previous run.

Related errors


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

Appendix: source

Thrown at paste-reply.mjs:125

  };
}

/**
 * Append a candidate to the candidates JSON file, creating the file/array if
 * missing, without disturbing any existing entries. Exported for direct unit
 * testing. Returns the total candidate count after the append.
 */
export function appendCandidate(candidate, candidatesPath = CANDIDATES_PATH) {
  let candidates = [];
  if (fs.existsSync(candidatesPath)) {
    let parsed;
    try {
      parsed = JSON.parse(fs.readFileSync(candidatesPath, 'utf-8'));
    } catch (e) {
      throw new Error(`Could not parse existing candidates file at ${candidatesPath}: ${e.message}`);
    }
    if (!Array.isArray(parsed)) {
      throw new Error(`Existing candidates file at ${candidatesPath} is not a JSON array`);
    }
    candidates = parsed;
  } else {
    fs.mkdirSync(path.dirname(candidatesPath), { recursive: true });
  }
  candidates.push(candidate);
  // Write-then-rename so an interrupted write (crash, signal, disk full)
  // can never leave the real candidates file truncated/corrupted.
  const tmpPath = `${candidatesPath}.tmp`;
  fs.writeFileSync(tmpPath, JSON.stringify(candidates, null, 2), 'utf-8');
  renameSyncWithRetry(tmpPath, candidatesPath);
  return candidates.length;
}

// Collect subject/from/body from stdin via a single readline.Interface and a
// tiny manual state machine driven off its 'line' event.
//
// Earlier drafts chained `rl.question()` calls (one interface per prompt, or

View on GitHub (pinned to aac998c7ed)