Files
linkme/config.ts
T
rodado 3aa4365fb9 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.
2026-08-27 09:02:50 +02:00

201 lines
5.5 KiB
TypeScript

export type WorkType = "onsite" | "remote" | "hybrid";
export type JobType = "full-time" | "part-time" | "contract" | "temporary" | "internship";
export type TimePosted = "day" | "week" | "month";
export type Seniority = "internship" | "entry" | "associate" | "mid-senior" | "director" | "executive";
export interface SearchConfig {
label: string;
/** Use a raw LinkedIn URL instead of building one from filters. */
url?: string;
keywords?: string;
geoId?: string;
location?: string;
workType?: WorkType[];
jobType?: JobType[];
timePosted?: TimePosted;
seniority?: Seniority[];
}
export const KEYWORDS = [
"Software Engineer",
"Solutions Engineer",
"Full Stack Engineer",
"Backend Engineer",
"Data Engineer",
"AI Engineer",
"Applications Engineer",
"Platform Engineer",
"DevOps Engineer",
"Staff Engineer",
"Principal Engineer",
"Tech Lead",
"Developer",
"Ruby on Rails",
"Python",
"Node.js",
"TypeScript",
"Artificial Intelligence",
"Machine Learning",
"Data Science",
"AI Consultant",
"ML Engineer",
"Director of Engineering",
"Engineering Manager",
"Lead Developer",
"System Engineer",
"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[] = [
{
label: "Basel (city)",
keywords: KEYWORDS,
geoId: "103383283",
location: "Basel",
workType: ["onsite", "hybrid"],
timePosted: "month",
},
{
label: "Basel Metropolitan Area",
keywords: KEYWORDS,
geoId: "90009885",
location: "Basel Metropolitan Area",
workType: ["onsite", "hybrid"],
timePosted: "month",
},
{
label: "Basel-Country",
keywords: KEYWORDS,
geoId: "107117530",
location: "Basel-Country",
workType: ["onsite", "hybrid"],
timePosted: "month",
},
{
label: "Bern",
keywords: KEYWORDS,
location: "Bern, Switzerland",
workType: ["onsite", "hybrid"],
timePosted: "month",
},
{
label: "Zürich",
keywords: KEYWORDS,
geoId: "102436504",
location: "Zürich, Switzerland",
workType: ["onsite", "hybrid"],
timePosted: "month",
},
{
label: "Zürich Metropolitan Area",
keywords: KEYWORDS,
geoId: "107814425",
location: "Zürich Metropolitan Area",
workType: ["onsite", "hybrid"],
timePosted: "month",
},
{
label: "Zürich Area",
keywords: KEYWORDS,
geoId: "90009888",
location: "Zürich Area",
workType: ["onsite", "hybrid"],
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 })
);