santifer/career-ops · warning

⚠️ Skipping : header row with no data row

Error message

⚠️  Skipping ${filename}: header row with no data row

What it means

merge-tracker.mjs refuses to merge a headed tracker-addition TSV whose first line looks like a header (column labels) but is followed by zero data rows. The header-row-plus-one-data-row shape is the contract parseHeadedAddition (merge-tracker.mjs:781) relies on to resolve every field by name; with no row there is nothing to merge, so it warns and returns null instead of silently merging a stub. This keeps malformed batch output in batch/tracker-additions/ from corrupting applications.md.

Solutions

  1. Open the flagged TSV and write the single data row beneath the header, one tab-separated cell per label in the same order.
  2. Delete the empty headed file if no evaluation actually happened, then regenerate it from the evaluation run.
  3. Re-run the emitter (evaluation worker or batch prompt) that was supposed to write the row, and check its exit status/output this time.
  4. If the row was written without a trailing newline elsewhere, verify the file actually ends with the data line (some writers buffer output).

Example fix

// before (batch/tracker-additions/042-acme.tsv)
num	date	company	role	score	status	pdf	report

// after
num	date	company	role	score	status	pdf	report
42	2026-09-16	Acme	Senior Backend Engineer	4.2/5	Evaluated	❌	[042](reports/042-acme-2026-09-16.md)
Defensive patterns

Strategy: validation

Validate before calling

const lines = content.split(/\r?\n/).filter(l => l.trim() !== '');
if (lines.length < 2) {
  throw new Error(`${file}: headed TSV needs a header row plus exactly one data row (got ${lines.length} line(s))`);
}

Type guard

const isWellFormedHeadedTsv = (content) => {
  const lines = String(content ?? '').split(/\r?\n/).filter(l => l.trim() !== '');
  return lines.length === 2;
};

Prevention

When it happens

Trigger: A file in batch/tracker-additions/{num}-{company-slug}.tsv contains only the header row (num\tdate\tcompany\t...) with no second line, and merge-tracker.mjs runs over the additions directory. Calling parseTsvContent(content, filename) where content's only non-empty line is recognized by looksLikeTsvHeaderRow.

Common situations: A script or LLM writer created the TSV, errored out before writing the data row, and left an orphan header; a template was copied but never filled in; an editor or shell heredoc truncated the file after the first line; the writer emitted the header and intended to append the row later.

Understand the failure class

Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.

Related errors


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

Appendix: source

Thrown at merge-tracker.mjs:800

  const { map, missing, duplicates, unknown } = resolveTsvColumns(splitAdditionCells(lines[0]));
  if (duplicates.length) {
    console.warn(`⚠️  Skipping ${filename}: header labels the same column twice (${[...new Set(duplicates)].join(', ')}) — refusing to guess which one carries the value`);
    return null;
  }
  if (missing.length) {
    console.warn(`⚠️  Skipping ${filename}: header is missing required column(s): ${missing.join(', ')}`);
    return null;
  }
  if (unknown.length) {
    // Not fatal: an unrecognized label is a column this version has no meaning
    // for, and dropping it loses nothing the tracker could store anyway. Say so
    // rather than swallowing it, since the usual cause is a typo'd label.
    console.warn(`⚠️  ${filename}: ignoring unrecognized header column(s): ${unknown.join(', ')}`);
  }

  const dataLines = lines.slice(1);
  if (dataLines.length === 0) {
    console.warn(`⚠️  Skipping ${filename}: header row with no data row`);
    return null;
  }
  if (dataLines.length > 1) {
    // One addition per file is the contract every caller assumes (the merge
    // loop takes one addition per TSV). Rejecting is louder than merging the
    // first row and dropping the rest, which would lose evaluations silently.
    console.warn(`⚠️  Skipping ${filename}: ${dataLines.length} data rows — one addition per file`);
    return null;
  }

  const parts = splitAdditionCells(dataLines[0]);
  // Only the REQUIRED cells must actually be there. A row whose trailing
  // optional value is empty is routinely written without its final tab —
  // openrouter-runner emits `…\treport\t\n` for an absent note, and an LLM
  // writer asked for "leave the last field empty" often just stops — so
  // demanding a cell for every label would reject canonical rows. Absent and
  // empty read the same here, which is what the batch and web prompts already
  // promise their writers.

View on GitHub (pinned to aac998c7ed)