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
- 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'.
- If the emitter writes status-first, change either its cell order or its header order so they agree.
- Format the score as 'X.X/5' (or use a sentinel 'N/A' / '—' / '-') if it currently holds a bare number or prose.
- 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
- Keep the data row's cell order identical to the header's label order — never mix the legacy status-first order into a headed file.
- Format scores as 'X.X/5' and use 'N/A', '—' or '-' when there is no score.
- Strip markdown bold before writing (the parser tolerates it, but canonical output is unbolded).
- Generate the row from the same array you generate the header from, so they can never disagree.
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
- ⚠️ Skipping : cannot tell score from status in columns 5–6…
- ⚠️ Skipping : a URL sits under " " while the "url" cell is…
- ⚠️ Skipping : data row has field(s), missing the required…
- ⚠️ Skipping : data rows — one addition per file
- ⚠️ Skipping : header row with no data row
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)