{"record":{"id":"fa39536a9438d4a7","repo":"santifer/career-ops","slug":"payload-cv-requires-a-non-empty-dedupkey-used-for","errorCode":null,"errorMessage":"payload.cv requires a non-empty dedupKey (used for dedup/idempotency)","messagePattern":"payload\\.cv requires a non-empty dedupKey \\(used for dedup/idempotency\\)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"add-entry.mjs","lineNumber":183,"sourceCode":" * Pure core: given the current file contents and a payload, compute the new\n * contents and a per-target status. No I/O — this is what the tests exercise.\n * @returns {{ cv: string, articleDigest: string, result: object }}\n */\nexport function applyAdd(payload, { cvText = null, articleText = null } = {}) {\n  if (!payload || typeof payload !== 'object' || (!payload.cv && !payload.articleDigest)) {\n    throw new Error('payload must include at least one of: cv, articleDigest');\n  }\n\n  const result = {};\n  let cv = cvText;\n  let articleDigest = articleText;\n\n  if (payload.cv) {\n    const { section, dedupKey, entry } = payload.cv;\n    if (!section || !entry) throw new Error('payload.cv requires { section, entry }');\n    // dedupKey is what makes the insert idempotent — refuse to add without one\n    // rather than silently allowing duplicate re-runs.\n    if (!normalizeKey(dedupKey)) throw new Error('payload.cv requires a non-empty dedupKey (used for dedup/idempotency)');\n    if (cvText === null) throw new Error(`cv.md not found — cannot add to a CV that does not exist`);\n    if (cvHasEntry(cvText, section, dedupKey)) {\n      result.cv = { status: 'duplicate', section };\n    } else {\n      cv = insertIntoCvSection(cvText, section, entry);\n      result.cv = { status: 'added', section };\n    }\n  }\n\n  if (payload.articleDigest) {\n    const { dedupKey, entry } = payload.articleDigest;\n    if (!entry) throw new Error('payload.articleDigest requires { entry }');\n    if (!normalizeKey(dedupKey)) throw new Error('payload.articleDigest requires a non-empty dedupKey (used for dedup/idempotency)');\n    // article-digest.md is optional; create it from a header when missing.\n    const current = articleText === null\n      ? '# Article Digest -- Proof Points\\n\\nCompact proof points from portfolio projects. Read by career-ops at evaluation time.\\n'\n      : articleText;\n    if (articleDigestHasEntry(current, dedupKey)) {","sourceCodeStart":165,"sourceCodeEnd":201,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/add-entry.mjs#L165-L201","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Add a stable non-empty dedupKey, e.g. a slug of the item: dedupKey: 'proj:cv-santiago'.","Derive dedupKey deterministically from entry content (slugified title or hash) so re-runs of the same item dedup correctly.","Ensure the key generation never yields '' — guard normalizeKey(input) || fallbackKey before calling applyAdd."],"exampleFix":"// before\nawait applyAdd({ cv: { section: 'Projects', entry } }, { cvText });\n\n// after\nawait applyAdd(\n  { cv: { section: 'Projects', dedupKey: entry.toLowerCase().replace(/[^a-z0-9]+/g, '-'), entry } },\n  { cvText }\n);","handlingStrategy":"validation","validationCode":"function hasDedupKey(target) {\n  return typeof target.dedupKey === 'string' && target.dedupKey.trim().length > 0;\n}\nif (payload.cv && !hasDedupKey(payload.cv)) throw new Error('cv payload needs a non-empty dedupKey');","typeGuard":"function hasNonEmptyKey(t) {\n  return t != null && typeof t.dedupKey === 'string' && t.dedupKey.trim() !== '';\n}","tryCatchPattern":"try {\n  applyAdd(payload, { cvText });\n} catch (e) {\n  if (e.message.includes('non-empty dedupKey')) {\n    console.error('Add a stable dedupKey (slug or hash) to make insertion idempotent');\n  } else throw e;\n}","preventionTips":["Always derive dedupKey deterministically from the entry (slugified title or content hash).","Make dedupKey a required field in the payload-builder's type/interface.","Unit-test that running the same add twice yields status 'duplicate' the second time."],"tags":["validation","idempotency","dedup"],"backgroundTag":"empty-required-field","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}