{"record":{"id":"8813ae98ae640390","repo":"santifer/career-ops","slug":"keep-a-num-and-b-num-exact-title-matc","errorCode":null,"errorMessage":"⚠️  Keep #${a.num} and #${b.num}: exact-title match but advanced status requires exact report identity","messagePattern":"⚠️  Keep #(.+?) and #(.+?): exact-title match but advanced status requires exact report identity","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"dedup-tracker.mjs","lineNumber":231,"sourceCode":" *\n * @param {object} a - First parsed applications.md row.\n * @param {object} b - Second parsed applications.md row.\n * @returns {boolean} True when dedup may cluster the two rows as duplicates.\n */\nfunction roleMatch(a, b) {\n  if (sameReportIdentity(a, b)) return true;\n  if (normalizeRole(a.role) !== normalizeRole(b.role)) return false;\n\n  // Exact-title duplicates that have entered the real application pipeline are\n  // kept separate. A user may already have applied to one row; deleting it\n  // because a higher-scored exact-title sibling exists would lose status,\n  // report, and notes. Keep both unless the rows point to the exact same\n  // report identity.\n  if (isAdvancedStatus(a.status) || isAdvancedStatus(b.status)) {\n    const key = pairKey(a, b);\n    if (!protectedTitlePairs.has(key)) {\n      protectedTitlePairs.add(key);\n      console.warn(`⚠️  Keep #${a.num} and #${b.num}: exact-title match but advanced status requires exact report identity`);\n    }\n    return false;\n  }\n\n  return true;\n}\n\n/**\n * Parse a tracker score cell into a numeric value for keeper selection.\n *\n * Scores may include Markdown bolding or a `/5` suffix. Dedup only needs the\n * numeric part so it can keep the highest-scored duplicate row in a cluster.\n *\n * @param {string} s - Raw score cell such as `4.3/5` or `**4.3/5**`.\n * @returns {number} Parsed score, or 0 when no number is present.\n */\nfunction parseScore(s) {\n  const m = s.replace(/\\*\\*/g, '').match(/([\\d.]+)/);","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/dedup-tracker.mjs#L213-L249","documentation":"dedup-tracker.mjs roleMatch() normally collapses two tracker rows with exact-title matches, but when either row already has an 'advanced' status (Interview, Offer, Hired, etc.), a merge would destroy process history. The function therefore refuses to merge and warns that both rows are kept unless they point to the exact same report identity.","triggerScenarios":"Running `node dedup-tracker.mjs` over data/applications.md where two rows fuzzy/exact-match on title AND at least one status is advanced (isAdvancedStatus) AND the pair does not resolve to the identical report. roleMatch returns false and prints this warning once per pair (protected via protectedTitlePairs set).","commonSituations":"A company reposted the same role title in a new requisition while the old application already reached Interview; a scanner ingested the same title at two locations/req IDs; user re-applied to a role previously interviewed for.","solutions":["Decide if the two rows are genuinely distinct openings — if yes, do nothing; the warning is correct behavior","Add the req/posting ID to the notes of both rows so future merges can distinguish them (REQ_NUMBER_RE: 'req JR-10423', 'job id 88214', etc.)","If they are truly duplicates with identical report links, verify the report identity matches so the guard allows merging","If one row is stale, close it with set-status.mjs <num> Discarded instead of merging"],"exampleFix":"// before: two rows, same title, no req id, one in Interview\n| 12 | ... | Acme | Backend Engineer | Interview | ... |\n| 41 | ... | Acme | Backend Engineer | Evaluated | ... |\n// after: disambiguate with req IDs in notes\n| 12 | ... | Backend Engineer ... req JR-10423 |\n| 41 | ... | Backend Engineer ... req JR-20877 |","handlingStrategy":"validation","validationCode":"// before running dedup-tracker, ensure same-title rows carry req IDs in notes\nconst rows = parseTracker();\nconst dupTitle = rows.filter(r => r.title === t);\nconst needsReqId = dupTitle.some(r => isAdvancedStatus(r.status)) && dupTitle.length > 1;\nif (needsReqId) console.warn('add req/posting id to notes for', t);","typeGuard":"const REQ_ID_RE = /\\b(?:job id|posting id|requisition|req|jr|job|posting|ref|r_)\\s*[A-Za-z]*\\d/i;\nconst hasReqId = (row) => REQ_ID_RE.test(row.notes ?? '');","tryCatchPattern":null,"preventionTips":["Always put the req/posting ID in tracker notes when a company has multiple similar openings","Never merge rows whose status has progressed past Evaluated without verifying identical report identity","Review dedup-tracker output warnings before accepting any merge"],"tags":["deduplication","tracker","data-integrity"],"backgroundTag":"conflicting-config-options","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"}