santifer/career-ops · info
⚠️ Keep #${a.num} and #${b.num}: exact-title match but adva
Error message
⚠️ Keep #${a.num} and #${b.num}: exact-title match but advanced status requires exact report identity What it means
dedup-tracker.mjs is telling you it deliberately did NOT merge two tracker rows whose normalized titles match exactly, because at least one row has an advanced status (Applied, Responded, Interview, Offer, Hired). Deleting a lower-scored exact-title sibling could destroy real application state — status, report link, notes — so both rows are kept unless they point at the exact same report identity (sameReportIdentity). The warning fires once per pair via the protectedTitlePairs set.
Source
Thrown at dedup-tracker.mjs:230
*
* @param {object} a - First parsed applications.md row.
* @param {object} b - Second parsed applications.md row.
* @returns {boolean} True when dedup may cluster the two rows as duplicates.
*/
function roleMatch(a, b) {
if (sameReportIdentity(a, b)) return true;
if (normalizeRole(a.role) !== normalizeRole(b.role)) return false;
// Exact-title duplicates that have entered the real application pipeline are
// kept separate. A user may already have applied to one row; deleting it
// because a higher-scored exact-title sibling exists would lose status,
// report, and notes. Keep both unless the rows point to the exact same
// report identity.
if (isAdvancedStatus(a.status) || isAdvancedStatus(b.status)) {
const key = pairKey(a, b);
if (!protectedTitlePairs.has(key)) {
protectedTitlePairs.add(key);
console.warn(`⚠️ Keep #${a.num} and #${b.num}: exact-title match but advanced status requires exact report identity`);
}
return false;
}
return true;
}
/**
* Parse a tracker score cell into a numeric value for keeper selection.
*
* Scores may include Markdown bolding or a `/5` suffix. Dedup only needs the
* numeric part so it can keep the highest-scored duplicate row in a cluster.
*
* @param {string} s - Raw score cell such as `4.3/5` or `**4.3/5**`.
* @returns {number} Parsed score, or 0 when no number is present.
*/
function parseScore(s) {
const m = s.replace(/\*\*/g, '').match(/([\d.]+)/);View on GitHub (pinned to 60398d6549)
Solutions
- If both rows are the SAME application, merge by hand: keep the advanced-status row, copy the report link and notes onto it, then delete the other with a careful manual edit.
- If they are genuinely different requisitions, add the req/job IDs to both rows' notes (e.g. 'req JR-10423') so dedup and other tooling can tell them apart permanently.
- Otherwise do nothing — keeping both is intentional data-safety behavior.
Example fix
# before — two rows, exact same title, one already applied | 42 | 2026-08-01 | Acme | Data Engineer | 4.2/5 | Applied | ❌ | [42](reports/042-acme-2026-08-01.md) | | | 87 | 2026-08-18 | Acme | Data Engineer | 4.5/5 | Evaluated | ❌ | [87](reports/087-acme-2026-08-18.md) | | # after — different requisitions: disambiguate both rows with req ids in notes | 42 | 2026-08-01 | Acme | Data Engineer | 4.2/5 | Applied | ❌ | [42](reports/042-acme-2026-08-01.md) | req JR-10423 | | 87 | 2026-08-18 | Acme | Data Engineer | 4.5/5 | Evaluated | ❌ | [87](reports/087-acme-2026-08-18.md) | req JR-10987 |
Defensive patterns
Strategy: type-guard
Validate before calling
const ADVANCED = new Set(['Applied', 'Responded', 'Interview', 'Offer', 'Hired']); const rowsSafeToAutoMerge = (a, b) => !ADVANCED.has(a.status) && !ADVANCED.has(b.status);
Type guard
const isAdvancedStatus = (s) => ['Applied', 'Responded', 'Interview', 'Offer', 'Hired'].includes(String(s ?? '').trim());
Prevention
- Never bulk-delete 'duplicate' rows that carry Applied-or-later statuses — status, report link, and notes are lost with them
- Record req/job IDs in the notes of same-title requisitions; it is the only durable disambiguator
- Review protected pairs manually after every dedup run instead of forcing a merge
When it happens
Trigger: The same company+role appears twice — e.g. a re-scan re-evaluated a posting you already applied to, or a backfilled row sits beside an original now in Interview — and node dedup-tracker.mjs runs over them.
Common situations: Re-scans after applying; reposted requisitions; leveled variants sharing one title; periodic tracker-hygiene sessions.
Related errors
- ⚠️ Additions carry a URL but this tracker has no URL column
- Skipped #${r.num}: row no longer exists in the tracker
- payload must include at least one of: cv, articleDigest
- payload.cv requires { section, entry }
- cv.md not found — cannot add to a CV that does not exist
AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20).
Data as JSON: /api/errors/8813ae98ae640390.
Report an issue: GitHub.