santifer/career-ops · error · TypeError

Report number must be a positive integer, got

Error message

Report number must be a positive integer, got ${num}

What it means

formatReportNumber converts a report ID into its canonical 3-digit zero-padded form (e.g. 42 → '042') and validates the input first: it must be a safe positive integer. This TypeError is thrown when the value passed is 0, negative, fractional, NaN, non-numeric, or a numeric string, protecting the report-file numbering scheme from producing unsortable or colliding filenames.

Solutions

  1. Coerce with Number() and validate before calling: only pass Number.isSafeInteger(x) && x >= 1 values
  2. Check the upstream source of the number — CLI args must be parsed (Number.parseInt(arg, 10)) before formatting
  3. If computing the next report number, ensure the max-extraction handles an empty tracker by starting at 1
  4. Reject the operation upstream with a clear message instead of letting 0/NaN reach the formatter

Example fix

// before
formatReportNumber(process.argv[2]); // "42" → TypeError
// after
const n = Number.parseInt(process.argv[2], 10);
if (Number.isSafeInteger(n) && n >= 1) formatReportNumber(n);
Defensive patterns

Strategy: validation

Validate before calling

// validate before formatting
function toReportNumber(input) {
  const n = typeof input === 'number' ? input : Number.parseInt(String(input ?? ''), 10);
  if (!Number.isSafeInteger(n) || n < 1) {
    throw new TypeError(`Report number must be a positive integer, got ${input}`);
  }
  return n;
}

Type guard

function isValidReportNumber(v) {
  return Number.isSafeInteger(v) && v >= 1;
}

Try / catch

try {
  const id = formatReportNumber(num);
} catch (e) {
  if (e instanceof TypeError && e.message.startsWith('Report number must be a positive integer')) {
    console.warn(`Bad report number ${num}: parse CLI args / tracker rows into integers first.`);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling formatReportNumber with a string parsed from user input ('42' not Number('42')), a value of 0 when no reports exist yet, a NaN from parseInt on a malformed argument, or a float from a division/average computed upstream.

Common situations: CLI argument parsing that forgot Number(); tracker rows using 'N/A' sentinels passed to the formatter; computing max+1 over an empty set yielding NaN or 1 misread as 0; tests passing strings for convenience.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at reserve-report-num.mjs:44

import { getCareerOpsRoot } from './path-resolver.mjs';
import {
  extractTrackerReportNumbers, parseTrackerRow, resolveColumns,
} from './tracker-parse.mjs';
import {
  acquireTrackerLock, canonicalizeTrackerPath, resolveTrackerPath, trackerLockDirFor,
} from './tracker-utils.mjs';
import { isMainModule } from './lib/is-main-module.mjs';

const ROOT = getCareerOpsRoot();
const MAX_SENTINEL_AGE_MS = 4 * 60 * 60 * 1000;
const MAX_RETRIES = 50;
const MAX_COUNT = 50;
const RESERVATION_TOKEN = Symbol('career-ops-report-reservation-token');

/** Format a report ID with a minimum width of three digits. */
export function formatReportNumber(num) {
  if (!Number.isSafeInteger(num) || num < 1) {
    throw new TypeError(`Report number must be a positive integer, got ${num}`);
  }
  return String(num).padStart(3, '0');
}

function reportsDirFor(options = {}) {
  return resolve(options.reportsDir
    || process.env.CAREER_OPS_REPORTS_DIR
    || join(options.rootDir || ROOT, 'reports'));
}

function trackerPathFor(options = {}) {
  return options.trackerPath
    ? canonicalizeTrackerPath(options.trackerPath)
    : resolveTrackerPath(options.rootDir || ROOT);
}

// A bare date file is not a report. `scan-ats-full.mjs --md-out reports/` writes
// its digest as `reports/YYYY-MM-DD.md`, which matches `/^(\d+)-/` and is read

View on GitHub (pinned to aac998c7ed)