santifer/career-ops · warning

⚠️ Skipping : data rows — one addition per file

Error message

⚠️  Skipping ${filename}: ${dataLines.length} data rows — one addition per file

What it means

The headed addition parser enforces 'exactly one data row per file'. Every caller of the merge loop assumes one addition per TSV, so a file with 2+ data rows is rejected outright rather than merging the first row and silently dropping the rest. The warn-and-null keeps extra evaluations from being lost without a trace.

Solutions

  1. Split the file: write each data row into its own {num}-{company-slug}.tsv under batch/tracker-additions/, keeping the header row in each.
  2. Re-run the offending emitter with instructions to produce exactly one data row per file (the batch prompt's documented contract).
  3. If the extra rows are accidental duplicates, trim the file to the single correct row.
  4. After fixing, re-run node merge-tracker.mjs and confirm the warning is gone and the row merged.

Example fix

// before (one file, two rows)
num	date	company	role	score	status	pdf	report
42	2026-09-16	Acme	Backend	4.2/5	Evaluated	❌	[042](reports/042.md)
43	2026-09-16	Globex	Backend	3.9/5	Evaluated	❌	[043](reports/043.md)

// after: 042-acme.tsv has row 42, 043-globex.tsv has row 43, each with the same header
Defensive patterns

Strategy: validation

Validate before calling

const dataRows = content.split(/\r?\n/).filter(l => l.trim() !== '').slice(1);
if (dataRows.length !== 1) {
  throw new Error(`${file}: exactly one data row per file required (got ${dataRows.length})`);
}

Type guard

const isSingleAdditionFile = (content) =>
  String(content ?? '').split(/\r?\n/).filter(l => l.trim() !== '').length === 2;

Prevention

When it happens

Trigger: parseHeadedAddition (merge-tracker.mjs:803) sees lines.slice(1).length > 1 — a headed TSV in batch/tracker-additions/ carries two or more non-empty lines after the header. Typically a writer batched several evaluations into one file, violating the one-addition-per-file contract.

Common situations: An LLM batch worker wrote all of a run's rows into one TSV instead of one file per evaluation; someone concatenated TSVs with cat; a spreadsheet export flattened multiple rows into one file; a copy-paste loop appended rows to the same file across runs.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at merge-tracker.mjs:807

    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.
  //
  // This is not the defense against a SHIFTED row: an omitted interior cell
  // slides every later value one column left, which the score corroboration
  // below catches by content regardless of the row's width.
  const shortLabels = Object.entries(map).filter(([, i]) => i >= parts.length).map(([k]) => k);
  const missingRequiredCells = shortLabels.filter(k => TSV_REQUIRED_FIELDS.includes(k));
  if (missingRequiredCells.length) {

View on GitHub (pinned to aac998c7ed)