santifer/career-ops · warning

⚠️ Skipping : a URL sits under " " while the "url" cell is…

Error message

⚠️  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

What it means

When a headed row is one or more cells short AND its missing labels include the optional 'url' column, the parser checks for a typed corroborating signal: an http(s):// value sitting under a different label while the 'url' label has no cell. That proves the optional cells are shifted one or more columns left of their labels — the URL would be stored as notes and the dedup key left empty. The file is skipped rather than merged with the wrong mapping.

Solutions

  1. Rewrite the data row so it has exactly one cell per header label, writing an empty cell (consecutive tabs) for every optional value you omit — the shifted URL then lands under 'url'.
  2. Alternatively drop the 'url' column from the header if you never send URLs, or use a fully-populated 10-cell row.
  3. Check the emitter's field ordering: it must emit cells in exactly the header's label order with placeholders for skipped optionals.
  4. After fixing, re-run merge-tracker.mjs and confirm the row merges with the URL in the tracker's URL column.

Example fix

// before (location omitted without empty cell — notes/url shifted left)
num	date	company	role	score	status	pdf	report	notes	via	location	url
42	...	Acme	Backend	4.2/5	Evaluated	❌	[042](reports/042.md)	Remote friendly	https://jobs.acme.com/123	

// after (empty cell for the omitted optional)
42	...	Acme	Backend	4.2/5	Evaluated	❌	[042](reports/042.md)	Remote friendly			https://jobs.acme.com/123
Defensive patterns

Strategy: validation

Validate before calling

const header = lines[0].split('\t');
const cells = lines[1].split('\t');
if (cells.length !== header.length) {
  throw new Error(`${file}: ${header.length - cells.length} cell(s) short — write an empty cell (extra tab) for every omitted optional value`);
}

Prevention

When it happens

Trigger: parseHeadedAddition (merge-tracker.mjs:856-863): map.url exists in the header, shortLabels includes 'url' (the row has fewer cells than the url column index), and some non-url-labelled cell matches /^https?:\/\/\S*$/i. E.g. a 9-cell row for a 10-label header where notes and url were both written but location was omitted without an empty cell.

Common situations: A writer dropped an interior optional cell (location or via) instead of writing an empty cell for it, sliding notes/url left; headerless-style positional writing was used against a headed template; a spreadsheet export skipped empty cells entirely.

Related errors


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

Appendix: source

Thrown at merge-tracker.mjs:861

  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;
    }
  }

  // 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;
  }

View on GitHub (pinned to aac998c7ed)