feat: dedupe the Ignored (German/French) digest count across runs and sources

Dropped jobs never reach the CSV, so nothing remembered them -- the same
posting got re-flagged and re-counted every single bun daily run (5x/day),
and separately every time it resurfaced under a different link/provider
(e.g. Consult & Pepper's recurring Senior Fullstack Software Engineer
reposted on job-room.ch every few days and again on LinkedIn). A persisted
results/.lang-dropped-seen.json now keys on normalized title+company so
each underlying job only counts once, ever.
This commit is contained in:
2026-09-14 12:12:49 +02:00
parent 0249800278
commit d401dd5f9a
+44 -2
View File
@@ -62,6 +62,36 @@ const date = new Date().toISOString().slice(0, 10);
const newRows: string[] = [];
let totalLangDropped = 0; // jobs dropped for German/French content — never reach the CSV, so daily.ts can't see them on its own
// Persisted across runs so the Telegram digest's "Ignored (German/French)"
// count reflects genuinely new drops, not the same posting getting re-flagged
// every run (dropped jobs never reach the CSV, so nothing else remembers
// them) or the same underlying job counted again each time it resurfaces
// under a different link/provider (e.g. Consult & Pepper's "Senior Fullstack
// Software Engineer" reposted on job-room.ch every few days, and separately
// on LinkedIn) — keyed on normalized title+company rather than the link.
const LANG_SEEN = "./results/.lang-dropped-seen.json";
const langSeen = new Set<string>(
await Bun.file(LANG_SEEN)
.json()
.catch(() => [])
);
function langKey(title: string, company: string): string {
const t = title
.replace(/[([][^)\]]*[)\]]/g, "") // drop "(all genders)", "(m/w/d)", "(a)", "(80 – 100%)" noise
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
return `${t}|${company.trim().toLowerCase()}`;
}
// Only counts (and remembers) a drop the first time this title+company is
// seen; still runs for every dropped job so `langSeen` stays complete.
function countLangDrop(title: string, company: string): void {
const key = langKey(title, company);
if (langSeen.has(key)) return;
langSeen.add(key);
totalLangDropped++;
}
function csvCell(val: string): string {
return `"${val.replace(/"/g, '""')}"`;
}
@@ -115,10 +145,17 @@ async function processLinkedInSearch(config: (typeof searches)[number]): Promise
// One fetch, two checks: written in German/French, or written in English
// but demanding German above B1.
const viable = await isViableContent(job.id);
if (viable.retry) {
// Couldn't read the description at all — leave it out of the CSV so
// next run's fresh fetch judges it properly, instead of defaulting to
// "keep" and risking a German-requiring posting slipping through.
console.log(viable.reason);
continue;
}
if (!viable.ok) {
console.log(`dropped (${viable.reason})`);
langDropped++;
totalLangDropped++;
countLangDrop(job.title, job.company);
continue;
}
console.log("ok");
@@ -167,10 +204,14 @@ async function processExtraSource({ label, fetch: fetchFn }: (typeof extraSource
if (!job.contentChecked) {
process.stdout.write(` [${label}] checking content: ${job.title} … `);
const viable = await isViableUrl(job.link);
if (viable.retry) {
console.log(viable.reason);
continue;
}
if (!viable.ok) {
console.log(`dropped (${viable.reason})`);
langDropped++;
totalLangDropped++;
countLangDrop(job.title, job.company);
continue;
}
console.log("ok");
@@ -198,6 +239,7 @@ await Promise.all([
// without listing rejects that were never even parsed into a Row.
const LANG_DROPPED = "./results/.lang-dropped.json";
await Bun.write(LANG_DROPPED, JSON.stringify({ date, count: totalLangDropped }));
await Bun.write(LANG_SEEN, JSON.stringify([...langSeen]));
if (newRows.length === 0) {
console.log("\nNo new jobs.");