santifer/career-ops · warning

payload.articleDigest requires a non-empty dedupKey (used fo

Error message

payload.articleDigest requires a non-empty dedupKey (used for dedup/idempotency)

What it means

Emitted by Check 13 of verify-pipeline.mjs, which calls tracker-sync-check.mjs's checkTrackerSync({appsFile}) to compare data/applications.md against data/active-interviews.md. Mismatches classified resolution 'auto-tier1' are resolvable purely by the canonical lifecycle order (templates/states.yml): neither status is a rollback, so the stale file (m.staleIn) can be updated to m.suggestedStatus without human judgement. The message names both statuses, the suggestion, and the stale side.

Source

Thrown at add-entry.mjs:193

  if (payload.cv) {
    const { section, dedupKey, entry } = payload.cv;
    if (!section || !entry) throw new Error('payload.cv requires { section, entry }');
    // dedupKey is what makes the insert idempotent — refuse to add without one
    // rather than silently allowing duplicate re-runs.
    if (!normalizeKey(dedupKey)) throw new Error('payload.cv requires a non-empty dedupKey (used for dedup/idempotency)');
    if (cvText === null) throw new Error(`cv.md not found — cannot add to a CV that does not exist`);
    if (cvHasEntry(cvText, section, dedupKey)) {
      result.cv = { status: 'duplicate', section };
    } else {
      cv = insertIntoCvSection(cvText, section, entry);
      result.cv = { status: 'added', section };
    }
  }

  if (payload.articleDigest) {
    const { dedupKey, entry } = payload.articleDigest;
    if (!entry) throw new Error('payload.articleDigest requires { entry }');
    if (!normalizeKey(dedupKey)) throw new Error('payload.articleDigest requires a non-empty dedupKey (used for dedup/idempotency)');
    // article-digest.md is optional; create it from a header when missing.
    const current = articleText === null
      ? '# Article Digest -- Proof Points\n\nCompact proof points from portfolio projects. Read by career-ops at evaluation time.\n'
      : articleText;
    if (articleDigestHasEntry(current, dedupKey)) {
      result.articleDigest = { status: 'duplicate' };
      articleDigest = articleText;
    } else {
      articleDigest = appendArticleDigest(current, entry);
      result.articleDigest = { status: articleText === null ? 'created' : 'added' };
    }
  }

  return { cv, articleDigest, result };
}

async function readStdin() {
  const chunks = [];

View on GitHub (pinned to 60398d6549)

Solutions

  1. Run `node tracker-sync-check.mjs` for the full mismatch detail
  2. Update the stale side with `node set-status.mjs <report#|company> <State>` so both files hold the suggested status
  3. Re-run `node verify-pipeline.mjs` to confirm the files report in sync
  4. Habit: change status only through set-status.mjs, never by hand-editing either table

Example fix

// before:
applications.md #17: Responded | active-interviews.md: Interview

// after:
node set-status.mjs 17 Interview
// both files now say Interview
Defensive patterns

Strategy: validation

Validate before calling

// Run BEFORE/AROUND any status change: compare the two files yourself using the
// canonical lifecycle order, so drift never accumulates.
const ORDER = ['Evaluated', 'Applied', 'Responded', 'Interview', 'Offer', 'Hired']; // canonical prefix of templates/states.yml
const rank = (s) => ORDER.indexOf(s);
export function isAutoResolvable(appStatus, aiStatus) {
  const a = rank(appStatus), b = rank(aiStatus);
  return a >= 0 && b >= 0 && a !== b; // different forward states -> the older one is stale
}

Type guard

const isCanonicalForwardPair = (a, b) => rank(a) >= 0 && rank(b) >= 0; // both on the forward ladder — sync is mechanical

Prevention

When it happens

Trigger: A round logged in active-interviews.md moved a company to Interview while the applications.md row still says Responded (or the mirror case) — canonical order (Responded → Interview) decides which file is stale and what the converged state is.

Common situations: Updating one file after an interview round and forgetting the other; editing active-interviews.md by hand after debriefs instead of going through set-status.mjs; batch workers writing the tracker without touching active-interviews.

Related errors


AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20). Data as JSON: /api/errors/8b5fb02b64f2dc2e. Report an issue: GitHub.