// Does a job match Rafael's profile? Title/company-level scoring, used to keep // the Telegram digest to real leads instead of everything the scraper finds. // // Hard rejects encode standing decisions: // - Microsoft stack (.NET/C#/Azure-first/Windows/SharePoint/Power Platform) // - Jobgether (aggregator, not an employer) // - ALLPS (postings require German despite passing the English filter) // - life-science domains (also filtered at fetch time by BIO_DOMAIN_RE) // - non-software functions (sales, HR, finance, marketing, support) export const MS_STACK_RE = /\.net\b|\bc#|\basp\.net\b|sharepoint|dynamics\s*365|power\s*(?:platform|bi|apps|automate)|\bsccm\b|\bmecm\b|intune|autopilot|windows[\s\/]|\bm365\b|microsoft\s*365|\bazure\b/i; export const WRONG_STACK_RE = /\bjava\b(?!script)|\bspring\s*boot\b|\bc\+\+\b|\bembedded\b|\bcobol\b|\babap\b|\bsap\b|\bdelphi\b/i; export const BLOCKED_COMPANY_RE = /\bjobgether\b|\ballps\b/i; export const BIO_RE = /\bbio(?:informatic|statistic|logic)\w*|\bpatholog\w*|\bgenomic\w*|\bproteomic\w*|\bGMP\b|\bMSAT\b|\bdrug\s+(?:discovery|design)\b|\bchemist(?:ry)?\b|\bclinical\s+(?:trial|research)\b|\bbiomarker\b|\bassay\b/i; export const NON_SOFTWARE_RE = /\b(?:sales\s+(?:director|manager|representative)|account\s+(?:executive|manager)|business\s+development|recruit(?:er|ment)|talent\s+acquisition|marketing|copywriter|\bHR\b|human\s+resources|accountant|controller|auditor|receptionist|customer\s+(?:support|service)\s+agent|help\s?desk|service\s+desk|1st\s+level)\b/i; /** Core stack and domains from cv/polo.md — each hit adds to the score. */ const SIGNALS: [RegExp, number, string][] = [ [/\bpython\b/i, 3, "Python"], [/\btypescript\b|\bnode\.?js\b|\bjavascript\b/i, 3, "TS/Node"], [/\bruby\b|\brails\b/i, 4, "Ruby/Rails"], [/\breact\b/i, 2, "React"], [/\bplatform\s+engineer|\binfrastructure\s+engineer|\bsre\b|site\s+reliability/i, 4, "platform/SRE"], [/\bdevops\b|\bdevsecops\b|kubernetes|\bk8s\b|terraform|\bci\/cd\b/i, 4, "DevOps"], [/\bdata\s+(?:engineer|platform|pipeline)/i, 4, "data eng"], [/\bai\b|\bml\b|machine\s+learning|\bllm\b|\bgenai\b|\brag\b|mlops/i, 3, "AI/ML"], [/\bcloud\b|\baws\b|solutions?\s+architect/i, 3, "cloud/AWS"], [/\bsecurity\b/i, 2, "security"], [/\bbackend\b|\bfull.?stack\b|\bapi\b|\bintegration\b/i, 2, "backend/API"], [/forward\s+deployed|\bfield\s+engineer|solutions?\s+engineer|customer\s+engineer/i, 4, "forward-deployed"], [/\bsoftware\s+engineer\b|\bdeveloper\b/i, 2, "software"], // Tech Lead is an explicit target role (see CLAUDE.md), but a bare title like // "Technical Lead" or "Tech Lead" alone only ever hit the generic seniority // bucket below (2pts, same as "senior"/"staff") — below minScore(4), so it // silently never reached the digest. Give it its own weight so it clears // the bar on its own, same as the other role-specific signals above. [/\btech(?:nical)?\s*lead\b/i, 3, "tech lead"], // seniority — he's Staff-level; junior titles are already dropped upstream [/\b(?:senior|staff|principal|lead|head\s+of|architect)\b/i, 2, "senior"], ]; export interface Match { ok: boolean; score: number; reasons: string[]; rejectedBy?: string; } export function matchesProfile(title: string, company = "", minScore = 4): Match { const t = `${title}`; if (BLOCKED_COMPANY_RE.test(company) || BLOCKED_COMPANY_RE.test(t)) return { ok: false, score: 0, reasons: [], rejectedBy: "blocked company" }; if (MS_STACK_RE.test(t)) return { ok: false, score: 0, reasons: [], rejectedBy: "Microsoft stack" }; if (WRONG_STACK_RE.test(t)) return { ok: false, score: 0, reasons: [], rejectedBy: "wrong stack (Java/C++/SAP)" }; if (BIO_RE.test(t)) return { ok: false, score: 0, reasons: [], rejectedBy: "life-science domain" }; if (NON_SOFTWARE_RE.test(t)) return { ok: false, score: 0, reasons: [], rejectedBy: "non-software role" }; let score = 0; const reasons: string[] = []; for (const [re, pts, label] of SIGNALS) { if (re.test(t)) { score += pts; reasons.push(label); } } return { ok: score >= minScore, score, reasons, rejectedBy: score >= minScore ? undefined : "weak match" }; }