santifer/career-ops · warning

⚠️ Skipping : required cell(s) present but empty: — use the…

Error message

⚠️  Skipping ${filename}: required cell(s) present but empty: ${blankRequiredCells.join(', ')} — use the documented sentinel (— / N/A) rather than a blank cell

What it means

A required cell that is present in the data row but blank (empty after trimming) is rejected: blank is never the way to say 'none' in this format. Every required field has a documented value or sentinel ('—' / 'N/A' for score, '—' for report, '❌' for pdf). Left unchecked, an empty status would reach validateStatus(''), which coerces to 'Evaluated' — recording an evaluation state the row never claimed.

Solutions

  1. Replace each blank required cell named in the warning with its documented value or sentinel: score → 'X.X/5', 'N/A', '—' or '-'; status → a canonical state from templates/states.yml; pdf → '✅' or '❌'; report → the markdown link or '—'; num/date/company/role → real values.
  2. For a backfilled row with no evaluation, use the score sentinel 'N/A', '—' or '-' explicitly rather than leaving the cell empty.
  3. Fix the emitter to substitute sentinels instead of empty strings for unknown values.
  4. Re-run node merge-tracker.mjs after editing and confirm the row merges.

Example fix

// before (blank status and score cells)
42	2026-09-16	Acme	Backend Engineer			❌	[042](reports/042.md)

// after
42	2026-09-16	Acme	Backend Engineer	4.2/5	Evaluated	❌	[042](reports/042.md)
// or for a never-scored backfill:
42	2026-09-16	Acme	Backend Engineer	N/A	Applied	❌	—
Defensive patterns

Strategy: validation

Validate before calling

const REQUIRED = ['num','date','company','role','score','status','pdf','report'];
const blank = REQUIRED.filter(k => String(cells[header.indexOf(k)] ?? '').trim() === '');
if (blank.length) throw new Error(`${file}: blank required cell(s) ${blank.join(', ')} — use sentinels (— / N/A / ❌)`);

Prevention

When it happens

Trigger: parseHeadedAddition (merge-tracker.mjs:843) finds TSV_REQUIRED_FIELDS entries where at(k) === '' — the cell exists at the right column index but holds only whitespace or nothing (e.g. a row like 42\t\tAcme\t... with an empty date cell).

Common situations: An LLM writer left a placeholder it then stripped; a spreadsheet export wrote empty cells for unknown values; a script interpolated an unset shell variable (\t${SCORE}\t producing an empty cell); a human hand-edited the row and deleted a value instead of substituting the sentinel.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at merge-tracker.mjs:844

    console.warn(`⚠️  Skipping ${filename}: data row has ${parts.length} field(s), missing the required cell(s): ${missingRequiredCells.join(', ')}`);
    return null;
  }

  const at = (k) => (map[k] != null ? String(parts[map[k]] ?? '').trim() : '');
  const optional = (k) => {
    const v = at(k);
    return PLACEHOLDER_CELL.test(v) ? '' : v;
  };

  // A required cell that is PRESENT but blank is a writer that did not supply
  // the value, and blank is never the way to say "none" here: every required
  // field has a documented value, and the "no data" cases have sentinels
  // (`—` / `N/A` for score, `—` for report, `❌` for pdf). Left unchecked, an
  // empty status reaches validateStatus(''), which warns and returns
  // "Evaluated" — recording a real evaluation state the row never claimed.
  const blankRequiredCells = TSV_REQUIRED_FIELDS.filter(k => at(k) === '');
  if (blankRequiredCells.length) {
    console.warn(`⚠️  Skipping ${filename}: required cell(s) present but empty: ${blankRequiredCells.join(', ')} — use the documented sentinel (— / N/A) rather than a blank cell`);
    return null;
  }

  // The optional tail can only be trusted when a short row's missing cells are
  // genuinely its LAST ones. Position cannot tell "notes omitted, url written"
  // from "notes written, url omitted" — both are one cell short — so the one
  // TYPED optional column is the corroboration. A cell that IS a URL, sitting
  // under a label that is not `url` while the `url` label has no cell at all,
  // means every optional value is one column left of its label. Merging that
  // stores the posting URL as the row's NOTES and leaves the dedup key empty,
  // which is precisely the mapping the header exists to protect.
  if (map.url != null && shortLabels.includes('url')) {
    const misplacedUrl = Object.entries(map)
      .filter(([k, i]) => k !== 'url' && i < parts.length && /^https?:\/\/\S*$/i.test(String(parts[i] ?? '').trim()))
      .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;

View on GitHub (pinned to aac998c7ed)