feat(jobs): suppress duplicate postings after skip or apply
CI / Linting (Biome) (push) Failing after 41s
CI / Tests (push) Successful in 5m25s
CI / Type Check (adzuna-extractor) (push) Successful in 1m8s
CI / Type Check (gradcracker-extractor) (push) Successful in 1m12s
CI / Type Check (hiringcafe-extractor) (push) Successful in 1m9s
CI / Type Check (orchestrator) (push) Successful in 1m25s
CI / Type Check (startupjobs-extractor) (push) Successful in 1m9s
CI / Type Check (ukvisajobs-extractor) (push) Successful in 1m9s
CI / Documentation (push) Failing after 1m56s

Dedup by employer+title and description at import; cascade skip on dismiss; hide repeats in the job list. Document product scope and duplicate detection in docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-16 18:50:11 -04:00
co-authored by Cursor
parent 5401f384c1
commit 17c4d4490a
15 changed files with 471 additions and 19 deletions
+25
View File
@@ -1,6 +1,8 @@
import { describe, expect, it } from "vitest";
import {
buildJobContentFingerprint,
buildJobDescriptionFingerprint,
collectJobDedupKeys,
normalizeEmployerForFingerprint,
normalizeTitleForFingerprint,
} from "./job-fingerprint";
@@ -64,6 +66,29 @@ describe("buildJobContentFingerprint", () => {
expect(a).not.toBe(b);
});
it("matches reposts with the same employer and description body", () => {
const description =
"We are hiring an Automation Test Engineer to build scalable test frameworks. ".repeat(
4,
);
const a = buildJobDescriptionFingerprint({
employer: "Joveo",
jobDescription: description,
});
const b = buildJobDescriptionFingerprint({
employer: "Joveo",
jobDescription: description,
});
expect(a).toBe(b);
expect(
collectJobDedupKeys({
employer: "Joveo",
title: "SDET",
jobDescription: description,
}),
).toContain(a);
});
describe("normalizers", () => {
it("normalizeEmployerForFingerprint strips legal suffixes", () => {
expect(normalizeEmployerForFingerprint("Acme Corporation")).toBe("acme");
+46
View File
@@ -75,3 +75,49 @@ export function buildJobContentFingerprint(args: {
if (!employer || !title) return null;
return `${employer}::${title}`;
}
const DESCRIPTION_MIN_CHARS = 80;
export function normalizeDescriptionForFingerprint(
jobDescription: string | null | undefined,
): string {
if (!jobDescription?.trim()) return "";
let value = stripDiacritics(jobDescription.toLowerCase());
value = value.replace(/<[^>]+>/g, " ");
value = value.replace(PUNCTUATION_RE, " ");
value = value.replace(WHITESPACE_RE, " ").trim();
if (value.length < DESCRIPTION_MIN_CHARS) return "";
return value.slice(0, 400);
}
/**
* Same employer + materially similar description (cross-posted copy).
*/
export function buildJobDescriptionFingerprint(args: {
employer: string | null | undefined;
jobDescription: string | null | undefined;
}): string | null {
const employer = normalizeEmployerForFingerprint(args.employer);
const description = normalizeDescriptionForFingerprint(args.jobDescription);
if (!employer || !description) return null;
return `${employer}::desc::${description}`;
}
export function collectJobDedupKeys(args: {
employer: string | null | undefined;
title: string | null | undefined;
jobDescription?: string | null | undefined;
}): string[] {
const keys = new Set<string>();
const content = buildJobContentFingerprint({
employer: args.employer,
title: args.title,
});
if (content) keys.add(content);
const description = buildJobDescriptionFingerprint({
employer: args.employer,
jobDescription: args.jobDescription,
});
if (description) keys.add(description);
return [...keys];
}