fix(discovery): block countries in vague locations via job description
CI / Linting (Biome) (push) Failing after 41s
CI / Tests (push) Successful in 5m22s
CI / Type Check (adzuna-extractor) (push) Successful in 1m9s
CI / Type Check (gradcracker-extractor) (push) Successful in 1m14s
CI / Type Check (hiringcafe-extractor) (push) Successful in 1m11s
CI / Type Check (orchestrator) (push) Successful in 1m28s
CI / Type Check (startupjobs-extractor) (push) Successful in 1m13s
CI / Type Check (ukvisajobs-extractor) (push) Successful in 1m12s
CI / Documentation (push) Successful in 2m0s
CI / Linting (Biome) (push) Failing after 41s
CI / Tests (push) Successful in 5m22s
CI / Type Check (adzuna-extractor) (push) Successful in 1m9s
CI / Type Check (gradcracker-extractor) (push) Successful in 1m14s
CI / Type Check (hiringcafe-extractor) (push) Successful in 1m11s
CI / Type Check (orchestrator) (push) Successful in 1m28s
CI / Type Check (startupjobs-extractor) (push) Successful in 1m13s
CI / Type Check (ukvisajobs-extractor) (push) Successful in 1m12s
CI / Documentation (push) Successful in 2m0s
QAJobsBoard and similar feeds often store Worldwide/Remote while the real country is only in the description. Scan title and description when location is vague, and prefer concrete locations from QAJobsBoard postings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
inferCountryKeysFromJobText,
|
||||
isVagueJobLocation,
|
||||
jobMatchesBlockedCountries,
|
||||
normalizeBlockedCountryTokens,
|
||||
resolveBlockedCountriesFromStoredString,
|
||||
@@ -34,4 +36,41 @@ describe("blocked-countries", () => {
|
||||
expect(jobMatchesBlockedCountries("Remote", blocked)).toBe(false);
|
||||
expect(jobMatchesBlockedCountries(null, blocked)).toBe(false);
|
||||
});
|
||||
|
||||
it("treats worldwide and remote-only locations as vague", () => {
|
||||
expect(isVagueJobLocation("Worldwide")).toBe(true);
|
||||
expect(isVagueJobLocation("Remote")).toBe(true);
|
||||
expect(isVagueJobLocation("Toronto, Canada")).toBe(false);
|
||||
});
|
||||
|
||||
it("finds blocked countries in description when location is worldwide", () => {
|
||||
const blocked = resolveBlockedCountriesFromStoredString('["india"]');
|
||||
expect(
|
||||
jobMatchesBlockedCountries(
|
||||
{
|
||||
location: "Worldwide",
|
||||
jobDescription:
|
||||
"Job Location: Mumbai/Nagpur. We are hiring in India for this role.",
|
||||
},
|
||||
blocked,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
jobMatchesBlockedCountries(
|
||||
{
|
||||
location: "Worldwide",
|
||||
jobDescription: "Fully remote team across North America.",
|
||||
},
|
||||
blocked,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("infers country names embedded in free text", () => {
|
||||
expect(
|
||||
inferCountryKeysFromJobText(
|
||||
"Harrier is hiring in India. Job Location: Mumbai/Nagpur",
|
||||
),
|
||||
).toContain("india");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,27 @@ import { inferCountryKeysFromJobLocation } from "./search-cities.js";
|
||||
|
||||
const supportedCountryKeySet = new Set(SUPPORTED_COUNTRY_KEYS);
|
||||
|
||||
/** Location strings that do not pin a hiring country (check description too). */
|
||||
const VAGUE_LOCATION_VALUES = new Set([
|
||||
"worldwide",
|
||||
"global",
|
||||
"anywhere",
|
||||
"remote",
|
||||
"wfh",
|
||||
"work from home",
|
||||
"unknown",
|
||||
"multiple locations",
|
||||
"multiple countries",
|
||||
]);
|
||||
|
||||
const VAGUE_COUNTRY_KEYS = new Set(["worldwide", "global"]);
|
||||
|
||||
export interface JobBlockedCountrySignals {
|
||||
location?: string | null;
|
||||
jobDescription?: string | null;
|
||||
title?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse stored settings value for blocked countries.
|
||||
* Accepts JSON string array (normal) or legacy plain comma/newline-separated text.
|
||||
@@ -43,14 +64,65 @@ export function normalizeBlockedCountryTokens(tokens: string[]): string[] {
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
/** True when the job location mentions a blocked country (unknown location is kept). */
|
||||
export function isVagueJobLocation(location: string | null | undefined): boolean {
|
||||
if (!location?.trim()) return true;
|
||||
const normalized = location.trim().toLowerCase();
|
||||
if (VAGUE_LOCATION_VALUES.has(normalized)) return true;
|
||||
const keys = inferCountryKeysFromJobLocation(location);
|
||||
if (keys.length === 0) return true;
|
||||
return keys.every((key) => VAGUE_COUNTRY_KEYS.has(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer supported country keys mentioned anywhere in free text (title, description).
|
||||
*/
|
||||
export function inferCountryKeysFromJobText(
|
||||
text: string | null | undefined,
|
||||
): string[] {
|
||||
if (!text?.trim()) return [];
|
||||
const keys = new Set(inferCountryKeysFromJobLocation(text));
|
||||
const lower = text.toLowerCase();
|
||||
for (const countryKey of SUPPORTED_COUNTRY_KEYS) {
|
||||
if (VAGUE_COUNTRY_KEYS.has(countryKey)) continue;
|
||||
const pattern = countryKey.replace(/\s+/g, "\\s+");
|
||||
if (new RegExp(`\\b${pattern}\\b`, "i").test(lower)) {
|
||||
keys.add(countryKey);
|
||||
}
|
||||
}
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
function collectJobCountryKeys(signals: JobBlockedCountrySignals): string[] {
|
||||
const keys = new Set<string>();
|
||||
for (const key of inferCountryKeysFromJobLocation(signals.location)) {
|
||||
keys.add(key);
|
||||
}
|
||||
if (isVagueJobLocation(signals.location)) {
|
||||
const blob = [signals.title, signals.jobDescription]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
for (const key of inferCountryKeysFromJobText(blob)) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the job mentions a blocked country in location and/or (when location
|
||||
* is vague) title/description. Unknown location with no country in text is kept.
|
||||
*/
|
||||
export function jobMatchesBlockedCountries(
|
||||
location: string | null | undefined,
|
||||
locationOrSignals: string | null | undefined | JobBlockedCountrySignals,
|
||||
blockedCountryKeys: readonly string[],
|
||||
): boolean {
|
||||
if (blockedCountryKeys.length === 0) return false;
|
||||
const blocked = new Set(blockedCountryKeys);
|
||||
const jobCountries = inferCountryKeysFromJobLocation(location);
|
||||
const signals: JobBlockedCountrySignals =
|
||||
typeof locationOrSignals === "object" && locationOrSignals !== null
|
||||
? locationOrSignals
|
||||
: { location: locationOrSignals };
|
||||
const jobCountries = collectJobCountryKeys(signals);
|
||||
if (jobCountries.length === 0) return false;
|
||||
return jobCountries.some((key) => blocked.has(key));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user