diff --git a/.gitignore b/.gitignore index ce92899..5235d43 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,10 @@ job_scout/state/* # application_cohort.json — cohort slot tracker (core/adjacent/stretch mix) !job_scout/state/decisions.json !job_scout/state/application_cohort.json +!job_scout/state/applications.json +!job_scout/state/history/ +job_scout/state/history/* +!job_scout/state/history/application_status_before_2026-09-11.json # One-off job-board data pulls (debug artifacts) *_jd.json diff --git a/FIX_PROGRESS.md b/FIX_PROGRESS.md index 76e168b..64d3448 100644 --- a/FIX_PROGRESS.md +++ b/FIX_PROGRESS.md @@ -6,12 +6,28 @@ Do not rewrite submitted application archives or invent missing evidence. ## Resume point -**All findings F01–F19 are complete.** +**Follow-up review corrections implemented and verified.** The previous DONE labels below +describe the first implementation. This checklist records the subsequent corrections: -Remaining known gaps, none of them a finding: the visual page-by-page sampling was not repeated -(the F18 work measured page-one geometry from the compiled PDF instead), and the 13 older -packages listed below still carry genuine findings under today's rules - deliberately, because -F17 classifies archives as evidence rather than rewriting them. +- [x] Make authoritative application registry and migration history visible to Git. +- [x] Fix scanner context/markup and basic schema validation. +- [x] Fix country precedence, related decisions, new-only state and serialize scout writers. +- [x] Fix chronological application updates. +- [x] Make manifest content-sensitive and independent of absent local PDFs. +- [x] Fix heading measurement and actual education template. +- [x] Add DK/NO guidance; remove residual scope/Capgemini/GPA contradictions. +- [x] Run regression tests, isolated checkout checks and template visual QA. + +Verification: 49/49 tests pass both locally and in an isolated copy of all Git-included +working-tree files, with no ignored PDFs or runtime state. Canonical, registry-view and +manifest checks pass. The professional template builds to two pages and was visually checked. +Historical submitted documents were not changed. No new dependencies or framework introduced. +Manifest PDF hashes detect changed files; they intentionally do not claim a verified build association. +The scanner remains a mechanical check; semantic claim review is still required. + +Historical first-pass limitation: only geometry was measured then; the corrected template has +now also been visually checked. The older packages listed below still carry historical findings; +they are preserved as submission records, not reused as career-evidence sources. Read this file and inspect `git log`/`git diff` before resuming; do not repeat completed work. Checkpoints: `afdef7e` (F01+F02) · `fc5f3e4` (F03–F06) · `ce5b7f5` (F07) · `3519b2f` (F08–F10) · `afd8174` (F11–F13) · `ce8619b` (F14) · `c7e8d4e` (F15–F16) · `eecdbda` (F17) · plus F18–F19. diff --git a/README.md b/README.md index cf7147b..1611fdd 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,17 @@ from its numeric rubric needs `--fit-override-reason`; it is never silently rela Character counts are available only as readability diagnostics. There are no fixed character bands, equal-length bullet rules, or page-fill quotas. +Run the offline regression suite with `python -m unittest discover -s tests -v`. +The application registry and its import history are versioned; seen-state and generated PDFs are not. +`package_manifest.py --check` checks source/class content and any recorded PDFs that are present. +Missing local PDFs are permitted. After inspecting changes, run `package_manifest.py` to refresh +source hashes, or add `--record-pdfs` to explicitly record current PDF hashes. These snapshots +detect edits; they do not certify that an existing PDF was built from a particular source. + +Scout writers are serialized with `job_scout/state/scout.lock`. After a crash, remove a stale +lock only after verifying no scout process is still running. Successful reports, including +`--new-only` and `--only=...`, mark their delivered results seen without changing other companies. + ## Output and prerequisites The system writes `.tex` source only. Compile locally with a LaTeX distribution such as MiKTeX, TeX Live, or MacTeX. Generated resumes should also be checked through PDF rendering and text extraction before submission. diff --git a/job_scout/scout.py b/job_scout/scout.py index d90ff8e..998a0e5 100644 --- a/job_scout/scout.py +++ b/job_scout/scout.py @@ -39,6 +39,7 @@ import re import ssl import sys import tempfile +from contextlib import contextmanager import time from concurrent.futures import ThreadPoolExecutor, as_completed from functools import lru_cache @@ -65,7 +66,7 @@ CH_LOCATION_KEYWORDS = [ "st. gallen", "sankt gallen", "aarau", "baden", "emmen", "duebendorf", "dübendorf", "frauenfeld", "biel", "bienne", "solothurn", "chur", "sion", "neuchâtel", "neuchatel", "fribourg", "freiburg", "lugano", "bellinzona", "kloten", "wallisellen", "opfikon", - "lupfig", "villigen", "rotkreuz", "allschwil", "kaiseraugst", "payerne", "kastellaun", + "lupfig", "villigen", "rotkreuz", "allschwil", "kaiseraugst", "payerne", ] DK_LOCATION_KEYWORDS = [ @@ -745,7 +746,9 @@ def fetch_greenhouse(args): "title": j.get("title", ""), "location": loc_blob, "url": j.get("absolute_url"), - "posted": j.get("updated_at", ""), + "posted": j.get("published_at", ""), + "updated_at": j.get("updated_at", ""), + "date_provenance": "Greenhouse updated_at is an update, not a publication date", "description": desc[:2500], }) return jobs @@ -798,7 +801,8 @@ def fetch_smartrecruiters(args): content = data.get("content", []) or [] for p in content: loc = p.get("location") or {} - parts = [loc.get("fullLocation") or loc.get("city") or ""] + country = (loc.get("country") or "").upper() + parts = [loc.get("fullLocation") or loc.get("city") or "", country] if loc.get("remote"): parts.append("Remote") if loc.get("hybrid"): @@ -810,6 +814,7 @@ def fetch_smartrecruiters(args): "id": str(p.get("id")), "title": p.get("name", ""), "location": loc_str, + "country": country, "url": f"https://jobs.smartrecruiters.com/{company}/{p.get('id')}", "posted": p.get("releasedDate", ""), "description": " ".join(filter(None, [dept, func])), @@ -1781,7 +1786,17 @@ def _place_match(low, keywords): return False -def location_eligibility(loc_text, policy="default"): +COUNTRY_NAMES = { + "CH": ("switzerland", "schweiz", "suisse", "svizzera", "ch"), + "DK": ("denmark", "danmark", "dk"), "NO": ("norway", "norge", "no"), + "DE": ("germany", "deutschland", "de"), "AT": ("austria", "österreich", "at"), + "FR": ("france", "frankreich", "fr"), "IT": ("italy", "italien", "it"), + "GB": ("united kingdom", "great britain", "uk", "gb"), + "US": ("united states", "usa", "us"), "NL": ("netherlands", "niederlande", "nl"), +} + + +def location_eligibility(loc_text, policy="default", country=None): """Decide whether a posting's location is in-scope. Returns (eligible, in_ch, is_remote, is_relocation). @@ -1801,9 +1816,23 @@ def location_eligibility(loc_text, policy="default"): if not loc_text: return False, False, False, False low = loc_text.lower() - in_ch = _place_match(low, CH_LOCATION_KEYWORDS) - in_dk = _place_match(low, DK_LOCATION_KEYWORDS) - in_no = _place_match(low, NO_LOCATION_KEYWORDS) + # An adapter's country code is stronger than an ambiguous town name. In free text, + # recognize country names; short codes count only as comma/slash-separated fields. + countries = {country.upper()} if country else { + code for code, names in COUNTRY_NAMES.items() if any( + _place_match(low, [name]) if len(name) > 2 else + bool(re.search(r"(?:^|[,/])\s*" + name + r"\s*(?:$|[,/])", low)) for name in names)} + explicit_country = bool(countries) + excluded = set() + for code in countries: + for name in COUNTRY_NAMES.get(code, (code.lower(),)): + token = r"\b" + re.escape(name) + r"\b" + if re.search(r"(?:excluding|except|not in|außer|ausser|nicht in)\s+" + token, low) or re.search(token + r"\s+(?:excluded|not eligible|ausgeschlossen)", low): + excluded.add(code) + countries -= excluded + in_ch = "CH" in countries if explicit_country else _place_match(low, CH_LOCATION_KEYWORDS) + in_dk = "DK" in countries if explicit_country else _place_match(low, DK_LOCATION_KEYWORDS) + in_no = "NO" in countries if explicit_country else _place_match(low, NO_LOCATION_KEYWORDS) is_us_only = any(p in low for p in US_ONLY_PATTERNS) and not in_ch has_remote = bool(re.search(r"\bremote\b", low)) or "work from anywhere" in low europe_scope = any(k in low for k in EUROPE_SCOPE_KEYWORDS) @@ -1811,7 +1840,7 @@ def location_eligibility(loc_text, policy="default"): # Pan-regional scope counts even without the word "remote" (e.g. QuantCo "Europe"), # but NOT when the posting also says onsite or restricts residence to one country # (F12: "Europe" beside "London / Onsite" used to read as remote-eligible). - restricted = bool(_ONSITE.search(low)) or ( + restricted = "CH" in excluded or bool(_ONSITE.search(low)) or ( bool(_COUNTRY_ONLY.search(low)) and not (in_ch or in_dk)) is_europe_or_global = (europe_scope or global_scope) and not is_us_only and not restricted is_ch_remote = in_ch and has_remote @@ -2074,6 +2103,10 @@ def lookup_decision(dec_index, decisions, url): return related +def is_same_decision(decision): + return decision is not None and decision.get("matched_by") != "slug" + + def save_decisions(decisions): _atomic_write_json(DECISIONS_FILE, decisions) @@ -2169,8 +2202,8 @@ def write_report(path, results, errors, new_only, include_weak, stats=None, tota if not include_weak and weak: lines.append(f"\n_Hiding {len(weak)} weak/noise roles (score < 2). Use --include-weak to show._") - n_decided = sum(1 for r in results - if lookup_decision(decisions, _raw_decisions(), r["url"]) is not None) + n_decided = sum(1 for r in results if is_same_decision( + lookup_decision(decisions, _raw_decisions(), r["url"]))) if n_decided: shown = "hidden" if hide_decided else "tagged inline" lines.append(f"_{n_decided} role(s) already in the decision log ({shown}; " @@ -2188,7 +2221,7 @@ def write_report(path, results, errors, new_only, include_weak, stats=None, tota for bucket_name, bucket in buckets: shown = [r for r in bucket if not (hide_decided and - lookup_decision(decisions, _raw_decisions(), r["url"]) is not None)] + is_same_decision(lookup_decision(decisions, _raw_decisions(), r["url"])))] if not shown: continue lines.append(f"\n## {bucket_name} - {len(shown)} role(s)\n") @@ -2205,6 +2238,8 @@ def write_report(path, results, errors, new_only, include_weak, stats=None, tota lines.append(f"- Posted: {r['posted']}") if r.get("deadline"): lines.append(f"- **Bewerbungsfrist: {r['deadline']}**") + if r.get("updated_at"): + lines.append(f"- Updated: {r['updated_at']}") if r.get("unit"): lines.append(f"- Einheit: {r['unit']}") if r.get("compensation"): @@ -2322,7 +2357,7 @@ def _process_company(cid, display, args, jobs, seen, today, last_scrape): for j in jobs: jid = str(j.get("id") or j.get("url")) ok, in_ch, is_remote, is_relocation = location_eligibility( - j.get("location", ""), policy=policy + j.get("location", ""), policy=policy, country=j.get("country") ) if not ok: continue @@ -2359,6 +2394,7 @@ def _process_company(cid, display, args, jobs, seen, today, last_scrape): "company": display, "company_id": cid, "title": j["title"], "location": j["location"], "url": j["url"], "posted": j.get("posted", ""), + "updated_at": j.get("updated_at", ""), "deadline": j.get("deadline", ""), "date_provenance": j.get("date_provenance", ""), "unit": j.get("unit", ""), @@ -2386,7 +2422,7 @@ def _process_company(cid, display, args, jobs, seen, today, last_scrape): return results, stat, warnings -def main(): +def _main(): today = datetime.now(timezone.utc).strftime("%Y-%m-%d") # Record a decision and exit: --decide "" [note words...] @@ -2540,6 +2576,7 @@ def main(): dump = [{ "company": r["company"], "title": r["title"], "location": r["location"], "url": r["url"], "posted": r["posted"], "deadline": r.get("deadline", ""), + "updated_at": r.get("updated_at", ""), "date_provenance": r.get("date_provenance", ""), "unit": r.get("unit", ""), "compensation": r.get("compensation", ""), "contact": r.get("contact", ""), "is_new": r["is_new"], @@ -2554,10 +2591,9 @@ def main(): # Report and dump are on disk, so the run genuinely delivered: now the roles may be # marked seen. A restricted run must not mark anything seen at all - it did not look at # the whole board, and doing so would hide those roles from the next full run. - if scope: - print("Restricted run: seen-state deliberately NOT updated.", file=sys.stderr) - else: - save_seen(seen) + # All returned matches have been delivered in the JSON, even for new-only or + # one-company runs. Unvisited companies remain unchanged in the loaded seen map. + save_seen(seen) n_new = sum(1 for r in all_results if r["is_new"]) print(f"\nReport written: {report_path}", file=sys.stderr) @@ -2598,5 +2634,29 @@ def main(): # ============================================================================== +@contextmanager +def writer_lock(): + path = ROOT / "state" / "scout.lock" + path.parent.mkdir(parents=True, exist_ok=True) + try: + descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + except FileExistsError: + raise SystemExit("Another scout writer is active. If a prior run crashed, verify it has stopped before removing state/scout.lock.") + try: + with os.fdopen(descriptor, "w") as stream: + stream.write(str(os.getpid())) + yield + finally: + path.unlink(missing_ok=True) + + +def main(): + if any(arg in sys.argv for arg in ("--help", "-h")): + return _main() + with writer_lock(): + _raw_decisions.cache_clear() + return _main() + + if __name__ == "__main__": main() diff --git a/job_scout/state/applications.json b/job_scout/state/applications.json new file mode 100644 index 0000000..a37d320 --- /dev/null +++ b/job_scout/state/applications.json @@ -0,0 +1,660 @@ +{ + "schema_version": 1, + "cohort_metadata": { + "cohort_id": "evidence-first-2026-01", + "status": "active (log, not a capped cohort)", + "started": "2026-07-27", + "target_mix": { + "core": 7, + "adjacent": 2, + "stretch": 1 + }, + "definitions": { + "core": "Evidence Fit 75+ and no hard-gate failure", + "adjacent": "Evidence Fit 60-74, no required Gap, normally at most one serious preferred gap", + "stretch": "Evidence Fit below 60 or a defining responsibility is only Adjacent" + }, + "policy_change_2026_09_05": "Slot caps (7 core / 2 adjacent / 1 stretch) DROPPED by user decision. target_mix is retained only as a record of the original design and is no longer read by cohort_tracker.py or enforced anywhere. This file continues as a durable application log. Channel is now the rationed variable: no application goes out cold when a named contact is published and reachable. See resume_builder/reference/application_strategy.md." + }, + "applications": [ + { + "company": "NVIDIA", + "role": "Senior DevOps Engineer, Zurich (JR2024256)", + "application_date": "2026-09-05", + "fit_class": "stretch", + "evidence_fit": 64.0, + "hard_gate": "fail", + "channel": "weak", + "outcome": "applied", + "id": "app-9068f462d9b9", + "label": "NVIDIA - Senior DevOps Engineer, Zürich (req JR2024256)", + "next_action": "**Await response. Raise the working model at first contact** - it went in unresolved and a 5-day-onsite seat would be a late no. Note: `claims.json` has no shell-scripting entry, so **Bash is omitted** from Skills although the JD names it and ten years of Debian admin plus Ansible work plainly involve it - same shape as the Ansible/Linux gaps found 2026-08-27. Confirm with the user and record it. Groovy stays out regardless.", + "current_summary": "SUBMITTED 2026-09-05.", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**SUBMITTED 2026-09-05.** Recorded in the application record (now 7 entries) and in the scout decision log. ⚠ **Hybrid vs 5-day onsite was never resolved before sending**, so the working model is now a **live screening topic**, not a pre-cleared one - the same pattern as RUAG's comp/PSP. Thun→Zürich is ~1h50 each way; a 5-day-onsite answer is the main disqualifier to watch for. Comp is *not* a risk (levels.fyi Greater Zürich IC3 median ≈ CHF 204k, IC4 ≈ CHF 379k). Channel **Weak - the 7th consecutive cold submit**, though this posting published no named contact, so cold was the only option available. Submission PDFs `Dennis_Thiessen_Resume.pdf` and `Dennis_Thiessen_Cover_Letter.pdf` exist and are MD5-verified against source; letter recompiled so its date reads 5 Sep 2026. ⚠ **The letter carries `\today` and the submission copies are copies, not build targets - if it is sent on a later day, recompile the letter and refresh both copies first** (`config.md` Output Rules). Submitted despite the hard-gate failures on an explicit user override, the same shape as RUAG AI Engineer C5I. Resume 2pp/13 bullets + 1pp cover letter (279 body words), both validators PASS 0 warnings, 0 boxes, visually inspected. Measured **Document Quality 82/100** at critique; **both Tier 1 findings were fixed 2026-09-05** - `Ubuntu` removed from the skills line (0 canonical; `PP-3` is Debian only, and the Debian→Ubuntu transfer argument belongs in an interview answer, not a skills line) and `security hardening` now marked *(personal server)* per the explicit `claims.json` instruction. Four honest ATS terms were added from existing evidence (configuration management, service reliability, container orchestration, observability), lifting JD term coverage **50%→61%**; no bullet was touched, so the budget stayed at 13 and cadence could not regress. **The critique is now STALE BY DESIGN - the document is better than 82 but that number was not re-measured, so never quote a post-edit estimate as a score.** The headline reorder was deliberately NOT applied: it is a positioning call for the user and `\\headline` sits in the FIXED header block. Cover-letter hook is first-party verified (NVIDIA's own live Zürich posting JR2022878); **a hook was DROPPED at verification** - the Phase 0 plan's \"Alps / Swiss AI Initiative with ETH Zürich and EPFL\" is **not** supported by the first-party NVIDIA newsroom release, which mentions neither EPFL nor any Swiss AI Initiative; that came from secondary coverage. The letter names **both** honest gaps out loud - on-prem networking, and that employer-side Linux/Ansible ended Dec 2022 with only a personal server carrying currency. **⚠ BLOCKING: the working model is still unresolved and no document fix changes that.** Session `output/NVIDIA_SrDevOps_Zurich/session_nvidia_sr_devops.md`; JD scraped live via Playwright and saved verbatim (posted ~2026-09-01, very fresh). Evidence Fit **64/100**, fit class **Stretch**, **hard gate FAIL on two counts**: (1) required line *\"Strong expertise in on-premises networking and compute infrastructure\"* is a **Gap** - networking is 0 canonical and is not substitutable from data-platform work; (2) **hybrid vs onsite is UNRESOLVED** and the JD is silent - Thun→Zürich is ~1h50 each way. Required tally **3 Direct / 3 Adjacent / 1 Gap**. Recorded as an override rather than a passing gate, the same shape as RUAG AI Engineer C5I. (At the time this also had no free cohort slot; **that constraint no longer exists** - caps dropped 2026-09-05.) **Correction to the same day's scout triage: it called two required lines Gaps; only Q5b networking is one. Q2 (10+ yrs DevOps) is Adjacent and defensible** - a continuous CI/CD/automation thread runs Generali 2015 → today across five employers; the honest weakness is that those roles were titled data/test/research engineer, not DevOps. **The finding that changes the calculus: comp.** levels.fyi Greater Zürich puts NVIDIA IC3 median ≈ CHF 204k and IC4 ≈ CHF 379k, the highest-paying SWE employer in the city - the first role in the log where comp is a reason to *accept* a long commute rather than decline. Caveat: self-reported and equity-heavy after an extraordinary NVDA run; treat base as the reliable part. Strongest match is **R3, CI/CD pipelines for R&D engineering teams** - literally his career shape (`FC-1` Jenkins at a Fraunhofer institute, `BS-6` Ansible/Jenkins for fab analytics teams, `SW-3` Kubernetes + GitLab today). Zürich is NVIDIA's HPC/AI infrastructure-software R&D hub - first-party verified from NVIDIA's own live Zürich posting JR2022878 (*\"building the next generation of scalable AI infrastructure ... distributed training, real-time inference, and communication efficiency across large systems\"*), which is the hook the letter uses. **The Alps / Swiss AI Initiative framing was checked and dropped** - see above. **Honest gaps that travel with it:** on-prem networking (0), HPC (0), bare metal/switches/QEMU-KVM (0), RHEL/CentOS (0 - Debian→Ubuntu transfers, Red Hat does not), and **Ansible + Linux-at-work both ended Dec 2022** with currency carried only by `PP-3`, a personal server whose canonical entry forbids using it as physical-infrastructure evidence. No metric is quotable anywhere.", + "legacy_next_action": "**Await response. Raise the working model at first contact** - it went in unresolved and a 5-day-onsite seat would be a late no. Note: `claims.json` has no shell-scripting entry, so **Bash is omitted** from Skills although the JD names it and ten years of Debian admin plus Ansible work plainly involve it - same shape as the Ansible/Linux gaps found 2026-08-27. Confirm with the user and record it. Groovy stays out regardless." + }, + { + "type": "applied", + "date": "2026-09-05" + } + ], + "cohort_id": "evidence-first-2026-01" + }, + { + "company": "RUAG MRO Holding AG (Business Area C5I)", + "role": "AI Engineer C5I (w/m/d), Thun", + "application_date": "2026-08-27", + "fit_class": "stretch", + "evidence_fit": 74.0, + "hard_gate": "fail", + "channel": "weak", + "outcome": "applied", + "id": "app-ca0990d47bd0", + "label": "RUAG C5I - AI Engineer C5I, Thun (application ID 18027)", + "next_action": "Await response. Optional but still worthwhile: call **Marco Heinzen** about technical scope and level, and **Frank Haugwitz** about compensation and PSP/project eligibility. **NEW CONTACT 2026-09-07 — `Lars Kauz`, Head of ICT Infrastructure C5I, +41 79 293 93 82, lars.kauz@ruag.ch**, published on the System Engineer C5I posting. Channel is the rationed variable since the 09-05 cohort change, so a published, reachable name matters — **but he heads ICT Infrastructure, the function whose req was ruled out for VMware/VDI/Windows content, and that is NOT demonstrably the Kubernetes platform group Senior DevOps C5I sits in.** He is a warm entry into C5I, not a verified channel for rank 1; Haugwitz stays the recorded gate. Do not react to a single outcome with positioning changes (`application_strategy.md` §38/§74).", + "current_summary": "SUBMITTED 2026-08-27", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**SUBMITTED 2026-08-27** via jobs.ruag.ch (portal only; email and post refused). Evidence Fit **74/100**, fit class **Stretch**, hard gate **FAIL** (R1 and R2, the two title-defining responsibilities, are Adjacent not Direct), Document Quality **93/100** after a full critique-and-fix round (85 pre-edit), truth and provenance 24/25, **0 Tier 1 findings at Round 2**, channel **Weak/cold**. Submitted: German CV **2 pages, 13 bullets** + German motivation letter **1 page, 295 words**; both validators PASS, 0 boxes, text-order and visual QA clean, submission PDFs MD5-verified before sending. **Cohort evidence-first-2026-01 is now 6/10 and the sole Stretch slot is CONSUMED (Core 3/7, Adjacent 2/2, Stretch 1/1)** - any further application must clear Core or Adjacent on its own merits. Positioning is his canonical title **Staff Data, Analytics & AI Engineer**, carried by `BS-1` production ML-inference integration, `BS-6` Linux/Ansible, `SW-3`/`SW-7`/`SW-2` current Kubernetes and data-product work, `PP-3` current self-hosted Linux and hardening, `SW-5` DevSecOps tied to C5I's stated operating model, and the canonical `BW-1` officer career. The letter **names the AI-platform gap in one sentence** and pivots to the operations/platform side. **Honest gaps that travel with it:** no AI/LLM platform built or operated, no compute/GPU cluster, no RAG/retrieval implementation, no formal LLM evaluation; ML/AI-framework depth is certification-context only. **Compensation, PSP/project eligibility and role level were NEVER resolved before sending, so all three are now live screening topics rather than pre-cleared ones.** ⚠ **Marco Heinzen's published direct line (+41 79 568 14 96) was never used** - this went in as a pure cold submit, the sixth consecutive one. **If rejected, that is NOT a signal to drop RUAG:** five other RUAG C5I reqs sit in the scout log, and **Senior DevOps Engineer C5I** is already shortlisted and is a **materially better fit than this one**, sitting on the lane where his evidence is Direct rather than Adjacent (still live and verified on the board 2026-09-04). The **Data Lakehouse / Senior Data Platform Engineer** req was a title-match prior that the JD does not support — it was reposted 2026-09-04 as **Senior Data Engineer (MSSQL)**, requires on-prem MSSQL / Talend / SAP S/4HANA (0 canonical each), and is **ruled out**. **Scout run 2026-09-07: the RUAG C5I backlog is now SETTLED — undecided RUAG rows went 69 → 3** (`ICT Architect`, `Software Engineer C5I`, `Cyber Security Consultant` remain open; `ICT Architect C5I` stays `paused`). Do not re-triage that board from scratch; read `decisions.json`, where every rule-out note states whether it came from a JD read or from the title/family pattern. Three JD reads that cycle all ruled out: **Test Manager C5I** (the closest call — it escapes the networking gap entirely and BW-1 scores, but required *\"fundierte Erfahrung als Test Engineer\"* rests on `VIZRT`, **ten months, ended 2018-05**, so it is thin and stale, plus a lane exit and a third C5I req while 18027 is pending), **System Engineer C5I** (VMware + Firewall + Netzwerk, all 0), and **ICT Solution Architect Business Applications** (required *\"mehrjährige Erfahrung als Solution Architect\"* — a title `feedback_swisscom_datamesh_ownership` forbids — and the duties are *\"unter Anleitung\"*, a supporting seat below authoring).", + "legacy_next_action": "Await response. Optional but still worthwhile: call **Marco Heinzen** about technical scope and level, and **Frank Haugwitz** about compensation and PSP/project eligibility. **NEW CONTACT 2026-09-07 — `Lars Kauz`, Head of ICT Infrastructure C5I, +41 79 293 93 82, lars.kauz@ruag.ch**, published on the System Engineer C5I posting. Channel is the rationed variable since the 09-05 cohort change, so a published, reachable name matters — **but he heads ICT Infrastructure, the function whose req was ruled out for VMware/VDI/Windows content, and that is NOT demonstrably the Kubernetes platform group Senior DevOps C5I sits in.** He is a warm entry into C5I, not a verified channel for rank 1; Haugwitz stays the recorded gate. Do not react to a single outcome with positioning changes (`application_strategy.md` §38/§74)." + }, + { + "type": "applied", + "date": "2026-08-27" + } + ], + "cohort_id": "evidence-first-2026-01" + }, + { + "company": "Schweizer Armee - Kommando Cyber", + "role": "DevOps Engineer III (Data Platform)", + "application_date": "2026-08-27", + "fit_class": "core", + "evidence_fit": 79.0, + "hard_gate": "pass", + "channel": "weak", + "outcome": "rejected_no_interview", + "submitted": "2026-08-27", + "rejection_date": "2026-09-10", + "days_to_disposition": 14, + "note": "Form-letter rejection from Besarta Mehmeti, no interview, 14 days to disposition - the fastest in the log after Google Business Home. The letter is boilerplate ('Profil und unsere Kriterien nicht in allen Punkten uebereinstimmen') and carries ZERO diagnostic content, so it is not a signal about the package and is no reason to re-audit it. Same disposition class as Microsoft ISE, Google Business Home and Aker BP: a cold-channel no-interview rejection does not implicate document quality on its own. WORTH WATCHING, NOT CONCLUDING: this row and the SBB row are identical on every recorded dimension - core, Evidence Fit 79, hard gate pass, Document Quality 92, channel weak - and they went opposite ways (SBB converted to an interview, this died in 14 days). That shows the fit score did not discriminate; it does NOT show what did. n=1 on each side. Do not react with positioning changes (application_strategy.md 38/74). Channel note: Marcel Matthey-Doret (+41 58 46 78409) was published and never called - the fourth published, reachable contact left unused after Heinzen (RUAG), Wienandts (SBB) and Marthinsen (Aker BP). Consistent with the 2026-09-05 channel policy, but NOT evidence for it: the single interview in this log came from a cold submit.", + "id": "app-e7389ad9d46f", + "label": "Schweizer Armee - Kommando Cyber (Kdo Cy), DevOps Engineer III (Data Platform), Zimmerwald (Ref JRQ$540-19848)", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED - REJECTED 2026-09-10", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED - REJECTED 2026-09-10** (submitted 2026-08-27, **14 days to disposition**, no interview - the fastest in the log after Google Business Home). Form letter from **Besarta Mehmeti**; the text is boilerplate (*„Profil und unsere Kriterien nicht in allen Punkten übereinstimmen\"*) and carries **zero diagnostic content** - it is not a signal about the package and is **no reason to re-audit it**. Same disposition class as Microsoft ISE, Google Business Home and Aker BP: **a cold-channel no-interview rejection does not implicate document quality on its own.** ⚠ **WORTH WATCHING, NOT CONCLUDING:** this application and **SBB** are identical on every recorded dimension - Core, Evidence Fit **79**, hard gate PASS, Document Quality **92**, channel Weak - and they went opposite ways: SBB converted to an interview, this died in 14 days. **That shows the fit score did not discriminate; it does not show what did.** n=1 on each side - do not react with positioning changes (`application_strategy.md` §38/§74). **Channel:** Marcel Matthey-Doret's published line (+41 58 46 78409) was never called - the **fourth** published, reachable contact left unused after Heinzen (RUAG), Wienandts (SBB) and Marthinsen (Aker BP). Consistent with the 2026-09-05 channel policy but **not evidence for it**: the only interview in the log came from a *cold* submit whose resume was missing all three ATS literals. **Kdo Cy as an organisation is NOT closed** - 11 Kdo Cy roles were open on the portal at submission time and only this req is decided. Evidence Fit **79/Core**, hard gate **PASS**, Document Quality **92/100**, channel **Weak**. Submitted package: German 2-page CV with **15 bullets** and 1-page motivation letter with **300 body words**; validators PASS, 0 boxes, text order and visual QA clean. Canonical **BW-1** records the six-year Bundeswehr officer career and anchors the letter's military/public-service motivation. User explicitly accepted the critique's remaining BS-1 wording issue (*„ohne manuellen Eingriff“*) as good enough and submitted unchanged. Honest gaps remain physical datacenter work, GPGPU, ClickHouse and named Pull-GitOps practice. Cohort evidence-first-2026-01 is now **5/10, Core 3/7** *(pre-dates the 2026-09-05 cap drop; counts are a record, not a budget)*.", + "legacy_next_action": "Done - rejection recorded in `application_cohort.json`, `decisions.json` and the session file. Optional and low-cost: a one-line feedback request to **Besarta Mehmeti**, which also builds a contact at an org with other open reqs." + }, + { + "type": "applied", + "date": "2026-08-27" + }, + { + "type": "rejected_no_interview", + "date": "2026-09-10" + } + ], + "cohort_id": "evidence-first-2026-01" + }, + { + "company": "Citadel Securities", + "role": "Platform Engineer, Research Platform Engineering (Zurich/NY/Miami)", + "application_date": "2026-08-26", + "fit_class": "adjacent", + "evidence_fit": 71.0, + "hard_gate": "pass", + "channel": "weak", + "outcome": "applied", + "id": "app-ef2bec6fe379", + "label": "Citadel Securities — Platform Engineer, Research Platform Engineering, Zürich (also NY/Miami)", + "next_action": "**Await response** (submitted 2026-08-26). No document work pending. **Two questions travel with this application and must both be asked at first contact, before any interview effort:** (1) **is the application even against the Zürich seat?** — the apply form offered no Zürich, so it may sit against a US req, which is a hard NO on relocation; (2) **hybrid or 5-day onsite?** — unresolved at submission, and 5-day onsite is a likely NO-GO on the Bern constraint. Same shape as the NVIDIA working-model issue: it went out unresolved, so it is now a live screening topic rather than a pre-cleared one. Also unasked: the Zürich compensation figure — the posting's $175–350k is a US/NY pay-transparency disclosure.", + "current_summary": "SUBMITTED 2026-08-26 — open, awaiting response.", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**SUBMITTED 2026-08-26 — open, awaiting response.** Recorded as `adjacent` in `application_cohort.json` (outcome `applied`) and as `applied` in `decisions.json`. *Slot language from the capped-cohort era has been removed here: the caps were dropped 2026-09-05 and counts are a record, not a budget.* ⚠️ **KEY FINDING — the application form's preferred-work-location selector offered NO Zurich**, despite the JD saying \"Miami, Zurich or New York\" and the site's own Zurich filter returning this req. Careers site is operated by **Citadel Enterprise Americas LLC**; the apply flow is likely US-entity, or the Zurich seat is closed while the posting stands. **This partly supersedes the working-model risk** — if no Zurich seat is reachable through that form, hybrid-vs-onsite is moot and the application may sit against a US req, a hard NO on relocation. **Rule for any future Citadel application: check the form's location selector BEFORE building a package** — the posting's stated locations and the site filter both proved unreliable. User applied with low expectation; cold channel, evergreen standing req. 2pp resume (13 bullets) + 1pp CL (270 words), both validator **PASS, 0 warnings**; JD term coverage 51%→68% after Tier 1 fixes. Critique **STALE by design** (86/100 pre-edit; user chose to submit as-is over re-critiquing; est. ~91 post-edit, never quote as a score). **CORRECTED 2026-09-10:** this row previously said *\"Cohort entry and decision-log entry deliberately NOT written — no confirmed send date\"* while its own status line read SUBMITTED. **Both entries exist and both are dated 2026-08-26** — the sentence was pre-submission text that survived the send. **The working-model question is still open and travels with the application:** Evidence Fit **71/100 (Adjacent, mid-band; revised down from 74)**, qualifications gate PASS but **a practical constraint is UNRESOLVED**, which `critique_framework.md` treats as a NO-GO trigger. **The JD is silent on hybrid vs onsite (0 grep matches), and Ken Griffin is on record that Citadel returned to the office 5 days a week and calls it his most important leadership decision.** If that holds for Zurich, it means ~2–2.5h daily commuting from Bern or relocating — breaking the Bern-based / 2–3 day hybrid bar. Only contrary evidence is a stale 2022 anonymous Fishbowl post about Operations. **Must be asked, not researched** — same class as the SBB K band. Practical constraints scored 5/5 in Phase 0 on an unexamined assumption; corrected to 2/5. **Zero Gaps among the 7 minimum quals**, and all five preferred technologies (Kafka, Kubernetes, Spark, Airflow, distributed DBs) are Direct — **the strongest raw stack alignment in the log**. Title-defining function **splits**: R2 (ingestion/transformation/storage/lifecycle of large datasets) is Direct and describes SW-1/SW-7 exactly; **R1 (design and build the research platform itself) is Adjacent** — the Aker BP authoring-vs-building shape again, and unwritable-around under Scope Discipline. **Classification caveat:** `application_strategy.md`'s \"a defining responsibility is only Adjacent → Stretch\" would strictly class this Stretch; recorded Adjacent per the Aker BP precedent (the caps this once turned on no longer exist). Gaps: platform authoring at firm scale, no professional quant/finance (now partly mitigated by new **PP-1/PP-2**), SDKs (0 evidence), low-latency/HFT (0 evidence — thesis throughput figure is explicitly barred by `thesis_limits`). **Go is NOT a gap** — Q3 is disjunctive and Python satisfies it. **$175–350k in the posting is a US/NY pay-transparency disclosure, NOT the Zurich number — must be asked.** Channel **Weak/cold** — it was the 5th consecutive cold application, in an office visibly staffed from Google and ETH (platform research head Costas Bekas; Nicolai Meinshausen leads principal research). Cover letter **was written and submitted** (1pp, 270 words); the PP-1 domain story carries it.", + "legacy_next_action": "**Await response** (submitted 2026-08-26). No document work pending. **Two questions travel with this application and must both be asked at first contact, before any interview effort:** (1) **is the application even against the Zürich seat?** — the apply form offered no Zürich, so it may sit against a US req, which is a hard NO on relocation; (2) **hybrid or 5-day onsite?** — unresolved at submission, and 5-day onsite is a likely NO-GO on the Bern constraint. Same shape as the NVIDIA working-model issue: it went out unresolved, so it is now a live screening topic rather than a pre-cleared one. Also unasked: the Zürich compensation figure — the posting's $175–350k is a US/NY pay-transparency disclosure." + }, + { + "type": "applied", + "date": "2026-08-26" + } + ], + "cohort_id": "evidence-first-2026-01" + }, + { + "company": "SBB", + "role": "Data Engineer (m/w/d), Asset Management Infrastrukturanlagen, Bern", + "job_id": "103755", + "application_date": "2026-08-25", + "fit_class": "core", + "evidence_fit": 79.0, + "hard_gate": "pass", + "channel": "weak", + "outcome": "withdrawn_by_candidate", + "submitted": "2026-08-25", + "interview": { + "stage": "Stage 2 of 4 - Virtuelles Kennenlernen mit HR und Fuehrungskraft", + "invited": "2026-08-28", + "scheduled": "2026-09-09 08:30", + "duration_min": 45, + "format": "Microsoft Teams", + "held": "2026-09-09", + "result": "Held 2026-09-09 (HR + Scrum Master). Candidate WITHDREW 2026-09-11 after SBB confirmed there was no room to move on the Einstufung.", + "user_read": "User reported 'ein paar Red Flags' and judged that the Rahmenbedingungen do not compensate for them. The specific red flags were NOT captured while fresh - if this is reopened, ask before relying on memory. Leaning negative but explicitly undecided as of 2026-09-10; he asked to leave it open pending SBB's feedback." + }, + "withdrawn": "2026-09-11", + "why_withdrawn": "Total package was a net DOWNGRADE on four counts: 41h vs 40h week; 25 days holiday + Brueckentage vs 28 + 5 from Zeitautonomie; 3 office days vs 2; and the band ceiling above. Plus a qualitative red flag: he spoke only to HR and the Scrum Master and perceived DIFFICULTIES IN THE TEAM. That one is not fixable by a salary offer - note it if SBB or a similar Bern-tier seat comes back.", + "lesson": "SBB-SPECIFIC targeting rule (user 2026-09-11): at SBB, Anforderungsniveau K is too low - aim at Niveau L and/or Kader. NARROWED the same day at the user's instruction: Anforderungsniveau is SBB's OWN system (https://company.sbb.ch/de/jobs-karriere/arbeitgeberin/lohn.html) and does NOT generalise - jobs.admin.ch uses Lohnklassen, RUAG its own scheme, PostFinance per-posting bands. ~145k is an SBB ceiling, not a tier anchor. THUN-AREA EMPLOYERS (RUAG, Kdo Cy) ARE EXPLICITLY OUT of this reasoning: near-zero commute plus benefits a band does not show - he named childcare-cost coverage at the Schweizer Armee/Kdo Cy as potentially far more generous (UNVERIFIED - ask). Never decline a Thun-area role on band alone. What DOES generalise: judge by headroom in the band, and price the total package (working time, holiday, office days), not the entry number.", + "id": "app-799e2880a22e", + "label": "SBB - Data Engineer (m/w/d), Asset Management Infrastrukturanlagen, Bern (Job ID 103755)", + "next_action": "Closed. For future SBB roles assess Niveau L/Kader; do not generalize SBB bands to other employers.", + "current_summary": "Withdrew after interview and confirmation that SBB could not improve the classification. Niveau K ceiling and total package unsuitable for this application.", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED - APPLICATION WITHDRAWN BY THE CANDIDATE 2026-09-11.** Interview held 2026-09-09 (HR + Scrum Master, **not** the People Leader the brief expected); SBB confirmed there was no room on the Einstufung, and he withdrew. **This is the cohort's only interview and it ended as a NO from his side, not theirs.** 💰 **HARD COMP FIGURES - supersedes the earlier +10% estimate and is the reference point for the whole Bern/Thun tier:** **Anforderungsniveau K tops out at ~CHF 145k.** He sits at **~CHF 135k inside a Swisscom band that runs to ~CHF 160k**. The decisive argument was **headroom, not the absolute number** - he would have entered K at its ceiling merely to break even, with nothing above. ⚠ **The package was a net DOWNGRADE on four counts:** 41h vs 40h week · 25 days holiday + Brückentage vs 28 + 5 from Zeitautonomie · 3 office days vs 2 · the band ceiling. ⚠ **Qualitative red flag worth carrying forward:** he spoke only to HR and the Scrum Master and **perceived difficulties in the team** - that is not repairable by a salary offer. 🎯 **TARGETING RULE — SBB ONLY: Anforderungsniveau K is too low; at SBB aim at NIVEAU L and/or KADER.** ⚠ **Deliberately NOT generalised** (user, 2026-09-11): *Anforderungsniveau* is **SBB's own** salary system (published at ) and does not map onto other employers — **jobs.admin.ch/Schweizer Armee uses Lohnklassen**, RUAG has its own scheme, PostFinance publishes per-posting bands. ~145k is an **SBB** ceiling, not a tier anchor. ⚠ **Thun and surroundings (RUAG, Kdo Cy) are explicitly OUT of this reasoning:** near-zero commute plus benefits that a salary band does not show — he named **childcare-cost coverage at the Schweizer Armee/Kdo Cy as potentially far more generous** (unverified — ask, do not assume). **Never decline a Thun-area role on band alone before pricing the benefits.** What *does* carry across employers: judge by **headroom in the band**, and price the **total package** (working time, holiday, office days), not the entry salary. Original record: interview was stage 2 of 4, **INTERVIEW HELD 2026-09-09.** SBB asked him to come back on whether the role interests him; **SBB's own feedback is still outstanding.** User's summary: *„ok\"*, but **ein paar Red Flags**, and in his read the Rahmenbedingungen do not compensate for them - **leaning negative, explicitly undecided as of 2026-09-10, and he asked to leave it open.** ⚠ **The specific red flags were NOT captured while fresh** - anyone reopening this must ask rather than rely on memory. 💰 **First real read on Anforderungsniveau K:** his post-interview estimate is that roughly **+10% on the current ~140k** looks achievable (very roughly ~155k). **This is HIS ESTIMATE of what is negotiable, not a band SBB is recorded as quoting** - whether the K range was named on the call is unrecorded. Well below the 180k bar, above current pay. Recorded in `comp_worksheet_sbb.md` as the **first calibration point for the entire Bern/Thun local tier** (RUAG, Swissgrid, BKW, BFH), which was previously pure guesswork - treat as an estimate, never quote as a figure. **INTERVIEW INVITED 2026-08-28 - 9. September 2026, 08:30, 45 Min, MS Teams.** Per the posting's own process this is **stage 2 of 4**, *\"Virtuelles Kennenlernen mit HR und Fuehrungskraft\"* - **HR together with Andri Wienandts**, not a pure HR screen. Brief written to `output/SBB_DataEngineer_AssetMgmt/interview_brief_sbb_2026-09-09.md`. **This is the FIRST interview of the evidence-first cohort** (6 applications, 2 rejections, 1 conversion) and it converted from a **COLD submit** - the channel plan called for phoning Wienandts first and that never happened. **The resume never contained Snowflake, dbt or Power BI**, the three literals an ATS keyword screen would have keyed on; the session had recorded that risk as \"accepted, not solvable\". It did not filter him out, so a human read the dossier and the honest-substitution framing survived first contact. **One data point, do not over-read it** - but it is evidence against assuming honest omissions are fatal at the screen. Submitted 2026-08-25: 2pp English resume, 13 bullets, no cover letter (SBB waives it), Evidence Fit **79/100 Core**, hard gate **PASS**, Document Quality **92/100**, channel **Weak/cold**. Best practical fit in the log - Bern-based, German-language, no relocation, EU citizen + B permit. **Four things unresolved before submitting and now live call topics:** (1) **Anforderungsniveau K** is a capped GAV band, figures not public, very likely below the 180k bar; (2) the seat reads **lateral or below** current Staff + Component Owner scope, the same shape as the declined BKW, and the JD's only architecture language is a personal skill not an ownership mandate; (3) **RAMSI** Java/Spring Boot/Angular is a whole responsibility line on a stack that is historical or absent - ask its share, do not oversell; (4) Kidz Care scales on gross household income, so far below 90% at this band. Named gaps to answer honestly: Snowflake, dbt, Argo Workflows, Helm, Power BI, Spring Boot, Angular - all non-canonical, all survivable only because the JD prefixes the stack with \"z. B.\". Thesis stays a **methods prototype**: no real operational data, no accuracy figures, PSO surveyed only, throughput figure barred.", + "legacy_next_action": "Done - withdrawn and recorded in `application_cohort.json`, `decisions.json`, `comp_worksheet_sbb.md` and memory. **Carry forward the headroom + total-package test, NOT the K→L mapping** — that one is SBB-specific and was explicitly narrowed by the user on 2026-09-11.", + "legacy_open_risk": "Anforderungsniveau K is a capped GAV band, figures not public; seat may read lateral or below current Staff + Component Owner scope.", + "legacy_note": "FIRST interview invitation in the evidence-first cohort. Converted from a COLD submit - the published warm contact (Andri Wienandts, +41 79 364 62 53) was never called. The resume never contained Snowflake, dbt or Power BI, the three literals an ATS keyword screen would have keyed on, so a human read the dossier.", + "legacy_comp_datapoint": "HARD FIGURES, from the interview - supersedes the earlier +10% estimate. Anforderungsniveau K TOPS OUT AT ~CHF 145k. His own position: ~CHF 135k today inside a Swisscom band that runs to ~CHF 160k. So the decisive point was NOT the absolute number but the HEADROOM: he would have had to enter K at its ceiling just to break even, with nothing left above. First hard calibration of a Swiss public-sector band and the reference point for the whole Bern/Thun tier (RUAG, Swissgrid, BKW, BFH, PostFinance)." + }, + { + "type": "applied", + "date": "2026-08-25" + }, + { + "type": "interview_invited", + "date": "2026-08-28" + }, + { + "type": "interview_held", + "date": "2026-09-09" + }, + { + "type": "withdrawn_by_candidate", + "date": "2026-09-11" + } + ], + "cohort_id": "evidence-first-2026-01" + }, + { + "id": "app-b34da8c3f4a3", + "label": "AWS (AWS EMEA SARL, Switzerland Branch) — Senior Forward Deployed Engineer, Zürich (job 10504263)", + "company": "AWS (AWS EMEA SARL, Switzerland Branch)", + "role": "AWS (AWS EMEA SARL, Switzerland Branch) — Senior Forward Deployed Engineer, Zürich (job 10504263)", + "outcome": "not_proceeding", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — NO-GO 2026-08-18", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — NO-GO 2026-08-18** at the Phase 0 gate. Evidence Fit **69/100** (Adjacent). Minimum-qual gate passed (all 4 Basic Quals Direct) but the *title-defining* capability — customer embedding — is a Gap that `claims.json` globally forbids claiming, and R2 multi-agent/retrieval is a second Gap. User declined: **69 is below the fit bar worth spending a package on.** Consistent with revealed behaviour — every application actually submitted scored **77.5–89**; nothing below 75 has ever been sent. **No cohort slot consumed (Adjacent stays 1/2).** Phase 0 analysis retained in the session file for reuse if a better-fitting AWS req appears.", + "legacy_next_action": "Done — no package built" + } + ] + }, + { + "id": "app-dfd0467ead74", + "label": "Google — Forward Deployed Engineer III, Google Cloud GTM (French, German), Zürich (req 78350205438567110)", + "company": "Google", + "role": "Google — Forward Deployed Engineer III, Google Cloud GTM (French, German), Zürich (req 78350205438567110)", + "outcome": "not_proceeding", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — NOT PROCEEDING 2026-08-18.", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — NOT PROCEEDING 2026-08-18.** Phase 0 was done (Evidence Fit 67/100, hard gate PASS) but user declined to proceed after **two Google rejections** (Merchant Data Science, Business Home): the bar is now that a Google req must fit *very* well to be worth a third attempt, and this one was Adjacent with an agent-orchestration Gap. **Frees the Adjacent cohort slot (back to 1/2).** Phase 1 never started.", + "legacy_next_action": "Done — do not reopen without a materially stronger Google req" + } + ] + }, + { + "id": "app-2dce75760765", + "label": "SDU Center for Energy Informatics, Odense DK — PhD, Theme 2 Predictive Maintenance & Asset Management of Smart Energy Networks (job 4159)", + "company": "SDU Center for Energy Informatics, Odense DK", + "role": "SDU Center for Energy Informatics, Odense DK — PhD, Theme 2 Predictive Maintenance & Asset Management of Smart Energy Networks (job 4159)", + "outcome": "not_proceeding", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — NOT PROCEEDING 2026-08-18.", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — NOT PROCEEDING 2026-08-18.** Enquiry email to Prof. Bo Nørregaard Jørgensen (sent 2026-08-02) went unanswered for 16 days; user dismissed the idea. Genuine thesis fit (vibration condition monitoring, RBR+ANN hybrid) but the blockers were never the documents — 2 letters of recommendation 13 years out of academia, and DKK 37,075/mo ≈ CHF 56k against a 180k bar. Deadline was 2026-08-20.", + "legacy_next_action": "Done — no further action" + } + ] + }, + { + "company": "Aker BP ASA", + "role": "Data Product Architect - AI-ready Data Products", + "application_date": "2026-07-29", + "fit_class": "adjacent", + "evidence_fit": 79.0, + "hard_gate": "pass", + "channel": "weak", + "outcome": "rejected_no_interview", + "submitted": "2026-07-29", + "rejection_date": "2026-08-27", + "id": "app-80e038df976c", + "label": "Aker BP ASA — Data Product Architect, AI-ready Data Products (FINN 469067315)", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — REJECTED 2026-08-27", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — REJECTED 2026-08-27** (submitted 2026-07-30, ~28 days to disposition, no interview; the session file and cohort record record the submission as 2026-07-29 — a one-day inconsistency, never reconciled). Adjacent, **Evidence Fit 79/100**, Document Quality 93/100, hard gate PASS, **Channel Weak/cold** — the optional follow-up email to Per Olav Marthinsen was left unsent, so this went in as a pure cold submit. Stavanger/Oslo/Trondheim, English working language, no Norwegian and no clearance required, EEA work rights. 2pp resume + 1pp CL, both validator PASS, no Tier 1 truth findings — **a cold-channel no-interview rejection does not implicate document quality on its own** (same disposition class as Microsoft ISE and Google Business Home). Honest practitioner framing: builds governed data products *inside* Swisscom's Data Mesh; role *authored* an enterprise framework — real level stretch, never fabricated authorship. **Atlassian Compass closed the catalogue-tool gap** (74→79). Standing gaps if a similar req appears: framework authorship, no energy domain, AWS vs their Microsoft/Cognite stack, **zero verified metrics**. **Cohort slot stays consumed** (evidence-first-2026-01, Adjacent 1/2 — a rejection does not free a slot; cohort remains 4/10). **The Norway lane stays OPEN and selective** — NATO JWC Stavanger is still live, and Equinor/Telenor and other strong Norwegian employers remain in scope per the user's standing instruction. Never frame this lane as closed.", + "legacy_next_action": "Done — cohort rejection recorded; do not react to this single outcome with positioning changes (`application_strategy.md` §38/§74)" + }, + { + "type": "applied", + "date": "2026-07-29" + }, + { + "type": "rejected_no_interview", + "date": "2026-08-27" + }, + { + "type": "record_correction", + "date": "2026-09-11", + "details": "Application date CONFIRMED BY THE USER as 2026-07-29, settling the one-day conflict between the cohort record (07-29) and the CLAUDE.md session cell (07-30). The legacy import event above still quotes 07-30; it is preserved verbatim as the historical text and is not edited." + } + ], + "cohort_id": "evidence-first-2026-01", + "fit_override_reason": "Recorded Adjacent although Evidence Fit 79 with a passing hard gate would score Core under the numeric rubric. The original rationale is NOT RECOVERABLE: the user was asked on 2026-09-11 and does not remember, and judged it immaterial because the role is closed and rejected. Deliberately NOT reclassified to Core - that would be the silent reclassification the review objected to, and it would rewrite a historical judgement to make the arithmetic tidy. The contemporaneous note points at a real reason rather than an error: the title-defining responsibility (authoring an enterprise framework) was only Adjacent, and application_strategy.md says a defining responsibility that is only Adjacent pushes a role down a class. Treated as a judgement recorded under the pre-2026-09-05 rubric, not as a data error. No future record may cite this one as precedent without stating its own reason.", + "rubric_version": "pre-2026-09-05-cohort", + "classification_note_for_stats": "Fit-class counts that include this row mix one pre-2026-09-05 judgement with post-rubric entries. Immaterial for a closed role; state it if the distribution is ever used as evidence." + }, + { + "company": "Google", + "role": "Software Engineer III, Business Home", + "application_date": "2026-07-27", + "fit_class": "core", + "evidence_fit": 89.0, + "hard_gate": "pass", + "channel": "weak", + "outcome": "rejected_no_interview", + "id": "app-767097db424a", + "label": "Google - Software Engineer III, Business Home, Zurich (req 93922217108087494)", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED - NOT PROCEEDING 2026-07-28", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED - NOT PROCEEDING 2026-07-28** (submitted 2026-07-27; Core, Evidence Fit 89/100, Document Quality 94/100; no interview). Rapid early-screen disposition; known risks were Mid-level versus current Staff scope, data/platform versus product-SWE positioning, preferred algorithms/accessibility gaps, and a cold channel.", + "legacy_next_action": "Done - cohort rejection recorded; do not treat this outcome alone as a document-quality failure" + }, + { + "type": "applied", + "date": "2026-07-27" + }, + { + "type": "rejected_no_interview", + "date": "2026-07-28" + } + ], + "cohort_id": "evidence-first-2026-01" + }, + { + "id": "app-4b478d0db972", + "label": "Microsoft — Principal Forward Deployed Engineer, SWE (German Speaking), Zürich (req 200043897)", + "company": "Microsoft", + "role": "Microsoft — Principal Forward Deployed Engineer, SWE (German Speaking), Zürich (req 200043897)", + "outcome": "applied", + "next_action": "Done — await response", + "current_summary": "SUBMITTED 2026-07-27", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**SUBMITTED 2026-07-27** (84.2/100 Pass 2; finalized 2-page resume + 1-page cover letter). Strong native-German, Staff progression, production ownership and enterprise-data-readiness case; honest gaps remain in end-to-end LLM delivery, Azure AI and strategic-account FDE experience.", + "legacy_next_action": "Done — await response" + } + ] + }, + { + "id": "app-7c94f6a9d318", + "label": "BIS Basel — Senior Data & Analytics Engineer, AI (jr100429, 3-yr term)", + "company": "BIS Basel", + "role": "BIS Basel — Senior Data & Analytics Engineer, AI (jr100429, 3-yr term)", + "outcome": "rejected_no_interview", + "next_action": "Abgeschlossen: Absage erhalten; keine weitere Aktion offen.", + "current_summary": "Absage am 2026-09-11 vom Nutzer gemeldet. Im bisherigen Verlauf ist kein Interview dokumentiert; ein Absagegrund wurde nicht genannt.", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**SENT 2026-07-10** (80.5/100; deadline 2026-07-24; hybrid Basel, English-working intl org). Critique flags LLM-depth probe (integration/config vs. ownership) as the screening risk — prep honest answer before any call", + "legacy_next_action": "Prep interview brief when screening lands" + }, + { + "type": "rejected_no_interview", + "date": "2026-09-11", + "note": "Absage am 2026-09-11 vom Nutzer gemeldet. Im bisherigen Verlauf ist kein Interview dokumentiert; ein Absagegrund wurde nicht genannt.", + "outcome": "rejected_no_interview" + } + ] + }, + { + "id": "app-b6b86e70c1c1", + "label": "NATO JWC Stavanger — Staff Officer, AI Engineer (2030 Digitalisation, G15, 3-yr PLN)", + "company": "NATO JWC Stavanger", + "role": "NATO JWC Stavanger — Staff Officer, AI Engineer (2030 Digitalisation, G15, 3-yr PLN)", + "outcome": "applied", + "next_action": "Await screening; no action until contact", + "current_summary": "SENT 2026-07-10", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**SENT 2026-07-10** (77.5/100; deadline 2026-08-09; final interviews 2nd half Oct 2026). Borderline screen: Azure-cert \"or equivalent\", LLM depth, no current clearance; German national + officer service are the legibility assets. Tax-free NOK 93,933/mo + allowances", + "legacy_next_action": "Await screening; no action until contact" + } + ] + }, + { + "id": "app-767a898d5119", + "label": "Microsoft — Senior SWE, Industry Solutions Engineering (ISE), Zürich (req 200040836)", + "company": "Microsoft", + "role": "Microsoft — Senior SWE, Industry Solutions Engineering (ISE), Zürich (req 200040836)", + "outcome": "rejected_no_interview", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — REJECTED 2026-08-11", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — REJECTED 2026-08-11** (submitted 2026-07-03, 85.8/100 Pass 2; 2pp resume + 1pp CL; verbatim Eightfold JD; IC4 base CHF 146.2–245.9k; applied ~7 days after posting). No interview, ~39 days to disposition. Cold-channel application; does not implicate document quality on its own. The separate Microsoft **Principal FDE** req 200043897 is a different pipeline and remains open", + "legacy_next_action": "Done — cohort rejection recorded" + }, + { + "type": "rejected_no_interview", + "date": "2026-08-11" + } + ] + }, + { + "id": "app-54a40d825b99", + "label": "Kraken (Payward) — SRE, AI Agents (remote, CH-eligible)", + "company": "Kraken (Payward)", + "role": "Kraken (Payward) — SRE, AI Agents (remote, CH-eligible)", + "outcome": "rejected_no_interview", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — REJECTED 2026-06-17", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — REJECTED 2026-06-17** (applied 2026-06-15 ~87.2/100, no interview). Honest gaps (NO Terraform/SRE-title/LangGraph) likely the filter; 4th Kraken req declined/rejected to date", + "legacy_next_action": "Done" + }, + { + "type": "rejected_no_interview", + "date": "2026-06-17" + } + ] + }, + { + "id": "app-cca2669274dd", + "label": "Google — Senior Data Engineer (Merchant Data Science), Zürich/MV", + "company": "Google", + "role": "Google — Senior Data Engineer (Merchant Data Science), Zürich/MV", + "outcome": "rejected_no_interview", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — NOT PROCEEDING 2026-07-24", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — NOT PROCEEDING 2026-07-24** (applied 2026-06-15, 85.5/100; passed Google Hiring Assessment 2026-06-20, no interview). Assessment pass remains recorded separately. The official 90-day wait applies only to reapplying for the same job; different Google roles remain eligible, subject to 3 applications per rolling 30 days.", + "legacy_next_action": "Done — target a different strong-fit Google req" + }, + { + "type": "rejected_no_interview", + "date": "2026-07-24" + } + ] + }, + { + "id": "app-c86f83521a8f", + "label": "Snowflake — Sr SWE, Enterprise (Observe by Snowflake), Zürich", + "company": "Snowflake", + "role": "Snowflake — Sr SWE, Enterprise (Observe by Snowflake), Zürich", + "outcome": "closed_no_response", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — NO RESPONSE 2026-08-25", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — NO RESPONSE 2026-08-25** (sent 2026-06-06, ~86/100; 2pp resume + 1pp CL; real Ashby JD; comp CHF 176–253k base; NO C++ gate). Closed by user decision at **80 days silent** — past the longest real disposition in the log (Equinor, 79 days). **No rejection was ever received: this is a presumed-dead call, not a rejection — do not count it in rejection statistics or treat it as a document-quality signal.** Cold channel. Pre-dates the evidence-first-2026-01 cohort (started 2026-07-27), so **no cohort slot freed**. Highest-scoring package in the log at ~86 and it drew no reply at all — the strongest single argument in the log for the channel problem over the document problem. **Unblocks the Snowflake Observe Metrics Platform req** (`db4f0492`), paused 2026-07-28 solely to avoid stacking a second cold application on the same org while this one was open", + "legacy_next_action": "Done — if Snowflake is still wanted, the Metrics Platform req is now open to pursue, but route it through a recruiter rather than another cold submit" + } + ] + }, + { + "id": "app-899a050e07df", + "label": "Isovalent (Cisco) Sr Data Engineer, Observability", + "company": "Isovalent (Cisco) Sr Data Engineer, Observability", + "role": "Isovalent (Cisco) Sr Data Engineer, Observability", + "outcome": "role_closed", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — role pulled", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — role pulled** (live Cisco scrape 2026-06-02: not on board; Recruitee link dead). Package finalized ~86/100, SHELVED for reuse", + "legacy_next_action": "Done — retarget PDFs to next live data-eng req (QuantCo/Grafana/Confluent)" + } + ] + }, + { + "id": "app-7331db5903c0", + "label": "Google Zürich Sr SWE Infrastructure (Data Pipeline)", + "company": "Google Zürich Sr SWE Infrastructure (Data Pipeline)", + "role": "Google Zürich Sr SWE Infrastructure (Data Pipeline)", + "outcome": "not_proceeding", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — DROPPED + DELETED 2026-06-02", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — DROPPED + DELETED 2026-06-02** (poor fit). Live JD = Core infra/systems SWE with **C++ as a MINIMUM qual**, off-thesis vs `user_positioning`. Output folder deleted (was built on a fabricated JD).", + "legacy_next_action": "Done — do not reattempt this req" + } + ] + }, + { + "id": "app-d2c54a5d0a90", + "label": "Kraken AI Infrastructure", + "company": "Kraken AI Infrastructure", + "role": "Kraken AI Infrastructure", + "outcome": "rejected_no_interview", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — REJECTED", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — REJECTED** (applied, no interview)", + "legacy_next_action": "Done" + }, + { + "type": "rejected_no_interview", + "date": null + } + ] + }, + { + "id": "app-56b2e2bf9319", + "label": "Infineon Doctoral", + "company": "Infineon Doctoral", + "role": "Infineon Doctoral", + "outcome": "withdrawn_by_candidate", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — withdrew", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — withdrew** (got interview invite, declined: no relocation to Germany)", + "legacy_next_action": "Done" + }, + { + "type": "interview_invited", + "date": null + } + ] + }, + { + "id": "app-e559939c6a59", + "label": "Infineon AI Engineer", + "company": "Infineon AI Engineer", + "role": "Infineon AI Engineer", + "outcome": "not_proceeding", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — not applied", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — not applied** (no relocation to Germany)", + "legacy_next_action": "Done" + } + ] + }, + { + "id": "app-3f0d4f280d47", + "label": "Apple Data Engineer (ISE, Zurich)", + "company": "Apple Data Engineer (ISE, Zurich)", + "role": "Apple Data Engineer (ISE, Zurich)", + "outcome": "rejected_no_interview", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — REJECTED", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — REJECTED** (no interview)", + "legacy_next_action": "Done" + }, + { + "type": "rejected_no_interview", + "date": null + } + ] + }, + { + "id": "app-78b71cc7208c", + "label": "Google FDE GenAI (Zurich)", + "company": "Google FDE GenAI (Zurich)", + "role": "Google FDE GenAI (Zurich)", + "outcome": "paused", + "next_action": "Likely abandon", + "current_summary": "PAUSED — GenAI evidence gap too large; redirecting to data-eng/MLOps roles", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "PAUSED — GenAI evidence gap too large; redirecting to data-eng/MLOps roles", + "legacy_next_action": "Likely abandon" + } + ] + }, + { + "id": "app-0e0744827fda", + "label": "Equinor — Manager / AI Architect, Agentic systems (Stavanger/Oslo/Rotvoll/Sandsli; req JR106747)", + "company": "Equinor", + "role": "Equinor — Manager / AI Architect, Agentic systems (Stavanger/Oslo/Rotvoll/Sandsli; req JR106747)", + "outcome": "rejected_no_interview", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — REJECTED 2026-08-14", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — REJECTED 2026-08-14** (submitted 2026-05-27, ~80/100; posting deadline 05.06.2026). No interview, **79 days** to disposition — the longest turnaround recorded. Cold channel. This was a **Manager** req, not a pure IC architect role, so level/scope stretch compounds the standing Norway per-role gates (norsk working-language, no NO clearance — **not** comp: the 180k bar is CH-only and does not apply to Norway). **Pre-dates the evidence-first-2026-01 cohort (started 2026-07-27) — does not free a cohort slot.** **The Norway lane stays OPEN but selective** — Equinor, Telenor and NATO JWC remain in the scout; the user has explicitly said he still wants to reach out for very good Norwegian roles, Equinor included, after this rejection. Never frame this lane as closed.", + "legacy_next_action": "Done — cohort-external rejection recorded" + }, + { + "type": "rejected_no_interview", + "date": "2026-08-14" + } + ] + }, + { + "id": "app-2bf0a4c5a55f", + "label": "QuantCo Cloud Engineer (Europe/Zürich)", + "company": "QuantCo Cloud Engineer (Europe/Zürich)", + "role": "QuantCo Cloud Engineer (Europe/Zürich)", + "outcome": "rejected_no_interview", + "next_action": "Closed; no pending application action. Read recorded history before reopening.", + "current_summary": "CLOSED — REJECTED", + "events": [ + { + "type": "legacy_status_import", + "recorded_at": "2026-09-11", + "source": "CLAUDE.md / application_cohort.json", + "details": "**CLOSED — REJECTED** (applied 2026-06-01 ~82/100, no interview; rejection 2026-06-15)", + "legacy_next_action": "Done" + }, + { + "type": "rejected_no_interview", + "date": null + } + ] + } + ], + "targeting_notes": [ + "SBB Niveau K/L is SBB-specific. The reported approximate CHF 145k K ceiling is not a salary anchor for other employers.", + "Assess headroom and total package: working time, holidays, office days, commute and verified benefits. Never decline a Thun-area role on salary band alone.", + "Childcare-cost coverage at the Swiss Army/Kdo Cy remains unverified; confirm before pricing it.", + "Norway remains open selectively; the Swiss compensation threshold does not automatically apply there.", + "An interview does not prove how an ATS or recruiter evaluated the application. No causal inference from a single conversion." + ] +} diff --git a/job_scout/state/decisions.json b/job_scout/state/decisions.json index 14456a8..5a9b9df 100644 --- a/job_scout/state/decisions.json +++ b/job_scout/state/decisions.json @@ -468,13 +468,6 @@ "note": "Claude judgment 2026-07-06: trading-desk dealer role; you want platform/data-eng inside trading, not the trading seat", "date": "2026-07-06" }, - "https://www.bis.org/careers/vacancies/jr100429.htm": { - "company": "BIS (Basel)", - "title": "Senior Data & Analytics Engineer – Artificial Intelligence", - "decision": "applied", - "note": "Applied 2026-07-10 (80.5/100 package). Was shortlisted 2026-07-06: core data-eng + spelled-out AI angle; BIS Basel is an explicit international-org lane in your target list", - "date": "2026-07-10" - }, "https://job.bkw.com/offene-stellen/business-controller-energy-mobility-telecom-m-w-d/447ae323-707b-4145-bbbe-91f8d4b32f7b": { "company": "BKW (Bern)", "title": "Business Controller Energy, Mobility & Telecom (m/w/d)", @@ -2425,5 +2418,12 @@ "decision": "skip", "note": "USER DECISION 2026-09-11: overqualified. The requirement floor is HF/FH or Techniker HF - no university degree is asked for at all - and he holds an M.Eng. EVIDENCE FOR THE LEVEL READ, from Kdo Cy own postings on the same board: LK 24 roles all require a Hochschul-/Fachhochschul-/Universitaetsabschluss (ICT-Architekt III, Cyber Security Advisor, Portfolio-Manager, Systemingenieur elektronische Operationen), LK 26 requires a Studium at FH/UNI/ETH, and LK 18 roles require an Informatik-EFZ. This posting sits between EFZ and the degree tier, so roughly LK 20-22; its own Lohnklasse field said \"Keine Angabe\". Against ~135k that is very likely a pay cut - EPA Lohntabelle 2026 maxima: LK 20 131,870 / LK 21 137,341 / LK 22 142,832, and those are CEILINGS, not entry salaries. NOT A CAPABILITY REJECTION - the opposite. Technically this was the cleanest fit in the log for a DevOps/platform role: Ansible required (BS-6 intensive, incl. self-written plugins), Kubernetes, CI/CD, IaC, observability with Prometheus/Grafana/ELK (BS-4) - and NO networking line, the gap that killed RUAG Principal System Engineering, RUAG System Engineer C5I and the NVIDIA hard gate. The gaps were vendor substitutions only: RHEL vs Debian, OpenShift vs Kubernetes, AAP vs Ansible. IF A SIMILAR POSTING APPEARS AT A HIGHER LEVEL, TAKE IT SERIOUSLY. Screen jobs.admin.ch roles on the education floor first: it predicts the Lohnklasse better than the title does. Kdo Cy as an organisation is NOT closed - the DevOps Engineer III rejection of 2026-09-10 and this skip are two reqs, not a verdict on the employer. Deadline was 2026-10-14.", "date": "2026-09-11" + }, + "https://www.bis.org/careers/vacancies/jr100429.htm": { + "company": "BIS (Basel)", + "title": "Senior Data & Analytics Engineer – Artificial Intelligence", + "decision": "rejected", + "note": "Applied 2026-07-10. Rejection received 2026-09-11, reported by user. No interview recorded; rejection reason not provided. Application closed.", + "date": "2026-09-11" } } \ No newline at end of file diff --git a/job_scout/state/history/application_status_before_2026-09-11.json b/job_scout/state/history/application_status_before_2026-09-11.json new file mode 100644 index 0000000..1162aaf --- /dev/null +++ b/job_scout/state/history/application_status_before_2026-09-11.json @@ -0,0 +1,118 @@ +{ + "agent_sections": { + "AGENTS.md": "\n\n_Update this section when starting/finishing a JD._\n\n| Session | Status | Next Command |\n|---------|--------|-------------|\n| NVIDIA - **Senior DevOps Engineer**, **Zürich** (req JR2024256) | **SUBMITTED 2026-09-05.** Evidence Fit **64/100**, fit class **Stretch**, hard gate **FAIL x2**, channel **Weak** (7th consecutive cold submit; this posting published no named contact). 2pp resume 13 bullets + 1pp cover letter 279 words; both validators PASS 0 warnings, 0 boxes, 0 Tier 1 findings open. Measured Document Quality **82/100 pre-edit**; both Tier 1 findings were then closed, so it is better than 82 but **not re-measured - never quote a post-edit estimate as a score**. Submitted on an explicit user override. ⚠ **Hybrid vs 5-day onsite was never resolved before sending** - now a live screening topic, not pre-cleared. Thun→Zürich is ~1h50 each way. Comp is *not* a risk (levels.fyi Greater Zürich IC3 median ≈ CHF 204k). Honest gaps named in the letter: on-prem networking (0 canonical) and that employer-side Linux/Ansible ended Dec 2022, with only the personal `PP-3` server carrying currency. | Await response. **Raise the working model at first contact** - a 5-day-onsite seat would be a late no. |\n| SBB - **Data Engineer (m/w/d)**, Asset Management Infrastrukturanlagen, **Bern** (Job ID 103755) | ⭐ **INTERVIEW INVITED 2026-08-28 - 9. September 2026, 08:30, 45 Min, MS Teams. The only live interview in the log.** Stage **2 of 4** per the posting: *\"Virtuelles Kennenlernen mit HR und Fuehrungskraft\"*, so **HR together with Andri Wienandts**, not a pure HR screen. Brief: `output/SBB_DataEngineer_AssetMgmt/interview_brief_sbb_2026-09-09.md`. **First conversion of the evidence-first cohort** (6 applications, 2 rejections) and it came from a **COLD submit** - the warm contact printed on the posting was never called. The resume never contained **Snowflake, dbt or Power BI**, the literals an ATS screen would key on, and it still got through: a human read it and the honest-substitution framing held. One data point, do not over-read it. Submitted 2026-08-25, Core, Evidence Fit **79/100**, hard gate PASS, Document Quality **92/100**, no cover letter (SBB waives it). Best practical fit in the log: Bern, German-language, no relocation, EU + B permit. **Four unresolved items, now live call topics:** Anforderungsniveau **K** band (capped GAV, not public, likely below the 180k bar); the seat reads **lateral or below** current Staff + Component Owner scope (the declined-BKW shape); **RAMSI** Java/Spring Boot/Angular is a full responsibility line on a historical/absent stack; Kidz Care scales on gross household income. Gaps to answer honestly: Snowflake, dbt, Argo, Helm, Power BI, Spring Boot, Angular - survivable only because the JD says \"z. B.\". Thesis stays a **methods prototype**: no operational data, no accuracy figures, PSO surveyed only. | **Decide before 9.9.:** what he does if K lands at 130-150k, if architecture is set elsewhere, or if RAMSI is heavily Angular. Any one can be a no - better decided in advance than improvised on camera. |\n| RUAG C5I - **AI Engineer C5I**, **Thun** (application ID 18027) | **SUBMITTED 2026-08-27** via jobs.ruag.ch (portal only). Evidence Fit **74/100**, fit class **Stretch**, hard gate **FAIL** (R1/R2, the title-defining responsibilities, are Adjacent not Direct), Document Quality **93/100** after a critique-and-fix round, channel **Weak/cold**. German CV 2 pages/13 bullets + motivation letter 1 page/295 words; validators PASS, 0 boxes, visual QA clean. (Cohort slot language here is historical: the **7/2/1 caps were dropped 2026-09-05** - see `application_strategy.md`.) Positioning is his canonical title Staff Data, Analytics & AI Engineer; `PP-3` carries current Linux/hardening evidence and `SW-5` ties DevSecOps to C5I's stated model. The letter names the AI-platform gap in one sentence. Honest gaps: no AI/LLM platform, no compute/GPU cluster, no RAG/retrieval, ML/AI frameworks certification-context only. **Compensation, PSP/project eligibility and level were never resolved and are now live screening topics.** Marco Heinzen's published direct line was never used. **If rejected, do not drop RUAG** - **Senior DevOps Engineer C5I** is shortlisted and fits better (still live on the board 2026-09-04). The **Data Lakehouse / Senior Data Platform Engineer** req was a title-match prior the JD does not support: reposted 2026-09-04 as **Senior Data Engineer (MSSQL)**, requiring on-prem MSSQL/Talend/SAP S/4HANA - 0 canonical each - and **ruled out**. | Await response; optional calls to Marco Heinzen (scope/level) and Frank Haugwitz (comp/PSP). |\n| Schweizer Armee - **Kommando Cyber (Kdo Cy)**, DevOps Engineer III (Data Platform), **Zimmerwald** (Ref JRQ$540-19848) | **SUBMITTED 2026-08-27.** Evidence Fit **79/Core**, hard gate **PASS**, Document Quality **92/100**, channel **Weak**. German 2-page CV plus 1-page motivation letter; validators PASS, 0 boxes, visual QA clean. BW-1 surfaces the six-year Bundeswehr officer career. User explicitly accepted the critique's remaining BS-1 wording issue (*„ohne manuellen Eingriff“*) as good enough and submitted unchanged. Cohort evidence-first-2026-01 is now 5/10, Core 3/7. | Await response; optional post-submit call to Marcel Matthey-Doret about Lohnklasse, Engineer-III level and development path. |\n| Google - Software Engineer III, Business Home, Zurich (req 93922217108087494) | **CLOSED - NOT PROCEEDING 2026-07-28** (submitted 2026-07-27; Core, Evidence Fit 89/100, Document Quality 94/100; no interview). Rapid early-screen disposition; known risks were Mid-level versus current Staff scope, data/platform versus product-SWE positioning, preferred algorithms/accessibility gaps, and a cold channel. | Done - cohort rejection recorded; do not treat this outcome alone as a document-quality failure |\n| Microsoft — Principal Forward Deployed Engineer, SWE (German Speaking), Zürich (req 200043897) | **SUBMITTED 2026-07-27** (84.2/100 Pass 2; finalized 2-page resume + 1-page cover letter). Strong native-German, Staff progression, production ownership and enterprise-data-readiness case; honest gaps remain in end-to-end LLM delivery, Azure AI and strategic-account FDE experience. | Done — await response |\n| BIS Basel — Senior Data & Analytics Engineer, AI (jr100429, 3-yr term) | **SENT 2026-07-10** (80.5/100; deadline 2026-07-24; hybrid Basel, English-working intl org). Critique flags LLM-depth probe (integration/config vs. ownership) as the screening risk — prep honest answer before any call | Prep interview brief when screening lands |\n| NATO JWC Stavanger — Staff Officer, AI Engineer (2030 Digitalisation, G15, 3-yr PLN) | **SENT 2026-07-10** (77.5/100; deadline 2026-08-09; final interviews 2nd half Oct 2026). Borderline screen: Azure-cert \"or equivalent\", LLM depth, no current clearance; German national + officer service are the legibility assets. Tax-free NOK 93,933/mo + allowances | Await screening; no action until contact |\n| Microsoft — Senior SWE, Industry Solutions Engineering (ISE), Zürich (req 200040836) | **SENT 2026-07-03** (85.8/100 Pass 2; 2pp resume + 1pp CL; verbatim Eightfold JD; IC4 base CHF 146.2–245.9k; applied ~7 days after posting). Same-day build-to-critique-to-submit. Decisions.json logged as applied | Done — await response |\n| Kraken (Payward) — SRE, AI Agents (remote, CH-eligible) | **CLOSED — REJECTED 2026-06-17** (applied 2026-06-15 ~87.2/100, no interview). Honest gaps (NO Terraform/SRE-title/LangGraph) likely the filter; 4th Kraken req declined/rejected to date | Done |\n| Google — Senior Data Engineer (Merchant Data Science), Zürich/MV | **CLOSED — NOT PROCEEDING 2026-07-24** (applied 2026-06-15, 85.5/100; passed Google Hiring Assessment 2026-06-20, no interview). Assessment pass remains recorded separately. The official 90-day wait applies only to reapplying for the same job; different Google roles remain eligible, subject to 3 applications per rolling 30 days. | Done — target a different strong-fit Google req |\n| Snowflake — Sr SWE, Enterprise (Observe by Snowflake), Zürich | **SENT 2026-06-06** (~86/100; 2pp resume + 1pp CL; real Ashby JD; comp CHF 176–253k base; NO C++ gate). Tier 1+2 applied; Vizrt low-latency skipped per user. Best-fit role in the 2026-06 search | Done — await response |\n| Isovalent (Cisco) Sr Data Engineer, Observability | **CLOSED — role pulled** (live Cisco scrape 2026-06-02: not on board; Recruitee link dead). Package finalized ~86/100, SHELVED for reuse | Done — retarget PDFs to next live data-eng req (QuantCo/Grafana/Confluent) |\n| Google Zürich Sr SWE Infrastructure (Data Pipeline) | **CLOSED — DROPPED + DELETED 2026-06-02** (poor fit). Live JD = Core infra/systems SWE with **C++ as a MINIMUM qual**, off-thesis vs `user_positioning`. Output folder deleted (was built on a fabricated JD). | Done — do not reattempt this req |\n| Kraken AI Infrastructure | **CLOSED — REJECTED** (applied, no interview) | Done |\n| Infineon Doctoral | **CLOSED — withdrew** (got interview invite, declined: no relocation to Germany) | Done |\n| Infineon AI Engineer | **CLOSED — not applied** (no relocation to Germany) | Done |\n| Apple Data Engineer (ISE, Zurich) | **CLOSED — REJECTED** (no interview) | Done |\n| Google FDE GenAI (Zurich) | PAUSED — GenAI evidence gap too large; redirecting to data-eng/MLOps roles | Likely abandon |\n| Equinor AI Architect (Norway) | **SENT** (~80/100) | Done — await response |\n| QuantCo Cloud Engineer (Europe/Zürich) | **CLOSED — REJECTED** (applied 2026-06-01 ~82/100, no interview; rejection 2026-06-15) | Done |\n\n---\n\n", + "CLAUDE.md": "\n\n_Update this section when starting/finishing a JD._\n\n| Session | Status | Next Command |\n|---------|--------|-------------|\n| NVIDIA - **Senior DevOps Engineer**, **Zürich** (req JR2024256) | **SUBMITTED 2026-09-05.** Recorded in the application record (now 7 entries) and in the scout decision log. ⚠ **Hybrid vs 5-day onsite was never resolved before sending**, so the working model is now a **live screening topic**, not a pre-cleared one - the same pattern as RUAG's comp/PSP. Thun→Zürich is ~1h50 each way; a 5-day-onsite answer is the main disqualifier to watch for. Comp is *not* a risk (levels.fyi Greater Zürich IC3 median ≈ CHF 204k, IC4 ≈ CHF 379k). Channel **Weak - the 7th consecutive cold submit**, though this posting published no named contact, so cold was the only option available. Submission PDFs `Dennis_Thiessen_Resume.pdf` and `Dennis_Thiessen_Cover_Letter.pdf` exist and are MD5-verified against source; letter recompiled so its date reads 5 Sep 2026. ⚠ **The letter carries `\today` and the submission copies are copies, not build targets - if it is sent on a later day, recompile the letter and refresh both copies first** (`config.md` Output Rules). Submitted despite the hard-gate failures on an explicit user override, the same shape as RUAG AI Engineer C5I. Resume 2pp/13 bullets + 1pp cover letter (279 body words), both validators PASS 0 warnings, 0 boxes, visually inspected. Measured **Document Quality 82/100** at critique; **both Tier 1 findings were fixed 2026-09-05** - `Ubuntu` removed from the skills line (0 canonical; `PP-3` is Debian only, and the Debian→Ubuntu transfer argument belongs in an interview answer, not a skills line) and `security hardening` now marked *(personal server)* per the explicit `claims.json` instruction. Four honest ATS terms were added from existing evidence (configuration management, service reliability, container orchestration, observability), lifting JD term coverage **50%→61%**; no bullet was touched, so the budget stayed at 13 and cadence could not regress. **The critique is now STALE BY DESIGN - the document is better than 82 but that number was not re-measured, so never quote a post-edit estimate as a score.** The headline reorder was deliberately NOT applied: it is a positioning call for the user and `\\headline` sits in the FIXED header block. Cover-letter hook is first-party verified (NVIDIA's own live Zürich posting JR2022878); **a hook was DROPPED at verification** - the Phase 0 plan's \"Alps / Swiss AI Initiative with ETH Zürich and EPFL\" is **not** supported by the first-party NVIDIA newsroom release, which mentions neither EPFL nor any Swiss AI Initiative; that came from secondary coverage. The letter names **both** honest gaps out loud - on-prem networking, and that employer-side Linux/Ansible ended Dec 2022 with only a personal server carrying currency. **⚠ BLOCKING: the working model is still unresolved and no document fix changes that.** Session `output/NVIDIA_SrDevOps_Zurich/session_nvidia_sr_devops.md`; JD scraped live via Playwright and saved verbatim (posted ~2026-09-01, very fresh). Evidence Fit **64/100**, fit class **Stretch**, **hard gate FAIL on two counts**: (1) required line *\"Strong expertise in on-premises networking and compute infrastructure\"* is a **Gap** - networking is 0 canonical and is not substitutable from data-platform work; (2) **hybrid vs onsite is UNRESOLVED** and the JD is silent - Thun→Zürich is ~1h50 each way. Required tally **3 Direct / 3 Adjacent / 1 Gap**. Recorded as an override rather than a passing gate, the same shape as RUAG AI Engineer C5I. (At the time this also had no free cohort slot; **that constraint no longer exists** - caps dropped 2026-09-05.) **Correction to the same day's scout triage: it called two required lines Gaps; only Q5b networking is one. Q2 (10+ yrs DevOps) is Adjacent and defensible** - a continuous CI/CD/automation thread runs Generali 2015 → today across five employers; the honest weakness is that those roles were titled data/test/research engineer, not DevOps. **The finding that changes the calculus: comp.** levels.fyi Greater Zürich puts NVIDIA IC3 median ≈ CHF 204k and IC4 ≈ CHF 379k, the highest-paying SWE employer in the city - the first role in the log where comp is a reason to *accept* a long commute rather than decline. Caveat: self-reported and equity-heavy after an extraordinary NVDA run; treat base as the reliable part. Strongest match is **R3, CI/CD pipelines for R&D engineering teams** - literally his career shape (`FC-1` Jenkins at a Fraunhofer institute, `BS-6` Ansible/Jenkins for fab analytics teams, `SW-3` Kubernetes + GitLab today). Zürich is NVIDIA's HPC/AI infrastructure-software R&D hub - first-party verified from NVIDIA's own live Zürich posting JR2022878 (*\"building the next generation of scalable AI infrastructure ... distributed training, real-time inference, and communication efficiency across large systems\"*), which is the hook the letter uses. **The Alps / Swiss AI Initiative framing was checked and dropped** - see above. **Honest gaps that travel with it:** on-prem networking (0), HPC (0), bare metal/switches/QEMU-KVM (0), RHEL/CentOS (0 - Debian→Ubuntu transfers, Red Hat does not), and **Ansible + Linux-at-work both ended Dec 2022** with currency carried only by `PP-3`, a personal server whose canonical entry forbids using it as physical-infrastructure evidence. No metric is quotable anywhere. | **Await response. Raise the working model at first contact** - it went in unresolved and a 5-day-onsite seat would be a late no. Note: `claims.json` has no shell-scripting entry, so **Bash is omitted** from Skills although the JD names it and ten years of Debian admin plus Ansible work plainly involve it - same shape as the Ansible/Linux gaps found 2026-08-27. Confirm with the user and record it. Groovy stays out regardless. |\n| RUAG C5I - **AI Engineer C5I**, **Thun** (application ID 18027) | **SUBMITTED 2026-08-27** via jobs.ruag.ch (portal only; email and post refused). Evidence Fit **74/100**, fit class **Stretch**, hard gate **FAIL** (R1 and R2, the two title-defining responsibilities, are Adjacent not Direct), Document Quality **93/100** after a full critique-and-fix round (85 pre-edit), truth and provenance 24/25, **0 Tier 1 findings at Round 2**, channel **Weak/cold**. Submitted: German CV **2 pages, 13 bullets** + German motivation letter **1 page, 295 words**; both validators PASS, 0 boxes, text-order and visual QA clean, submission PDFs MD5-verified before sending. **Cohort evidence-first-2026-01 is now 6/10 and the sole Stretch slot is CONSUMED (Core 3/7, Adjacent 2/2, Stretch 1/1)** - any further application must clear Core or Adjacent on its own merits. Positioning is his canonical title **Staff Data, Analytics & AI Engineer**, carried by `BS-1` production ML-inference integration, `BS-6` Linux/Ansible, `SW-3`/`SW-7`/`SW-2` current Kubernetes and data-product work, `PP-3` current self-hosted Linux and hardening, `SW-5` DevSecOps tied to C5I's stated operating model, and the canonical `BW-1` officer career. The letter **names the AI-platform gap in one sentence** and pivots to the operations/platform side. **Honest gaps that travel with it:** no AI/LLM platform built or operated, no compute/GPU cluster, no RAG/retrieval implementation, no formal LLM evaluation; ML/AI-framework depth is certification-context only. **Compensation, PSP/project eligibility and role level were NEVER resolved before sending, so all three are now live screening topics rather than pre-cleared ones.** ⚠ **Marco Heinzen's published direct line (+41 79 568 14 96) was never used** - this went in as a pure cold submit, the sixth consecutive one. **If rejected, that is NOT a signal to drop RUAG:** five other RUAG C5I reqs sit in the scout log, and **Senior DevOps Engineer C5I** is already shortlisted and is a **materially better fit than this one**, sitting on the lane where his evidence is Direct rather than Adjacent (still live and verified on the board 2026-09-04). The **Data Lakehouse / Senior Data Platform Engineer** req was a title-match prior that the JD does not support — it was reposted 2026-09-04 as **Senior Data Engineer (MSSQL)**, requires on-prem MSSQL / Talend / SAP S/4HANA (0 canonical each), and is **ruled out**. **Scout run 2026-09-07: the RUAG C5I backlog is now SETTLED — undecided RUAG rows went 69 → 3** (`ICT Architect`, `Software Engineer C5I`, `Cyber Security Consultant` remain open; `ICT Architect C5I` stays `paused`). Do not re-triage that board from scratch; read `decisions.json`, where every rule-out note states whether it came from a JD read or from the title/family pattern. Three JD reads that cycle all ruled out: **Test Manager C5I** (the closest call — it escapes the networking gap entirely and BW-1 scores, but required *\"fundierte Erfahrung als Test Engineer\"* rests on `VIZRT`, **ten months, ended 2018-05**, so it is thin and stale, plus a lane exit and a third C5I req while 18027 is pending), **System Engineer C5I** (VMware + Firewall + Netzwerk, all 0), and **ICT Solution Architect Business Applications** (required *\"mehrjährige Erfahrung als Solution Architect\"* — a title `feedback_swisscom_datamesh_ownership` forbids — and the duties are *\"unter Anleitung\"*, a supporting seat below authoring). | Await response. Optional but still worthwhile: call **Marco Heinzen** about technical scope and level, and **Frank Haugwitz** about compensation and PSP/project eligibility. **NEW CONTACT 2026-09-07 — `Lars Kauz`, Head of ICT Infrastructure C5I, +41 79 293 93 82, lars.kauz@ruag.ch**, published on the System Engineer C5I posting. Channel is the rationed variable since the 09-05 cohort change, so a published, reachable name matters — **but he heads ICT Infrastructure, the function whose req was ruled out for VMware/VDI/Windows content, and that is NOT demonstrably the Kubernetes platform group Senior DevOps C5I sits in.** He is a warm entry into C5I, not a verified channel for rank 1; Haugwitz stays the recorded gate. Do not react to a single outcome with positioning changes (`application_strategy.md` §38/§74). |\n| Schweizer Armee - **Kommando Cyber (Kdo Cy)**, DevOps Engineer III (Data Platform), **Zimmerwald** (Ref JRQ$540-19848) | **CLOSED - REJECTED 2026-09-10** (submitted 2026-08-27, **14 days to disposition**, no interview - the fastest in the log after Google Business Home). Form letter from **Besarta Mehmeti**; the text is boilerplate (*„Profil und unsere Kriterien nicht in allen Punkten übereinstimmen\"*) and carries **zero diagnostic content** - it is not a signal about the package and is **no reason to re-audit it**. Same disposition class as Microsoft ISE, Google Business Home and Aker BP: **a cold-channel no-interview rejection does not implicate document quality on its own.** ⚠ **WORTH WATCHING, NOT CONCLUDING:** this application and **SBB** are identical on every recorded dimension - Core, Evidence Fit **79**, hard gate PASS, Document Quality **92**, channel Weak - and they went opposite ways: SBB converted to an interview, this died in 14 days. **That shows the fit score did not discriminate; it does not show what did.** n=1 on each side - do not react with positioning changes (`application_strategy.md` §38/§74). **Channel:** Marcel Matthey-Doret's published line (+41 58 46 78409) was never called - the **fourth** published, reachable contact left unused after Heinzen (RUAG), Wienandts (SBB) and Marthinsen (Aker BP). Consistent with the 2026-09-05 channel policy but **not evidence for it**: the only interview in the log came from a *cold* submit whose resume was missing all three ATS literals. **Kdo Cy as an organisation is NOT closed** - 11 Kdo Cy roles were open on the portal at submission time and only this req is decided. Evidence Fit **79/Core**, hard gate **PASS**, Document Quality **92/100**, channel **Weak**. Submitted package: German 2-page CV with **15 bullets** and 1-page motivation letter with **300 body words**; validators PASS, 0 boxes, text order and visual QA clean. Canonical **BW-1** records the six-year Bundeswehr officer career and anchors the letter's military/public-service motivation. User explicitly accepted the critique's remaining BS-1 wording issue (*„ohne manuellen Eingriff“*) as good enough and submitted unchanged. Honest gaps remain physical datacenter work, GPGPU, ClickHouse and named Pull-GitOps practice. Cohort evidence-first-2026-01 is now **5/10, Core 3/7** *(pre-dates the 2026-09-05 cap drop; counts are a record, not a budget)*. | Done - rejection recorded in `application_cohort.json`, `decisions.json` and the session file. Optional and low-cost: a one-line feedback request to **Besarta Mehmeti**, which also builds a contact at an org with other open reqs. |\n| Citadel Securities — Platform Engineer, Research Platform Engineering, **Zürich** (also NY/Miami) | **SUBMITTED 2026-08-26 — open, awaiting response.** Recorded as `adjacent` in `application_cohort.json` (outcome `applied`) and as `applied` in `decisions.json`. *Slot language from the capped-cohort era has been removed here: the caps were dropped 2026-09-05 and counts are a record, not a budget.* ⚠️ **KEY FINDING — the application form's preferred-work-location selector offered NO Zurich**, despite the JD saying \"Miami, Zurich or New York\" and the site's own Zurich filter returning this req. Careers site is operated by **Citadel Enterprise Americas LLC**; the apply flow is likely US-entity, or the Zurich seat is closed while the posting stands. **This partly supersedes the working-model risk** — if no Zurich seat is reachable through that form, hybrid-vs-onsite is moot and the application may sit against a US req, a hard NO on relocation. **Rule for any future Citadel application: check the form's location selector BEFORE building a package** — the posting's stated locations and the site filter both proved unreliable. User applied with low expectation; cold channel, evergreen standing req. 2pp resume (13 bullets) + 1pp CL (270 words), both validator **PASS, 0 warnings**; JD term coverage 51%→68% after Tier 1 fixes. Critique **STALE by design** (86/100 pre-edit; user chose to submit as-is over re-critiquing; est. ~91 post-edit, never quote as a score). **CORRECTED 2026-09-10:** this row previously said *\"Cohort entry and decision-log entry deliberately NOT written — no confirmed send date\"* while its own status line read SUBMITTED. **Both entries exist and both are dated 2026-08-26** — the sentence was pre-submission text that survived the send. **The working-model question is still open and travels with the application:** Evidence Fit **71/100 (Adjacent, mid-band; revised down from 74)**, qualifications gate PASS but **a practical constraint is UNRESOLVED**, which `critique_framework.md` treats as a NO-GO trigger. **The JD is silent on hybrid vs onsite (0 grep matches), and Ken Griffin is on record that Citadel returned to the office 5 days a week and calls it his most important leadership decision.** If that holds for Zurich, it means ~2–2.5h daily commuting from Bern or relocating — breaking the Bern-based / 2–3 day hybrid bar. Only contrary evidence is a stale 2022 anonymous Fishbowl post about Operations. **Must be asked, not researched** — same class as the SBB K band. Practical constraints scored 5/5 in Phase 0 on an unexamined assumption; corrected to 2/5. **Zero Gaps among the 7 minimum quals**, and all five preferred technologies (Kafka, Kubernetes, Spark, Airflow, distributed DBs) are Direct — **the strongest raw stack alignment in the log**. Title-defining function **splits**: R2 (ingestion/transformation/storage/lifecycle of large datasets) is Direct and describes SW-1/SW-7 exactly; **R1 (design and build the research platform itself) is Adjacent** — the Aker BP authoring-vs-building shape again, and unwritable-around under Scope Discipline. **Classification caveat:** `application_strategy.md`'s \"a defining responsibility is only Adjacent → Stretch\" would strictly class this Stretch; recorded Adjacent per the Aker BP precedent (the caps this once turned on no longer exist). Gaps: platform authoring at firm scale, no professional quant/finance (now partly mitigated by new **PP-1/PP-2**), SDKs (0 evidence), low-latency/HFT (0 evidence — thesis throughput figure is explicitly barred by `thesis_limits`). **Go is NOT a gap** — Q3 is disjunctive and Python satisfies it. **$175–350k in the posting is a US/NY pay-transparency disclosure, NOT the Zurich number — must be asked.** Channel **Weak/cold** — it was the 5th consecutive cold application, in an office visibly staffed from Google and ETH (platform research head Costas Bekas; Nicolai Meinshausen leads principal research). Cover letter **was written and submitted** (1pp, 270 words); the PP-1 domain story carries it. | **Await response** (submitted 2026-08-26). No document work pending. **Two questions travel with this application and must both be asked at first contact, before any interview effort:** (1) **is the application even against the Zürich seat?** — the apply form offered no Zürich, so it may sit against a US req, which is a hard NO on relocation; (2) **hybrid or 5-day onsite?** — unresolved at submission, and 5-day onsite is a likely NO-GO on the Bern constraint. Same shape as the NVIDIA working-model issue: it went out unresolved, so it is now a live screening topic rather than a pre-cleared one. Also unasked: the Zürich compensation figure — the posting's $175–350k is a US/NY pay-transparency disclosure. |\n| SBB - **Data Engineer (m/w/d)**, Asset Management Infrastrukturanlagen, **Bern** (Job ID 103755) | **CLOSED - APPLICATION WITHDRAWN BY THE CANDIDATE 2026-09-11.** Interview held 2026-09-09 (HR + Scrum Master, **not** the People Leader the brief expected); SBB confirmed there was no room on the Einstufung, and he withdrew. **This is the cohort's only interview and it ended as a NO from his side, not theirs.** 💰 **HARD COMP FIGURES - supersedes the earlier +10% estimate and is the reference point for the whole Bern/Thun tier:** **Anforderungsniveau K tops out at ~CHF 145k.** He sits at **~CHF 135k inside a Swisscom band that runs to ~CHF 160k**. The decisive argument was **headroom, not the absolute number** - he would have entered K at its ceiling merely to break even, with nothing above. ⚠ **The package was a net DOWNGRADE on four counts:** 41h vs 40h week · 25 days holiday + Brückentage vs 28 + 5 from Zeitautonomie · 3 office days vs 2 · the band ceiling. ⚠ **Qualitative red flag worth carrying forward:** he spoke only to HR and the Scrum Master and **perceived difficulties in the team** - that is not repairable by a salary offer. 🎯 **TARGETING RULE — SBB ONLY: Anforderungsniveau K is too low; at SBB aim at NIVEAU L and/or KADER.** ⚠ **Deliberately NOT generalised** (user, 2026-09-11): *Anforderungsniveau* is **SBB's own** salary system (published at ) and does not map onto other employers — **jobs.admin.ch/Schweizer Armee uses Lohnklassen**, RUAG has its own scheme, PostFinance publishes per-posting bands. ~145k is an **SBB** ceiling, not a tier anchor. ⚠ **Thun and surroundings (RUAG, Kdo Cy) are explicitly OUT of this reasoning:** near-zero commute plus benefits that a salary band does not show — he named **childcare-cost coverage at the Schweizer Armee/Kdo Cy as potentially far more generous** (unverified — ask, do not assume). **Never decline a Thun-area role on band alone before pricing the benefits.** What *does* carry across employers: judge by **headroom in the band**, and price the **total package** (working time, holiday, office days), not the entry salary. Original record: interview was stage 2 of 4, **INTERVIEW HELD 2026-09-09.** SBB asked him to come back on whether the role interests him; **SBB's own feedback is still outstanding.** User's summary: *„ok\"*, but **ein paar Red Flags**, and in his read the Rahmenbedingungen do not compensate for them - **leaning negative, explicitly undecided as of 2026-09-10, and he asked to leave it open.** ⚠ **The specific red flags were NOT captured while fresh** - anyone reopening this must ask rather than rely on memory. 💰 **First real read on Anforderungsniveau K:** his post-interview estimate is that roughly **+10% on the current ~140k** looks achievable (very roughly ~155k). **This is HIS ESTIMATE of what is negotiable, not a band SBB is recorded as quoting** - whether the K range was named on the call is unrecorded. Well below the 180k bar, above current pay. Recorded in `comp_worksheet_sbb.md` as the **first calibration point for the entire Bern/Thun local tier** (RUAG, Swissgrid, BKW, BFH), which was previously pure guesswork - treat as an estimate, never quote as a figure. **INTERVIEW INVITED 2026-08-28 - 9. September 2026, 08:30, 45 Min, MS Teams.** Per the posting's own process this is **stage 2 of 4**, *\"Virtuelles Kennenlernen mit HR und Fuehrungskraft\"* - **HR together with Andri Wienandts**, not a pure HR screen. Brief written to `output/SBB_DataEngineer_AssetMgmt/interview_brief_sbb_2026-09-09.md`. **This is the FIRST interview of the evidence-first cohort** (6 applications, 2 rejections, 1 conversion) and it converted from a **COLD submit** - the channel plan called for phoning Wienandts first and that never happened. **The resume never contained Snowflake, dbt or Power BI**, the three literals an ATS keyword screen would have keyed on; the session had recorded that risk as \"accepted, not solvable\". It did not filter him out, so a human read the dossier and the honest-substitution framing survived first contact. **One data point, do not over-read it** - but it is evidence against assuming honest omissions are fatal at the screen. Submitted 2026-08-25: 2pp English resume, 13 bullets, no cover letter (SBB waives it), Evidence Fit **79/100 Core**, hard gate **PASS**, Document Quality **92/100**, channel **Weak/cold**. Best practical fit in the log - Bern-based, German-language, no relocation, EU citizen + B permit. **Four things unresolved before submitting and now live call topics:** (1) **Anforderungsniveau K** is a capped GAV band, figures not public, very likely below the 180k bar; (2) the seat reads **lateral or below** current Staff + Component Owner scope, the same shape as the declined BKW, and the JD's only architecture language is a personal skill not an ownership mandate; (3) **RAMSI** Java/Spring Boot/Angular is a whole responsibility line on a stack that is historical or absent - ask its share, do not oversell; (4) Kidz Care scales on gross household income, so far below 90% at this band. Named gaps to answer honestly: Snowflake, dbt, Argo Workflows, Helm, Power BI, Spring Boot, Angular - all non-canonical, all survivable only because the JD prefixes the stack with \"z. B.\". Thesis stays a **methods prototype**: no real operational data, no accuracy figures, PSO surveyed only, throughput figure barred. | Done - withdrawn and recorded in `application_cohort.json`, `decisions.json`, `comp_worksheet_sbb.md` and memory. **Carry forward the headroom + total-package test, NOT the K→L mapping** — that one is SBB-specific and was explicitly narrowed by the user on 2026-09-11. |\n| AWS (AWS EMEA SARL, Switzerland Branch) — Senior Forward Deployed Engineer, Zürich (job 10504263) | **CLOSED — NO-GO 2026-08-18** at the Phase 0 gate. Evidence Fit **69/100** (Adjacent). Minimum-qual gate passed (all 4 Basic Quals Direct) but the *title-defining* capability — customer embedding — is a Gap that `claims.json` globally forbids claiming, and R2 multi-agent/retrieval is a second Gap. User declined: **69 is below the fit bar worth spending a package on.** Consistent with revealed behaviour — every application actually submitted scored **77.5–89**; nothing below 75 has ever been sent. **No cohort slot consumed (Adjacent stays 1/2).** Phase 0 analysis retained in the session file for reuse if a better-fitting AWS req appears. | Done — no package built |\n| Google — Forward Deployed Engineer III, Google Cloud GTM (French, German), Zürich (req 78350205438567110) | **CLOSED — NOT PROCEEDING 2026-08-18.** Phase 0 was done (Evidence Fit 67/100, hard gate PASS) but user declined to proceed after **two Google rejections** (Merchant Data Science, Business Home): the bar is now that a Google req must fit *very* well to be worth a third attempt, and this one was Adjacent with an agent-orchestration Gap. **Frees the Adjacent cohort slot (back to 1/2).** Phase 1 never started. | Done — do not reopen without a materially stronger Google req |\n| SDU Center for Energy Informatics, Odense DK — PhD, Theme 2 Predictive Maintenance & Asset Management of Smart Energy Networks (job 4159) | **CLOSED — NOT PROCEEDING 2026-08-18.** Enquiry email to Prof. Bo Nørregaard Jørgensen (sent 2026-08-02) went unanswered for 16 days; user dismissed the idea. Genuine thesis fit (vibration condition monitoring, RBR+ANN hybrid) but the blockers were never the documents — 2 letters of recommendation 13 years out of academia, and DKK 37,075/mo ≈ CHF 56k against a 180k bar. Deadline was 2026-08-20. | Done — no further action |\n| Aker BP ASA — Data Product Architect, AI-ready Data Products (FINN 469067315) | **CLOSED — REJECTED 2026-08-27** (submitted 2026-07-30, ~28 days to disposition, no interview; the session file and cohort record record the submission as 2026-07-29 — a one-day inconsistency, never reconciled). Adjacent, **Evidence Fit 79/100**, Document Quality 93/100, hard gate PASS, **Channel Weak/cold** — the optional follow-up email to Per Olav Marthinsen was left unsent, so this went in as a pure cold submit. Stavanger/Oslo/Trondheim, English working language, no Norwegian and no clearance required, EEA work rights. 2pp resume + 1pp CL, both validator PASS, no Tier 1 truth findings — **a cold-channel no-interview rejection does not implicate document quality on its own** (same disposition class as Microsoft ISE and Google Business Home). Honest practitioner framing: builds governed data products *inside* Swisscom's Data Mesh; role *authored* an enterprise framework — real level stretch, never fabricated authorship. **Atlassian Compass closed the catalogue-tool gap** (74→79). Standing gaps if a similar req appears: framework authorship, no energy domain, AWS vs their Microsoft/Cognite stack, **zero verified metrics**. **Cohort slot stays consumed** (evidence-first-2026-01, Adjacent 1/2 — a rejection does not free a slot; cohort remains 4/10). **The Norway lane stays OPEN and selective** — NATO JWC Stavanger is still live, and Equinor/Telenor and other strong Norwegian employers remain in scope per the user's standing instruction. Never frame this lane as closed. | Done — cohort rejection recorded; do not react to this single outcome with positioning changes (`application_strategy.md` §38/§74) |\n| Google - Software Engineer III, Business Home, Zurich (req 93922217108087494) | **CLOSED - NOT PROCEEDING 2026-07-28** (submitted 2026-07-27; Core, Evidence Fit 89/100, Document Quality 94/100; no interview). Rapid early-screen disposition; known risks were Mid-level versus current Staff scope, data/platform versus product-SWE positioning, preferred algorithms/accessibility gaps, and a cold channel. | Done - cohort rejection recorded; do not treat this outcome alone as a document-quality failure |\n| Microsoft — Principal Forward Deployed Engineer, SWE (German Speaking), Zürich (req 200043897) | **SUBMITTED 2026-07-27** (84.2/100 Pass 2; finalized 2-page resume + 1-page cover letter). Strong native-German, Staff progression, production ownership and enterprise-data-readiness case; honest gaps remain in end-to-end LLM delivery, Azure AI and strategic-account FDE experience. | Done — await response |\n| BIS Basel — Senior Data & Analytics Engineer, AI (jr100429, 3-yr term) | **SENT 2026-07-10** (80.5/100; deadline 2026-07-24; hybrid Basel, English-working intl org). Critique flags LLM-depth probe (integration/config vs. ownership) as the screening risk — prep honest answer before any call | Prep interview brief when screening lands |\n| NATO JWC Stavanger — Staff Officer, AI Engineer (2030 Digitalisation, G15, 3-yr PLN) | **SENT 2026-07-10** (77.5/100; deadline 2026-08-09; final interviews 2nd half Oct 2026). Borderline screen: Azure-cert \"or equivalent\", LLM depth, no current clearance; German national + officer service are the legibility assets. Tax-free NOK 93,933/mo + allowances | Await screening; no action until contact |\n| Microsoft — Senior SWE, Industry Solutions Engineering (ISE), Zürich (req 200040836) | **CLOSED — REJECTED 2026-08-11** (submitted 2026-07-03, 85.8/100 Pass 2; 2pp resume + 1pp CL; verbatim Eightfold JD; IC4 base CHF 146.2–245.9k; applied ~7 days after posting). No interview, ~39 days to disposition. Cold-channel application; does not implicate document quality on its own. The separate Microsoft **Principal FDE** req 200043897 is a different pipeline and remains open | Done — cohort rejection recorded |\n| Kraken (Payward) — SRE, AI Agents (remote, CH-eligible) | **CLOSED — REJECTED 2026-06-17** (applied 2026-06-15 ~87.2/100, no interview). Honest gaps (NO Terraform/SRE-title/LangGraph) likely the filter; 4th Kraken req declined/rejected to date | Done |\n| Google — Senior Data Engineer (Merchant Data Science), Zürich/MV | **CLOSED — NOT PROCEEDING 2026-07-24** (applied 2026-06-15, 85.5/100; passed Google Hiring Assessment 2026-06-20, no interview). Assessment pass remains recorded separately. The official 90-day wait applies only to reapplying for the same job; different Google roles remain eligible, subject to 3 applications per rolling 30 days. | Done — target a different strong-fit Google req |\n| Snowflake — Sr SWE, Enterprise (Observe by Snowflake), Zürich | **CLOSED — NO RESPONSE 2026-08-25** (sent 2026-06-06, ~86/100; 2pp resume + 1pp CL; real Ashby JD; comp CHF 176–253k base; NO C++ gate). Closed by user decision at **80 days silent** — past the longest real disposition in the log (Equinor, 79 days). **No rejection was ever received: this is a presumed-dead call, not a rejection — do not count it in rejection statistics or treat it as a document-quality signal.** Cold channel. Pre-dates the evidence-first-2026-01 cohort (started 2026-07-27), so **no cohort slot freed**. Highest-scoring package in the log at ~86 and it drew no reply at all — the strongest single argument in the log for the channel problem over the document problem. **Unblocks the Snowflake Observe Metrics Platform req** (`db4f0492`), paused 2026-07-28 solely to avoid stacking a second cold application on the same org while this one was open | Done — if Snowflake is still wanted, the Metrics Platform req is now open to pursue, but route it through a recruiter rather than another cold submit |\n| Isovalent (Cisco) Sr Data Engineer, Observability | **CLOSED — role pulled** (live Cisco scrape 2026-06-02: not on board; Recruitee link dead). Package finalized ~86/100, SHELVED for reuse | Done — retarget PDFs to next live data-eng req (QuantCo/Grafana/Confluent) |\n| Google Zürich Sr SWE Infrastructure (Data Pipeline) | **CLOSED — DROPPED + DELETED 2026-06-02** (poor fit). Live JD = Core infra/systems SWE with **C++ as a MINIMUM qual**, off-thesis vs `user_positioning`. Output folder deleted (was built on a fabricated JD). | Done — do not reattempt this req |\n| Kraken AI Infrastructure | **CLOSED — REJECTED** (applied, no interview) | Done |\n| Infineon Doctoral | **CLOSED — withdrew** (got interview invite, declined: no relocation to Germany) | Done |\n| Infineon AI Engineer | **CLOSED — not applied** (no relocation to Germany) | Done |\n| Apple Data Engineer (ISE, Zurich) | **CLOSED — REJECTED** (no interview) | Done |\n| Google FDE GenAI (Zurich) | PAUSED — GenAI evidence gap too large; redirecting to data-eng/MLOps roles | Likely abandon |\n| Equinor — Manager / AI Architect, Agentic systems (Stavanger/Oslo/Rotvoll/Sandsli; req JR106747) | **CLOSED — REJECTED 2026-08-14** (submitted 2026-05-27, ~80/100; posting deadline 05.06.2026). No interview, **79 days** to disposition — the longest turnaround recorded. Cold channel. This was a **Manager** req, not a pure IC architect role, so level/scope stretch compounds the standing Norway per-role gates (norsk working-language, no NO clearance — **not** comp: the 180k bar is CH-only and does not apply to Norway). **Pre-dates the evidence-first-2026-01 cohort (started 2026-07-27) — does not free a cohort slot.** **The Norway lane stays OPEN but selective** — Equinor, Telenor and NATO JWC remain in the scout; the user has explicitly said he still wants to reach out for very good Norwegian roles, Equinor included, after this rejection. Never frame this lane as closed. | Done — cohort-external rejection recorded |\n| QuantCo Cloud Engineer (Europe/Zürich) | **CLOSED — REJECTED** (applied 2026-06-01 ~82/100, no interview; rejection 2026-06-15) | Done |\n\n---\n\n" + }, + "cohort": { + "cohort_id": "evidence-first-2026-01", + "status": "active (log, not a capped cohort)", + "started": "2026-07-27", + "target_mix": { + "core": 7, + "adjacent": 2, + "stretch": 1 + }, + "definitions": { + "core": "Evidence Fit 75+ and no hard-gate failure", + "adjacent": "Evidence Fit 60-74, no required Gap, normally at most one serious preferred gap", + "stretch": "Evidence Fit below 60 or a defining responsibility is only Adjacent" + }, + "policy_change_2026_09_05": "Slot caps (7 core / 2 adjacent / 1 stretch) DROPPED by user decision. target_mix is retained only as a record of the original design and is no longer read by cohort_tracker.py or enforced anywhere. This file continues as a durable application log. Channel is now the rationed variable: no application goes out cold when a named contact is published and reachable. See resume_builder/reference/application_strategy.md.", + "applications": [ + { + "company": "Google", + "role": "Software Engineer III, Business Home", + "application_date": "2026-07-27", + "fit_class": "core", + "evidence_fit": 89.0, + "hard_gate": "pass", + "channel": "weak", + "outcome": "rejected_no_interview" + }, + { + "company": "Aker BP ASA", + "role": "Data Product Architect - AI-ready Data Products", + "application_date": "2026-07-29", + "fit_class": "adjacent", + "evidence_fit": 79.0, + "hard_gate": "pass", + "channel": "weak", + "outcome": "rejected_no_interview", + "submitted": "2026-07-29", + "rejection_date": "2026-08-27" + }, + { + "company": "SBB", + "role": "Data Engineer (m/w/d), Asset Management Infrastrukturanlagen, Bern", + "job_id": "103755", + "application_date": "2026-08-25", + "fit_class": "core", + "evidence_fit": 79.0, + "hard_gate": "pass", + "channel": "weak", + "outcome": "withdrawn_by_candidate", + "submitted": "2026-08-25", + "open_risk": "Anforderungsniveau K is a capped GAV band, figures not public; seat may read lateral or below current Staff + Component Owner scope.", + "interview": { + "stage": "Stage 2 of 4 - Virtuelles Kennenlernen mit HR und Fuehrungskraft", + "invited": "2026-08-28", + "scheduled": "2026-09-09 08:30", + "duration_min": 45, + "format": "Microsoft Teams", + "held": "2026-09-09", + "result": "Held 2026-09-09 (HR + Scrum Master). Candidate WITHDREW 2026-09-11 after SBB confirmed there was no room to move on the Einstufung.", + "user_read": "User reported 'ein paar Red Flags' and judged that the Rahmenbedingungen do not compensate for them. The specific red flags were NOT captured while fresh - if this is reopened, ask before relying on memory. Leaning negative but explicitly undecided as of 2026-09-10; he asked to leave it open pending SBB's feedback." + }, + "note": "FIRST interview invitation in the evidence-first cohort. Converted from a COLD submit - the published warm contact (Andri Wienandts, +41 79 364 62 53) was never called. The resume never contained Snowflake, dbt or Power BI, the three literals an ATS keyword screen would have keyed on, so a human read the dossier.", + "comp_datapoint": "HARD FIGURES, from the interview - supersedes the earlier +10% estimate. Anforderungsniveau K TOPS OUT AT ~CHF 145k. His own position: ~CHF 135k today inside a Swisscom band that runs to ~CHF 160k. So the decisive point was NOT the absolute number but the HEADROOM: he would have had to enter K at its ceiling just to break even, with nothing left above. First hard calibration of a Swiss public-sector band and the reference point for the whole Bern/Thun tier (RUAG, Swissgrid, BKW, BFH, PostFinance).", + "withdrawn": "2026-09-11", + "why_withdrawn": "Total package was a net DOWNGRADE on four counts: 41h vs 40h week; 25 days holiday + Brueckentage vs 28 + 5 from Zeitautonomie; 3 office days vs 2; and the band ceiling above. Plus a qualitative red flag: he spoke only to HR and the Scrum Master and perceived DIFFICULTIES IN THE TEAM. That one is not fixable by a salary offer - note it if SBB or a similar Bern-tier seat comes back.", + "lesson": "SBB-SPECIFIC targeting rule (user 2026-09-11): at SBB, Anforderungsniveau K is too low - aim at Niveau L and/or Kader. NARROWED the same day at the user's instruction: Anforderungsniveau is SBB's OWN system (https://company.sbb.ch/de/jobs-karriere/arbeitgeberin/lohn.html) and does NOT generalise - jobs.admin.ch uses Lohnklassen, RUAG its own scheme, PostFinance per-posting bands. ~145k is an SBB ceiling, not a tier anchor. THUN-AREA EMPLOYERS (RUAG, Kdo Cy) ARE EXPLICITLY OUT of this reasoning: near-zero commute plus benefits a band does not show - he named childcare-cost coverage at the Schweizer Armee/Kdo Cy as potentially far more generous (UNVERIFIED - ask). Never decline a Thun-area role on band alone. What DOES generalise: judge by headroom in the band, and price the total package (working time, holiday, office days), not the entry number." + }, + { + "company": "Citadel Securities", + "role": "Platform Engineer, Research Platform Engineering (Zurich/NY/Miami)", + "application_date": "2026-08-26", + "fit_class": "adjacent", + "evidence_fit": 71.0, + "hard_gate": "pass", + "channel": "weak", + "outcome": "applied" + }, + { + "company": "Schweizer Armee - Kommando Cyber", + "role": "DevOps Engineer III (Data Platform)", + "application_date": "2026-08-27", + "fit_class": "core", + "evidence_fit": 79.0, + "hard_gate": "pass", + "channel": "weak", + "outcome": "rejected_no_interview", + "submitted": "2026-08-27", + "rejection_date": "2026-09-10", + "days_to_disposition": 14, + "note": "Form-letter rejection from Besarta Mehmeti, no interview, 14 days to disposition - the fastest in the log after Google Business Home. The letter is boilerplate ('Profil und unsere Kriterien nicht in allen Punkten uebereinstimmen') and carries ZERO diagnostic content, so it is not a signal about the package and is no reason to re-audit it. Same disposition class as Microsoft ISE, Google Business Home and Aker BP: a cold-channel no-interview rejection does not implicate document quality on its own. WORTH WATCHING, NOT CONCLUDING: this row and the SBB row are identical on every recorded dimension - core, Evidence Fit 79, hard gate pass, Document Quality 92, channel weak - and they went opposite ways (SBB converted to an interview, this died in 14 days). That shows the fit score did not discriminate; it does NOT show what did. n=1 on each side. Do not react with positioning changes (application_strategy.md 38/74). Channel note: Marcel Matthey-Doret (+41 58 46 78409) was published and never called - the fourth published, reachable contact left unused after Heinzen (RUAG), Wienandts (SBB) and Marthinsen (Aker BP). Consistent with the 2026-09-05 channel policy, but NOT evidence for it: the single interview in this log came from a cold submit." + }, + { + "company": "RUAG MRO Holding AG (Business Area C5I)", + "role": "AI Engineer C5I (w/m/d), Thun", + "application_date": "2026-08-27", + "fit_class": "stretch", + "evidence_fit": 74.0, + "hard_gate": "fail", + "channel": "weak", + "outcome": "applied" + }, + { + "company": "NVIDIA", + "role": "Senior DevOps Engineer, Zurich (JR2024256)", + "application_date": "2026-09-05", + "fit_class": "stretch", + "evidence_fit": 64.0, + "hard_gate": "fail", + "channel": "weak", + "outcome": "applied" + } + ] + } +} diff --git a/output/BIS/session_bis_senior_data_analytics_ai.md b/output/BIS/session_bis_senior_data_analytics_ai.md index 1a6b475..31c2926 100644 --- a/output/BIS/session_bis_senior_data_analytics_ai.md +++ b/output/BIS/session_bis_senior_data_analytics_ai.md @@ -1,5 +1,10 @@ # Session: Bank for International Settlements — Senior Data & Analytics Engineer – Artificial Intelligence +> **Abgeschlossen – Absage erhalten am 11.09.2026**, vom Nutzer gemeldet. +> Kein Interview im bisherigen Verlauf dokumentiert; Absagegrund nicht genannt. +> Aktueller Status: `job_scout/state/applications.json`, Eintrag `app-7c94f6a9d318`. +> Die folgenden Angaben dokumentieren die damalige Bewerbungsvorbereitung. + ## JD Info - **File:** JDs/bis_senior_data_analytics_ai.txt diff --git a/resume_builder/bundles/bundle_data_platform.md b/resume_builder/bundles/bundle_data_platform.md index 13b8374..bbcf30f 100644 --- a/resume_builder/bundles/bundle_data_platform.md +++ b/resume_builder/bundles/bundle_data_platform.md @@ -66,7 +66,7 @@ | SW-3 | K8s + GitLab | **Lead bullet** — "Deployed and operated Python data applications on Kubernetes with GitLab CI/CD; drove infrastructure automation in agile DevOps team" | K8s + CI/CD ownership = core platform signal | | SW-1 | AWS migration | "Migrated pipelines in his owned domains from legacy Teradata/Oracle processing onto Swisscom's AWS platform (Glue, Athena/Iceberg, Redshift, Airflow, CloudFormation)" | Scoped migration delivery plus AWS breadth | | SW-2 | Component Owner | "Owned Fulfillment ETL pipelines (Oracle/Kafka → Teradata) — platform reliability, Data Governance compliance, 2nd/3rd-level support and on-call duty" | Platform SLA + on-call = reliability engineer signal | -| BS-1 | ML inference | "Containerized and orchestrated ML inference (Docker, K8s, Ansible) into a continuously operating 24/7 semiconductor production environment" | Production-grade containerization in an environment that cannot be paused. **Do not write *zero-downtime*** — canonical BS-1 evidences integration into continuous production, not a measured zero-downtime deployment property | +| BS-1 | ML inference | "Containerized and orchestrated ML inference (Docker, K8s, Ansible) into a continuously operating 24/7 semiconductor production environment" | Production-grade containerization in a continuously operating fab where a software defect could halt production. This is a failure consequence, not a deployment-window restriction. **Do not write *zero-downtime*** — canonical BS-1 evidences integration into continuous production, not a measured zero-downtime deployment property | | BS-4 | ELK PoC | "Built an anomaly-detection **proof of concept** on ELK + Kafka (Docker) with Grafana, Prometheus and Loki monitoring, validating centralized logging and alerting for 24/7 semiconductor production" | PoC-scoped observability breadth — **the PoC framing is mandatory, not optional** | | SW-5 | Security Champion | "2025/2026 team Security Champion; include only if the JD explicitly requires security or DevSecOps exposure" | Secondary team-role evidence, not ownership or certification | | BS-6 | Linux/Ansible | "Administered and automated Linux servers carrying the ML platform, Docker hosts and backend services at Bosch; wrote Ansible extensions including a credential plugin with local caching" | **Infrastructure-as-Code and Linux operations evidence.** Added 2026-09-11 (review F07): BS-6 existed in `claims.json` and `experience_bosch.md` since 2026-08-27 but appeared in NO bundle, so the selection paths could not see it. Currency limit: employer-side Linux/Ansible ended Dec 2022; `PP-3` (personal Debian server) carries the current half and must be named as personal. GitOps stays forbidden — execution was push-based from Jenkins/GitLab, not agent-reconciled | diff --git a/resume_builder/canonical/claims.json b/resume_builder/canonical/claims.json index 5687a2e..990dfec 100644 --- a/resume_builder/canonical/claims.json +++ b/resume_builder/canonical/claims.json @@ -50,12 +50,12 @@ "thesis_institution": "Tongji University, Shanghai", "thesis_title": "Development of a Web-Based Remote Fault Diagnosis System", "final_grade": "1.6 (German scale, 1.0 best / 4.0 lowest pass) -- user-confirmed 2026-08-26", - "final_grade_us_4pt_orientation_only": "3.4 -- modified Bavarian formula, US = 1 + 3*(4.0-G)/(4.0-1.0), which simplifies to 5.0 - G. Use ONLY when a form demands a 4.0-scale GPA; always state the German scale alongside it. Never present as a natively-awarded GPA. ORIENTATION AID, NOT A CANONICAL QUALIFICATION and NOT a recognised conversion: the modified Bavarian formula converts FOREIGN grades INTO the German system, so inverting it to produce a US GPA is not an accepted method, and the TUM source describes even its forward calculator as orientation only. The canonical fact is `final_grade` on the German scale. Supply this only when a specific recipient demands a 4.0-scale GPA; if that recipient publishes its own method, use THEIRS and record which was used. Demoted 2026-09-11, review finding F10.", "thesis_grade": "1.0", "thesis_relative_grade": "ECTS grade B; official certificate on file. B is formally the next 25% after the top 10%, i.e. the 10-35% band -- so the correct and safe phrasing is 'top 35%' (user-confirmed 2026-08-26). Do NOT write 'top 30%' or 'top 25%': B is a band, not a point, and narrowing it overstates the standing. The original 'top 35%' in this record was correct; a briefly-considered 30% was the error.", "thesis_methods": "Verified against the thesis PDF 2026-08-02. Domain: vibration-based condition monitoring of CNC machine tools (piezoelectric accelerometers on spindle, tool rest, lathe body). Features: speed, load and the dimensionless waveform, peak, pulse, margin and kurtosis indices. Surveyed CBR, PSO, RBR and ANN; SELECTED a hybrid of rule-based reasoning (interpretable thresholds) plus a 7-10-3 feed-forward ANN (faultstates green/yellow/red) to handle noisy data that static rules cannot classify. Extensible plug-in architecture for swappable diagnosis methods and data collectors. Java/GWT client-server on MySQL. Evaluation measured throughput and latency, NOT model accuracy: the pipeline sustained ~500 samples/s against tri-axial vibration sensors delivering 72.9 kSPS, so the thesis concluded data reduction, filtering or batch scheduling is required for real-time use.", "thesis_limits": "PSO was SURVEYED ONLY and NOT used in the implementation -- never claim it as an applied method. No real operational data: training data came from a cited thesis, test data was modified plus a random generator. No model-accuracy figures exist. Back-propagation retraining was listed as future work, not implemented. Frame as a methods prototype/framework, never as a validated production predictive-maintenance system.", - "transcripts_language": "English-language originals available for B.Eng. and M.Eng." + "transcripts_language": "English-language originals available for B.Eng. and M.Eng.", + "grade_output_rule": "Use the confirmed German original grade and its scale. Omit grades from industry resumes and letters unless concretely relevant. Do not convert with the inverse Bavarian formula. If a recipient requires a US GPA, use only its specified conversion method and record that method; otherwise ask the recipient how to enter a German grade." }, { "id": "EDU-BENG", @@ -64,8 +64,7 @@ "start": "2009-10", "end": "2012-10", "final_grade": "2.4 (German scale, 1.0 best / 4.0 lowest pass) -- user-confirmed 2026-08-26", - "final_grade_us_4pt_orientation_only": "2.6 -- modified Bavarian formula (5.0 - G). Supply ONLY when a form requires a bachelor GPA; leave blank where optional. Always state the German scale alongside it, because the conversion makes German degrees read weaker to a US audience than they are: 2.4 is a solid 'gut'. Never present as a natively-awarded GPA. ORIENTATION AID, NOT A CANONICAL QUALIFICATION and NOT a recognised conversion: the modified Bavarian formula converts FOREIGN grades INTO the German system, so inverting it to produce a US GPA is not an accepted method, and the TUM source describes even its forward calculator as orientation only. The canonical fact is `final_grade` on the German scale. Supply this only when a specific recipient demands a 4.0-scale GPA; if that recipient publishes its own method, use THEIRS and record which was used. Demoted 2026-09-11, review finding F10.", - "grade_output_rule": "Neither degree grade goes on the resume or in a cover letter by default. Industry resumes omit grades; the M.Eng. thesis grade 1.0 is the only figure worth surfacing, and only where academic performance is genuinely valued. These fields exist for application-form GPA questions and credential checks. The German grades are the canonical facts; the *_orientation_only conversions are neither canonical nor a recognised method and must never be presented as an awarded GPA (F10, 2026-09-11)." + "grade_output_rule": "Use the confirmed German original grade and its scale. Omit grades from industry resumes and letters unless concretely relevant. Do not convert with the inverse Bavarian formula. If a recipient requires a US GPA, use only its specified conversion method and record that method; otherwise ask the recipient how to enter a German grade." } ], "employment": [ diff --git a/resume_builder/canonical/package_manifest.json b/resume_builder/canonical/package_manifest.json index 92febef..ba4e9d7 100644 --- a/resume_builder/canonical/package_manifest.json +++ b/resume_builder/canonical/package_manifest.json @@ -1,12 +1,12 @@ { - "schema_version": 1, - "generated_note": "Generated by package_manifest.py (review finding F17). A PDF in a folder is not evidence that the package is reproducible. This file records what would actually happen on a rebuild. It is descriptive: nothing here authorises rewriting a submitted document.", + "schema_version": 2, + "generated_note": "Generated by package_manifest.py (review finding F17). A PDF in a folder is not evidence that the package is reproducible. Source and class hashes detect edits; optional pdf_artifacts are observations, not verified source-to-PDF mappings. Absent PDFs are allowed; present PDFs must match their recorded hash. Nothing here authorises rewriting a submitted document.", "packages": { "AkerBP_Data_Product_Architect": { - "sources": [ - "e2e_akerbp_data_product_architect_cover_letter.tex", - "e2e_akerbp_data_product_architect_resume.tex" - ], + "sources": { + "e2e_akerbp_data_product_architect_cover_letter.tex": "922759ac4bc7a52c", + "e2e_akerbp_data_product_architect_resume.tex": "d0a0f9942a882567" + }, "class_files": { "resume.cls": "b3a032a850cd0d6d" }, @@ -14,71 +14,71 @@ "article", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_akerbp_data_product_architect_cover_letter.pdf", - "e2e_akerbp_data_product_architect_resume.pdf" - ], "uses_today": [], "problems": [], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "76c7dacfb00b5df3", + "Dennis_Thiessen_Resume.pdf": "0023acf689a8b4e7", + "e2e_akerbp_data_product_architect_cover_letter.pdf": "76c7dacfb00b5df3", + "e2e_akerbp_data_product_architect_resume.pdf": "0023acf689a8b4e7" + }, "reuse_classification": "recorded" }, "Apple_Data_Engineer": { - "sources": [ - "e2e_apple_data_engineer_cover_letter.tex", - "e2e_apple_data_engineer_resume.tex" - ], + "sources": { + "e2e_apple_data_engineer_cover_letter.tex": "34abad48e3a82634", + "e2e_apple_data_engineer_resume.tex": "c3594ddcb32131da" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "e2e_apple_data_engineer_cover_letter.pdf", - "e2e_apple_data_engineer_resume.pdf" - ], "uses_today": [ "e2e_apple_data_engineer_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_apple_data_engineer_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "e2e_apple_data_engineer_cover_letter.pdf": "06ee82291bbc9a4c", + "e2e_apple_data_engineer_resume.pdf": "37aeb66d208346f5" + }, "reuse_classification": "recorded" }, "BIS": { - "sources": [ - "e2e_bis_senior_data_analytics_ai_cover_letter.tex", - "e2e_bis_senior_data_analytics_ai_resume.tex" - ], + "sources": { + "e2e_bis_senior_data_analytics_ai_cover_letter.tex": "4f037d7fcfd1ccfc", + "e2e_bis_senior_data_analytics_ai_resume.tex": "4ecc6a98ab82ea2b" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_bis_senior_data_analytics_ai_cover_letter.pdf", - "e2e_bis_senior_data_analytics_ai_resume.pdf" - ], "uses_today": [ "e2e_bis_senior_data_analytics_ai_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_bis_senior_data_analytics_ai_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "c584d131e37899fa", + "Dennis_Thiessen_Resume.pdf": "5b554ae2e658d960", + "e2e_bis_senior_data_analytics_ai_cover_letter.pdf": "c584d131e37899fa", + "e2e_bis_senior_data_analytics_ai_resume.pdf": "5b554ae2e658d960" + }, "reuse_classification": "recorded" }, "CitadelSecurities_PlatformEngineer": { - "sources": [ - "e2e_citadel_securities_platform_engineer_cover_letter.tex", - "e2e_citadel_securities_platform_engineer_resume.tex" - ], + "sources": { + "e2e_citadel_securities_platform_engineer_cover_letter.tex": "4f2bf629635d3413", + "e2e_citadel_securities_platform_engineer_resume.tex": "5f2ac36737162b29" + }, "class_files": { "resume.cls": "b3a032a850cd0d6d" }, @@ -86,49 +86,49 @@ "article", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_citadel_securities_platform_engineer_cover_letter.pdf", - "e2e_citadel_securities_platform_engineer_resume.pdf" - ], "uses_today": [], "problems": [], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "e95631c56fd02a71", + "Dennis_Thiessen_Resume.pdf": "cb7a13b2215796f2", + "e2e_citadel_securities_platform_engineer_cover_letter.pdf": "e95631c56fd02a71", + "e2e_citadel_securities_platform_engineer_resume.pdf": "cb7a13b2215796f2" + }, "reuse_classification": "recorded" }, "Equinor_AI_Manager": { - "sources": [ - "e2e_equinor_ai_manager_cover_letter.tex", - "e2e_equinor_ai_manager_resume.tex" - ], + "sources": { + "e2e_equinor_ai_manager_cover_letter.tex": "390a7ea37836a93d", + "e2e_equinor_ai_manager_resume.tex": "f57087d53a777328" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_equinor_ai_manager_cover_letter.pdf", - "e2e_equinor_ai_manager_resume.pdf" - ], "uses_today": [ "e2e_equinor_ai_manager_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_equinor_ai_manager_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "224303daba825066", + "Dennis_Thiessen_Resume.pdf": "634e207667086dd7", + "e2e_equinor_ai_manager_cover_letter.pdf": "224303daba825066", + "e2e_equinor_ai_manager_resume.pdf": "634e207667086dd7" + }, "reuse_classification": "recorded" }, "Google_Business_Home": { - "sources": [ - "e2e_google_software_engineer_iii_business_home_cover_letter.tex", - "e2e_google_software_engineer_iii_business_home_cover_letter_v2.tex", - "e2e_google_software_engineer_iii_business_home_resume.tex", - "e2e_google_software_engineer_iii_business_home_resume_v2.tex" - ], + "sources": { + "e2e_google_software_engineer_iii_business_home_cover_letter.tex": "c68e33850a6baf42", + "e2e_google_software_engineer_iii_business_home_cover_letter_v2.tex": "2fe40e60a2defb12", + "e2e_google_software_engineer_iii_business_home_resume.tex": "f47788e596d87529", + "e2e_google_software_engineer_iii_business_home_resume_v2.tex": "d895021d7b5850cb" + }, "class_files": { "resume.cls": "b3a032a850cd0d6d" }, @@ -136,124 +136,124 @@ "article", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_google_software_engineer_iii_business_home_cover_letter.pdf", - "e2e_google_software_engineer_iii_business_home_cover_letter_v2.pdf", - "e2e_google_software_engineer_iii_business_home_resume.pdf", - "e2e_google_software_engineer_iii_business_home_resume_v2.pdf" - ], "uses_today": [], "problems": [], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "57c111039dc2c4ee", + "Dennis_Thiessen_Resume.pdf": "e9fe51e4661fc36c", + "e2e_google_software_engineer_iii_business_home_cover_letter.pdf": "e3643f9d623e404d", + "e2e_google_software_engineer_iii_business_home_cover_letter_v2.pdf": "57c111039dc2c4ee", + "e2e_google_software_engineer_iii_business_home_resume.pdf": "c4cb40153f7af748", + "e2e_google_software_engineer_iii_business_home_resume_v2.pdf": "e9fe51e4661fc36c" + }, "reuse_classification": "recorded" }, "Google_Senior_Data_Engineer": { - "sources": [ - "e2e_google_senior_data_engineer_cover_letter.tex", - "e2e_google_senior_data_engineer_resume.tex" - ], + "sources": { + "e2e_google_senior_data_engineer_cover_letter.tex": "966fd9ff789fb4a4", + "e2e_google_senior_data_engineer_resume.tex": "1e3e56ab426172ab" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_google_senior_data_engineer_cover_letter.pdf", - "e2e_google_senior_data_engineer_resume.pdf" - ], "uses_today": [ "e2e_google_senior_data_engineer_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_google_senior_data_engineer_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "c826c22521359372", + "Dennis_Thiessen_Resume.pdf": "25121fee8915acd3", + "e2e_google_senior_data_engineer_cover_letter.pdf": "c826c22521359372", + "e2e_google_senior_data_engineer_resume.pdf": "25121fee8915acd3" + }, "reuse_classification": "recorded" }, "Infineon": { - "sources": [ - "e2e_infineon_doctoral_cover_letter.tex", - "e2e_infineon_doctoral_resume.tex" - ], + "sources": { + "e2e_infineon_doctoral_cover_letter.tex": "4149a4a2ed2f619f", + "e2e_infineon_doctoral_resume.tex": "090946510d744691" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_infineon_doctoral_cover_letter.pdf", - "e2e_infineon_doctoral_resume.pdf" - ], "uses_today": [ "e2e_infineon_doctoral_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_infineon_doctoral_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "5d785ca3c4b1a903", + "Dennis_Thiessen_Resume.pdf": "a445f2351982b735", + "e2e_infineon_doctoral_cover_letter.pdf": "5d785ca3c4b1a903", + "e2e_infineon_doctoral_resume.pdf": "a445f2351982b735" + }, "reuse_classification": "recorded" }, "Infineon_AI_Engineer": { - "sources": [ - "e2e_infineon_ai_engineer_cover_letter.tex", - "e2e_infineon_ai_engineer_resume.tex" - ], + "sources": { + "e2e_infineon_ai_engineer_cover_letter.tex": "a8cccec6b8d685b9", + "e2e_infineon_ai_engineer_resume.tex": "b7957e35ee342bca" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "e2e_infineon_ai_engineer_resume.pdf" - ], "uses_today": [ "e2e_infineon_ai_engineer_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_infineon_ai_engineer_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "e2e_infineon_ai_engineer_resume.pdf": "810ec7007a8bfe8e" + }, "reuse_classification": "recorded" }, "Isovalent_DataEngineer": { - "sources": [ - "e2e_isovalent_data_engineer_cover_letter.tex", - "e2e_isovalent_data_engineer_resume.tex" - ], + "sources": { + "e2e_isovalent_data_engineer_cover_letter.tex": "33c6a2943f288893", + "e2e_isovalent_data_engineer_resume.tex": "b8edf89223ef8fb2" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_isovalent_data_engineer_cover_letter.pdf", - "e2e_isovalent_data_engineer_resume.pdf" - ], "uses_today": [ "e2e_isovalent_data_engineer_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_isovalent_data_engineer_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "546364b3102d00b7", + "Dennis_Thiessen_Resume.pdf": "f35745cc987473fc", + "e2e_isovalent_data_engineer_cover_letter.pdf": "546364b3102d00b7", + "e2e_isovalent_data_engineer_resume.pdf": "f35745cc987473fc" + }, "reuse_classification": "recorded" }, "KdoCy_DevOps_Data_Platform": { - "sources": [ - "e2e_kdocy_devops_data_platform_cover_letter.tex", - "e2e_kdocy_devops_data_platform_resume.tex" - ], + "sources": { + "e2e_kdocy_devops_data_platform_cover_letter.tex": "29d114255a1310cc", + "e2e_kdocy_devops_data_platform_resume.tex": "de3fcb42d1c5c478" + }, "class_files": { "resume.cls": "b3a032a850cd0d6d" }, @@ -261,151 +261,151 @@ "article", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Lebenslauf.pdf", - "Dennis_Thiessen_Motivationsschreiben.pdf", - "e2e_kdocy_devops_data_platform_cover_letter.pdf", - "e2e_kdocy_devops_data_platform_resume.pdf" - ], "uses_today": [ "e2e_kdocy_devops_data_platform_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_kdocy_devops_data_platform_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Lebenslauf.pdf": "ec9fbeb6864a43da", + "Dennis_Thiessen_Motivationsschreiben.pdf": "53be036b3ce12148", + "e2e_kdocy_devops_data_platform_cover_letter.pdf": "55d7aa1fd97b2eb6", + "e2e_kdocy_devops_data_platform_resume.pdf": "4929981906919523" + }, "reuse_classification": "recorded" }, "Kraken_AI_Infrastructure": { - "sources": [ - "e2e_kraken_ai_infra_cover_letter.tex", - "e2e_kraken_ai_infra_resume.tex" - ], + "sources": { + "e2e_kraken_ai_infra_cover_letter.tex": "32eb97aedefb1702", + "e2e_kraken_ai_infra_resume.tex": "14350c33e1dbc18e" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "Senior Software Engineer – AI Infrastructure @ Kraken.pdf", - "e2e_kraken_ai_infra_cover_letter.pdf", - "e2e_kraken_ai_infra_resume.pdf" - ], "uses_today": [ "e2e_kraken_ai_infra_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_kraken_ai_infra_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "10d0deb132c3ecd8", + "Dennis_Thiessen_Resume.pdf": "0ff9a52ceb6521a3", + "e2e_kraken_ai_infra_cover_letter.pdf": "10d0deb132c3ecd8", + "e2e_kraken_ai_infra_resume.pdf": "0ff9a52ceb6521a3", + "Senior Software Engineer – AI Infrastructure @ Kraken.pdf": "716232dd1d83cbfc" + }, "reuse_classification": "recorded" }, "Kraken_SRE_AI_Agents": { - "sources": [ - "e2e_kraken_sre_ai_agents_cover_letter.tex", - "e2e_kraken_sre_ai_agents_resume.tex" - ], + "sources": { + "e2e_kraken_sre_ai_agents_cover_letter.tex": "bf215661bc0e6936", + "e2e_kraken_sre_ai_agents_resume.tex": "b8d1a3cfae03186a" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_kraken_sre_ai_agents_cover_letter.pdf", - "e2e_kraken_sre_ai_agents_resume.pdf" - ], "uses_today": [ "e2e_kraken_sre_ai_agents_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_kraken_sre_ai_agents_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "a08cff46ef5f4653", + "Dennis_Thiessen_Resume.pdf": "9c04ea9735e8611a", + "e2e_kraken_sre_ai_agents_cover_letter.pdf": "a08cff46ef5f4653", + "e2e_kraken_sre_ai_agents_resume.pdf": "9c04ea9735e8611a" + }, "reuse_classification": "recorded" }, "Microsoft_ISE_Senior_SWE": { - "sources": [ - "e2e_microsoft_ise_cover_letter.tex", - "e2e_microsoft_ise_resume.tex" - ], + "sources": { + "e2e_microsoft_ise_cover_letter.tex": "72ea729b7bd5c8da", + "e2e_microsoft_ise_resume.tex": "f61845e8087b5696" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_microsoft_ise_cover_letter.pdf", - "e2e_microsoft_ise_resume.pdf" - ], "uses_today": [ "e2e_microsoft_ise_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_microsoft_ise_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "7f6df9105f06f572", + "Dennis_Thiessen_Resume.pdf": "8c4f308234298916", + "e2e_microsoft_ise_cover_letter.pdf": "7f6df9105f06f572", + "e2e_microsoft_ise_resume.pdf": "8c4f308234298916" + }, "reuse_classification": "recorded" }, "Microsoft_Principal_FDE_SWE": { - "sources": [ - "e2e_microsoft_principal_fde_swe_cover_letter.tex", - "e2e_microsoft_principal_fde_swe_resume.tex" - ], + "sources": { + "e2e_microsoft_principal_fde_swe_cover_letter.tex": "8095827a769ece44", + "e2e_microsoft_principal_fde_swe_resume.tex": "5be341c9dcccca28" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_microsoft_principal_fde_swe_cover_letter.pdf", - "e2e_microsoft_principal_fde_swe_resume.pdf" - ], "uses_today": [ "e2e_microsoft_principal_fde_swe_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_microsoft_principal_fde_swe_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "bf00c589a7742547", + "Dennis_Thiessen_Resume.pdf": "9a95e3452e2e3539", + "e2e_microsoft_principal_fde_swe_cover_letter.pdf": "bf00c589a7742547", + "e2e_microsoft_principal_fde_swe_resume.pdf": "9a95e3452e2e3539" + }, "reuse_classification": "recorded" }, "NATO_AI_ENGINEER": { - "sources": [ - "e2e_nato_ai_engineer_cover_letter.tex", - "e2e_nato_ai_engineer_resume.tex" - ], + "sources": { + "e2e_nato_ai_engineer_cover_letter.tex": "863c590ea8a987c1", + "e2e_nato_ai_engineer_resume.tex": "e3b80997712dd9d7" + }, "class_files": {}, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_nato_ai_engineer_resume.pdf" - ], "uses_today": [], "problems": [ "source distribution incomplete: requires resume.cls, which is not in the folder. A build here only succeeds if a copy happens to exist elsewhere on the machine. Do NOT repair by copying the newest class in - the repo carries several class variants and older macros differ." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "0239a9ca995cb825", + "Dennis_Thiessen_Resume.pdf": "3d96a1e30b0adb64", + "e2e_nato_ai_engineer_resume.pdf": "428e242a2ca7b453" + }, "reuse_classification": "recorded" }, "NVIDIA_SrDevOps_Zurich": { - "sources": [ - "e2e_nvidia_sr_devops_cover_letter.tex", - "e2e_nvidia_sr_devops_resume.tex" - ], + "sources": { + "e2e_nvidia_sr_devops_cover_letter.tex": "2252b98f7a7e0ca0", + "e2e_nvidia_sr_devops_resume.tex": "69787364df7d6f02" + }, "class_files": { "resume.cls": "b3a032a850cd0d6d" }, @@ -413,51 +413,51 @@ "article", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_nvidia_sr_devops_cover_letter.pdf", - "e2e_nvidia_sr_devops_resume.pdf" - ], "uses_today": [ "e2e_nvidia_sr_devops_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_nvidia_sr_devops_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "275a8a10fb9bb9eb", + "Dennis_Thiessen_Resume.pdf": "03ea1f4fe89d6fbf", + "e2e_nvidia_sr_devops_cover_letter.pdf": "275a8a10fb9bb9eb", + "e2e_nvidia_sr_devops_resume.pdf": "03ea1f4fe89d6fbf" + }, "reuse_classification": "recorded" }, "QuantCo_Cloud_Engineer": { - "sources": [ - "e2e_quantco_cloud_engineer_cover_letter.tex", - "e2e_quantco_cloud_engineer_resume.tex" - ], + "sources": { + "e2e_quantco_cloud_engineer_cover_letter.tex": "f8ef588da7fe1232", + "e2e_quantco_cloud_engineer_resume.tex": "a8ebc1b56f623949" + }, "class_files": { - "resume.cls": "b26f882a42d30d90" + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_quantco_cloud_engineer_cover_letter.pdf", - "e2e_quantco_cloud_engineer_resume.pdf" - ], "uses_today": [ "e2e_quantco_cloud_engineer_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_quantco_cloud_engineer_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "484a41d552102b4f", + "Dennis_Thiessen_Resume.pdf": "564338361852073f", + "e2e_quantco_cloud_engineer_cover_letter.pdf": "484a41d552102b4f", + "e2e_quantco_cloud_engineer_resume.pdf": "564338361852073f" + }, "reuse_classification": "recorded" }, "RUAG_AI_Engineer_C5I": { - "sources": [ - "e2e_ruag_ai_engineer_c5i_cover_letter.tex", - "e2e_ruag_ai_engineer_c5i_resume.tex" - ], + "sources": { + "e2e_ruag_ai_engineer_c5i_cover_letter.tex": "2b3d966dc0590b08", + "e2e_ruag_ai_engineer_c5i_resume.tex": "2cf799344c3d91c6" + }, "class_files": { "resume.cls": "b52a6f7123ae9067" }, @@ -465,62 +465,62 @@ "article", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Lebenslauf.pdf", - "Dennis_Thiessen_Motivationsschreiben.pdf", - "e2e_ruag_ai_engineer_c5i_cover_letter.pdf", - "e2e_ruag_ai_engineer_c5i_resume.pdf" - ], "uses_today": [ "e2e_ruag_ai_engineer_c5i_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_ruag_ai_engineer_c5i_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Lebenslauf.pdf": "f29ab6445b4e7ff5", + "Dennis_Thiessen_Motivationsschreiben.pdf": "336d54ed07f205c7", + "e2e_ruag_ai_engineer_c5i_cover_letter.pdf": "336d54ed07f205c7", + "e2e_ruag_ai_engineer_c5i_resume.pdf": "f29ab6445b4e7ff5" + }, "reuse_classification": "recorded" }, "SBB_DataEngineer_AssetMgmt": { - "sources": [ - "e2e_sbb_data_engineer_asset_mgmt_resume.tex" - ], + "sources": { + "e2e_sbb_data_engineer_asset_mgmt_resume.tex": "cd4e8a18fa0a235a" + }, "class_files": { "resume.cls": "b3a032a850cd0d6d" }, "requires_class": [ "resume" ], - "pdfs": [ - "e2e_sbb_data_engineer_asset_mgmt_resume.pdf" - ], "uses_today": [], "problems": [], + "pdf_artifacts": { + "e2e_sbb_data_engineer_asset_mgmt_resume.pdf": "669fedaa340a89c7" + }, "reuse_classification": "recorded" }, "Snowflake_Observe_Enterprise": { - "sources": [ - "e2e_snowflake_observe_enterprise_cover_letter.tex", - "e2e_snowflake_observe_enterprise_resume.tex" - ], + "sources": { + "e2e_snowflake_observe_enterprise_cover_letter.tex": "f0d01764e943776f", + "e2e_snowflake_observe_enterprise_resume.tex": "cde2a2326fa2913e" + }, "class_files": { - "cv.cls": "a623d286e808b27e", - "resume.cls": "b26f882a42d30d90" + "cv.cls": "30bf0ca5b3d0b718", + "resume.cls": "721ed8d5e80fcc54" }, "requires_class": [ "moderncv", "resume" ], - "pdfs": [ - "Dennis_Thiessen_Cover_Letter.pdf", - "Dennis_Thiessen_Resume.pdf", - "e2e_snowflake_observe_enterprise_cover_letter.pdf", - "e2e_snowflake_observe_enterprise_resume.pdf" - ], "uses_today": [ "e2e_snowflake_observe_enterprise_cover_letter.tex" ], "problems": [ "rebuild is date-dependent: e2e_snowflake_observe_enterprise_cover_letter.tex use \\today, so recompiling a historical package silently restamps it with the current date. New packages must carry the explicit application date instead." ], + "pdf_artifacts": { + "Dennis_Thiessen_Cover_Letter.pdf": "555ece186371a9ef", + "Dennis_Thiessen_Resume.pdf": "122dab5f5e5ec70d", + "e2e_snowflake_observe_enterprise_cover_letter.pdf": "555ece186371a9ef", + "e2e_snowflake_observe_enterprise_resume.pdf": "122dab5f5e5ec70d" + }, "reuse_classification": "recorded" } } diff --git a/resume_builder/experience/experience_capgemini.md b/resume_builder/experience/experience_capgemini.md index 595e804..28cbeee 100644 --- a/resume_builder/experience/experience_capgemini.md +++ b/resume_builder/experience/experience_capgemini.md @@ -3,7 +3,7 @@ ### Cross-Position Section -**Career arc framing:** Capgemini was Dennis's first professional role after the Bundeswehr — a 6-month consulting engagement in the Technology Services APPS division, working on test automation for a transport logistics client. Despite its brevity, the Zeugnis rates performance as "sehr gut" (vollsten Zufriedenheit — top tier), and the employer deeply regrets departure. This role anchors the test automation thread that runs through Generali and Vizrt. Typically omitted from 2-page resumes; included in full CV for timeline completeness. +**Career arc framing:** Capgemini was Dennis's first professional role after the Bundeswehr — a 6-month consulting engagement in the Technology Services APPS division, working on test automation for a transport logistics client. Despite its brevity, the Zeugnis rates performance as "sehr gut" (vollsten Zufriedenheit — top tier), and the employer deeply regrets departure. This role anchors the test automation thread that runs through Generali and Vizrt. Historical record only. Standing user preference: omit from all resumes, CVs and cover letters; do not fill or explain the intentional gap. **Resume recommendation:** Omit from 2-page resume unless targeting roles where test automation breadth from earliest career adds value. Include on full CV (5-page) under early career section. @@ -38,5 +38,5 @@ **Resume inclusion guidance:** - 2-page resume: **Omit entirely** -- 5-page CV: Include as single condensed bullet under "Early Career" or within timeline continuity section +- Any CV, including academic: **Omit entirely**, per standing user preference. - Cover letter: Not relevant to mention diff --git a/resume_builder/helpers/application_tracker.py b/resume_builder/helpers/application_tracker.py index ec14dcd..68688df 100644 --- a/resume_builder/helpers/application_tracker.py +++ b/resume_builder/helpers/application_tracker.py @@ -57,7 +57,9 @@ def update_application(data: dict, args: argparse.Namespace) -> None: row = matches[0] if row.get("application_date") and args.date < row["application_date"]: raise ValueError("event date precedes application date") - outcome_dates = [event["date"] for event in row.get("events", []) if event.get("outcome") and event.get("date")] + outcome_dates = [event["date"] for event in row.get("events", []) + if (event.get("outcome") or event.get("type") in records.OUTCOMES) and event.get("date")] + outcome_dates.extend(row[key] for key in ("withdrawn", "rejection_date") if row.get(key)) if args.outcome and outcome_dates and args.date < max(outcome_dates): raise ValueError("older outcome cannot replace current state; record it as a historical note") event = {"type": args.event or args.outcome or "note", "date": args.date} diff --git a/resume_builder/helpers/package_manifest.py b/resume_builder/helpers/package_manifest.py index eaf5787..6085796 100644 --- a/resume_builder/helpers/package_manifest.py +++ b/resume_builder/helpers/package_manifest.py @@ -6,9 +6,9 @@ A package folder that contains a PDF looks finished. It is not necessarily repro one happens to be installed elsewhere, and its submitted PDF differs in content from the e2e source's PDF. The existing PDF hid both problems. -This records, per package: which sources exist, the hash of the class each one would compile -against, whether a PDF is present, and whether the submission-named PDF still matches the -e2e one. It does NOT rewrite anything - historical packages are evidence. +Records source/class content hashes. Optional PDF snapshots detect changed local artifacts; +missing PDFs are allowed because generated PDFs are deliberately not tracked in Git. +Filename association is not proof that a PDF was built from the current source. python resume_builder/helpers/package_manifest.py # write the manifest python resume_builder/helpers/package_manifest.py --check # fail if it is stale @@ -34,17 +34,19 @@ CTAN_CLASSES = {"article", "report", "book", "letter", "moderncv", "scrartcl", " def digest(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest()[:16] + # Git/Windows may change line endings without changing the TeX source. + data = path.read_text(encoding="utf-8").encode("utf-8") if path.suffix in {".tex", ".cls"} else path.read_bytes() + return hashlib.sha256(data).hexdigest()[:16] -def describe(folder: Path) -> dict: - sources = sorted(p.name for p in folder.glob("*.tex")) +def describe(folder: Path, record_pdfs: bool = False) -> dict: + sources = {p.name: digest(p) for p in sorted(folder.glob("*.tex"))} classes = {p.name: digest(p) for p in folder.glob("*.cls")} - pdfs = sorted(p.name for p in folder.glob("*.pdf")) needs = set() uses_today = [] for name in sources: text = (folder / name).read_text(encoding="utf-8", errors="replace") + text = re.sub(r"(? dict: f"rebuild is date-dependent: {', '.join(uses_today)} use \\today, so recompiling a " f"historical package silently restamps it with the current date. New packages must " f"carry the explicit application date instead.") - if sources and not pdfs: - problems.append("no PDF present: nothing reviewable without a build") - - return { + entry = { "sources": sources, "class_files": classes, "requires_class": sorted(needs), - "pdfs": pdfs, "uses_today": uses_today, "problems": problems, } + if record_pdfs: + entry["pdf_artifacts"] = {p.name: digest(p) for p in sorted(folder.glob("*.pdf"))} + return entry -def build() -> dict: +def build(record_pdfs: bool = False) -> dict: register = json.loads(REGISTER.read_text(encoding="utf-8")) classified = set() for key in ("unsafe_do_not_reuse", "historical_revalidate", "paused_no_package"): @@ -96,7 +97,7 @@ def build() -> dict: # submission and has nothing to classify for reuse. if not any(folder.glob("*resume*.tex")) and not any(folder.glob("*cover*letter*.tex")): continue - entry = describe(folder) + entry = describe(folder, record_pdfs) entry["reuse_classification"] = ( "recorded" if f"output/{folder.name}" in classified else "UNCLASSIFIED") if f"output/{folder.name}" not in classified: @@ -107,32 +108,57 @@ def build() -> dict: packages[folder.name] = entry return { - "schema_version": 1, + "schema_version": 2, "generated_note": ( "Generated by package_manifest.py (review finding F17). A PDF in a folder is not " - "evidence that the package is reproducible. This file records what would actually " - "happen on a rebuild. It is descriptive: nothing here authorises rewriting a " + "evidence that the package is reproducible. Source and class hashes detect edits; " + "optional pdf_artifacts are observations, not verified source-to-PDF mappings. " + "Absent PDFs are allowed; present PDFs must match their recorded hash. " + "Nothing here authorises rewriting a " "submitted document."), "packages": packages, } +def check_manifest(recorded: dict, current: dict, output: Path = OUTPUT) -> list[str]: + # Compare the portable source inventory separately from optional local build products. + baseline = json.loads(json.dumps(recorded)) + errors = [] + for name, entry in baseline.get("packages", {}).items(): + for filename, expected in entry.pop("pdf_artifacts", {}).items(): + path = output / name / filename + if path.exists() and digest(path) != expected: + errors.append(f"changed PDF artifact: {name}/{filename}; inspect before recording a new baseline") + if baseline != current: + errors.append("source manifest is stale; inspect changes, then run package_manifest.py") + return errors + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--check", action="store_true", help="fail if the manifest is stale") + parser.add_argument("--record-pdfs", action="store_true", help="explicitly snapshot current local PDF hashes (not proof of source association)") args = parser.parse_args() - manifest = build() - text = json.dumps(manifest, indent=2, ensure_ascii=False) + "\n" + if args.check and args.record_pdfs: + parser.error("--check cannot refresh PDF baselines") + recorded = json.loads(MANIFEST.read_text(encoding="utf-8")) if MANIFEST.exists() else {} + manifest = build(record_pdfs=args.record_pdfs) if args.check: - current = MANIFEST.read_text(encoding="utf-8") if MANIFEST.exists() else "" - if current != text: - print("ERROR: package manifest is stale; re-run package_manifest.py") + errors = check_manifest(recorded, manifest) + if errors: + for error in errors: + print("ERROR: " + error) return 1 print("Package manifest is current.") return 0 - MANIFEST.write_text(text, encoding="utf-8") + if not args.record_pdfs: + for name, entry in manifest["packages"].items(): + old = recorded.get("packages", {}).get(name, {}) + if "pdf_artifacts" in old: + entry["pdf_artifacts"] = old["pdf_artifacts"] + MANIFEST.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") flagged = {name: entry for name, entry in manifest["packages"].items() if entry["problems"]} print(f"{len(manifest['packages'])} packages described, {len(flagged)} with findings\n") for name, entry in flagged.items(): diff --git a/resume_builder/helpers/page_metrics.py b/resume_builder/helpers/page_metrics.py index 275f5ff..6ee62df 100644 --- a/resume_builder/helpers/page_metrics.py +++ b/resume_builder/helpers/page_metrics.py @@ -8,38 +8,38 @@ The review reported ~50-55% for four packages. This reproduces that measurement compiled PDF so a change can be checked rather than asserted. The percentage is a diagnostic, never a target: no pixel goal, no forced page-two filling. """ -import io import re +import shutil import subprocess import sys import tempfile from pathlib import Path -sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") -PDFTOTEXT = Path(r"C:\Users\Dennis\AppData\Local\Programs\MiKTeX\miktex\bin\x64\pdftotext.exe") HEADINGS = ("professional experience", "berufserfahrung", "experience") def measure(pdf: Path) -> dict: + executable = shutil.which("pdftotext") + if not executable: + raise RuntimeError("pdftotext is required on PATH (Poppler or MiKTeX)") with tempfile.TemporaryDirectory() as work: out = Path(work) / "bbox.xml" - subprocess.run([str(PDFTOTEXT), "-bbox", "-f", "1", "-l", "1", str(pdf), str(out)], + subprocess.run([executable, "-bbox", "-f", "1", "-l", "1", str(pdf), str(out)], capture_output=True, check=True) xml = out.read_text(encoding="utf-8", errors="replace") page = re.search(r'([^<]*)', xml)] - first_y = None - for index, (y, text) in enumerate(words): - if text in ("professional", "berufserfahrung", "experience"): - following = " ".join(w for _, w in words[index:index + 2]) - if any(h.startswith(following.split()[0]) for h in HEADINGS): - first_y = y - break - lines = len({round(y, 1) for y, _ in words}) + rows = {} + for y, text in words: + rows.setdefault(round(y, 1), []).append(text) + first_y = next((y for y, row in sorted(rows.items()) if " ".join(row).strip() in HEADINGS), None) + lines = len(rows) return { "height": height, "experience_y": first_y, diff --git a/resume_builder/helpers/validate_resume_system.py b/resume_builder/helpers/validate_resume_system.py index 1b68126..c9b802e 100644 --- a/resume_builder/helpers/validate_resume_system.py +++ b/resume_builder/helpers/validate_resume_system.py @@ -51,6 +51,16 @@ def canonical_checks(claims: dict, history: dict) -> tuple[list[str], list[str]] if duplicates: errors.append(f"duplicate {collection} ids: {', '.join(duplicates)}") + for collection in ("education", "employment"): + for item in claims.get(collection, []): + for period in [item, *item.get("title_history", [])]: + for field in ("start", "end"): + value = period.get(field) + if value == "present" and field == "end": + continue + if value is not None and (not isinstance(value, str) or not re.fullmatch(r"\d{4}-(?:0[1-9]|1[0-2])", value)): + errors.append(f"{item.get('id')}: invalid {field} date {value!r}; expected YYYY-MM") + skills = claims.get("skills", []) skill_names = [item.get("name") for item in skills] duplicates = sorted({item for item in skill_names if skill_names.count(item) > 1}) @@ -95,15 +105,26 @@ def canonical_checks(claims: dict, history: dict) -> tuple[list[str], list[str]] missing_block = [c.get("id") for c in claim_list if not c.get("evidence")] if missing_block: errors.append("claims without an evidence block: " + ", ".join(map(str, missing_block))) + for claim in claim_list: + evidence = claim.get("evidence") + if not isinstance(evidence, dict): + errors.append(f"{claim.get('id')}: evidence must be an object") + continue + for field in ("source", "ownership"): + if not isinstance(evidence.get(field), str) or not evidence[field].strip(): + errors.append(f"{claim.get('id')}: evidence.{field} must be a nonempty string (or 'unrecorded')") + source = evidence.get("source", "") + if isinstance(source, str) and re.fullmatch(r"[\w./-]+\.(?:md|pdf|tex|json|txt)", source) and not (ROOT / source).is_file(): + errors.append(f"{claim.get('id')}: evidence source file does not exist: {source}") undated = [c.get("id") for c in claim_list - if (c.get("evidence") or {}).get("last_verified") == "unrecorded"] + if isinstance(c.get("evidence"), dict) and c["evidence"].get("last_verified") == "unrecorded"] if undated: warnings.append( f"{len(undated)} of {len(claim_list)} claims have last_verified 'unrecorded' " f"({', '.join(map(str, undated[:6]))}{', ...' if len(undated) > 6 else ''}). " "Record a real date when one is confirmed; never infer one to clear this hint.") - if claims["identity"].get("swiss_permit", "").startswith("UNVERIFIED"): + if (claims.get("identity") or {}).get("swiss_permit", "").startswith("UNVERIFIED"): warnings.append("Swiss permit type remains unverified; generators must ask before naming it") return errors, warnings @@ -148,7 +169,9 @@ _MARKUP = re.compile(r"\\[a-zA-Z@]+\s*(?:\[[^\]]*\])?") _PLACEHOLDER = re.compile(r"\[GENERATE[^\]]*\]|\bTODO\b|\bFIXME\b|\bXXX\b|\bLOREM IPSUM\b|<<[^>]*>>", re.I) # "no Terraform experience", "keine Terraform-Erfahrung", "not worked with Terraform" _NEGATION = re.compile( - r"\b(?:no|not|never|without|kein|keine|keinerlei|ohne|nicht)\b[^.;]{0,60}$", re.I) + r"\b(?:(?:no|without|keine?|keinerlei|ohne)\s+(?:(?:experience|erfahrung)\s+(?:with|in|mit)\s+)?" + r"|(?:not|never)\s+(?:used|worked with)\s+|nicht\s+(?:mit\s+)?)$", re.I) +_INLINE = re.compile(r"\\(?:textbf|textit|textnormal|textrm|textsf|texttt|emph|underline)\{([^{}]*)\}") # LaTeX accent macros: \"a, 'e, \`e, \^o, \~n, plus \ss. German documents use these instead @@ -179,6 +202,8 @@ def visible_text(raw: str) -> str: * Whitespace is collapsed so a term split across a line break still matches. """ text = _deaccent(_COMMENT.sub("", raw)) + while _INLINE.search(text): + text = _INLINE.sub(r"\1", text) text = _MARKUP.sub(" ", text) text = text.replace("{", " ").replace("}", " ").replace("~", " ") return re.sub(r"\s+", " ", text) @@ -218,6 +243,17 @@ def _negated(haystack: str, term: str) -> bool: return bool(hits) and all(_NEGATION.search(haystack[max(0, hit.start() - 70):hit.start()]) for hit in hits) +def _employer_mention(text: str, name: str) -> bool: + """Narrow exception for a company name, never for a bare technology list.""" + if name.lower() != "snowflake": + return False + hits = list(re.finditer(r"\bsnowflake\b", text)) + return bool(hits) and all( + re.search(r"\b(?:application to|applying to|position at|role at|bewerbung bei|bewerbung an)\s*$", text[max(0, hit.start()-60):hit.start()]) + or re.match(r"\s+(?:inc\b|incorporated\b)", text[hit.end():]) + for hit in hits) + + def scan_document(path: Path, claims: dict, template: bool = False) -> list[str]: """Deterministic, mechanically checkable properties of a generated document. @@ -229,9 +265,9 @@ def scan_document(path: Path, claims: dict, template: bool = False) -> list[str] raw = read_text(path) text = visible_text(raw) lowered = text.lower() - # Second view with grouping braces closed up rather than spaced out, so a name split by - # markup -- extbf{Snow}flake -- cannot slip past the skill comparison. - dense = re.sub(r"\s+", " ", _MARKUP.sub(" ", _deaccent(_COMMENT.sub("", raw))).replace("{", "").replace("}", "")).lower() + # Keep context local to a sentence, bullet or skills row. + contexts = [visible_text(part).lower() for part in re.split( + r"\\(?:item|skillline|compactentry)\b|[.!?;]\s+|\n\s*\n", _COMMENT.sub("", raw))] for pattern in claims.get("global_forbidden_output_patterns", []): if pattern.lower() in lowered: @@ -248,15 +284,11 @@ def scan_document(path: Path, claims: dict, template: bool = False) -> list[str] for skill in claims.get("skills", []): name, output = skill["name"], skill.get("output") - seen_plain = _mentions(lowered, name) - seen_dense = _mentions(dense, name) - if output == "forbidden" and (seen_plain or seen_dense) and not _negated(lowered if seen_plain else dense, name): + mentions = [part for part in contexts if _mentions(part, name)] + if output == "forbidden" and any(not _negated(part, name) and not _employer_mention(part, name) for part in mentions): errors.append(f"{path}: forbidden or unverified skill: {name}") - if output == "certification-context-only" and _mentions(lowered, name): - window = " ".join( - lowered[max(0, m.start() - 200):m.start() + 200] - for m in re.finditer(re.escape(name.lower()), lowered)) - if not re.search(r"certificat|zertifik|coursera|nanodegree|kurs|course|training|weiterbildung", window): + if output == "certification-context-only": + if any(not _negated(part, name) and not re.search(r"certificat|zertifik|coursera|nanodegree|\bkurs\b|course|weiterbildung", part) for part in mentions): errors.append( f"{path}: {name} is certification-context-only and appears without that context") diff --git a/resume_builder/reference/cl_reference.md b/resume_builder/reference/cl_reference.md index dbc7ef3..62be8a8 100644 --- a/resume_builder/reference/cl_reference.md +++ b/resume_builder/reference/cl_reference.md @@ -1,6 +1,6 @@ # Cover Letter Reference -> A cover letter is optional by default. The resume must earn a screen on its own. +> The resume must stand alone. Decide on a letter using the employer and market context below. ## Decision Gate @@ -20,6 +20,14 @@ Normally skip it when: Record `Cover Letter Decision: YES/NO` and the reason in the session file before writing. +For Denmark and Norway, normally prepare a short, specific letter when the process permits it; +do not automatically skip it merely because the field is optional. Explain the actual connection +to the employer, relevant evidence and any confirmed country/location motivation. One page is +the normal limit. Employer instructions against letters or motivation questions in the portal +take precedence. If no specific additional value exists, record why the letter is omitted. +Sources reviewed 2026-09-11: [Workindenmark](https://www.workindenmark.dk/job-search-in-denmark/your-cover-letter), +[NAV](https://arbeidsplassen.nav.no/tips-til-jobbsoknaden). + ## Evidence Rules 1. Read `resume_builder/canonical/claims.json` first. diff --git a/resume_builder/reference/current_applications.md b/resume_builder/reference/current_applications.md index 99a3059..ba17c11 100644 --- a/resume_builder/reference/current_applications.md +++ b/resume_builder/reference/current_applications.md @@ -16,7 +16,7 @@ Use current outcome and next action below. Imported legacy notes are historical, | Aker BP ASA — Data Product Architect, AI-ready Data Products (FINN 469067315) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. | | Google - Software Engineer III, Business Home, Zurich (req 93922217108087494) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. | | Microsoft — Principal Forward Deployed Engineer, SWE (German Speaking), Zürich (req 200043897) | applied | not recorded | Done — await response | -| BIS Basel — Senior Data & Analytics Engineer, AI (jr100429, 3-yr term) | applied | not recorded | Prep interview brief when screening lands | +| BIS Basel — Senior Data & Analytics Engineer, AI (jr100429, 3-yr term) | rejected_no_interview | not recorded | Abgeschlossen: Absage erhalten; keine weitere Aktion offen. | | NATO JWC Stavanger — Staff Officer, AI Engineer (2030 Digitalisation, G15, 3-yr PLN) | applied | not recorded | Await screening; no action until contact | | Microsoft — Senior SWE, Industry Solutions Engineering (ISE), Zürich (req 200040836) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. | | Kraken (Payward) — SRE, AI Agents (remote, CH-eligible) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. | diff --git a/resume_builder/reference/resume_reference.md b/resume_builder/reference/resume_reference.md index ec5c692..b993033 100644 --- a/resume_builder/reference/resume_reference.md +++ b/resume_builder/reference/resume_reference.md @@ -11,9 +11,13 @@ Choose before planning content: |---|---|---| | International Tech | FAANG, US-tech, international scale-up, English-language engineering role | 2 pages; no photo or demographic data; conventional headers | | Swiss/DACH | Traditional Swiss/German/Austrian employer or explicit local dossier request | 2 pages normally; optional photo only by user choice; references/certificates separate | +| Denmark | Danish employer without a more specific format | Normally 2 pages; tailored professional profile (roughly 4–8 lines), explain unfamiliar employers and relevant collaboration; assess a specific motivation letter | +| Norway | Norwegian employer without a more specific format | Normally 2 pages; tailored key qualifications (roughly 4–6 lines), clear responsibilities and results; assess a specific motivation letter | | Employer-specific | NATO, public sector, academic or a mandated portal format | Follow the employer's explicit rules | -International Tech is the default even when the role is physically located in Europe. +International Tech remains the default for international tech recruiting. Use the local Denmark/Norway profile where appropriate; explicit employer requirements and user preferences take precedence. Profile lengths are guidance, not gates. Keep the standing preferences against demographic details; no photo is required. + +Market guidance: [Workindenmark profile](https://www.workindenmark.dk/job-search-in-denmark/your-cv/a-good-personal-profile-counts) and [Tekna CV](https://www.tekna.no/en/career/cv-and-cover-letter/how-to-write-a-good-cv/), reviewed 2026-09-11. Only state relocation or local-language plans if confirmed. ## 2. Evidence Preflight @@ -34,7 +38,7 @@ The reader must see these items in this order: 1. Name, professional identity, contact/location/work authorization. 2. Optional 2--3 line summary. -3. Four to six compact skills lines. +3. Compact skills block: normally 4–8 rendered lines, checked in the PDF. 4. Professional Experience, reverse chronological. 5. Education and relevant certifications once. @@ -70,14 +74,14 @@ A strong shorter resume is better than a full page padded with weak evidence. Wh The summary is optional. When used: -- Keep it to 2--3 rendered lines and normally two sentences. +- International Tech/Swiss-DACH: normally 2–3 rendered lines. Denmark/Norway: use the profile guidance above, without padding. - State the demonstrated professional identity and two strongest proof points. - Do not state the desired role as if it were prior experience. - Avoid credential lists and unsupported scale claims. ## 6. Skills -- Use 4--6 compact lines from `skills_taxonomy.md`. +- Use roughly 4–8 rendered lines from `skills_taxonomy.md`, not a count of source macros. - List only canonical `allowed` or context-appropriate `allowed-with-context` skills. - Certification-only skills stay in certification context. - Do not use Expert/Proficient/Familiar self-ratings. diff --git a/resume_builder/reference/shared_ops.md b/resume_builder/reference/shared_ops.md index 9014080..83d3c8e 100644 --- a/resume_builder/reference/shared_ops.md +++ b/resume_builder/reference/shared_ops.md @@ -33,7 +33,7 @@ Read `resume_builder/reference/application_strategy.md` and `critique_framework. ## Audience and Cover-Letter Decisions -Select International Tech, Swiss/DACH or Employer-specific format before writing. International Tech is default. +Select the appropriate profile in `resume_reference.md` before writing, including Denmark/Norway when applicable. International Tech is the default for international tech recruiting; employer requirements take precedence. Apply `cl_reference.md` and record `Cover Letter Decision: YES/NO`. A deliberately omitted letter is a valid complete package. diff --git a/resume_builder/reference/workflows/make-cl.md b/resume_builder/reference/workflows/make-cl.md index 5a0612a..a683fec 100644 --- a/resume_builder/reference/workflows/make-cl.md +++ b/resume_builder/reference/workflows/make-cl.md @@ -25,6 +25,7 @@ If the only possible content repeats resume bullets or paraphrases company news, - Use the selected audience profile and employer-requested language. - International Tech/industry: normally 200--300 words in two or three paragraphs. - Swiss/DACH: one A4 page, appropriate formality. +- Denmark/Norway: normally one page with specific employer and country motivation; follow `cl_reference.md` when deciding whether to write one. - Employer-specific: follow the requested format or portal limit. - Lead with candidate-role connection. - Add motivation, decision context or a genuine connection. diff --git a/resume_builder/reference/workflows/make-resume.md b/resume_builder/reference/workflows/make-resume.md index 22dc855..d414a0d 100644 --- a/resume_builder/reference/workflows/make-resume.md +++ b/resume_builder/reference/workflows/make-resume.md @@ -38,14 +38,14 @@ Proceed on a no-go only after the user explicitly overrides the stated reason. A ## Phase 2 — Audience and Content Plan -Select International Tech by default. Use Swiss/DACH or Employer-specific format only when the employer/context supports it. +Select the audience profile from `resume_reference.md`: International Tech, Swiss/DACH, Denmark, Norway or Employer-specific. Employer requirements take precedence. Read the matching role bundle, `skills_taxonomy.md`, `achievement_reframing_guide.md`, and only the experience files needed for selected claims. Plan: -- Optional 2--3 line summary. -- Four to six evidence-backed skills lines. +- Optional summary sized to the selected audience profile. +- Compact evidence-backed skills block, normally 4–8 rendered lines, verified in the PDF. - Swisscom 4--5 bullets. - Bosch 3--4 bullets. - Zero or one bullet for each older role. diff --git a/resume_builder/support/achievement_reframing_guide.md b/resume_builder/support/achievement_reframing_guide.md index 75fc523..94afc95 100644 --- a/resume_builder/support/achievement_reframing_guide.md +++ b/resume_builder/support/achievement_reframing_guide.md @@ -286,7 +286,7 @@ Each achievement has a **Significance** line (why it matters to any reader) and | Role Type | Priority | Lead Verb | Framing Angle | |-----------|----------|-----------|---------------| -| All roles | LOW | Implemented | CV only — do not include on 2-page resume | +| All roles | EXCLUDED | — | Historical record only; omit Capgemini from every document | --- diff --git a/resume_builder/templates/resume_template.tex b/resume_builder/templates/resume_template.tex index 7a7ad28..a0eafd4 100644 --- a/resume_builder/templates/resume_template.tex +++ b/resume_builder/templates/resume_template.tex @@ -40,7 +40,6 @@ Staff data engineer with over ten years of production software experience across \skillline{Languages}{Python, SQL; Java and C\# (professional, historical); C++ and JavaScript (limited historical use)} \skillline{Data}{Apache Kafka, Airflow, PySpark, Oracle, Teradata, Hadoop/Impala, data products, metadata and governance} \skillline{Cloud and platform}{AWS (S3, Glue, Athena/Iceberg, Redshift), CloudFormation, Kubernetes, Docker, GitLab CI/CD, Ansible} -\skillline{ML delivery}{Production inference deployment and support, on-call operations; ELK, Grafana and Prometheus proof-of-concept work} \skillline{Certifications}{AWS Solutions Architect -- Associate; Data Engineering with AWS; iSAQB CPSA-F; ITIL Foundation} \end{rSection} @@ -87,10 +86,12 @@ Staff data engineer with over ten years of production software experience across % Grades and thesis only where they carry concrete relevance; see claims.json % grade_output_rule, which keeps grades off by default. \begin{rSection}{Education} -\compactentry{M.Eng. Computer Aided Engineering (Software Design \& Engineering), Universität der Bundeswehr München}{Apr 2012 -- Oct 2013} -Thesis at Tongji University, Shanghai: \textit{Development of a Web-Based Remote Fault Diagnosis System}; grade 1.0. +\compactentry{M.Eng. Computer Aided Engineering}{Apr 2012 -- Oct 2013} +Universität der Bundeswehr München; Software Design \& Engineering. +% Add thesis or grade only when relevant to this application, using the canonical facts. -\compactentry{B.Eng. Information and Telecommunication Technologies, Universität der Bundeswehr München}{Oct 2009 -- Oct 2012} +\compactentry{B.Eng. Information and Telecommunication Technologies}{Oct 2009 -- Oct 2012} +Universität der Bundeswehr München. \end{rSection} \end{document} diff --git a/tests/test_review_corrections.py b/tests/test_review_corrections.py new file mode 100644 index 0000000..0e3e420 --- /dev/null +++ b/tests/test_review_corrections.py @@ -0,0 +1,158 @@ +"""Small regression cases from the follow-up review; no network or production writes.""" +from __future__ import annotations + +import contextlib +import copy +import io +import json +from pathlib import Path +import sys +import tempfile +import unittest +from argparse import Namespace +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path[:0] = [str(ROOT / "resume_builder/helpers"), str(ROOT / "job_scout")] +import application_records as records +import application_tracker as tracker +import package_manifest as manifest +import page_metrics +import scout +import validate_resume_system as validator + + +class ScannerRegressionTests(unittest.TestCase): + def scan(self, body): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "resume.tex" + path.write_text("dennis@thiessen.io +41 795 955 585\n" + body, encoding="utf-8") + return validator.scan_document(path, validator.load_json(validator.CLAIMS_PATH)) + + def test_context_cannot_be_borrowed_or_confused_with_model_training(self): + for body in ("Implemented production model training in PyTorch.", + "Coursera certification: PyTorch. Built production models in PyTorch.", + r"\skillline{Certificate}{Coursera, PyTorch}\skillline{Production}{PyTorch}"): + with self.subTest(body=body): + self.assertTrue(self.scan(body)) + self.assertEqual([], self.scan("IBM AI Engineering certificate: PyTorch.")) + + def test_inline_markup_and_unrelated_negation_do_not_hide_claims(self): + for body in (r"Built Lang\textbf{Chain} services.", + "Delivered services without downtime using Terraform."): + with self.subTest(body=body): + self.assertTrue(self.scan(body)) + self.assertEqual([], self.scan("I have no Terraform experience.")) + + def test_employer_name_is_not_a_skill_but_later_claim_still_is(self): + self.assertEqual([], self.scan("Application to Snowflake for Senior Software Engineer.")) + self.assertTrue(self.scan("Application to Snowflake. Built Snowflake pipelines.")) + + def test_missing_identity_invalid_period_and_empty_evidence_report_errors(self): + original = validator.load_json(validator.CLAIMS_PATH) + history = validator.load_json(validator.HISTORY_PATH) + data = copy.deepcopy(original) + data.pop("identity") + self.assertTrue(validator.canonical_checks(data, history)[0]) + data = copy.deepcopy(original) + data["employment"][0]["start"] = "2035-99" + data["claims"][0]["evidence"] = {"source": "missing/file.pdf"} + errors = validator.canonical_checks(data, history)[0] + for phrase in ("invalid start", "ownership", "does not exist"): + self.assertTrue(any(phrase in error for error in errors), errors) + + +class ScoutRegressionTests(unittest.TestCase): + def test_explicit_country_and_exclusion_override_town_name(self): + for location in ("Freiburg, Germany", "Baden, Austria", "Kastellaun, Germany", "Remote - Switzerland excluded"): + with self.subTest(location=location): + self.assertFalse(scout.location_eligibility(location)[0]) + self.assertTrue(scout.location_eligibility("Baar")[0]) + self.assertTrue(scout.location_eligibility("Wetzikon", country="CH")[0]) + self.assertTrue(scout.location_eligibility("Aarhus, Denmark")[0]) + self.assertTrue(scout.location_eligibility("Oslo, Norway", policy="norway_only")[0]) + + def test_smartrecruiters_preserves_country_through_matching(self): + payload = {"content": [{"id": "1", "name": "Data Engineer", "location": {"city": "Wetzikon", "country": "CH"}}], "totalFound": 1} + with patch.object(scout, "http_get_json", return_value=payload): + jobs = scout.fetch_smartrecruiters({"company": "example"}) + rows, _, _ = scout._process_company("example", "Example", {}, jobs, {}, "2026-09-11", {}) + self.assertEqual(1, len(rows)) + + def test_related_requisition_stays_visible_when_hiding_decided(self): + old = "https://jobs.ruag.ch/offene-stellen/data-engineer/11111111-1111-1111-1111-111111111111" + new = "https://jobs.ruag.ch/offene-stellen/data-engineer/22222222-2222-2222-2222-222222222222" + decisions = {old: {"decision": "skip", "date": "2026-09-01"}} + row = dict(company="RUAG", title="Data Engineer", location="Thun", url=new, score=3, pos=[], neg=[], in_ch=True, remote=False, relocation=False, is_new=True, posted="") + with tempfile.TemporaryDirectory() as directory, patch.object(scout, "_raw_decisions", return_value=decisions): + path = Path(directory) / "report.md" + scout.write_report(path, [row], [], False, True, decisions=scout.index_decisions(decisions), hide_decided=True) + text = path.read_text(encoding="utf-8") + self.assertIn(new, text) + self.assertIn("RELATED", text) + + def test_new_only_commits_delivered_results_and_preserves_other_companies(self): + job = dict(id="1", title="Data Engineer", location="Bern", url="https://example.com/1") + with tempfile.TemporaryDirectory() as directory, contextlib.ExitStack() as stack: + for name, value in {"ROOT": Path(directory), "REPORTS_DIR": Path(directory)/"reports", + "COMPANIES": [("example", "Example", "fake", {})]}.items(): + stack.enter_context(patch.object(scout, name, value)) + stack.enter_context(patch.object(scout, "load_seen", return_value={"other": {"old": {"title": "Untouched"}}})) + for name in ("load_decisions", "load_last_scrape"): + stack.enter_context(patch.object(scout, name, return_value={})) + stack.enter_context(patch.object(scout, "save_last_scrape")) + saved = stack.enter_context(patch.object(scout, "save_seen")) + stack.enter_context(patch.object(scout, "_close_browser")) + stack.enter_context(patch.object(scout, "_fetch_company", return_value=("example", "Example", "fake", {}, [job], None, 0.0))) + stack.enter_context(patch.object(sys, "argv", ["scout.py", "--new-only"])) + stack.enter_context(contextlib.redirect_stdout(io.StringIO())) + stack.enter_context(contextlib.redirect_stderr(io.StringIO())) + scout.main() + state = saved.call_args.args[0] + self.assertIn("1", state["example"]) + self.assertIn("old", state["other"]) + + def test_concurrent_scout_writer_is_rejected(self): + with tempfile.TemporaryDirectory() as directory, patch.object(scout, "ROOT", Path(directory)): + with scout.writer_lock(): + with self.assertRaises(SystemExit), scout.writer_lock(): + pass + self.assertFalse((Path(directory)/"state/scout.lock").exists()) + + +class ArtifactRegressionTests(unittest.TestCase): + def test_manifest_detects_contents_but_allows_missing_local_pdf(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + folder = root / "example" + folder.mkdir() + source = folder/"resume.tex" + source.write_text("\\documentclass{article}\nOriginal", encoding="utf-8") + pdf = folder/"resume.pdf" + pdf.write_bytes(b"first PDF") + recorded = {"packages": {"example": manifest.describe(folder, record_pdfs=True)}} + current = {"packages": {"example": manifest.describe(folder)}} + self.assertEqual([], manifest.check_manifest(recorded, current, root)) + pdf.unlink() + self.assertEqual([], manifest.check_manifest(recorded, current, root)) + pdf.write_bytes(b"changed PDF") + self.assertTrue(manifest.check_manifest(recorded, current, root)) + source.write_text("\\documentclass{article}\nChanged claims", encoding="utf-8") + self.assertNotEqual(current["packages"]["example"], manifest.describe(folder)) + + def test_page_metric_matches_heading_not_summary_word(self): + def bbox(args, **kwargs): + Path(args[-1]).write_text('' + 'Ten' + 'experience' + 'Professional' + 'Experience') + with patch.object(page_metrics.shutil, "which", return_value="pdftotext"), patch.object(page_metrics.subprocess, "run", side_effect=bbox): + self.assertEqual(49.9, page_metrics.measure(Path("unused.pdf"))["experience_pct"]) + + def test_older_imported_event_cannot_reopen_withdrawn_application(self): + data = records.load() + row = next(row for row in data["applications"] if row["company"] == "SBB") + args = Namespace(id=row["id"], date="2026-09-10", event=None, outcome="applied", note="older event", next_action=None) + with self.assertRaises(ValueError): + tracker.update_application(data, args)