santifer/career-ops · critical

tracker-parse.mjs: cannot load tracker-aliases.json

Error message

tracker-parse.mjs: cannot load tracker-aliases.json (${e.message}). The file ships with career-ops next to tracker-parse.mjs — restore it from the repo or re-run: node update-system.mjs apply

What it means

tracker-parse.mjs loads its alias table (HEADER_ALIASES) from tracker-aliases.json at module init via an import.meta.url-relative readFileSync. If that JSON is missing or unreadable, the module cannot map tracker column names, so it throws immediately at import time instead of failing later on a mis-parsed tracker. The file ships with career-ops next to tracker-parse.mjs.

Solutions

  1. Run `node update-system.mjs apply` to restore shipped system files including tracker-aliases.json
  2. Restore tracker-aliases.json from the career-ops repo (git checkout -- tracker-aliases.json or re-download next to tracker-parse.mjs)
  3. Check file permissions on tracker-aliases.json (readable by the running user)
  4. Verify the file parses as JSON (node -e "JSON.parse(require('fs').readFileSync('tracker-aliases.json','utf-8'))") to rule out truncation/corruption

Example fix

// before
cp tracker-parse.mjs ~/tools/   # copied script without its JSON companion -> throws at import
// after
cp tracker-parse.mjs tracker-aliases.json ~/tools/  # keep both files together
Defensive patterns

Strategy: fallback

Validate before calling

import { existsSync } from 'fs';
if (!existsSync(new URL('./tracker-aliases.json', import.meta.url))) {
  throw new Error('tracker-aliases.json missing; run: node update-system.mjs apply');
}

Type guard

const isAliasFileOk = (dir) => existsSync(join(dir, 'tracker-aliases.json'));

Try / catch

try {
  const { HEADER_ALIASES } = await import('./tracker-parse.mjs');
} catch (e) {
  if (String(e.message).includes('tracker-aliases.json')) {
    // restore shipped files then re-import
  }
}

Prevention

When it happens

Trigger: Any import/require of tracker-parse.mjs when tracker-aliases.json is absent from the module directory, was deleted, or cannot be read (permissions, partial update checkout, packaging that dropped JSON assets).

Common situations: Partial git checkout or sparse clone that skipped JSON files; a system update that replaced tracker-parse.mjs but not its companion data file; copying the .mjs file alone to another location; npm/packaging rules that exclude .json siblings; restrictive file permissions after running as another user.

Related errors


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

Appendix: source

Thrown at tracker-parse.mjs:40

/**
 * Header text (lowercased) → canonical field name. Includes ES aliases.
 * Loaded from tracker-aliases.json — the ONE shared alias table, which the web
 * read path (web/src/lib/tracker-table.mjs) also loads at runtime, so the two
 * can never drift (PR #1598 review). Add new aliases in the JSON, not here.
 *
 * A missing or corrupt JSON is a broken install (the file ships alongside this
 * module in SYSTEM_PATHS/BOOTSTRAP_PATHS): fail fast with an actionable
 * message rather than degrading silently — a quiet fallback here would
 * reintroduce exactly the reader drift the shared table exists to prevent.
 * (The web loader degrades to the legacy fixed order instead because it reads
 * the file from a user-configured root at request time.)
 */
export const HEADER_ALIASES = (() => {
  const src = new URL('./tracker-aliases.json', import.meta.url);
  try {
    return JSON.parse(readFileSync(src, 'utf-8'));
  } catch (e) {
    throw new Error(
      `tracker-parse.mjs: cannot load tracker-aliases.json (${e.message}). ` +
      'The file ships with career-ops next to tracker-parse.mjs — restore it ' +
      'from the repo or re-run: node update-system.mjs apply',
    );
  }
})();

/**
 * A score cell in the tracker: `N/5` or `N.N/5` (any precision), or the
 * sentinels `N/A` / `DUP` / `—` (em dash) / `-` (hyphen). Markdown bold is
 * stripped first. `—`/`-` mirror the tracker's own "no data" convention used
 * in every other column (Report, PDF, etc.) — see #1799: a backfilled entry
 * with no evaluation (e.g. a rejection for a role never run through
 * `oferta`) needs a score-cell sentinel too, not just `N/A`. A status label
 * never matches this, which is what makes it a reliable discriminator between
 * the score and status columns regardless of their order (#1427).
 */
export const SCORE_CELL_RE = /^\d+(?:\.\d+)?\/5$/;

View on GitHub (pinned to aac998c7ed)