santifer/career-ops · error · Error
payload.cv requires a non-empty dedupKey (used for dedup/ide
Error message
payload.cv requires a non-empty dedupKey (used for dedup/idempotency)
What it means
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.
Source
Thrown at add-entry.mjs:159
* 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 9b17a8ac97)
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' }`.
Example fix
// before
applyAdd({ cv: { section: 'Experience', dedupKey: '', entry: '- X' } }); // throws
// after
applyAdd({
cv: {
section: 'Experience',
dedupKey: 'acme-staff-eng-2024',
entry: '- Staff Engineer @ Acme (2024): led platform team'
}
}); Defensive patterns
Strategy: validation
Validate before calling
function validKey(k) {
return typeof k === 'string' && k.trim().replace(/[^a-z0-9_-]/gi, '').length > 0;
}
if (payload.cv && !validKey(payload.cv.dedupKey)) {
throw new Error('Provide a stable dedupKey (e.g. role-company-year)');
} Type guard
function hasDedupKey(o) {
return !!o && typeof o.dedupKey === 'string' && o.dedupKey.trim() !== '';
} Prevention
- 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' }`.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- payload.cv requires { section, entry }
- payload must include at least one of: cv, articleDigest
- payload.articleDigest requires { entry }
- CV section order diverges from cv.md: rendered ${renderedOrd
- Application answer state must be one of: ${[...VALID_STATES]
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/fa39536a9438d4a7.
Report an issue: GitHub.