santifer/career-ops · warning

⚠️ Skipping : the column labelled "score" reads " ", which…

Error message

⚠️  Skipping ${filename}: the column labelled "score" reads "${score}", which is not a score or a documented sentinel (X.X/5, N/A, —, -) — check the header labels match the value order

What it means

In a headed TSV the header already declares which column holds the score, so the parser no longer picks an order — it only verifies the labelled cell holds what the label promises: a score like 'X.X/5' or a sentinel ('N/A', '—', '-'). A cell that matches neither is a hard reject, because the value it catches is an emitter that wrote its VALUES in one order and its LABELS in another — the silent score/status swap in a new costume. Merging would store garbage (or a status) as the score.

Solutions

  1. Align the data row's cell order with the header labels — put the score value ('X.X/5' or sentinel) under the 'score' label and the canonical status under 'status'.
  2. If the emitter writes status-first, change either its cell order or its header order so they agree.
  3. Format the score as 'X.X/5' (or use a sentinel 'N/A' / '—' / '-') if it currently holds a bare number or prose.
  4. Verify by re-running node merge-tracker.mjs and confirming the row merges with the correct score.

Example fix

// before (values written status-first under score-first labels)
score	status
Evaluated	4.2/5

// after
score	status
4.2/5	Evaluated
Defensive patterns

Strategy: validation

Validate before calling

const scoreCell = cells[header.indexOf('score')].replace(/\*\*/g, '').trim();
if (!/^\d\.\d\/5$/.test(scoreCell) && !['N/A','—','-'].includes(scoreCell)) {
  throw new Error(`${file}: cell labelled "score" reads "${scoreCell}" — not a score or sentinel; check label/value order`);
}

Type guard

const looksLikeScore = (v) => /^\d\.\d\/5$/.test(String(v ?? '').replace(/\*\*/g, '').trim());

Prevention

When it happens

Trigger: parseHeadedAddition (merge-tracker.mjs:869-878): looksLikeScoreCell(score) returns false for the cell under the 'score' label (after stripping markdown bold). E.g. the header says score then status, but the row's cells were written status-first, so the score label reads 'Applied'.

Common situations: A writer used the legacy headerless order (status before score) under the headed template's labels; hand-editing swapped two cells; a generator emitted columns alphabetically while the header stayed canonical; the score was written as a bare number like '4.2' without '/5' in a format the pattern doesn't accept.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at merge-tracker.mjs:877

      .map(([k]) => k);
    if (misplacedUrl.length) {
      console.warn(`⚠️  Skipping ${filename}: a URL sits under "${misplacedUrl.join('", "')}" while the "url" cell is absent — the optional cells are shifted; write an empty cell for each optional value you omit`);
      return null;
    }
  }

  // Write-canonical: the tracker stores scores unbolded (verify-pipeline
  // rejects bold scores), so strip any markdown bold from the incoming cell.
  const score = at('score').replace(/\*\*/g, '').trim();
  if (!looksLikeScoreCell(score)) {
    // Corroboration, not disambiguation: the header already said which column
    // this is, so this can no longer pick an order — it only asks whether the
    // labelled cell holds what the label promises. It stays a hard gate because
    // the value it catches is an emitter that wrote its VALUES in one order and
    // its LABELS in another, which is the silent swap in a new costume. The
    // headerless path already refuses a score cell of this shape (neither cell
    // score-like → undecidable), so this is the same strictness, not new.
    console.warn(`⚠️  Skipping ${filename}: the column labelled "score" reads "${score}", which is not a score or a documented sentinel (X.X/5, N/A, —, -) — check the header labels match the value order`);
    return null;
  }

  return {
    num: parseTrackerNum(at('num')),
    date: at('date'),
    company: at('company'),
    role: at('role'),
    status: validateStatus(at('status')),
    score,
    pdf: at('pdf'),
    report: at('report'),
    notes: optional('notes'),
    // A `via` COLUMN carries the agency name plainly; tolerate a writer that
    // also brings the headerless form's `via=` tag along.
    via: optional('via').replace(/^via=/i, '').trim(),
    location: optional('location'),
    url: optional('url'),

View on GitHub (pinned to aac998c7ed)