fix: catch job-room.ch's structured German-fluency requirement

languageSkills[] (e.g. {languageIsoCode: "de", spokenLevel: "PROFICIENT"})
sits outside jobDescriptions[].description, so requiresGerman()/
isViableDescription() never saw it — the UI renders this as
"Deutsch (Fliessend)", which is exactly what kept reaching Telegram.
Measured: 69 of 1843 ads required PROFICIENT German, prose check caught
only 3.
This commit is contained in:
2026-09-04 11:18:15 +02:00
parent fccecb2254
commit b2e5c7865a
2 changed files with 24 additions and 0 deletions
+23
View File
@@ -236,9 +236,28 @@ interface JobRoomAd {
jobDescriptions?: Array<{ title: string; description?: string }>;
company?: { name?: string; city?: string };
location?: { city?: string; cantonCode?: string };
languageSkills?: JobRoomLanguageSkill[];
};
}
interface JobRoomLanguageSkill {
languageIsoCode?: string;
spokenLevel?: string;
writtenLevel?: string;
}
// job-room's language requirement is a structured field, not prose — it never
// touches jobDescriptions[].description, so isViableDescription()/requiresGerman()
// can't see it. The UI renders it as e.g. "Deutsch (Fliessend)"; PROFICIENT is
// that "Fliessend" (fluent/C1+) tier, well above Rafael's German B1. BASIC and
// INTERMEDIATE stay in scope.
function jobRoomRequiresGerman(skills?: JobRoomLanguageSkill[]): boolean {
if (!skills) return false;
return skills.some(
(s) => s.languageIsoCode === "de" && (s.spokenLevel === "PROFICIENT" || s.writtenLevel === "PROFICIENT")
);
}
// jobs.ch links arrive with a tracking query string that would break dedup
// across runs if it ever changes; the path alone identifies the posting.
function stripTracking(url: string): string {
@@ -299,6 +318,10 @@ export async function fetchJobRoom(): Promise<Job[]> {
if (MS_STACK_RE.test(title)) continue;
if (PHARMA_ROLE_RE.test(title)) continue;
// Structured language requirement — separate from the free-text description
// entirely, so it must be checked even when the description reads clean.
if (jobRoomRequiresGerman(content?.languageSkills)) { langDropped++; continue; }
// A handful of ads ship without a description. isViableDescription passes
// those through by design, so leave contentChecked unset and let index.ts
// fall back to fetching the page.