feat: rebuild evidence-first application workflow

This commit is contained in:
2026-07-27 17:56:15 +02:00
parent c24892f381
commit fe5f24704f
57 changed files with 3815 additions and 3555 deletions
+40 -5
View File
@@ -255,6 +255,11 @@ COMPANIES = [
"scroll_count": 5,
"use_inner_text_as_blob": True,
"cookie_accept": ["button:has-text('Accept all')", "button:has-text('Reject all')"],
# Results are paged 20 at a time and "page" is 1-indexed — without this only the
# first 20 of ~48 Zürich roles were ever scraped.
"page_param": "page",
"page_param_start": 2,
"max_pages": 6,
}),
("apple", "Apple", "playwright", {
"url": "https://jobs.apple.com/en-us/search?location=switzerland-CHE",
@@ -534,7 +539,8 @@ def fetch_pcsx(args):
while True:
url = f"{base}?domain={domain}&query=&location={urllib.parse.quote(location)}&start={start}&num=50"
data = http_get_json(url, headers={"Referer": f"https://apply.careers.microsoft.com/careers?location={urllib.parse.quote(location)}"})
positions = (data.get("data") or {}).get("positions", []) or []
payload = data.get("data") or {}
positions = payload.get("positions", []) or []
for p in positions:
locs = p.get("locations") or []
jobs.append({
@@ -545,9 +551,16 @@ def fetch_pcsx(args):
"posted": p.get("postedTs", ""),
"description": (p.get("description") or "")[:2000],
})
if not positions or len(positions) < 50:
# The API serves ~10 rows per call regardless of `num`, so a short page is NOT
# end-of-results — page until a call comes back empty or `count` is reached.
# (The old `len(positions) < 50` break capped Microsoft at its first 10 CH roles
# and hid the entire Zürich Principal-FDE cluster sitting on page 2.)
if not positions:
break
start += len(positions)
total = payload.get("count")
if isinstance(total, int) and start >= total:
break
if start >= 500:
break
return jobs
@@ -1016,6 +1029,23 @@ def _absolutize(href, prefix):
return prefix.rstrip("/") + "/" + cleaned
def _strip_query_param(url, param):
"""Drop a single query parameter from a URL, preserving the rest.
Job-card hrefs inherit the listing page's query string, so with query-param
pagination the same posting would get a different URL (and therefore a different
id) depending on which page it happened to land on — making every reshuffle look
like a brand-new job and orphaning its decision-log entry.
"""
if not url or f"{param}=" not in url:
return url
head, _, query = url.partition("?")
if not query:
return url
kept = [kv for kv in query.split("&") if kv and not kv.startswith(f"{param}=")]
return f"{head}?{'&'.join(kept)}" if kept else head
def _extract_location_from_blob(full, default=""):
"""Pull a short location line out of a job-card text blob.
@@ -1127,6 +1157,8 @@ def fetch_playwright(args):
link_el = card if not args.get("link_sel") else card.locator(args["link_sel"]).first
href = (link_el.get_attribute(args.get("link_attr", "href")) or "") if link_el.count() else ""
href = _absolutize(href, args.get("url_prefix", ""))
if args.get("page_param"):
href = _strip_query_param(href, args["page_param"])
if not title:
continue
@@ -1172,15 +1204,18 @@ def fetch_playwright(args):
page.wait_for_timeout(500)
except Exception:
pass
# Optional query-param pagination (e.g. Drupal "?page=N", 0-indexed). The base URL is
# page 0 (already loaded); fetch successive pages until one adds no new cards.
# Optional query-param pagination (e.g. Drupal "?page=N"). The base URL is the first
# page (already loaded); fetch successive pages until one adds no new cards.
# `page_param_start` is the value the SECOND page takes: 1 for 0-indexed boards
# (Drupal), 2 for 1-indexed ones (Google, where "?page=1" is just page one again).
page_param = args.get("page_param")
if page_param:
base = args["url"]
joiner = "&" if "?" in base else "?"
second = args.get("page_param_start", 1)
for p in range(args.get("max_pages", 8)):
if p > 0:
page.goto(f"{base}{joiner}{page_param}={p}", timeout=45000,
page.goto(f"{base}{joiner}{page_param}={second + p - 1}", timeout=45000,
wait_until="domcontentloaded")
added = scrape_current()
if p > 0 and added == 0:
+21 -14
View File
@@ -135,8 +135,8 @@
"https://www.google.com/about/careers/applications/jobs/results/87066954308690630-senior-data-engineer?location=Switzerland": {
"company": "Google",
"title": "Senior Data Engineer (Merchant Data Science)",
"decision": "applied",
"note": "Tier-1 DE fit; self-serve data products = team charter; Zurich team. | SENT 2026-06-15 (resume+CL finalized, 85.5/100). Clarify L4/L5 + comp 180k+ at recruiter stage. ADVANCED 2026-06-17: invited to 30-min Google Hiring Assessment.",
"decision": "rejected",
"note": "Tier-1 DE fit; self-serve data products matched team charter. SENT 2026-06-15 (85.5/100). Invited to Google Hiring Assessment 2026-06-17; PASSED 2026-06-20. CLOSED 2026-07-24: Google Careers status changed to Not proceeding; no interview. Assessment pass retained separately. Official reapply rule: 90 days for the same job only; other Google roles remain eligible subject to 3 applications per rolling 30 days.",
"date": "2026-06-15"
},
"https://jobs.ashbyhq.com/kraken.com/c331de1b-b75a-48f5-9d19-0e56ccb935ab": {
@@ -723,9 +723,9 @@
"https://databricks.com/company/careers/open-positions/job?gh_jid=8396801002": {
"company": "Databricks",
"title": "Senior Forward Deployed Engineer, Full Stack",
"decision": "shortlist",
"note": "Claude judgment 2026-07-06: forward-deployed lane is an explicit interest area; Databricks is a target data-infra company",
"date": "2026-07-06"
"decision": "skip",
"note": "Senior FDE, Full Stack is hybrid London-only; not CH-eligible. Stronger title match than OpenAI, but the required work location is a hard stop.",
"date": "2026-07-22"
},
"https://databricks.com/company/careers/open-positions/job?gh_jid=8439078002": {
"company": "Databricks",
@@ -737,16 +737,16 @@
"https://databricks.com/company/careers/open-positions/job?gh_jid=8506063002": {
"company": "Databricks",
"title": "Senior Specialist Solutions Architect - AI & ML Engineer",
"decision": "shortlist",
"note": "Claude judgment 2026-07-06: AI-platform positioning fit; Nordics-remote is an explicitly stated remote lane",
"date": "2026-07-06"
"decision": "skip",
"note": "Senior Specialist Solutions Architect AI/ML Engineer is located Finland, Remote-Denmark, or Stockholm only, and requires 7+ years hands-on ML plus enterprise GenAI/RAG/agents and pre-sales depth. Not CH-eligible and beyond verified LLM scope.",
"date": "2026-07-22"
},
"https://databricks.com/company/careers/open-positions/job?gh_jid=8303017002": {
"company": "Databricks",
"title": "Senior Staff Software Engineer - Delta",
"decision": "maybe",
"note": "Claude judgment 2026-07-06 (updated): downgraded from shortlist - same Senior Staff SWE title/level as the Unity Catalog req at the same company, which was confirmed to require 15+ years experience. Likely same bar; verify JD before pursuing.",
"date": "2026-07-06"
"decision": "skip",
"note": "Senior Staff SWE Delta is Zurich-based but requires 15+ years large-scale distributed-systems experience, low-level performance/debugging, and Delta Lake core/open-source work; not the data-platform integration profile.",
"date": "2026-07-22"
},
"https://databricks.com/company/careers/open-positions/job?gh_jid=8422483002": {
"company": "Databricks",
@@ -1311,9 +1311,9 @@
"https://novartis.wd3.myworkdayjobs.com/job/Basel-City/Forward-Deployed-Engineer--FDE--Data42_REQ-10079038-1": {
"company": "Novartis",
"title": "Forward Deployed Engineer (FDE) Data42",
"decision": "shortlist",
"note": "Claude judgment 2026-07-06: FDE + internal data platform (Data42) is a direct hit on your forward-deployed lane and data-eng stack, hands-on IC role not leadership",
"date": "2026-07-06"
"decision": "skip",
"note": "Essential requirements are an unrelated biomedical degree, 8+ years in biomedical research/bioengineering/bioinformatics, and hands-on Palantir Foundry delivery. Despite the data-engineering/FDE title, this is not an honest fit.",
"date": "2026-07-22"
},
"https://novartis.wd3.myworkdayjobs.com/job/Basel-City/Global-Head-of-Technology---Architecture-in-Biomedical-Research--BR-_REQ-10079333-1": {
"company": "Novartis",
@@ -1664,5 +1664,12 @@
"decision": "skip",
"note": "Cloud AI Architect — above experience level (user judgment 2026-07-18).",
"date": "2026-07-18"
},
"https://jobs.ashbyhq.com/openai/067788c8-505b-4296-bc05-d5699cb87a1f": {
"company": "",
"title": "",
"decision": "skip",
"note": "OpenAI FDE Zurich: decline to pursue. Structural gaps in customer-facing external deployments, current full-stack frontend delivery, production LLM ownership/evaluation; 50% travel also a deliberate lifestyle tradeoff. Keep core search on Staff Data Engineering, data/AI platform, and more evidence-aligned FDE roles.",
"date": "2026-07-22"
}
}