santifer/career-ops · error · Error

payload.cv requires a non-empty dedupKey (used for…

Error message

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

What it means

The dedupKey inside payload.cv is what makes insertions idempotent: applyAdd checks cvHasEntry(cvText, section, dedupKey) before inserting so repeated runs don't add duplicates. If dedupKey is absent or normalizes to an empty string (normalizeKey fails), the function throws rather than silently allowing duplicate re-runs.

Solutions

  1. Add a stable non-empty dedupKey, e.g. a slug of the item: dedupKey: 'proj:cv-santiago'.
  2. Derive dedupKey deterministically from entry content (slugified title or hash) so re-runs of the same item dedup correctly.
  3. Ensure the key generation never yields '' — guard normalizeKey(input) || fallbackKey before calling applyAdd.

Example fix

// before
await applyAdd({ cv: { section: 'Projects', entry } }, { cvText });

// after
await applyAdd(
  { cv: { section: 'Projects', dedupKey: entry.toLowerCase().replace(/[^a-z0-9]+/g, '-'), entry } },
  { cvText }
);
Defensive patterns

Strategy: validation

Validate before calling

function hasDedupKey(target) {
  return typeof target.dedupKey === 'string' && target.dedupKey.trim().length > 0;
}
if (payload.cv && !hasDedupKey(payload.cv)) throw new Error('cv payload needs a non-empty dedupKey');

Type guard

function hasNonEmptyKey(t) {
  return t != null && typeof t.dedupKey === 'string' && t.dedupKey.trim() !== '';
}

Try / catch

try {
  applyAdd(payload, { cvText });
} catch (e) {
  if (e.message.includes('non-empty dedupKey')) {
    console.error('Add a stable dedupKey (slug or hash) to make insertion idempotent');
  } else throw e;
}

Prevention

When it happens

Trigger: applyAdd({ cv: { section: 'Projects', entry: '...' } }) with dedupKey omitted, or with dedupKey: '' / whitespace-only, or a non-string normalizeKey cannot reduce to a non-empty key.

Common situations: Adding the first entry of a new section and assuming dedup isn't needed yet, generating payloads in a loop where dedupKey was only set for some items, or copy-pasting an example that omitted the field.

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/fa39536a9438d4a7. Report an issue: GitHub.

Appendix: source

Thrown at add-entry.mjs:183

 * Pure core: given the current file contents and a payload, compute the new
 * contents and a per-target status. No I/O — this is what the tests exercise.
 * @returns {{ cv: string, articleDigest: string, result: object }}
 */
export function applyAdd(payload, { cvText = null, articleText = null } = {}) {
  if (!payload || typeof payload !== 'object' || (!payload.cv && !payload.articleDigest)) {
    throw new Error('payload must include at least one of: cv, articleDigest');
  }

  const result = {};
  let cv = cvText;
  let articleDigest = articleText;

  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)) {

View on GitHub (pinned to aac998c7ed)