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
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:
@@ -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");
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user