feat: add role-family sweeps and domain/language filters

Broaden the search beyond engineer/developer titles and stop three
classes of bad result from ever reaching the CSV.

config.ts
- PLATFORM_KEYWORDS (architect, SRE, infra, security, MLOps),
  RESEARCH_KEYWORDS (research/scientific software, HPC, data platform)
  and FIELD_KEYWORDS (forward-deployed, customer/sales, founding).
  Separate OR-strings, not folded into KEYWORDS: LinkedIn ranks a
  focused query far better than one 50-term disjunction.
- sweep() generates one search per area per family. 7 searches -> 17.

fetch.ts
- BIO_DOMAIN_RE drops life-science roles (bioinformatics, biostats,
  GMP/MSAT, genomics, pathology, drug discovery). Filters on the role,
  never the employer: software jobs at Roche/Novartis/sciCORE stay.
- requiresGerman() drops English postings demanding German above B1.
  Two rules, both from real false results: the nearest marker wins
  ("Infrastructure skills are a plus" sat 95 chars before "Good
  knowledge of German"), and an intervening language name breaks the
  binding ("Fluent in English; German and/or French advantageous").
- GERMAN_TITLE_RE catches "(German-speaking)" without a page fetch.
- isViableContent/isViableUrl run the DE/FR and German-requirement
  checks on a single fetch; isEnglishContent/isEnglishUrl kept for
  recheck.ts and temp scripts.
- SOFTWARE_TITLE_RE widened so the new families survive it.

index.ts
- both content-check paths use the combined check and report which
  filter dropped a job.
This commit is contained in:
2026-08-27 09:02:50 +02:00
parent fb67c38edd
commit 3aa4365fb9
4 changed files with 327 additions and 24 deletions
+94
View File
@@ -46,6 +46,55 @@ export const KEYWORDS = [
"Solution Engineer",
].join(" OR ");
// --- Role families beyond the core engineer/developer titles ---
// Kept as separate OR-strings rather than folded into KEYWORDS: LinkedIn ranks
// a focused query far better than one 50-term disjunction, and each family
// needs a different set of areas (research is Basel-heavy, field/remote is not).
/** Platform, reliability, cloud and security — the DevOps/Swisscom lane. */
export const PLATFORM_KEYWORDS = [
"Solutions Architect",
"Cloud Architect",
"Site Reliability Engineer",
"Infrastructure Engineer",
"Cloud Engineer",
"Security Engineer",
"DevSecOps",
"Application Security Engineer",
"Cloud Security Engineer",
"MLOps Engineer",
"AI Platform Engineer",
"ML Platform Engineer",
"AI Infrastructure",
].join(" OR ");
/** Research software engineering — Basel pharma/university cluster.
* Deliberately no bioinformatics/computational-biology terms: those are
* domain-science roles, filtered out by BIO_DOMAIN_RE in fetch.ts. */
export const RESEARCH_KEYWORDS = [
"Research Software Engineer",
"Scientific Software Engineer",
"Scientific Programmer",
"Scientific Computing",
"Research Engineer",
"HPC Engineer",
"Data Platform Engineer",
].join(" OR ");
/** Customer-facing and early-stage product roles — the consulting lane. */
export const FIELD_KEYWORDS = [
"Forward Deployed Engineer",
"Field Engineer",
"Customer Engineer",
"Sales Engineer",
"Pre-Sales Engineer",
"Integration Engineer",
"API Engineer",
"Analytics Engineer",
"Founding Engineer",
"Product Engineer",
].join(" OR ");
// --- Define your searches here ---
export const searches: SearchConfig[] = [
{
@@ -104,3 +153,48 @@ export const searches: SearchConfig[] = [
timePosted: "month",
},
];
// --- Role-family sweeps ---------------------------------------------------
// Same filters as the core searches, run across a narrower set of areas.
// Research is Basel/Zürich only (that's where the institutes are); platform
// and field roles also sweep Switzerland-wide remote.
const CORE_AREAS: { label: string; geoId?: string; location: string }[] = [
{ label: "Basel Metropolitan Area", geoId: "90009885", location: "Basel Metropolitan Area" },
{ label: "Zürich Metropolitan Area", geoId: "107814425", location: "Zürich Metropolitan Area" },
{ label: "Bern", location: "Bern, Switzerland" },
];
const RESEARCH_AREAS = CORE_AREAS.filter((a) => a.label !== "Bern");
function sweep(
family: string,
keywords: string,
areas: typeof CORE_AREAS,
opts: { remote?: boolean } = {}
): SearchConfig[] {
const out: SearchConfig[] = areas.map((a) => ({
label: `${family} — ${a.label}`,
keywords,
geoId: a.geoId,
location: a.location,
workType: ["onsite", "hybrid"] as WorkType[],
timePosted: "month" as TimePosted,
}));
if (opts.remote) {
out.push({
label: `${family} — Switzerland Remote`,
keywords,
location: "Switzerland",
workType: ["remote"],
timePosted: "month",
});
}
return out;
}
searches.push(
...sweep("Platform", PLATFORM_KEYWORDS, CORE_AREAS, { remote: true }),
...sweep("Research", RESEARCH_KEYWORDS, RESEARCH_AREAS),
...sweep("Field", FIELD_KEYWORDS, CORE_AREAS, { remote: true })
);