{"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":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"add-entry.mjs","lineNumber":159,"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":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/add-entry.mjs#L141-L177","documentation":"Per-target validation in `applyAdd`: when `payload.cv` is present, it must carry a non-empty `dedupKey` (after normalization via `normalizeKey`). The dedupKey is the idempotency anchor — it lets `cvHasEntry` detect re-runs and refuse duplicates. Refusing to insert without one prevents silent duplicate accumulation across agent re-runs.","triggerScenarios":"Calling `applyAdd({ cv: { section, entry, dedupKey: '' } })`, `applyAdd({ cv: { section, entry, dedupKey: '   ' } })`, or omitting `dedupKey` entirely so it normalizes to empty. Any value whose normalized form is empty trips the guard.","commonSituations":"Agent forgets to mint a key; dedupKey derived from a value that happened to be all punctuation/spaces; caller reuses a helper that strips non-alphanumerics down to nothing; thinking dedup is optional.","solutions":["Provide a stable, descriptive dedupKey such as `role-company-2024` or `project-x-feature`.","Generate the key deterministically from the entry's identity (company+role+year, project+feature) so re-runs collapse to one row.","If you are extending the schema, validate `normalizeKey(dedupKey)` in your caller before invoking applyAdd.","Treat the key as the 'id' of this entry — never reuse two different meanings for the same key.","Add a test: same key twice → second result is `{ status: 'duplicate' }`."],"exampleFix":"// before\napplyAdd({ cv: { section: 'Experience', dedupKey: '', entry: '- X' } }); // throws\n// after\napplyAdd({\n  cv: {\n    section: 'Experience',\n    dedupKey: 'acme-staff-eng-2024',\n    entry: '- Staff Engineer @ Acme (2024): led platform team'\n  }\n});","handlingStrategy":"validation","validationCode":"function validKey(k) {\n  return typeof k === 'string' && k.trim().replace(/[^a-z0-9_-]/gi, '').length > 0;\n}\nif (payload.cv && !validKey(payload.cv.dedupKey)) {\n  throw new Error('Provide a stable dedupKey (e.g. role-company-year)');\n}","typeGuard":"function hasDedupKey(o) {\n  return !!o && typeof o.dedupKey === 'string' && o.dedupKey.trim() !== '';\n}","tryCatchPattern":null,"preventionTips":["Mint dedupKeys from stable identity (company+role+year, project+feature).","Reuse the same key for the same entry across re-runs to get the 'duplicate' status.","Never let two different entries share a key.","Test that a repeat insert returns `{ status: 'duplicate' }`."],"tags":["validation","idempotency","add-entry","cv","dedup"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}