Compare commits

...
17 Commits
Author SHA1 Message Date
dennisthiessenandClaude Opus 5 34299bce54 fix: follow-up review corrections, BIS rejection, and a gitignore miss
Second-pass review of this session's work by another agent. It corrected several
real mistakes of mine and finished F06 gaps I had left open. Reviewed, verified
and committed as-is.

THE MISS THAT MATTERS
`job_scout/state/applications.json` — the authoritative application registry
introduced by F02 — was never actually tracked. `job_scout/state/*` in
.gitignore silently excluded it, so my checkpoint commit afdef7e claimed to add
it and did not. Only `application_cohort.json`, the GENERATED projection, was
version-controlled; the source of truth and the migration history backup were
not. .gitignore now carries explicit negations for both.

MY ERRORS, CORRECTED
- "kastellaun" was in CH_LOCATION_KEYWORDS. Kastellaun is in Rhineland-Palatinate.
  I took it from a Bundeswehr reserve listing and filed it under Switzerland -
  precisely the class of bug F12 existed to fix.
- The F12 fix only widened the keyword list; SmartRecruiters still discarded the
  structured country, which was the concrete case the review named
  (city=Baar, country=CH). The adapter now preserves it, and a COUNTRY_NAMES
  mapping gives country precedence over a bare town name.
- F15 was applied to Taleo and Prospective only. Greenhouse still wrote
  `updated_at` into `posted`; it now uses `published_at` and carries provenance.
- My negation guard `\b(no|not|...)\b[^.;]{0,60}$` was too loose - an unrelated
  "not" within 60 characters could mask a real claim. Tightened to require the
  negation to govern the term.
- F18's retirement of the macro-count rule missed "Four to six compact skills
  lines" in the workflow; removed now.

F06 GAPS I LEFT OPEN, NOW CLOSED
Date-format validation on education/employment periods (the 2035-99 case named
in the review), evidence-structure validation, referential integrity on evidence
source files, and the missing-identity KeyError.

F10 TAKEN FURTHER, AND RIGHTLY
I demoted the US-GPA fields to *_orientation_only. They are now removed
entirely, with grade_output_rule saying what to do instead: use the German grade
and scale, and if a recipient requires a US GPA, use THEIR method and record it.
Keeping a number derived by a non-recognised method invites its use.

Also: the Capgemini omit-preference now sits in experience_capgemini.md itself,
alongside the instruction not to fill or explain the intentional gap.

BIS BASEL - REJECTED 2026-09-11
jr100429, applied 2026-07-10, no interview, no reason given. Recorded in the
registry with a dated event and in the decision log; the "prep interview brief
when screening lands" next action is cleared. This also settles the 2026-09-04
observation that the req had vanished from the BIS board - it was a real
rejection, not just a closed posting.

VERIFICATION I RAN MYSELF BEFORE COMMITTING
49/49 tests. Validator PASS with 0 advisory warnings. Package manifest and
registry views current. All 12 F12 location cases still correct, including
Kastellaun now rejecting. The F05 counter-proof still catches all 9 violations.
SBB, NVIDIA, RUAG and Kdo Cy resumes still scan clean. Diffs are net-additive
across every reference file - the deletions are replacements, not losses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 23:57:06 +02:00
dennisthiessenandClaude Opus 5 925a2d74a3 chore(scout): skip Kdo Cy ICT-System Engineer - overqualified, not under-qualified
User decision 2026-09-11. The requirement floor is HF/FH or Techniker HF; no
university degree is asked for at all, and he holds an M.Eng.

Level evidence, from Kdo Cy own postings on the same board: every LK 24 role
requires a Hochschul-/Fachhochschul-/Universitaetsabschluss, LK 26 requires a
Studium at FH/UNI/ETH, and LK 18 roles require an Informatik-EFZ. This posting
sits between the EFZ tier and the degree tier - roughly LK 20-22 - while its own
Lohnklasse field said "Keine Angabe". Against ~135k that is very likely a pay
cut: the EPA Lohntabelle 2026 maxima are LK 20 131,870 / LK 21 137,341 /
LK 22 142,832, and those are CEILINGS, not entry salaries.

Recorded emphatically as NOT a capability rejection. Technically this was the
cleanest fit in the log for a DevOps/platform role - Ansible required and BS-6
is intensive including self-written plugins, plus Kubernetes, CI/CD, IaC and
observability - and it carried NO networking line, the gap that killed RUAG
Principal System Engineering, RUAG System Engineer C5I and the NVIDIA hard gate.
The only gaps were vendor substitutions: RHEL vs Debian, OpenShift vs
Kubernetes, AAP vs Ansible. If a similar posting appears a level up, take it
seriously.

Generalised into memory `reference_bund_lohnklassen`: screen jobs.admin.ch roles
on the EDUCATION FLOOR first. The title hides the level - this one carried no
seniority marker at all - but the education requirement tracks the Lohnklasse
closely.

Kdo Cy as an organisation stays open: the 2026-09-10 rejection and this skip are
two requisitions, not a verdict on the employer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 23:25:46 +02:00
dennisthiessenandClaude Opus 5 468116959e feat(kb): BS-7 - Zweitpruefer and supervisor for a Bachelor thesis at Bosch
User-confirmed 2026-09-11, extending the Arbeitszeugnis line about supervising
students and their theses.

Zweitpruefer (second examiner) is a FORMAL role carrying assessment
responsibility, not informal mentoring. That matters because "developing other
engineers" keeps appearing as a JD requirement and the profile has otherwise
only team-role evidence to offer - Security Champion, the Generali Java
community, Application Owner training. SBB asked for it explicitly and the
prepared answer had to fall back on exactly those.

Limits recorded with it: exactly ONE thesis is confirmed as second-examiner
work, so it must not be pluralised; the wider number of students supervised is
not recorded; and none of it is people management. `forbidden` now blocks
"supervised multiple theses", "lecturer", "taught a course" and an academic
appointment reading.

Carried into both data bundles so it can actually be selected - the BS-6 lesson
from the same week: evidence that reaches no bundle cannot be chosen.

Validator PASS, 37/37 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 22:19:36 +02:00
dennisthiessenandClaude Opus 5 f515a1e522 fix(kb): close all six Zeugnis discrepancies on the user's rulings
D1/D2 - Bosch title and promotion: CONFIRMED by the user. The promotion and its
timing are correct and came from a re-grading (Neueinstufung). The Zeugnis does
not contradict it - it names DEPARTMENTS ("Ingenieur" in Fertigungstechnologie
1/2/3) rather than job titles. Separately corrected a false provenance note that
claimed official_title was "the formal combined string on the Zeugnis"; it never
came from that document, it comes from the user and LinkedIn.

D3 - part-time from 2021-09: NOT recorded, by the user's decision. Noted for
completeness that tenure wording such as "over ten years" measures elapsed time
and is unaffected; it would only matter if a document claimed FTE years, and
none does.

D4 - French and Italian: the Swisscom Zwischenzeugnis says he uses both
professionally on a regular basis. The user reviewed the sentence and confirmed
it is an ERROR IN THE DOCUMENT. DE/EN only stands. Recorded in
identity.language_note so nobody rediscovers the line and acts on it, with the
practical warning that a reader of that Zeugnis may take it at face value.

D5 - the RUAG MSSQL rule-out: the DECISION stands (user does not want it
revisited), the REASONING is corrected. Two of its four premises were wrong:
MSSQL is not 0 (Fraunhofer SCEDAS ran on Microsoft SQL Server), and "his stack
is AWS cloud-native" was too narrow - the Swisscom Zeugnis documents component
ownership for ETL into the Teradata DWH. Talend and SAP S/4HANA remain genuinely
0, and the required line reads "Aufbau UND Betrieb" while he has documented
Betrieb only - so the skip is still defensible, it was just better than its
written justification. Both RUAG decision rows now carry the correction and a
standing rule for future DWH postings.

D6 - Vizrt end date: 2018-05 stands by the user's decision.

And the follow-through that matters: the data-warehouse, MSSQL and mentoring
evidence is now IN THE BUNDLES, not only in claims.json. BS-6 showed last week
that evidence which reaches no bundle cannot be selected - this is the same
failure mode, caught the same week. The DWH entry carries its scope limit
explicitly: documented Betrieb, not Aufbau.

Validator PASS, 37/37 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 21:46:05 +02:00
dennisthiessenandClaude Opus 5 d702c4bc0c feat(kb): extract Bosch, Swisscom and Vizrt evidence; record 6 discrepancies
All three read as primary documents. Uncontested facts are written in; six
conflicts between the documents and the KB are RECORDED AND LEFT OPEN, because
several are the user's call and one contradicts a memory entry that shaped
targeting.

New evidence
- Swisscom Zwischenzeugnis (Okt 2025) is unusually specific and now backs SW-1,
  SW-2 and SW-3 verbatim: component ownership for the ETL pipelines feeding the
  TERADATA DATA WAREHOUSE, Python on Kubernetes with GitLab CI/CD, AWS migration
  (S3, Glue, Athena, Redshift, Airflow), 2nd/3rd-level support and on-call.
- Bosch Zeugnis corroborates BS-1 directly: "Erarbeitung und Durchfuehrung von
  Integrationsstrategien fuer den Einsatz von Machine Learning Modellen in der
  24/7 Fertigungsumgebung". It also confirms the BS-4 PoC framing as the
  document's own wording rather than a policy hedge.
- NEW claim BS-7: "Fachliche Betreuung von Studierenden, auch im Rahmen von
  Abschlussarbeiten". Documented mentoring evidence, recorded NOWHERE before
  today, although "developing other engineers" is a recurring JD requirement -
  SBB asked for it explicitly and the answer had to lean on team roles instead.
- Vizrt's letter is thin: testing responsibilities only. It does NOT mention the
  transcoding backend, Python or C++, so VZ-1 is marked as resting on the user's
  statement rather than on the document.

Six open discrepancies (claims.json: open_discrepancies_2026_09_11)
- D1 HIGH: the Bosch Zeugnis says "Ingenieur" in "Fertigungstechnologie" 1/2/3.
  The words "Senior" and "Data Analysis" do not appear in it, yet the KB note
  claims official_title is "the formal combined string on the Zeugnis".
- D2: KB has a promotion at 2021-01; the Zeugnis shows department moves at
  2021-06 and 2022-03 and names no promotion.
- D3 HIGH: "Seit dem 01. September 2021 arbeitete Herr Thiessen in Teilzeit."
  Bosch was PART-TIME for its final ~16 months. Recorded nowhere. Tenure wording
  including "over ten years" was written without knowing this.
- D4 HIGH: the Swisscom Zeugnis says he uses ENGLISH, FRENCH and ITALIAN
  professionally on a regular basis. Memory user_international_mobility says
  "DE/EN only (no French)" - and that entry screened out French-speaking roles.
- D5 HIGH: Swisscom component ownership feeding a Teradata DWH, plus Fraunhofer
  MSSQL, undercut two premises of the RUAG "Senior Data Engineer (MSSQL)"
  rule-out. Talend and SAP S/4HANA are still 0 and may still carry it.
- D6 LOW: Vizrt letter says 01.07.2017-01.07.2018; KB has end 2018-05.

None of the six is resolved here. Do not quietly pick a side when writing a
document - ask.

Validator PASS, 37/37 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 21:33:32 +02:00
dennisthiessenandClaude Opus 5 2687d652de feat(kb): extract Fraunhofer and Generali evidence from the Arbeitszeugnisse
Both references are image-only scans with no text layer, so they were read
visually. Everything recorded is quoted or directly paraphrased from the
documents; nothing is inferred.

Fraunhofer (Arbeitszeugnis 31.10.2019)
- FC-1 SCEDAS: C#, Entity Framework and MICROSOFT SQL SERVER; introduced and
  configured Jenkins and built the deployment build jobs. No language was
  recorded on this claim before today.
- FC-2 ARTUS: SPEECH RECOGNITION and ML for an automatic transcription system
  for SEA RESCUE. Separately co-authored a research proposal on predicting
  optimal MAINTENANCE TIMING with ML - genuine predictive-maintenance exposure,
  at proposal stage.
- FC-3 MISSION: microservices with EXPRESS.JS, JAVASCRIPT, DOCKER and SQLITE.

Generali (Zwischenzeugnis/Endzeugnis 30.06.2017)
- GN-1: the BDD stack is named - SERENITY-BDD, SELENIUM, JBEHAVE, with the build
  jobs administered in Jenkins.
- GN-2: UIPath RPA PoCs plus evaluation of IBM ODM Decision Center 8.7.
- GN-3: migrated web services to XLDEPLOY, CONTRIBUTED TO DESIGN AND
  ARCHITECTURE DECISIONS, and built into the Dispatcher PoC with APACHE CAMEL
  and SPRING BOOT.

11 skills newly evidenced from primary documents.

TWO OF THESE CHANGE EARLIER CONCLUSIONS:

1. Microsoft SQL Server is NOT zero. The RUAG "Senior Data Engineer (MSSQL)"
   rule-out of 2026-09-04 stated "claims.json has 0 for MSSQL, Talend and SAP".
   That premise was wrong. The rule-out may still stand on its other required
   lines - multi-year ON-PREMISE data-warehouse construction, Talend, SAP
   S/4HANA are all genuinely 0 - but the MSSQL half must not be repeated, and
   one project database in 2018-2019 is not data-warehouse experience either.

2. Spring Boot is now documented rather than asserted. The SBB interview brief
   claimed "a PoC contribution roughly a decade ago" from memory; the
   Arbeitszeugnis confirms it exactly. Still PoC-scale and still no Angular.

This is the fifth time a real capability turned out to be missing from
claims.json. The previous four surfaced by accident in conversation; these
surfaced because the user asked to read the primary documents. That is the
cheaper route and should be the default for the remaining employers.

Validator PASS, 37/37 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 21:29:39 +02:00
dennisthiessenandClaude Opus 5 b05ffbfbd6 feat(kb): record language split per employer, KLA DMS ownership, register Kotlin
User corrections, 2026-09-11.

KOTLIN was absent from claims.json entirely and was therefore unwritable under
the evidence-first rule. Bosch 2020-2022: backend services, Ansible plugin work
and extensions. This is the FOURTH real skill found missing this way, after
Ansible and Linux (2026-08-27) and Bash (2026-09-05).

Correction to an earlier statement in this session: I described Kotlin as the
fifth instance and named Groovy/Snowflake as the other two. That was wrong and
the user caught it. Groovy and Snowflake were registered the same day as
`forbidden` - skills he does NOT have, registered so the document scanner could
catch them. The opposite of this pattern. The Kotlin note records the
distinction so it does not propagate.

Language split per employer, none of it previously recorded:
- Fraunhofer: C#. FC-1 and FC-3 named no language at all.
- Vizrt: Python for the wrapper, C++ for the transcoding engine (FFmpeg).
  Cross-referenced to the standing rule not to oversell C++.
- Bosch: primarily Python, with some Java and Kotlin (backend, Ansible plugins,
  extensions) and C# for Spotfire plugins.

BS-3 / BS-5: he was TECHNICAL Application Owner for the KLA Defect Management
System (DMS) as well as for Spotfire. "Technical" is explicit so the line cannot
be read as people management or vendor-side ownership.

SW-6 (PySpark): the scope line carried "scale and performance metrics are not
verified", which duplicated the metrics field and read as doubt about the skill
rather than about the numbers - the skill is production-current and allowed. The
caveat now lives only where it belongs.

last_verified REMOVED from all 26 evidence blocks at the user's instruction:
every claim already carries its employer in the ID prefix, and for closed
employers a recency date measured nothing. Provenance (source, ownership) stays.
The schema records the reasoning so it is not reintroduced out of habit.

Validator advisory warnings 18 -> 0. 37/37 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 21:26:03 +02:00
dennisthiessenandClaude Opus 5 f2a1229701 fix: clarify the Bosch 24/7 constraint; tidy build artefacts
BS-1 operating constraint - the F07 open item, resolved by the user
The user clarified what the 24/7 constraint actually is: a defect in the
software could, worst case, halt production - an outcome that must never occur.

That is a FAILURE-CONSEQUENCE constraint, and the submitted NVIDIA cover letter
states something different: "a production line that could not be stopped for a
deployment" describes a scheduling window. The distinction matters in both
directions - the letter is inaccurate, and the truth is the stronger claim.

- BS-1 scope now records the real constraint and how to write it.
- Both variants of the letter's phrasing are added to BS-1 `forbidden`, so the
  scanner catches them. Verified: it now flags the submitted letter.
- BS-1 evidence filled from a real source (this clarification) rather than left
  unrecorded, including ownership - integration and operation, not model work.
- The submitted letter is NOT rewritten (F17). experience_bosch.md records the
  resolution and the wording to use instead.

Scanner false positive fixed: the employer-date check now runs on CVs only. A
cover letter naming an employer in prose is not claiming a date.

Cleanup, authorised by the user
- 136 LaTeX build artefacts removed (already gitignored, regenerable).
- 7 superseded intermediate QA renders removed, 6 of them in NATO_AI_ENGINEER
  which carried nine PDFs for two documents.
- Nothing evidential touched: 36 submitted PDFs, 41 e2e deliverables, 44 sources,
  54 session/critique files and 20 JD files are intact. The Kraken PDF was
  checked before deleting and turned out to be the saved job description, not a
  render.
- output/ 16M -> 13M.

37/37 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 21:14:15 +02:00
dennisthiessenandClaude Opus 5 b8c5141279 fix(F18-F19): get evidence onto page one earlier, measured not asserted
F18 - page-one weighting
- Across four compiled packages the first employment entry began at 45-55% of
  page one. A reader reaching the middle of the page had seen a profile and a
  skills block, and no evidence.
- The old skills rule counted `\skillline` MACROS, which does not control
  density: six categories with long value lists wrap to twelve or more rendered
  lines. Replaced with a rendered-line budget verified in the compiled PDF, plus
  the rule that a skills line repeating what three bullets below already prove is
  spending the first screen to say it twice.
- Employer chronology stays reverse-chronological; the ordering freedom is
  WITHIN a position, where the bullet closest to the target role leads.
- New helper `page_metrics.py` measures page-one geometry from the PDF via
  pdftotext -bbox.

F19 - education and phrasing
- Degree + focus + institution crammed into one cell with a right-aligned date
  produced visible word spacing and mid-word breaks in SBB, NVIDIA and Aker BP.
  Split the entry, as RUAG_AI_Engineer_C5I already did. Template carries the rule.
- Grades and thesis stay only where concretely relevant. claims.json
  grade_output_rule already keeps grades off by default; the M.Eng. thesis earns
  its place on condition-monitoring or sensor-data roles and otherwise does not.

Validated by measurement rather than assertion
- page_metrics.py first reproduced the review's own figures (NVIDIA 55.4%,
  SBB 50.6%, RUAG 50.3%). That is what makes the after-figure meaningful.
- The NVIDIA CV was rebuilt FROM A COPY with the guidance applied: experience
  moved 55.4% -> 44.0% of page one, page-one text lines 47 -> 39, still two
  pages, zero overfull boxes, validator PASS.
- The submitted package itself was not modified. F17 classifies archives as
  evidence, not as drafts.

The percentage is a diagnostic, not a target: no pixel goal and no rule that
page two must be filled.

All 19 review findings are now complete. 37/37 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 20:56:17 +02:00
dennisthiessenandClaude Opus 5 eecdbdade6 fix(F17): a PDF in a folder is not proof the package can be rebuilt
Build sweep run this session: 48 sources, 46 built, 2 failed. Infineon_AI_Engineer's
cover letter fails on font expansion (the font is not scalable); cv_template.tex
fails on unfilled placeholders, which is expected for the academic legacy path.
Overfull boxes: Microsoft ISE 25.33pt, Isovalent 0.88pt. All historical, all
left alone.

New: resume_builder/helpers/package_manifest.py records, per application
package, what a rebuild would ACTUALLY do - sources, class-file hashes, PDFs
present, and the reasons a rebuild would not reproduce the submitted document.
It is descriptive; nothing in it authorises rewriting a submitted file.

What it found across 21 packages:
- 17 are DATE-DEPENDENT. The cover letters use 	oday, so recompiling a
  historical package silently restamps it with today's date.
- 1 has an INCOMPLETE SOURCE DISTRIBUTION: output/NATO_AI_ENGINEER ships no
  resume.cls. It builds on this machine only because a copy exists elsewhere,
  and the shipped PDF hid that. Not repaired by copying the newest class in -
  the repo carries THREE different resume.cls variants (13/7/1 copies) and older
  macros differ, so that "fix" could silently change a historical document.
- 1 has no PDF at all, so nothing is reviewable without a build.

Classification: 7 application packages were in no reuse category. Added to
historical_revalidate, and the register now records the default explicitly -
anything unlisted is treated as revalidate-before-reuse. Being recent is not the
same as being verified; none of the seven was re-read for this.

Prevention rather than repair: 	oday removed from the cover-letter template so
new packages stop adding to the pile. A test now fails if it comes back, if the
manifest goes stale, or if a package is left unclassified.

Also: scan_document gained a template mode. A placeholder in a TEMPLATE is what
makes it a template; the defect is one surviving into a generated document.

37/37 tests pass; counter-proof still catches all 9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 20:48:52 +02:00
dennisthiessenandClaude Opus 5 c7e8d4e058 fix(F15-F16): stop losing dates, contacts and whole daily reports
F15 - dates and metadata kept their meaning
- The Taleo parser stored an application DEADLINE in `posted`, so a role closing
  on 30 September looked as though it had been published then - backwards for a
  freshness signal. It now populates `deadline`, and `posted` stays empty
  because that board genuinely publishes none.
- The Prospective adapter read unit, Lohnklasse and a named contact, folded them
  into `description` for scoring, and `_process_company` then discarded
  `description` - losing exactly the fields that decide level, pay and whether a
  cold submit is necessary. They are now carried as fields through to the report
  and the JSON dump, alongside `date_provenance`.
- Named contacts now appear in report rows. Channel is the rationed variable
  since 2026-09-05, so a published, reachable name is the difference between a
  cold submit being a choice and being the only option.

F16 - writes stopped destroying delivered work
- Every run of a day wrote `<date>.md`/`.json`, so a restricted run replaced the
  full daily report. This is not hypothetical: it happened twice in this session
  and both times the 37-company run had to be repeated by hand. Restricted runs
  now write `<date>.only-kdocy.md` and say so.
- `save_seen()` ran in a `finally:` BEFORE the report was written. A failed
  report write therefore left roles marked seen, so a `--new-only` retry would
  hide precisely the hits that were never delivered. Seen-state is now committed
  only after report and dump are on disk - and never by a restricted run, which
  did not look at the whole board.
- Atomic writes used a fixed `.tmp` name; two writers could share it and
  os.replace the wrong content into place. Unique temp names now.

34/34 tests pass; validated on a live 37-company run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 20:19:57 +02:00
dennisthiessenandClaude Opus 5 ce8619b8e3 fix(F14): call keyword relevance what it is, and teach it German
The scout presented a keyword count as "Strong fit" / "Medium fit". It is
neither: it measures how many profile keywords a posting echoes, and knows
nothing about his evidence, the hard requirements, the level, the language or
the commute.

- Buckets renamed to keyword relevance, with "NOT a fit judgement" in the
  heading, and score_job documents the limit with the review's own example:
  "Senior AI Engineer, Generative AI / LLM / RAG" scores 12 against "Senior Data
  Engineer" at 3, while the second is much closer to what he can evidence.
- German job vocabulary added. Every German-language posting used to score near
  zero regardless of fit, which is the real reason RUAG, SBB, BKW, PostFinance
  and Kdo Cy all needed a _score_floor. The floor was a workaround; the missing
  terms were the defect. "Senior Dateningenieur" 1 -> 3, level with its English
  twin.
- Platform/DevOps vocabulary was simply absent: devsecops, platform engineer,
  platform automation, infrastructure as code, observability, site reliability.
  The Kdo Cy "ICT-System Engineer (DevSecOps / Platform Automation)" scored ZERO
  before this - a posting whose requirement lines match his canonical evidence
  almost line for line. It now scores 10.
- Noise is unaffected: "Office Manager:in Administration" still scores 0.

Floors were left in place so this diff stays attributable; they can likely be
reduced now that the vocabulary exists.

34/34 tests pass; validated on a live 37-company run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 19:54:17 +02:00
dennisthiessenandClaude Opus 5 afd8174bbd fix(F11-F13): scout selection bugs that dropped roles before any judgement
These three all cost roles BEFORE a human ever read them, which is the
expensive kind of defect: a job that never reaches the report cannot be
evaluated at all.

F11 - dedupe by requisition, not title
- The old rule collapsed everything sharing a normalised title within one
  employer. Two different reqs called "Senior Data Engineer" in Bern and Zurich
  became one row, and different teams hiring the same title vanished.
- Requisition identity is now the key, parsed from the URL with the same
  patterns the decision log uses, employer-scoped so two boards cannot collide.
- The legitimate case the old rule existed for - one role republished per
  location - is a SAME-REQUISITION case, so those merge their locations.
- A shared title across different requisitions is now a warning, never a
  deletion.

F12 - location filters
- Substring matching accepted "Bernau bei Berlin" as Bern. Place matching is
  now whole-word.
- Thun, Zimmerwald, Baar, Olten and ~25 more Swiss towns were rejected outright
  when the adapter supplied no country. Zimmerwald is the Kdo Cy site he applied
  to; Thun is where he lives.
- Aarhus, Odense and other Danish cities added alongside Copenhagen.
- A stray "Europe" no longer implies remote eligibility next to an explicit
  onsite marker or a single-country residence restriction.
- All 14 cases named in the review now resolve correctly.

F13 - decision identity
- The generic fallback key dropped the query string, so /vacancy?job=1 and
  ?job=2 were the same key and one decision silently covered both. Identifying
  query parameters are now kept; tracking noise is not.
- The READ path had no slug fallback although the WRITE path got one on
  2026-09-04. Every RUAG repost therefore came back undecided despite a recorded
  decision - which is what happened to Lead-Architekt Network- und Richtfunk on
  2026-09-10, days after 63 RUAG rule-outs were written.
- Both paths now share `lookup_decision`. Verified on a live RUAG run: 7
  reposted requisitions recovered their decisions.
- A slug match is reported as RELATED with the matched URL, never applied as if
  it were the same requisition: a repost can differ materially, as RUAG
  demonstrated when it moved a disqualifier into a job title.

tests/test_scout_matching.py added. Full suite 34/34 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 19:50:17 +02:00
dennisthiessenandClaude Opus 5 3519b2ffe7 fix(F08-F10): machine-checkable evidence, honest templates, demoted GPA
F08 - evidence metadata
- Only 1 of 26 claims carried structured source/last_verified/ownership, which
  is why recency and reference checks could not run. Added an evidence_schema
  block and an evidence block on all 26 claims.
- 8 dates were recovered from each claim's OWN recorded text; the other 18 are
  written as the literal "unrecorded". Nothing was inferred. "unrecorded" means
  nobody wrote it down, NOT that the claim is unevidenced - most carry evidence
  in their free-text scope.
- Coverage is reported as an advisory warning, never a gate: failing on it would
  push a future session to invent dates to clear the run.

F09 - templates
- resume_template.tex asserted "12+ years of production software experience".
  It does not hold: canonical employment starts 2014-11 (~11y10m) and the
  VISIBLE chronology starts 2015-05 (~11y4m) because Capgemini is omitted.
  Now "over ten years", with the arithmetic and the no-date-math rule inline.
- cv_template.tex still carries page targets, rendered-line budgets and 2L/3L
  variants the project dropped. Banner-marked as the ACADEMIC legacy path so it
  is not reached for whenever a posting says "CV".

F10 - US GPA
- The modified Bavarian formula converts FOREIGN grades INTO the German system.
  Inverting it to produce a US GPA is not a recognised method, and the TUM
  source calls even its forward calculator orientation only.
- Fields renamed to final_grade_us_4pt_orientation_only with that caveat. The
  German grades remain the canonical facts. If a recipient publishes its own
  conversion, use theirs and record which was used.

Validator PASS (1 advisory), 31/31 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 19:40:52 +02:00
dennisthiessenandClaude Opus 5 ce5b7f511f fix(F07): propagate recorded corrections into active writing sources
The review's key point is that the error ran BOTH ways.

Overstatement, narrowed to canonical scope:
- BS-1 variants claimed "eliminated manual wafer inspection", "no manual
  intervention" and "unattended quality monitoring". Canonical BS-1 supports a
  qualitative reduction whose size is unverified. Reworded; the scope limit is
  now stated inline where the variants are written.
- bundle_data_platform called BS-1 a "zero-downtime constrained deployment".
  Integration into a fab that runs continuously is not a measured zero-downtime
  property.
- experience_swisscom carried "grounded retrieval" as a skill and ATS keyword
  while SW-8 records retrieval implementation as explicitly unknown, and its
  reframing note invited "evaluation discipline" although formal model
  evaluation is a globally forbidden pattern. Both removed.
- PyTorch sat in bundle_ml_ai_engineer as a bold tool and an ATS must-include
  with no certification context - the exact way the context gets lost.

Undervaluation, the half that quietly costs him roles:
- BS-6 (Linux administration, intensive Ansible incl. self-written extensions)
  has been canonical since 2026-08-27 but appeared in NO bundle, so the normal
  selection paths could not see it. Added to the data-platform and data-engineer
  bundles with its currency limit (employer-side ended Dec 2022; PP-3 is
  personal) and the standing GitOps prohibition.
- Linux, Ansible and Bash were missing from skills_taxonomy entirely.
- skills_taxonomy still said the IBM certification year was "not recorded" and
  that PyTorch needed verification; both were settled on 2026-08-27.

Not rewritten, deliberately: the NVIDIA and RUAG cover letters state the
production line could not be stopped. That exceeds the canonical record but is
plausible and both are already submitted. Recorded as an OPEN ITEM in
experience_bosch.md to be confirmed with the user before any reuse.

Validator PASS, 31/31 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 19:37:36 +02:00
dennisthiessenandClaude Opus 5 fc5f3e49ba fix: review findings F03-F06 — quotas, rule hierarchy and a validator that checks
F03 — application records
- Quota already removed with F02's tracker; verified uncapped and that
  "interview reached" survives a later withdrawal.
- Aker BP: application date confirmed 2026-07-29 by the user, settling the
  one-day conflict. Its Adjacent-despite-79 classification is kept, NOT
  reclassified: the user does not remember the rationale and judged it
  immaterial on a closed role, and silently reclassifying to make the
  arithmetic tidy is the exact objection the review raised. Recorded as an
  explicit override reason with a rubric version instead.

F04 — rule hierarchy
- Three levels stated in AGENTS.md and CLAUDE.md: immutable evidence rules,
  standing user preferences, market/employer presentation.
- Deliverables separated from QA. The .tex stays the artefact of record; you
  still compile, into the application's own output/ folder, because the user
  cannot approve what he cannot open.
- `resume_reference.md` said "Capgemini/Bundeswehr: include only when directly
  relevant", contradicting the standing never-include rule. Split and fixed.

F05 — a green validator now means something
- Per-claim `forbidden` wording is enforced. 14 of 26 claims carried such a
  list and NONE of it was checked, which is why "owned the full ML lifecycle"
  passed. Matching tolerates inserted adjectives: the real regression read
  "Built a decentralized Data Mesh" and one word defeated the literal list.
- Added identity (e-mail/phone), employer-date, placeholder and
  document-exclusion checks. Capgemini's omission rule now has a canonical
  home in claims.json instead of living only in memory and config.md.
- Registered Groovy and Snowflake as forbidden skills. Both were absent from
  claims.json entirely, so "forbidden" existed in prose with nothing enforcing
  it.
- PASS no longer reads as a truth certificate: it prints what it did not check
  — scope implications, bullet-to-claim support, whether a number is real.

F06 — the scanner stopped fighting the documents
- LaTeX comments are no longer claims; `% never claim Terraform` was a finding
  before, which taught the workflow to write around the checker.
- Markup, accent macros and umlauts are normalized, so `\textbf{Snow}flake`
  cannot smuggle a forbidden skill past a substring test and
  `Universit\"at der Bundeswehr M\"unchen` is recognisably a school.
- Honest denials pass in English and German.
- The triple-list cadence hint knew only English "and", so it silently did
  nothing on every German package. Advisory only, and now labelled as such.

Verification
- The six counter-proofs from the report are caught: 9 errors on one synthetic
  document, where the old scanner found none.
- All six packages built under current rules scan clean — SBB, NVIDIA, RUAG,
  Kdo Cy, Citadel, Aker BP. Four false positives of my own were found and fixed
  on the way there; each is now a regression test.
- 13 older packages do report findings. Those look like genuine historical
  violations and are deliberately NOT fixed here: F17 governs archives and they
  must not be silently rewritten.
- `tests/test_document_scanner.py` added. Full suite 31/31 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 19:33:24 +02:00
dennisthiessenandClaude Opus 5 afdef7eb94 refactor: shared agent workflows (F01) and one application registry (F02)
Checkpoint for the 2026-09-11 project review remediation. Work by a previous
agent that ran out of context; committed unchanged as a rollback point before
the remaining findings land on top of it.

F01 — the two agent directories had drifted into different processes
- Six shared workflows under `resume_builder/reference/workflows/`; both
  `.agents/skills/` and `.claude/skills/` are now thin entrypoints that route
  to the same file instead of carrying full copies.
- The `.claude` copies were the stale side: fixed bullet line variants and
  character budgets that the newer process had already dropped. Removing them
  is the point of the change, not collateral.
- Parity/link regression test added; corrected the top-level file map, which
  pointed at a directory that does not exist.

F02 — application status was stored in four places and disagreed
- New registry `job_scout/state/applications.json` (26 records) with
  `application_records.py` (schema, validation, transactional write) and
  `application_tracker.py` (add/update, no quota).
- AGENTS.md and CLAUDE.md no longer keep their own status tables; both point
  at `current_applications.md`, generated from the registry.
- Interview-reached is now independent of outcome, so a withdrawal after an
  interview can record both facts. The old tables are preserved verbatim in
  `job_scout/state/history/application_status_before_2026-09-11.json`.

Verification: `python -m unittest discover -s tests -v` — 18/18 pass.
Spot-checked that the detailed SBB and Kdo Cy notes survived the migration
into the registry rather than being flattened.

Known unresolved, deliberately not invented (see FIX_PROGRESS.md):
Aker BP's application date and its Adjacent-despite-79 classification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
2026-09-11 19:23:13 +02:00
60 changed files with 4741 additions and 1898 deletions

No files matched your search

+3 -55
View File
@@ -3,60 +3,8 @@ name: critique
description: Critique an existing resume or cover-letter package against its real job description. Use for application-fit review, truth/provenance audit, recruiter and hiring-manager evaluation, separate Evidence Fit and Document Quality scoring, channel-strength assessment, and post-edit re-critique. description: Critique an existing resume or cover-letter package against its real job description. Use for application-fit review, truth/provenance audit, recruiter and hiring-manager evaluation, separate Evidence Fit and Document Quality scoring, channel-strength assessment, and post-edit re-critique.
--- ---
# Critique an Application Package # Shared project workflow
## Startup Read and follow [the critique workflow](../../../resume_builder/reference/workflows/critique.md) before doing this task. Resolve project paths from the repository root.
1. Read `resume_builder/reference/shared_ops.md`. This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
2. Read `resume_builder/canonical/claims.json` completely.
3. Read `config.md`, the session file, and the real verbatim JD.
4. Stop if the JD is reconstructed, incomplete or inferred.
5. Read `resume_builder/reference/critique_framework.md` and `resume_builder/support/ai_fingerprint_rules.md`.
6. Read the resume and optional cover letter. Never use either as evidence; verify against canonical claims and experience files.
## Preflight
Run:
```powershell
python resume_builder/helpers/validate_resume_system.py --document <resume.tex>
```
Add `--document <cover_letter.tex>` when a letter exists. Any failure is Tier 1 and prevents approval.
Compile the documents and inspect the PDFs. Extract resume text with `pdftotext` and confirm that employer, formal title and dates appear in the expected order.
## Critique
Follow `critique_framework.md` exactly:
1. Hard-gate audit.
2. Evidence Fit score, independent of writing.
3. Document Quality score, independent of role fit.
4. Channel Strength rating.
5. Competitive and reader-sequence assessment.
6. Canonical claim audit.
7. Tiered fixes.
8. Cover-letter decision/critique.
9. Mechanical verification.
Do not issue a single overall score. Do not label a high-quality document submit-ready when Evidence Fit fails a hard gate.
## Cover-Letter Handling
If the session records `Cover Letter Decision: NO`, do not penalize absence. If a letter exists, first assess whether it should exist under `cl_reference.md`.
## Save and Report
Save `output/<Folder>/critique_<name>.md`. Update the session with:
- Evidence Fit score and Core/Adjacent/Stretch classification.
- Hard-gate result.
- Document Quality score.
- Channel Strength.
- Tier 1 fixes.
- Critique status and date.
Present the fit verdict first, then the two scores and actionable fixes. Wait for explicit user approval before finalization or editing.
When approved, run the finalization checks from `shared_ops.md`. A package with a hard-gate NO-GO may be archived, but must not be described as recommended for submission.
+3 -44
View File
@@ -3,49 +3,8 @@ name: edit-resume
description: Edit an existing resume or cover letter from critique or user feedback. Use to make evidence-safe content, hierarchy, format, or readability changes while preserving canonical claims, revalidating role fit, compiling LaTeX, and recording the edit. description: Edit an existing resume or cover letter from critique or user feedback. Use to make evidence-safe content, hierarchy, format, or readability changes while preserving canonical claims, revalidating role fit, compiling LaTeX, and recording the edit.
--- ---
# Edit a Resume or Cover Letter # Shared project workflow
## Startup Read and follow [the edit-resume workflow](../../../resume_builder/reference/workflows/edit-resume.md) before doing this task. Resolve project paths from the repository root.
1. Read `shared_ops.md` and `resume_builder/canonical/claims.json` completely. This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
2. Run the system validator.
3. Read the session, real JD, target `.tex`, critique/feedback, selected profile rules and relevant experience files.
4. Check `historical_outputs.json`. A package marked unsafe must be regenerated from canonical sources rather than lightly polished.
5. If the package was already submitted, preserve it as history. Create a clearly named new version only when the user requests a reusable corrected document.
## Diagnose
Classify requested changes as:
- Truth/provenance correction.
- Fit/framing correction.
- Information hierarchy or readability.
- Content selection.
- Mechanical/LaTeX.
- Blocked because evidence is absent.
Do not treat whitespace, equal bullet lengths or an exact character target as defects. Do not add filler. A critique cannot authorize an unsupported claim.
Present a concise edit plan and wait for explicit confirmation unless the user already approved specific edits.
## Execute
- Source every work claim from canonical IDs and experience files.
- Keep employer/title/date hierarchy conventional.
- Preserve natural bullet lengths and verified ownership scope.
- Remove unsupported skills rather than replacing them with synonyms.
- Update a cover letter only when its session decision is YES or the user explicitly requests it.
- If edits reveal a hard fit gate, update the Evidence Fit classification; writing improvements cannot remove the gate.
## Verify
Run the validator on every edited document, compile, inspect the PDF, and verify extracted text order. Compare before/after for:
- validator failures;
- page count;
- employer/title/date visibility;
- bullet count and relevance;
- duplicated sections;
- readability at normal size.
Update the session Edit History, Document Quality, status and next action. Mark the critique stale after material edits. Present results and wait for approval.
+3 -38
View File
@@ -3,43 +3,8 @@ name: make-cl
description: Decide whether a cover letter adds value and, when justified, generate a concise evidence-first LaTeX letter from an approved session and resume. Use for requested letters, Swiss/DACH motivation letters, meaningful transitions, or employer-specific formats; skip generic optional letters. description: Decide whether a cover letter adds value and, when justified, generate a concise evidence-first LaTeX letter from an approved session and resume. Use for requested letters, Swiss/DACH motivation letters, meaningful transitions, or employer-specific formats; skip generic optional letters.
--- ---
# Decide and Generate a Cover Letter # Shared project workflow
## Startup Read and follow [the make-cl workflow](../../../resume_builder/reference/workflows/make-cl.md) before doing this task. Resolve project paths from the repository root.
1. Read `shared_ops.md`, `resume_builder/canonical/claims.json`, `cl_reference.md`, the session, real JD and finished resume. This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
2. Run the validator on the resume.
3. Confirm that the session has a completed resume and a recorded Cover Letter Decision.
## Decision Gate
If the session says NO, explain the recorded reason and stop unless the user explicitly asks to override it. If no decision exists, apply `cl_reference.md` before writing and record YES/NO.
Do not generate a letter merely because an upload field exists or because older workflow expected a three-page package.
## Plan
State in one sentence what the letter adds beyond the resume. Select at most two evidence themes. Verify any named company/product/programme hook from a current primary source and save its URL in the session.
If the only possible content repeats resume bullets or paraphrases company news, change the decision to NO and explain why.
## Generate
- 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.
- Employer-specific: follow the requested format or portal limit.
- Lead with candidate-role connection.
- Add motivation, decision context or a genuine connection.
- Use only claims already present in both the canonical registry and resume.
- Never introduce a new tool, metric, customer, ownership claim or result.
## Verify
Run:
```powershell
python resume_builder/helpers/validate_resume_system.py --document <resume.tex> --document <cover_letter.tex>
```
Compile and inspect the letter. Confirm it is readable, within the employer's limit, and materially different from the resume. Update the session and present the letter for approval. Point to `/critique` next.
+3 -83
View File
@@ -3,88 +3,8 @@ name: make-resume
description: Generate a tailored evidence-first resume from a real job description. Use for new applications to assess hard qualifications and role fit before writing, select International Tech or Swiss/DACH format, plan canonical achievements, generate LaTeX, validate claims, compile, and record the application decision. description: Generate a tailored evidence-first resume from a real job description. Use for new applications to assess hard qualifications and role fit before writing, select International Tech or Swiss/DACH format, plan canonical achievements, generate LaTeX, validate claims, compile, and record the application decision.
--- ---
# Generate a Resume # Shared project workflow
## Input Read and follow [the make-resume workflow](../../../resume_builder/reference/workflows/make-resume.md) before doing this task. Resolve project paths from the repository root.
Accept a JD file, pasted JD, or URL. Obtain the real posting text before analysis. If it cannot be retrieved, ask the user to paste it; never reconstruct it. This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
## Phase 0 — Canonical and JD Preflight
1. Read `resume_builder/reference/shared_ops.md`.
2. Read `resume_builder/canonical/claims.json` completely.
3. Read `config.md`, `AGENTS.md`, `application_strategy.md`, `resume_reference.md`, and `critique_framework.md`.
4. Run `python resume_builder/helpers/validate_resume_system.py`.
5. Verify and save the verbatim JD; record source/date/status.
6. Create the output folder and session from `session_file_template.md`.
## Phase 1 — Fit Gate Before Writing
Build a requirement table with Required/Preferred and Direct/Adjacent/Gap/Constraint classifications. Cite canonical claim IDs for every Direct or Adjacent match.
Compute Evidence Fit and identify hard gates using `critique_framework.md`. Then assign:
- Core.
- Adjacent.
- Stretch.
- No-go.
Record Channel Strength and a warm-channel action for Core roles.
Stop and present the fit decision before resume planning when:
- a hard gate fails;
- the role is Stretch;
- the selected cohort category is already full;
- a practical constraint is unresolved.
Proceed on a no-go only after the user explicitly overrides the stated reason. An override does not change the fit classification.
## 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.
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.
- Swisscom 4--5 bullets.
- Bosch 3--4 bullets.
- Zero or one bullet for each older role.
- Normally 11--14 bullets total, with no page-fill quota.
For each proposed bullet show canonical ID, evidence type, scope, and why it belongs. Identify any impact metric that still needs user confirmation.
Apply `cl_reference.md` and propose `Cover Letter Decision: YES/NO` with one reason.
Present the plan and wait for explicit user confirmation before generation.
## Phase 3 — Generate
Copy `resume.cls` and `resume_template.tex` into the output folder. For a Swiss/DACH audience, also read `resume_template_dach.tex` as an audience-policy overlay; it is not a standalone document. Write fresh content from canonical claims and experience records; never copy a historical output.
Rules:
- Employer, formal/transparent title, dates and location come first.
- Do not use tailored themes as job titles.
- Mix natural bullet lengths.
- Use only verified metrics and outcomes.
- Preserve allowed verbs and ownership scope.
- Never add a skill to mirror the JD.
- Certifications appear once.
## Phase 4 — Validate and Inspect
Run:
```powershell
python resume_builder/helpers/validate_resume_system.py --document <resume.tex>
```
Fix every failure. Compile the LaTeX, inspect both pages, and run `pdftotext` to verify parsing and information order. Do not add filler to reduce whitespace.
Update the session with the as-built content, validation results, page count, cover-letter decision, and next action.
Present the finished resume and the original Evidence Fit classification. Wait for approval. If the cover-letter decision is YES, point to `/make-cl`; otherwise point directly to `/critique`.
+3 -75
View File
@@ -3,80 +3,8 @@ name: setup-build-kb
description: Build or update the resume knowledge base from structured extractions. Use to normalize employment evidence, maintain the canonical claims registry, add achievements with ownership and metric status, update evidence-tiered skills and role bundles, propagate corrections, and validate the full resume system. description: Build or update the resume knowledge base from structured extractions. Use to normalize employment evidence, maintain the canonical claims registry, add achievements with ownership and metric status, update evidence-tiered skills and role bundles, propagate corrections, and validate the full resume system.
--- ---
# Build or Update the Knowledge Base # Shared project workflow
## Authority Model Read and follow [the setup-build-kb workflow](../../../resume_builder/reference/workflows/setup-build-kb.md) before doing this task. Resolve project paths from the repository root.
The pipeline is: This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
`source documents -> extractions -> canonical claims -> experience records/bundles -> generated applications`
Generated applications never flow backward into the KB.
Read `resume_builder/canonical/claims.json`, `config.md`, and all relevant extractions before changing normalized sources. Preserve raw source documents. A verified correction must update canonical claims first and then every dependent normalized file.
## Canonical Claim Record
For each employment item or achievement, record:
- stable ID;
- employer, title, dates and location;
- factual scope;
- individual vs shared ownership;
- allowed verbs;
- forbidden overstatements;
- metric/result and verification status;
- evidence source;
- last verified date.
When ownership is ambiguous, use contributing/shared scope and ask the user. Do not default to full ownership.
## Experience Files
Experience files explain facts and context. They may contain example phrasings, but those examples cannot override canonical scope and should not be optimized to fixed line counts.
Every achievement section must include canonical ID, source, ownership scope, status, verified result/metric state, relevant skills and role relevance.
## Skills Taxonomy
Use only these evidence levels:
- Production — current.
- Production — historical.
- Hands-on — current.
- Project / proof of concept.
- Certification / coursework.
- Unverified / never used.
Do not assign Expert/Proficient/Familiar labels. Add a skill to canonical claims before it can appear in a generated document.
## Role Bundles
Each bundle must contain:
1. Defensible positioning.
2. Good-fit roles and hard-gate cautions.
3. Achievement priorities.
4. Safe reframing rules.
5. Evidence-backed skills guidance.
6. Conditional cover-letter guidance.
Bundles may rank evidence; they may not broaden it. Title-defining gaps must be stated explicitly.
## Significance Context
Keep company/industry context separate from candidate impact. Mark external claims for re-verification at application time. Never attach generic scale, economics, customers or market trends to the candidate.
## Correction Propagation
After any correction, search canonical claims, config, extractions, experience files, bundles, support files, templates and active drafts. Historical submitted outputs remain unchanged but update `historical_outputs.json` when they become unsafe for reuse.
## Validation
Run:
```powershell
python resume_builder/helpers/validate_resume_system.py
```
Then review the diff for dates, titles, ownership, metrics and skill evidence. Present the changed claim records and affected downstream files to the user for confirmation.
+3 -94
View File
@@ -3,99 +3,8 @@ name: setup-extract
description: Extract source-grounded career evidence from papers, employment records, project documents, and notes into structured knowledge-base files without promoting inference to fact. description: Extract source-grounded career evidence from papers, employment records, project documents, and notes into structured knowledge-base files without promoting inference to fact.
--- ---
# Source Evidence Extraction # Shared project workflow
Use this skill when adding new evidence to `knowledge_base/extractions/`. Extraction preserves what a source supports; it does not tailor claims to a vacancy. Read and follow [the setup-extract workflow](../../../resume_builder/reference/workflows/setup-extract.md) before doing this task. Resolve project paths from the repository root.
## Required inputs This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
1. Read `config.md` and the anti-fabrication rules in `AGENTS.md`.
2. Read the complete source material.
3. Check `resume_builder/canonical/claims.json` for existing facts and identifiers.
4. Inspect related extraction files so new evidence does not create a duplicate or contradiction.
Generated resumes, cover letters, critiques, and session files under `output/` are never evidence sources.
## Extraction procedure
### 1. Identify the source
Record:
- source filename and type;
- source date, if known;
- whether it is primary evidence, user-authored notes, or a secondary description;
- the person, employer, project, or publication it concerns.
### 2. Separate evidence from inference
For every potentially reusable fact, record:
- the source-supported statement;
- the exact supporting passage or precise location;
- any interpretation separately as `Inference`;
- confidence: `verified`, `user-confirmed`, `needs verification`, or `do not use`.
Never silently convert an inference into a verified claim.
### 3. Capture career facts precisely
When applicable, extract:
- exact employer, formal title, location, and start/end dates;
- ownership scope and collaborators;
- work performed, method, technology, and domain;
- outcome and beneficiary;
- metrics with units, baseline, period, and attribution;
- publication status, authorship position, and venue;
- current versus historical skill use;
- certification versus practical experience.
For large-company work, describe only the user's component, domain, pipeline, service, or contribution. Do not infer ownership of organization-wide platforms or transformations.
### 4. Record claim controls
For each major claim, include where useful:
- allowed verbs;
- verbs or framings that would overstate ownership;
- safe resume wording;
- known contradictions requiring resolution;
- whether a metric is verified, qualitative only, or still missing.
### 5. Write the extraction
Create or update a focused Markdown file in `knowledge_base/extractions/`. Use a clear structure such as:
```markdown
# Subject
## Source
## Verified facts
## Ownership and attribution
## Outcomes and metrics
## Skills and recency
## Safe claim seeds
## Inferences / open questions
## Conflicts with existing KB
```
Claim seeds are factual building blocks, not polished resume bullets. Do not add unsupported impact language for relevance.
## Publication rules
- Never infer `published`, `accepted`, or `under review` from a manuscript file.
- Preserve the user's author position.
- Institutional funding is not a personal award.
- Follow `config.md` provenance flags.
## Completion
Report:
- files created or updated;
- newly verified facts;
- unresolved contradictions or missing metrics;
- suggested next command: use `setup-build-kb` to promote reviewed evidence into the canonical registry, experience files, bundles, and support files.
Do not update canonical claims from an unreviewed extraction.
+5 -105
View File
@@ -1,111 +1,11 @@
--- ---
description: Re-critique existing resume/CV output files against a JD name: critique
description: Critique an existing resume or cover-letter package against its real job description. Use for application-fit review, truth/provenance audit, recruiter and hiring-manager evaluation, separate Evidence Fit and Document Quality scoring, channel-strength assessment, and post-edit re-critique.
user-invocable: true user-invocable: true
--- ---
# /critique # Shared project workflow
**User input:** `$ARGUMENTS` Read and follow [the critique workflow](../../../resume_builder/reference/workflows/critique.md) before doing this task. Resolve project paths from the repository root.
Parse `$ARGUMENTS`: This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
- Session file path (e.g., `output/Acme/session_acme_engineer.md`) → read session file, derive .tex paths from Output Files
- .tex file path(s) + JD source (existing format) → backward compatible
- Session name (e.g., `acme_engineer`) → find session file via derivation
If no CL .tex provided or found in session file, critique resume/CV alone (Part 7 adjustments noted below).
---
## Safety Rules
**Accuracy > Relevance > Impact > ATS > Brevity**
Read `config.md` Provenance Flags. Verify every claim against that table.
**JD integrity:** the critique is only as valid as the JD. If the JD source is a reconstruction/inference/template rather than the real posting (see `shared_ops.md` → JD Integrity), STOP — flag it to the user and offer to scrape the live posting (Playwright recipe) before critiquing. Never score against a fabricated JD.
Check `config.md` KB Corrections Log — do not flag corrected items as errors.
Use the email from `config.md` Personal Info — flag if a different email appears in output.
FIXED sections (from `config.md` FIXED Sections) are template-locked — do not flag for editing. Flag only VARIABLE sections.
---
## User Input During Execution
If the user provides feedback, corrections, or suggestions at any point:
1. Acknowledge the input immediately
2. If it changes scoring criteria or focus: adjust the critique accordingly
3. Never restart — resume from current position
---
## Startup
Read `resume_builder/reference/shared_ops.md` — Fresh Session Startup + Session File Derivation.
Read `CLAUDE.md` — check Active Sessions and KB Corrections.
Read `config.md` — load Provenance Flags, FIXED Sections, email.
Find and read the session file for the .tex being critiqued (use derivation protocol from shared_ops.md).
**Recovery check:**
- If CL not DONE in session file → "CL not yet generated. Run `/make-cl` first."
- If Critique: CURRENT → "Already critiqued (score X/100). Re-run? Waiting for confirmation."
- If Critique: STALE → "Edits made since last critique. Re-critiquing."
- If Critique: PENDING → proceed
---
## Protocol
1. **Read session file** — specifically note:
- **Company Context** → reviewer persona, "why this company"
- **Framing Strategy** → intentional reframing decisions (flag only execution inconsistencies, not the strategy itself)
- **Cover Letter Plan** → CL structure rationale
- **Critique Context** → reviewer persona, competitive landscape, domain vocabulary
- If session file lacks Company Context or Critique Context: do 1-2 web searches to fill gaps
2. Read `resume_builder/reference/critique_framework.md`
3. Read `resume_builder/support/ai_fingerprint_rules.md` — use Section 6 checklist in Part 7 verification
3. Read the .tex file(s) — derive paths from session file Output Files, or from `$ARGUMENTS`
4. Read the JD (path from `$ARGUMENTS` or session file)
5. Read the relevant bundle (`resume_builder/bundles/bundle_[role_type].md` — from session file)
6. Run char count:
```bash
python3 resume_builder/helpers/char_count.py -f [resume|cv] [file.tex]
```
7. Compile and visually verify:
```bash
pdflatex -interaction=nonstopmode -output-directory=output/<FolderName> [file.tex]
```
Use the Read tool to view the compiled PDF — check orphans, page fill, header wrapping.
If compile fails: note "COMPILE FAILED — visual checks could not be verified" in Part 8.
8. If a prior critique exists (`output/<FolderName>/critique_<name>.md`): read it and note previous score.
8b. **Paper Hook Verification:** If the CL cites named papers, PIs, programs, or publications, web-search to verify title, journal, year, and PI affiliation. Flag factual errors as Tier 1 fixes.
9. **Run the full critique per critique_framework.md. The output MUST contain ALL 8 sections** (even if the framework file has partially compacted, produce every section):
1. **Domain-Specialist Lens** — 7 elements:
(a) Reviewer persona (b) Company context (c) JD vocabulary extraction (d) Domain vocabulary map
(e) Gap ranking (fatal/serious/cosmetic) (f) Methodology transfer test (g) Competitive landscape
2. **Five-Perspective Read-Through** — ATS, Recruiter (10s), HR (30s), HM (2min), Technical (10min) — each with verdict
3. **Eight-Dimension Scoring** — weighted table summing to 100
(ATS 15%, Summary 10%, Skills 10%, Bullets 25%, Publications 10%, Narrative 15%, Visual 5%, Credibility 10%)
4. **Interview Likelihood** — per-reader probability + ceiling analysis
5. **Tiered Improvements** — Tier 1 (>=1pt each), Tier 2 (0.3-0.9), Tier 3 (<0.3)
6. **Interview Bridge Points** — 5-7 resume-to-interview talking points
7. **Cover Letter Critique** — 6 sub-checks (6A anti-patterns, 6B tailoring, 6C context-specific, 6D ATS, 6E structural, 6F package cohesion)
- **If no CL provided:** Skip 6A-6E. Run 6F as resume standalone assessment — evaluate whether the resume earns an interview without a CL. Note: "Cover letter not provided — package cohesion not assessed."
8. **Post-Generation Verification** — mechanical + content + structural checklists
10. Save to `output/<FolderName>/critique_<name>.md`
11. **Update session file** — Critique Summary (score, findings, tier 1 fixes), Status → Critique: CURRENT
12. **Update memory pointer** with new score
Progress: "Reading session file for framing context..." / "Running ATS keyword scan — 16/20 match..." / "Scoring 8 dimensions..." / "Score: 87.0/100"
### >>>>>> MANDATORY STOP <<<<<<
Present: score table + tier 1 actionable fixes + interview likelihood.
**You MUST wait for the user's explicit text response before continuing.**
If edits needed, tell user to run `/edit-resume`.
### When user approves / says "looks good" / finalizes:
Verify all expected files exist in `output/<FolderName>/`:
- session file, resume/CV .tex + .pdf, CL .tex + .pdf, critique .md
- Compile artifacts (.aux, .log, .out)
Confirm to user: "Package complete in output/<FolderName>/ — [list files]"
+5 -202
View File
@@ -1,208 +1,11 @@
--- ---
description: Edit existing resume/CV or cover letter from critique feedback and user suggestions name: edit-resume
description: Edit an existing resume or cover letter from critique or user feedback. Use to make evidence-safe content, hierarchy, format, or readability changes while preserving canonical claims, revalidating role fit, compiling LaTeX, and recording the edit.
user-invocable: true user-invocable: true
--- ---
# /edit-resume # Shared project workflow
**User input:** `$ARGUMENTS` Read and follow [the edit-resume workflow](../../../resume_builder/reference/workflows/edit-resume.md) before doing this task. Resolve project paths from the repository root.
Parse `$ARGUMENTS`: First argument is the .tex file path (required). A `.md` path is the critique file. Text in quotes is inline instructions. This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
- `/edit-resume output/Acme/e2e_acme_resume.tex`
- `/edit-resume output/Acme/e2e_acme_resume.tex output/Acme/critique_acme.md`
- `/edit-resume output/Acme/e2e_acme_resume.tex "shorten Position 1 header, fill last page"`
If only .tex path and no instructions: ask the user what to fix.
---
## Safety Rules (ALWAYS ENFORCED)
**Accuracy > Relevance > Impact > ATS > Brevity**
Read `config.md` Provenance Flags before editing any content. Verify every claim against that table.
- Use the email from `config.md` Personal Info in all outputs
- Source ALL bullet content from `resume_builder/experience/` files. Never fabricate.
- Resume bullets: ALL variable bullets must be 2L (CV: 2L/3L mix OK, check `config.md` Document Preferences)
- Run `python3 resume_builder/helpers/char_count.py` after edits — the tool is authoritative
### FIXED Sections — Refuse if Asked to Edit
Check `config.md` FIXED Sections for the list of template-locked sections. Say no and explain: these are template-locked across all outputs.
VARIABLE sections only: Summary, Technical Skills, Research Experience bullets/headers.
---
## User Input During Execution
If the user provides feedback, corrections, or suggestions at any point:
1. Acknowledge the input immediately
2. If it affects an already-applied edit: go back, fix it, re-run char count gate
3. If it changes the edit plan: update session file, adjust remaining edits
4. If it's a question: answer it, then continue from current step
5. Never restart a phase — resume from current position
---
## Startup
Read `resume_builder/reference/shared_ops.md` — Fresh Session Startup + Session File Derivation.
Read `CLAUDE.md` — check Active Sessions and KB Corrections.
Read `config.md` — load Provenance Flags, email, FIXED Sections, document preferences.
Find and read the session file (use derivation protocol from shared_ops.md).
**Recovery check:**
- Read session file, check for existing Edit N Status
- If Edit N Status shows IN_PROGRESS: read .tex, identify which edits are done, resume
- If no edit in progress: proceed to Phase 1
---
## Phase 1: Load Context
Read in this order:
1. **Session file** (`output/<FolderName>/session_<name>.md`) — note: Framing Strategy, Company Context, Bullet Plan, Edit History
2. `resume_builder/reference/resume_reference.md` — char limits, budgets, fixed sections
3. The .tex file being edited
4. Critique file (if provided in `$ARGUMENTS`)
5. JD file (path from session file's JD Info section)
6. Compile current .tex and record baseline page count
7. Run: `python3 resume_builder/helpers/char_count.py -f [resume|cv] [file.tex]`
**Record baseline in session file** under `## Edit [N] Baseline` (scan existing Edit History sections; next N = max existing + 1, or 1 if none):
```
## Edit [N] Baseline
- Pages: [N]
- Char violations: [list or "none"]
- Orphan violations: [list or "none"]
- White space last page: [N lines]
- Variable bullets: [N]
- Rendered lines: [N]
```
Progress: "Reading session file — [company], [role type] bundle..." / "Baseline: 2 pages, 0 char violations, 1 orphan..."
---
## Phase 2: Diagnose & Plan Edits
Gather change requests from THREE sources:
1. **User instructions** from `$ARGUMENTS` (highest priority)
2. **Critique file** (Tier 1 fixes first, then Tier 2)
3. **Auto-detected issues** from Phase 1 (char violations, orphans, page fill)
Cross-check against **session file framing strategy** — edits must stay consistent with decisions from `/make-resume`.
**For each change, classify:**
- **MODIFY:** Change text of existing bullet/summary/skills. Budget unchanged.
- **SWAP:** Replace one bullet with another. Budget unchanged if same variant.
- **ADD:** Insert new bullet. Budget increases by rendered lines.
- **REMOVE:** Drop a bullet. Budget decreases.
- **VARIANT CHANGE:** e.g., 2L → 3L. Budget changes by rendered line delta.
- **FIXED:** Blocked — show in plan with `[FIXED — cannot edit]` and explain why.
**Budget revalidation (if any change is ADD, REMOVE, SWAP-with-different-variant, or VARIANT CHANGE):**
Recalculate total variable bullets and rendered lines. Compare against budget from resume_reference.md.
If OVER budget: present overflow and ask user which bullet to drop or shorten.
Show: `Budget: [N] bullets ([M] rendered lines) vs target [T]. PASS/FAIL`
If edit targets **cover letter** (not resume/CV): note this — Phase 4 will use CL-specific gates. Load CL .tex path from session file Output Files section.
### >>>>>> MANDATORY STOP — DO NOT PROCEED <<<<<<
Present numbered edit plan. Each item shows: what, why, source, classification (MODIFY/ADD/SWAP/FIXED).
**You MUST wait for the user's explicit text response before continuing.**
Proceeding without confirmation may make unwanted edits that break package consistency.
---
## Phase 3: Load Reference Files (only confirmed edits)
Load ONLY what the confirmed edits need:
- **All edits:** `resume_builder/support/ai_fingerprint_rules.md` — scan for banned words/patterns before and after edits
- **Bullet expand/rewrite/add:** `resume_builder/experience/` files + matching bundle + `resume_builder/support/achievement_reframing_guide.md`
- **Summary rewrite:** Bundle (S2 summary guide) + `resume_builder/support/skills_taxonomy.md`
- **Cover letter edits:** `resume_builder/support/significance_*.md` + `resume_builder/reference/cl_reference.md`
- **Simple fixes** (orphans, headers, spacing): No extra files needed
---
## Phase 4: Execute Edits
Apply edits one section at a time. After each edited section:
1. Run char count gate:
```bash
python3 resume_builder/helpers/char_count.py -f [resume|cv] output/<FolderName>/[file].tex
```
2. Fix any OVER violations or orphans before next section
3. If a bullet expansion doesn't render as expected (1L when targeting 2L, or 3L), adjust immediately
Update session file Edit N Status after each individual edit:
- Edit 1 (orphan fix): DONE
- Edit 2 (Summary rewrite): IN_PROGRESS
### Resume/CV Verification Gates
| Gate | Check | If FAIL |
|------|-------|---------|
| Char count | No OVER violations | Fix bullet before proceeding |
| Page fill | Resume: <= 3 lines white space. CV: check rendered line target | Expand/trim variable bullets |
| Page count | Match `config.md` Document Preferences | Trim/expand variable content |
| Orphan | 2L bullet last line >= 70% | Pad or trim |
| Title width | Position title + date fits 1 line | Shorten title |
| Compile | Clean pdflatex | Fix LaTeX errors |
### Cover Letter Verification Gates (if CL was edited)
| Gate | Check | If FAIL |
|------|-------|---------|
| Word count | Industry 250-300, Lab/Academic 350-450 | Trim/expand |
| Page fill | 1pg: well-filled. 2pg: page 2 >= half filled before signature | Adjust |
| Paragraph count | Industry 3, Lab/Academic 4 | Restructure |
| Anti-patterns | No generic opener, no defensive framing, no credential dump | Rewrite |
| Package cohesion | CL claims traceable to resume bullets, no contradictions | Fix |
After all edits, compile:
```bash
pdflatex -interaction=nonstopmode -output-directory=output/<FolderName> output/<FolderName>/[file].tex
```
Use the Read tool to view the compiled PDF — check page count, white space, orphans, header wrapping.
Progress: "Editing Position 1 bullet 6 — was 184 chars, now 197..." / "Compiling... 2 pages, page fill OK"
---
## Phase 5: Update Session File & Present
1. **Append Edit History** (use the N from Phase 1 baseline):
```
### Edit [N] ([date]): [short description]
- Changes: [what changed]
- Source: critique item # / user request / auto-detected
- Verification: gates passed
```
2. **Compare against baseline:**
| Metric | Before | After | Delta |
|--------|--------|-------|-------|
| Page count | [N] | [N] | [+/-] |
| Char violations | [N] | [N] | [+/-] |
| Orphans | [N] | [N] | [+/-] |
| White space | [N] | [N] | [+/-] |
Flag any metric that worsened.
3. **Update Status** — mark critique as STALE if edits made after last critique. Update Next.
4. **Update memory pointer** if status changed.
5. **Present:** Changes summary + delta table + compiled PDF.
### >>>>>> MANDATORY STOP <<<<<<
Show results. Wait for user approval or further edits.
**You MUST wait for the user's explicit text response before continuing.**
### When user approves / says "looks good" / finalizes:
Run file organization from `resume_builder/reference/shared_ops.md` — Finalization check.
+5 -136
View File
@@ -1,142 +1,11 @@
--- ---
description: Generate a tailored cover letter from an existing session file and finished resume/CV name: make-cl
description: Decide whether a cover letter adds value and, when justified, generate a concise evidence-first LaTeX letter from an approved session and resume. Use for requested letters, Swiss/DACH motivation letters, meaningful transitions, or employer-specific formats; skip generic optional letters.
user-invocable: true user-invocable: true
--- ---
# /make-cl # Shared project workflow
**User input:** `$ARGUMENTS` Read and follow [the make-cl workflow](../../../resume_builder/reference/workflows/make-cl.md) before doing this task. Resolve project paths from the repository root.
Parse `$ARGUMENTS`: This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
- Session file path (e.g., `output/Acme/session_acme_engineer.md`) → read that session file
- Session name (e.g., `acme_engineer`) → find session file via shared_ops.md derivation
- Empty → check `CLAUDE.md` Active Sessions for latest
---
## Safety Rules (ALWAYS ENFORCED)
**Accuracy > Relevance > Impact > ATS > Brevity**
Read `config.md` Provenance Flags before generating any content. Verify every claim against that table.
- Use the email from `config.md` Personal Info in all outputs
- CL deepens what resume presents — never introduces new claims not traceable to resume bullets
- Source field context from `resume_builder/support/significance_*.md` files
---
## User Input During Execution
If the user provides feedback, corrections, or suggestions at any point:
1. Acknowledge the input immediately
2. If it affects already-written content: fix it, re-verify word count and anti-patterns
3. If it changes the framing: note the change in session file Framing Strategy
4. Never restart — resume from current position
---
## Startup
Read `resume_builder/reference/shared_ops.md` for session startup and file derivation.
Then:
1. Read `CLAUDE.md` — check Active Sessions and KB Corrections
2. Read `config.md` — load Provenance Flags, email, role types
3. Find and read the session file
4. **Recovery check:**
- If CL Status is DONE → "CL already generated. Run `/critique` next." Show next command. Stop.
- If CL Status is IN_PROGRESS → check if CL .tex exists, offer to resume or regenerate
- If Resume Status is not DONE → "Resume not yet generated. Run `/make-resume` first." Stop.
- If CL Status is PENDING → proceed to Phase 1
---
## Phase 1: Load Context
Read in this order:
1. **Session file** — specifically: Company Context, Cover Letter Plan, Framing Strategy, ATS Keywords
2. **Finished resume/CV .tex** — path from session file Output Files. Read to understand what CL must complement.
3. `resume_builder/reference/cl_reference.md` — CL format rules, paragraph templates, anti-patterns
4. `resume_builder/support/ai_fingerprint_rules.md` — Banned words, structural rules (CLs are most vulnerable)
5. The matching bundle from session file role type → `resume_builder/bundles/bundle_[role_type].md` — Section 5 (Cover Letter)
5. All significance files from `resume_builder/support/significance_*.md`
Update session file Status: `Cover Letter: IN_PROGRESS`
Progress: "Loading CL context — [company], [role type] bundle, [institution type]..."
---
## Phase 2: Generate Cover Letter
Read `resume_builder/templates/coverletter_template.tex`.
**Detect institution type** from session file Cover Letter Plan:
- Industry → 3 paragraphs, 250-300 words
- National Lab → 4 paragraphs, 350-450 words
- Academic → 4 paragraphs, 350-450 words (postdoc) or 450-650 words (faculty)
**Generate CL following cl_reference.md paragraph structure:**
- Use significance files for field-context depth (NOT resume bullet text)
- Use session file CL hooks and "why them" angle
- Ensure every major claim is traceable to a resume/CV bullet
- Open with a specific reference to their work — no generic openers
- Weave credentials into body paragraphs, not closing
Save to `output/<FolderName>/e2e_<name>_cover_letter.tex`
Progress: "Writing [institution type] cover letter — [N] paragraphs, targeting [N] words..."
### CL Hook Verification Gate (MANDATORY before presenting to user)
Web-search every hook used in the CL:
- Academic: PI name + cited paper/research area
- National Lab: named program, thrust area, or group publication
- Industry: product, technology, or company news referenced
Present evidence as:
> **Claim:** [what the CL says] → **Evidence:** [what the search found] → **Source:** [URL]
Flag any unverified item: **"UNVERIFIED — please confirm"**
Do NOT present the CL draft to the user until all hooks are verified or flagged.
---
## Phase 3: Compile & Verify
```bash
pdflatex -interaction=nonstopmode -output-directory=output/<FolderName> output/<FolderName>/e2e_<name>_cover_letter.tex
```
Use Read tool to view compiled PDF. Verify:
| Gate | Check | If FAIL |
|------|-------|---------|
| Word count | Industry 250-300, Lab/Academic 350-450 | Trim/expand |
| Page count | Resume package: 1 page. CV package: 1-2 pages | Adjust content |
| Page fill | 1pg: well-filled. 2pg: page 2 >= half filled before signature | Adjust |
| Anti-patterns | No generic opener, no defensive framing, no credential dump | Rewrite |
| Package cohesion | CL claims traceable to resume bullets, no contradictions | Fix |
| Compile | Clean pdflatex | Fix LaTeX errors |
Update session file:
- Add CL to Output Files
- Status: `Cover Letter: DONE`
- Add Next Critique command
Progress: "Compiled — 1 page, 278 words. Package cohesion verified."
### >>>>>> MANDATORY STOP — DO NOT PROCEED <<<<<<
Present: CL summary (word count, page count, key hooks used).
**You MUST wait for the user's explicit text response before continuing.**
If user requests changes: apply them, re-compile, re-verify. Update session file.
If user approves: update Status, present next command.
**Do NOT trigger file organization** — that happens after `/critique` approval.
"Cover letter done. Next steps:
1. /clear
2. [exact /critique command with session file path]"
+5 -235
View File
@@ -1,241 +1,11 @@
--- ---
description: Generate a tailored resume/CV from a JD name: make-resume
description: Generate a tailored evidence-first resume from a real job description. Use for new applications to assess hard qualifications and role fit before writing, select International Tech or Swiss/DACH format, plan canonical achievements, generate LaTeX, validate claims, compile, and record the application decision.
user-invocable: true user-invocable: true
--- ---
# /make-resume # Shared project workflow
**User input:** `$ARGUMENTS` Read and follow [the make-resume workflow](../../../resume_builder/reference/workflows/make-resume.md) before doing this task. Resolve project paths from the repository root.
Parse `$ARGUMENTS`: This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
- File path (e.g., `JDs/*.txt`) → read that file for the JD
- A URL → this is NOT a JD, it's a pointer. Fetch the real posting text first (see JD Integrity below), save it verbatim to `JDs/<company>_<role>.txt`, then proceed.
- Text after the path starting with "Focus:"/"Emphasize:"/"Downplay:" → focus directive
- "Quick:" prefix → Quick Mode (see below)
- Empty → ask the user for the JD
- Inline JD text (no file path) → save to `JDs/temp_<company>.txt`, proceed normally
**JD Integrity (read `shared_ops.md` → "JD Integrity"):** The JD must be the real posting, verbatim. NEVER invent, reconstruct, paraphrase, or infer JD content. `WebFetch` is JS-blind on careers boards — use the job_scout Playwright recipe in `shared_ops.md` to scrape JS-gated postings. If you cannot get the real text, STOP and ask the user to paste it. Do not proceed on a guessed JD.
---
## Safety Rules (ALWAYS ENFORCED)
**Accuracy > Relevance > Impact > ATS > Brevity**
Read `config.md` Provenance Flags before generating any content. Verify every claim against that table.
- **JD is ground truth — use the real posting verbatim. NEVER reconstruct, invent, or infer a JD** (see `shared_ops.md` → JD Integrity). If you can't get the real text, STOP and ask the user to paste it.
- Use the email from `config.md` Personal Info in all outputs
- Resume bullets: ALL variable bullets are 2L (CV: 2L/3L mix OK, check `config.md` Document Preferences)
- Source ALL bullet content from `resume_builder/experience/` files. Never fabricate.
- Run `python3 resume_builder/helpers/char_count.py` after each section — the tool is authoritative
---
## User Input During Execution
If the user provides feedback, corrections, or suggestions at any point:
1. Acknowledge the input immediately
2. If it affects an already-written section: go back, fix it, re-run char count gate
3. If it changes the bullet plan: update session file Bullet Plan
4. If it's a question: answer it, then continue from current step
5. Never restart a phase — resume from current position
---
## Startup
Read `resume_builder/reference/shared_ops.md` for session startup, file derivation, and organization protocols.
Then:
1. Read `CLAUDE.md` — check Active Sessions and KB Corrections
2. Read `config.md` — load Provenance Flags, email, document preferences, role types
3. If session file exists for this JD:
- Read session file, check Status
- Phase 0: DONE, Phase 1: PENDING → resume at Phase 1
- Phase 1: DONE → resume at Budget Gate
- Phase 2: IN_PROGRESS → read .tex, check what sections exist, resume from checkpoint
- Phase 2: DONE → "Resume already done. Run /make-cl next." Show next command. Stop.
4. If no session file: proceed to Phase 0
---
## Quick Mode
Trigger: `$ARGUMENTS` starts with "Quick:"
Defaults:
- Select all HIGH priority achievements from bundle's Priority Matrix as 2L
- Fill remaining budget with MEDIUM priority in Priority Matrix order
- Default format: 2-page resume (unless JD clearly requires CV)
- Skip Phase 0 STOP and Phase 1 STOP
- Keep Budget Gate (auto-pass if within target) and end-of-resume STOP
- Run all phases with progress commentary instead of interactive stops
---
## Phase 0: Research & Session Setup
**Read these files:**
1. The JD — the **real posting text**, verbatim. If `$ARGUMENTS` was a URL or the posting is on a JS-gated board, scrape it with the Playwright recipe in `shared_ops.md` (JD Integrity) BEFORE this step and save to `JDs/`/`output/<FolderName>/JD_<name>.txt`. If you cannot obtain the real text, STOP and ask the user to paste it — do NOT reconstruct or infer it.
2. `resume_builder/reference/resume_reference.md` — Budget Card, Section Specs, Char Limits, Page Budgets
3. `config.md` — Role-Type Decision Tree to identify the matching bundle
**Web Search (MANDATORY — 2-3 searches).** Load WebSearch via ToolSearch first.
1. `[Company] research & development [key JD domain]` — products, recent projects
2. `[Company] [specific technology from JD]` — concrete hooks for cover letter
3. `[Company] careers [role type] culture` OR recent news — hiring context
If web search returns no results: use JD text + training knowledge. Flag: "Web search returned limited results — CL hooks may be generic."
**Produce all of these (reference `resume_builder/reference/session_file_template.md` for format):**
- **JD Analysis** — classify every requirement as Direct / Bridge (with confidence) / Gap. Extract ATS keywords by category.
- **Company Context** — mission, role purpose, culture signals, "why them" angle (from web research)
- **Framing Strategy** — lead narrative, reframing map, emphasize/downplay, CL hooks, user focus directives
- **Critique Context** — reviewer persona, competitive landscape, domain vocabulary
- **Cover Letter Plan** — institution type, paragraph structure, hooks, jargon level
**Create output folder:**
Derive folder name from JD filename: `JDs/JD_Acme.txt` → `output/Acme/`
```bash
mkdir -p output/<FolderName>/
```
Write session file to `output/<FolderName>/session_<name>.md` (NOT flat `output/`).
All subsequent output files go in this folder.
**Verify completeness:** Re-read the session file. Confirm these 8 sections are non-empty: JD Info, Requirements table, ATS Keywords, Gap Assessment, Company Context, Framing Strategy, Critique Context, Cover Letter Plan. Fill any missing section before presenting.
**Write memory pointer** to `CLAUDE.md` Active Sessions.
**Update session file Status:** `Phase 0: DONE`
Progress: "Searching for [company] + [domain]..." / "JD analysis: X/Y requirements direct match, Z bridges, W gaps"
### >>>>>> MANDATORY STOP — DO NOT PROCEED <<<<<<
Present: research summary, role type + bundle, format, framing strategy.
Ask user to confirm: (1) role type + bundle, (2) format, (3) framing strategy.
**You MUST wait for the user's explicit text response before continuing.**
Proceeding without confirmation misaligns the entire resume and requires full regeneration.
---
## Phase 1: Plan Bullets
**Re-read `output/<FolderName>/session_<name>.md`** — specifically Framing Strategy and ATS Keywords.
**Read:**
1. The matching bundle from `config.md` Role Types → `resume_builder/bundles/bundle_[role_type].md` — Section 1 (Priority Matrix)
- For hybrid JDs: read both bundles. Use primary for Priority Matrix, secondary for Reframing Map on 1-2 bridging bullets.
2. All experience files from `resume_builder/experience/`
3. `resume_builder/support/achievement_reframing_guide.md`
4. `resume_builder/support/skills_taxonomy.md`
5. `resume_builder/support/pub_metadata.md`
**Present one table per position:**
**[Position Name] (Budget: N-M bullets, ~X-Y rendered lines)**
| | ID | Achievement | Variant | Lines | JD Match |
|---|---|-------------|---------|-------|----------|
| * | P1-1 | [short description] | 2L | 2 | Direct |
| * | P1-5 | [short description] | 2L | 2 | Direct |
| o | P1-3 | [short description] | 2L | 2 | Bridge |
| x | P1-7 | [short description] | -- | -- | Weak |
**Legend:** `*` = recommended (HIGH on Priority Matrix + Direct JD match) | `o` = available (MEDIUM priority or Bridge match) | `x` = not recommended (LOW priority or Gap)
**After all positions, show:**
- Recommended set total vs budget (from Quick Budget Card in resume_reference.md)
- Remaining budget slots and what could fill them
- Forced exclusions per provenance flags
- Focus directive impact (what changed vs Priority Matrix defaults)
- CV: confirm first bullet of first experience is 2L (page 1 rule)
**Update session file** — write Bullet Plan tables. Status: `Phase 1: DONE (N bullets confirmed)`
Progress: "Reading experience files for bullet candidates..." / "Recommending N bullets per position"
### >>>>>> MANDATORY STOP — DO NOT PROCEED <<<<<<
Present bullet plan. Wait for user to confirm/modify selections.
**You MUST wait for the user's explicit text response before continuing.**
If you proceed without confirmation, you will generate bullets the user didn't approve.
**Update session file with confirmed plan before continuing.**
---
## Budget Gate (AFTER user confirms bullet plan, BEFORE Phase 2)
**Re-read session file Bullet Plan section** to verify confirmed counts.
- Check budget targets from `resume_builder/reference/resume_reference.md` Budget Card.
- Show: `Budget: [N] bullets vs target [T]. PASS/FAIL`
- **FAIL = do not proceed. Reconcile with user first.**
---
## Phase 2: Generate
**Re-read to restore context after compaction:**
1. `output/<FolderName>/session_<name>.md` (framing + confirmed bullet plan)
2. `resume_builder/reference/critical_rules.md` — Character Limits, Bold Width Penalty, Orphan rules
3. `resume_builder/support/ai_fingerprint_rules.md` — Banned words, structural rules, post-gen checklist
**Read template:** `resume_builder/templates/resume_template.tex` or `cv_template.tex` + `.cls`
FIXED sections (from `config.md` FIXED Sections) are template-locked — only generate VARIABLE sections (Summary, Skills, Experience bullets/headers).
**Read section specs:** `resume_builder/reference/resume_reference.md` — Section-by-Section Specs for your format
**Generate section by section** (follow Section-by-Section Specs):
1. Summary → check against session framing strategy
- Update Status → `Phase 2: Summary DONE`
2. Technical Skills
- Update Status → `Phase 2: Skills DONE`
3. Each position's bullets → **CHAR COUNT GATE after each position**
- Position titles: bold theme + date must fit ONE line (see resume_reference.md). If wrapping, shorten title.
- After each position: Update Status → `Phase 2: [Position] DONE`
4. **PAGE FILL GATE after all experience**
Save .tex to `output/<FolderName>/e2e_<name>_resume.tex` or `_cv.tex`
**Update session file** — add Output Files.
Progress: "Writing Position 1 bullets (6 of 7)..." / "Bullet 4 is SHORT at 184 chars — padding" / "Compiling resume... 2 pages OK"
### CHAR COUNT GATE (per position)
```bash
python3 resume_builder/helpers/char_count.py -f [resume|cv] output/<FolderName>/[file].tex
```
No OVER violations. Last line of 2L bullets >= 70% fill. **Fix before next position.**
### PAGE FILL GATE
Resume: <= 3 lines white space on last page. CV: check rendered line target from resume_reference.md. **If FAIL: add/trim variable bullets.**
### COMPILE GATE
```bash
pdflatex -interaction=nonstopmode -output-directory=output/<FolderName> output/<FolderName>/e2e_<name>_resume.tex
```
Verify page counts match `config.md` Document Preferences. Use the Read tool to view compiled PDF — check orphans, header wrapping, page fill. **If FAIL: fix variable content, recompile.**
Run the Post-Generation Verification checklist from `resume_builder/reference/resume_reference.md` before proceeding.
Update Status → `Phase 2: Compile DONE`
---
## End of /make-resume
Update session file Status:
- `Resume: DONE`
- `Cover Letter: PENDING`
- `Critique: PENDING`
- `Next: /make-cl output/<FolderName>/session_<name>.md`
- `Next Critique: /critique output/<FolderName>/session_<name>.md`
### >>>>>> MANDATORY STOP <<<<<<
Present: resume compilation summary (pages, char count results, any violations fixed).
**You MUST wait for the user's explicit text response before continuing.**
"Resume compiled and verified. Next steps:
1. /clear
2. [exact /make-cl command with session file path]"
+5 -323
View File
@@ -1,329 +1,11 @@
--- ---
description: Synthesize completed extractions into the knowledge base files needed for resume generation name: setup-build-kb
description: Build or update the resume knowledge base from structured extractions. Use to normalize employment evidence, maintain the canonical claims registry, add achievements with ownership and metric status, update evidence-tiered skills and role bundles, propagate corrections, and validate the full resume system.
user-invocable: true user-invocable: true
--- ---
# /setup-build-kb # Shared project workflow
**User input:** `$ARGUMENTS` Read and follow [the setup-build-kb workflow](../../../resume_builder/reference/workflows/setup-build-kb.md) before doing this task. Resolve project paths from the repository root.
Parse `$ARGUMENTS`: This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
- Empty → full build (all phases)
- Phase number (e.g., `3`) → resume from that phase
- "experience" / "bundles" / "skills" / "pubs" / "reframing" / "significance" → run only that component
- "status" → show what's built and what's missing
---
## Startup
1. Read `CLAUDE.md` — check KB Corrections Log
2. Read `config.md` — load:
- Personal Info (positions, institutions, dates)
- Role Types table (defines which bundles to create)
- Provenance Flags (propagate to all KB files)
- Document Preferences (bullet variant defaults)
3. Read `knowledge_base/extractions/_INVENTORY.md` — verify extractions exist
4. Scan `resume_builder/` to see what's already built
**Pre-flight check:**
- If `_INVENTORY.md` is empty or has no entries: "No extractions found. Run `/setup-extract` first." Stop.
- If fewer than 2 extractions: warn "Only [N] extraction(s) found. KB quality improves with more papers. Continue anyway?"
Progress: "Found [N] extractions across [M] positions. Config has [K] role types defined."
---
## Phase 1: Build Experience Files
**Goal:** Create one experience file per position, containing all achievements organized for resume generation.
**Read:** All extraction files listed in `_INVENTORY.md`
**For each position** (from `config.md` or inferred from extraction metadata):
1. Group extractions by the position they belong to (based on dates, institution, or user clarification)
2. Ask the user to confirm grouping if ambiguous: "I've grouped these papers under [Position]. Correct?"
**Experience file format** (`resume_builder/experience/experience_<position_key>.md`):
```markdown
# Experience: [Position Title] — [Institution]
## [Date Range]
### Cross-Position Section
[Brief narrative connecting this position's work to the user's broader trajectory]
[CL framing content — how this position fits the career arc]
---
### Achievement [ID]: [Short Title]
**Source:** [extraction filename]
**Paper:** [citation or "internal"/"unpublished"]
**User's role:** [first author / contributing / sole developer]
**Status:** [published / under review / draft / internal]
**Context:** [1-2 sentences — what problem, why it matters]
**Bullet variants:**
- **2L:** [Full 2-line bullet text — STAR format, ~180-210 rendered characters]
- **3L:** [Full 3-line bullet text — for CV use, ~270-310 rendered characters]
- **1L:** [Condensed 1-line version — ~90-110 rendered characters, for tight budgets]
**Key skills:** [comma-separated list of skills this achievement demonstrates]
**ATS keywords:** [domain-specific terms an ATS might scan for]
**Reframing notes:** [how to emphasize different aspects for different role types]
---
[Repeat for each achievement]
```
### >>>>>> MANDATORY STOP — DO NOT PROCEED <<<<<<
Present the experience file(s) — show achievement count per position, total bullet variants.
Ask user to review: "Are the groupings correct? Any achievements missing or misattributed?"
**You MUST wait for the user's explicit text response before continuing.**
---
## Phase 2: Build Skills Taxonomy
**Goal:** Create a categorized inventory of all technical skills from extractions.
**Read:** All extraction files (Methods & Tools sections) + experience files from Phase 1
**Build** `resume_builder/support/skills_taxonomy.md`:
```markdown
# Skills Taxonomy
## Summary Stats
- Total unique skills: [N]
- Publications: [N] ([breakdown by status])
- Top methods: [ranked list]
## Categories
### [Category 1: e.g., Computational Methods]
| Skill | Proficiency | Evidence | Resume Weight |
|-------|-----------|----------|---------------|
| [skill] | [expert/proficient/familiar] | [paper IDs] | [HIGH/MED/LOW] |
### [Category 2: e.g., Programming & Software]
[same table format]
### [Category 3: e.g., Machine Learning]
[same table format]
[Continue for all categories — typically 4-7 categories]
```
**Proficiency levels:**
- **Expert:** Multiple first-author papers, developed custom tools
- **Proficient:** Used extensively in published work, comfortable teaching
- **Familiar:** Used in one project, or contributed to someone else's implementation
Progress: "Built taxonomy — [N] skills across [M] categories"
---
## Phase 3: Build Publication Metadata
**Goal:** Structured pub data for resume/CV generation.
**Build** `resume_builder/support/pub_metadata.md`:
```markdown
# Publication Metadata
## Summary
- Total publications: [N]
- First-author: [N] | Co-first: [N] | Contributing: [N]
- Published: [N] | Under review: [N] | In preparation: [N]
## Publication List
### First-Author / Co-First
| # | Citation (et al. format) | Journal | Year | Status | Key Topic |
|---|-------------------------|---------|------|--------|-----------|
| 1 | [Author et al., Journal, Year] | [journal] | [year] | [status] | [topic] |
### Contributing Author
[same table format]
### Under Review / In Preparation
[same table format, with provenance notes]
```
Progress: "Pub metadata — [N] first-author, [M] contributing, [K] under review"
---
## Phase 4: Build Achievement Reframing Guide
**Goal:** Per-achievement significance lines + framing directives for each role type.
**Read:** Experience files + `config.md` Role Types
**Build** `resume_builder/support/achievement_reframing_guide.md`:
```markdown
# Achievement Reframing Guide
## How to Use
For each achievement, `Significance:` provides a one-line framing cue.
The role-type table shows how to emphasize/de-emphasize for each target audience.
---
### [Achievement ID]: [Title]
**Significance:** [One sentence — why this matters broadly]
| Role Type | Emphasis | Lead Verb | Framing Angle |
|-----------|----------|-----------|---------------|
| [role 1] | HIGH | Developed | [emphasize X aspect] |
| [role 2] | MEDIUM | Applied | [bridge to Y domain] |
| [role 3] | LOW | -- | [omit or condense] |
**Overclaiming warning:** [if applicable — e.g., "Do not claim sole credit for experimental results"]
**First-pass checklist:** [ ] Verb matches author role [ ] Numbers from paper [ ] Status matches provenance
---
[Repeat for each achievement]
```
### >>>>>> MANDATORY STOP — DO NOT PROCEED <<<<<<
Present the reframing guide summary — show which achievements are HIGH for which role types.
Ask user: "Does this priority mapping look right for your target roles?"
**You MUST wait for the user's explicit text response before continuing.**
---
## Phase 5: Build Bundles
**Goal:** One bundle per role type from `config.md`, with 5 sections each.
**Read:** Experience files + Skills Taxonomy + Reframing Guide + `config.md` Role Types
**For each role type**, create `resume_builder/bundles/bundle_<role_type>.md`:
```markdown
# Bundle: [Role Type Name]
> Target employers: [from config.md]
> Tier: [from config.md]
---
## S1: Role Profile & Priority Matrix
**Positioning:** [1-2 sentences — how to position the user for this role type]
### Priority Matrix
| Priority | Achievement IDs | Rationale |
|----------|----------------|-----------|
| HIGH | [IDs] | [why these lead for this role type] |
| MEDIUM | [IDs] | [supporting evidence, bridge topics] |
| LOW | [IDs] | [omit unless budget allows or JD specifically asks] |
---
## S2: Summary Guide
**Headline pattern:** [Role-appropriate headline template]
**Building blocks:** [3-5 phrases that should appear in summaries for this role type]
**Avoid:** [terms/framings that don't fit this audience]
---
## S3: Achievement Reframing Map
[For each HIGH/MEDIUM achievement: which angle to use, which metrics to lead with]
| ID | Default Framing | This Role's Framing | Key Metric |
|----|----------------|--------------------|-----------|
| [ID] | [generic] | [role-specific angle] | [number to highlight] |
---
## S4: Skills Guide
**Bold tools (resume):** [3-5 tools to bold in Technical Skills for this role type]
**Must-include skills:** [skills that MUST appear for ATS match]
**Nice-to-have:** [skills to include if budget allows]
**Omit:** [skills irrelevant to this audience]
---
## S5: Cover Letter Guide
**Institution type:** [Industry / National Lab / Academic]
**Opening hook pattern:** [template for first paragraph opener]
**Key narrative thread:** [what story to tell across paragraphs]
**"Why them" angle:** [what to research about target employer]
**Avoid:** [CL anti-patterns for this role type]
```
Progress: "Building bundle for [role type] — [N] HIGH priority achievements, [M] bold tools"
---
## Phase 6: Build Significance Research Files
**Goal:** Field context for cover letters — NOT for resume bullets.
**For each position**, create `resume_builder/support/significance_<position_key>.md`:
```markdown
# Significance Research: [Position]
> Use in cover letters and summaries — NOT in resume bullet text.
> These provide field context that demonstrates the user understands the landscape.
---
### [Achievement ID]: Field Context
**The problem:** [What challenge does this address? Industry/scientific context]
**Competing approaches:** [What else exists? What are the limitations?]
**Why this matters:** [Market size, DOE/funding priorities, industry need]
**Differentiation:** [What makes the user's approach unique or better?]
---
[Repeat for each major achievement worth cover-letter depth]
### Field Overview: [Broad Topic]
[2-3 paragraphs of field context that multiple achievements contribute to]
[Useful for cover letter opening hooks and "why this matters" framing]
```
Progress: "Significance research — [N] achievements with field context, [M] field overviews"
---
## Final: Status Report
After all phases complete (or after the requested subset), present:
### KB Build Status
| Component | File | Status | Items |
|-----------|------|--------|-------|
| Experience files | `experience/*.md` | [DONE/MISSING] | [N achievements] |
| Skills taxonomy | `support/skills_taxonomy.md` | [DONE/MISSING] | [N skills] |
| Pub metadata | `support/pub_metadata.md` | [DONE/MISSING] | [N pubs] |
| Reframing guide | `support/achievement_reframing_guide.md` | [DONE/MISSING] | [N entries] |
| Bundles | `bundles/bundle_*.md` | [DONE/MISSING] | [N bundles] |
| Significance | `support/significance_*.md` | [DONE/MISSING] | [N files] |
### Ready for Generation?
- [ ] At least 1 experience file with 5+ achievements
- [ ] Skills taxonomy with 20+ skills
- [ ] At least 1 bundle matching a target role type
- [ ] Pub metadata complete
- [ ] Reframing guide covers all achievements
- [ ] Significance files for cover letter depth
If all checked: "Knowledge base is ready. Save a JD to `JDs/` and run `/make-resume JDs/<filename>.txt`"
If gaps: "[List what's missing and which phase to re-run]"
### >>>>>> MANDATORY STOP <<<<<<
Present status report. Wait for user confirmation.
**You MUST wait for the user's explicit text response before continuing.**
+5 -164
View File
@@ -1,170 +1,11 @@
--- ---
description: Extract structured information from research papers, PDFs, or code into knowledge base extractions name: setup-extract
description: Extract source-grounded career evidence from papers, employment records, project documents, and notes into structured knowledge-base files without promoting inference to fact.
user-invocable: true user-invocable: true
--- ---
# /setup-extract # Shared project workflow
**User input:** `$ARGUMENTS` Read and follow [the setup-extract workflow](../../../resume_builder/reference/workflows/setup-extract.md) before doing this task. Resolve project paths from the repository root.
Parse `$ARGUMENTS`: This shared file is the only workflow implementation for both agents. Preserve existing user authorization and resume from the application's recorded state.
- File path to a paper (e.g., `papers/Smith2024_catalyst.pdf`, `papers/project_report.tex`) → read that file
- Multiple paths separated by spaces → batch mode (process each sequentially)
- Empty → ask the user for the paper path or paste content
---
## Startup
1. Read `CLAUDE.md` — check KB Corrections Log for known issues
2. Read `config.md` — load Personal Info (to identify user's author position), Provenance Flags
3. Read `knowledge_base/extractions/_INVENTORY.md` — see what's already extracted, avoid duplicates
If the paper is already in the inventory:
- Show the existing extraction path
- Ask: "This paper is already extracted. Re-extract (overwrite) or skip?"
- Wait for user response before proceeding
---
## Phase 1: Read & Understand the Paper
Read the paper using the appropriate method:
- **PDF files:** Use the Read tool (supports PDF reading)
- **.tex source:** Read directly — often has more detail than the compiled PDF
- **If both exist:** Prefer .tex for content extraction, use PDF for figures/tables
**While reading, collect:**
1. Full title, all authors, year, journal/venue, DOI (if available)
2. The user's position in the author list (first, co-first, second, middle, last, corresponding)
3. Publication status (check `config.md` Provenance Flags first, then infer: published / under review / draft / internal)
4. All computational methods, experimental techniques, software, and frameworks mentioned
5. Quantitative results — speedups, accuracies, efficiencies, improvements over baselines
6. Novelty claims — "first-ever", "new framework", "novel approach", etc.
7. Collaboration indicators — other groups, institutions, shared resources
8. Funding acknowledgments
Progress: "Reading paper... [title] by [first author] et al., [year]"
---
## Phase 2: Clarify User's Role
If the user's contribution is not obvious from the paper (common for multi-author work), ask:
**Questions to ask (skip any that are already clear from the paper):**
1. "What was your specific contribution? (e.g., all computational work, specific analysis, code development)"
2. "Did you develop any tools, methods, or code used in this paper?"
3. "Were there other groups or institutions involved? What was your group's role?"
4. "Any quantitative results you can personally claim? (e.g., 'I ran all the simulations')"
5. "Is there anything in this paper that should NOT appear on your resume? (e.g., collaborator's experimental data)"
### >>>>>> MANDATORY STOP — DO NOT PROCEED <<<<<<
Present your understanding of the paper and ask the clarifying questions above.
**You MUST wait for the user's explicit text response before continuing.**
---
## Phase 3: Write Extraction
Create the extraction file at `knowledge_base/extractions/<AuthorYear_short_descriptor>.md`
**Naming convention:** `<FirstAuthorLastName><Year><2-3_word_descriptor>.md`
- Examples: `Smith2024_protein_stability.md`, `Chen2023_binding_affinity.md`
- If the user is first author: use their last name
- Normalize to lowercase with underscores
**Extraction format:**
```markdown
# [Full Paper Title]
## Metadata
- **Authors:** [author list — highlight user's name with bold]
- **Year:** [year]
- **Journal:** [journal/venue or "unpublished"/"internal"/"under review at X"]
- **DOI:** [DOI or "N/A"]
- **User's role:** [first author / co-first / contributing / corresponding]
- **Status:** [published | under review | draft | internal]
## Methods & Tools
- **Computational methods:** [e.g., MD, ML, FEA, CFD, etc. — be specific about methods, force fields, etc.]
- **Software/frameworks:** [e.g., GROMACS, PyTorch, ABAQUS, custom code, etc.]
- **Hardware/HPC:** [if mentioned — clusters, GPU resources, etc.]
- **Key techniques:** [specific methodological details that map to resume skills]
## Key Results
[Number each result. Include quantitative metrics wherever possible.]
1. [Result with numbers — e.g., "Achieved 5,000x speedup over brute-force screening"]
2. [Result — e.g., "Screened 8,500 variants, identified 7 top candidates"]
3. [...]
## Novelty Claims
[What's genuinely new — be precise, avoid overclaiming]
- [e.g., "First application of framework X to system Y"]
- [e.g., "New method combining A and B — no prior work exists"]
## Collaboration & Scope
- **Other groups:** [institutions, PIs involved]
- **User's specific contribution:** [from Phase 2 clarification]
- **Shared vs. sole work:** [what the user did alone vs. with others]
## Provenance Notes
- **Publication status:** [matches config.md if listed there]
- **Safe to claim:** [what the user can put on a resume without hedging]
- **Needs hedging:** [claims that require "contributed to" or "supported" framing]
- **Do NOT claim:** [results from collaborators, claims that would be overclaiming]
## Resume Bullet Seeds
[3-5 draft bullets in STAR format. These are seeds, not final text.]
[Use full-ownership verbs only for sole-contributor work. Hedge for shared work.]
1. [Action verb] + [what was done] + [quantitative result/impact]
2. [Action verb] + [method/tool developed] + [what it enabled]
3. [Action verb] + [scope — e.g., "across N systems"] + [outcome]
4. [Optional: collaboration-framed bullet]
5. [Optional: tool/infrastructure bullet]
```
Save the file. Show the user the complete extraction.
Progress: "Writing extraction for [short title]... [N] results identified, [M] bullet seeds drafted"
---
## Phase 4: Update Inventory
Read and update `knowledge_base/extractions/_INVENTORY.md`.
Add a row to the inventory table:
```
| [filename] | [short title] | [user's role] | [status] | [primary methods] | [date extracted] |
```
Present the updated inventory entry to the user.
---
## Phase 5: Next Steps
After extraction is complete, present:
1. **Extraction summary:** [N] methods, [M] quantitative results, [K] bullet seeds
2. **Provenance flags:** Any items that need special handling
3. **Suggested next action:**
- If more papers to extract: "Run `/setup-extract [next paper path]`"
- If all papers done: "Run `/setup-build-kb` to synthesize extractions into experience files and bundles"
### >>>>>> MANDATORY STOP <<<<<<
Present extraction summary. Wait for user feedback or next paper.
**You MUST wait for the user's explicit text response before continuing.**
---
## Batch Mode
If `$ARGUMENTS` contains multiple file paths:
1. Process each paper through Phases 1-4 sequentially
2. Ask Phase 2 clarifying questions for ALL papers at once (grouped) before writing any extractions
3. After all extractions: present combined inventory update and summary
4. Single STOP at the end (not per paper)
+4
View File
@@ -29,6 +29,10 @@ job_scout/state/*
# application_cohort.json — cohort slot tracker (core/adjacent/stretch mix) # application_cohort.json — cohort slot tracker (core/adjacent/stretch mix)
!job_scout/state/decisions.json !job_scout/state/decisions.json
!job_scout/state/application_cohort.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) # One-off job-board data pulls (debug artifacts)
*_jd.json *_jd.json
+29 -29
View File
@@ -7,7 +7,7 @@
## File Map ## File Map
``` ```
.Codex/skills/ .agents/skills/
├── setup-extract/SKILL.md # Extract from papers/files into structured extractions ├── setup-extract/SKILL.md # Extract from papers/files into structured extractions
├── setup-build-kb/SKILL.md # Build experience files, bundles, taxonomy from extractions ├── setup-build-kb/SKILL.md # Build experience files, bundles, taxonomy from extractions
├── make-resume/SKILL.md # Phase 0-2: JD research → bullet plan → resume/CV generation ├── make-resume/SKILL.md # Phase 0-2: JD research → bullet plan → resume/CV generation
@@ -52,10 +52,30 @@ You are simultaneously:
You write as the strategist but critique as the reader. You write as the strategist but critique as the reader.
**Hard rules:** **Rule hierarchy.** When two instructions conflict, the higher level wins. State which level
- Output .tex files ONLY. User compiles locally. you are applying when you deviate.
1. **Evidence rules (immutable).** Anti-fabrication, Scope Discipline, provenance, verb discipline,
`claims.json` as highest authority. These are never overridden by a JD, a market convention or a
user styling preference. **Accuracy > Relevance > Impact > ATS > Brevity.**
2. **Standing user preferences.** Recorded in `config.md` and the KB Corrections Log — e.g. Capgemini
is omitted from every document and the resulting gap is intentional. Changed only by the user.
3. **Market and employer presentation.** Profile choice (International Tech vs Swiss/DACH), section
order, language, cover-letter convention. Adapt these per target; they never license a level-1 or
level-2 breach.
**Deliverables vs QA — these are different things and were previously conflated:**
- **The `.tex` is the deliverable and the source of truth.** Never hand over a PDF as the artefact of
record, and never edit a PDF in place.
- **You still compile.** Run `validate_resume_system.py`, compile with the local LaTeX distribution,
and inspect the rendered pages before calling anything finished — a clean box count is not a
visual check.
- **Compile into the application's own `output/` folder**, not a temp directory, so the user can
review what he is approving (`config.md` Output Rules; rule added after a package shipped with no
reviewable PDF on 2026-08-27). "Output .tex only" means the .tex is authoritative, **not** that the
user should be handed nothing to look at.
- Refresh any submission-named copy on every recompile; it is a copy, not a build target.
- Read `config.md` for email, provenance flags, and output preferences. - Read `config.md` for email, provenance flags, and output preferences.
- **Accuracy > Relevance > Impact > ATS > Brevity**
## Evidence-First Workflow ## Evidence-First Workflow
@@ -154,34 +174,14 @@ Character counts are diagnostic only; they must never drive page filling or equa
## Active Sessions ## Active Sessions
_Update this section when starting/finishing a JD._ Read [current applications](resume_builder/reference/current_applications.md), generated from `job_scout/state/applications.json`. Update the registry through the tracker, then regenerate views. Do not maintain another status table here. Historical notes are preserved under `job_scout/state/history/`.
| Session | Status | Next Command |
|---------|--------|-------------|
| 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. |
| 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. |
| 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). |
| 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. |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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) |
| 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 |
| Kraken AI Infrastructure | **CLOSED — REJECTED** (applied, no interview) | Done |
| Infineon Doctoral | **CLOSED — withdrew** (got interview invite, declined: no relocation to Germany) | Done |
| Infineon AI Engineer | **CLOSED — not applied** (no relocation to Germany) | Done |
| Apple Data Engineer (ISE, Zurich) | **CLOSED — REJECTED** (no interview) | Done |
| Google FDE GenAI (Zurich) | PAUSED — GenAI evidence gap too large; redirecting to data-eng/MLOps roles | Likely abandon |
| Equinor AI Architect (Norway) | **SENT** (~80/100) | Done — await response |
| QuantCo Cloud Engineer (Europe/Zürich) | **CLOSED — REJECTED** (applied 2026-06-01 ~82/100, no interview; rejection 2026-06-15) | Done |
--- ---
## KB Corrections Log ## KB Corrections Log
_See `config.md` for user-specific corrections. Add verified errors here as you find them._ _See `config.md` for user-specific corrections. Add verified errors here as you find them._
## Maintenance resume point
For the authorized project-review fixes, read [FIX_PROGRESS.md](FIX_PROGRESS.md) before continuing. Both agent skill directories route to the shared workflows in `resume_builder/reference/workflows/`.
+29 -34
View File
@@ -7,7 +7,7 @@
## File Map ## File Map
``` ```
.claude/skills/ .agents/skills/
├── setup-extract/SKILL.md # Extract from papers/files into structured extractions ├── setup-extract/SKILL.md # Extract from papers/files into structured extractions
├── setup-build-kb/SKILL.md # Build experience files, bundles, taxonomy from extractions ├── setup-build-kb/SKILL.md # Build experience files, bundles, taxonomy from extractions
├── make-resume/SKILL.md # Phase 0-2: JD research → bullet plan → resume/CV generation ├── make-resume/SKILL.md # Phase 0-2: JD research → bullet plan → resume/CV generation
@@ -52,10 +52,30 @@ You are simultaneously:
You write as the strategist but critique as the reader. You write as the strategist but critique as the reader.
**Hard rules:** **Rule hierarchy.** When two instructions conflict, the higher level wins. State which level
- Output .tex files ONLY. User compiles locally. you are applying when you deviate.
1. **Evidence rules (immutable).** Anti-fabrication, Scope Discipline, provenance, verb discipline,
`claims.json` as highest authority. These are never overridden by a JD, a market convention or a
user styling preference. **Accuracy > Relevance > Impact > ATS > Brevity.**
2. **Standing user preferences.** Recorded in `config.md` and the KB Corrections Log — e.g. Capgemini
is omitted from every document and the resulting gap is intentional. Changed only by the user.
3. **Market and employer presentation.** Profile choice (International Tech vs Swiss/DACH), section
order, language, cover-letter convention. Adapt these per target; they never license a level-1 or
level-2 breach.
**Deliverables vs QA — these are different things and were previously conflated:**
- **The `.tex` is the deliverable and the source of truth.** Never hand over a PDF as the artefact of
record, and never edit a PDF in place.
- **You still compile.** Run `validate_resume_system.py`, compile with the local LaTeX distribution,
and inspect the rendered pages before calling anything finished — a clean box count is not a
visual check.
- **Compile into the application's own `output/` folder**, not a temp directory, so the user can
review what he is approving (`config.md` Output Rules; rule added after a package shipped with no
reviewable PDF on 2026-08-27). "Output .tex only" means the .tex is authoritative, **not** that the
user should be handed nothing to look at.
- Refresh any submission-named copy on every recompile; it is a copy, not a build target.
- Read `config.md` for email, provenance flags, and output preferences. - Read `config.md` for email, provenance flags, and output preferences.
- **Accuracy > Relevance > Impact > ATS > Brevity**
## Evidence-First Workflow ## Evidence-First Workflow
@@ -153,36 +173,7 @@ Character counts are diagnostic only; they must never drive page filling or equa
## Active Sessions ## Active Sessions
_Update this section when starting/finishing a JD._ Read [current applications](resume_builder/reference/current_applications.md), generated from `job_scout/state/applications.json`. Update the registry through the tracker, then regenerate views. Do not maintain another status table here. Historical notes are preserved under `job_scout/state/history/`.
| Session | Status | Next Command |
|---------|--------|-------------|
| 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 ` oday` 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 Line truncated
| 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). |
| 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. |
| 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. |
| 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 <https://company.sbb.ch/de/jobs-karriere/arbeitgeberin/lohn.html>) 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 responsibiliLine truncated
| 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 |
| 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 |
| 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 |
| 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) |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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) |
| 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 |
| Kraken AI Infrastructure | **CLOSED — REJECTED** (applied, no interview) | Done |
| Infineon Doctoral | **CLOSED — withdrew** (got interview invite, declined: no relocation to Germany) | Done |
| Infineon AI Engineer | **CLOSED — not applied** (no relocation to Germany) | Done |
| Apple Data Engineer (ISE, Zurich) | **CLOSED — REJECTED** (no interview) | Done |
| Google FDE GenAI (Zurich) | PAUSED — GenAI evidence gap too large; redirecting to data-eng/MLOps roles | Likely abandon |
| 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 |
| QuantCo Cloud Engineer (Europe/Zürich) | **CLOSED — REJECTED** (applied 2026-06-01 ~82/100, no interview; rejection 2026-06-15) | Done |
--- ---
@@ -216,3 +207,7 @@ _See `config.md` for user-specific corrections. Add verified errors here as you
| 2026-08-02 | **Bosch data types named.** Fab sensor/process data: defect-management records, wafer inspection images, PCM electrical parameters (user-confirmed). Previously the KB named no concrete sensor-data types for Bosch. | `claims.json` BS-2 | | 2026-08-02 | **Bosch data types named.** Fab sensor/process data: defect-management records, wafer inspection images, PCM electrical parameters (user-confirmed). Previously the KB named no concrete sensor-data types for Bosch. | `claims.json` BS-2 |
| 2026-07-27 | **Bullet density.** Fixed 1L/2L/3L and character-band rules made bullets uniform and encouraged page filling. | Replaced globally with natural-length, evidence-led bullets; character counts are diagnostic only. | | 2026-07-27 | **Bullet density.** Fixed 1L/2L/3L and character-band rules made bullets uniform and encouraged page filling. | Replaced globally with natural-length, evidence-led bullets; character counts are diagnostic only. |
| 2026-08-25 | **Cadence monoculture across every generated package.** Audit of all 18 packages in `output/` (368 bullets) found **45% used the same "X, Y and Z" triple**; the SBB package hit **85% — 11 of 13 bullets, every Swisscom and Bosch line** — plus two adjacent bullets both opening "Build and…". Not a truth defect: every bullet was accurate and the corpus is clean on AI vocabulary (0 cliché hits in 35 documents) and prose em-dashes (0.08/bullet). It is a *rhythm* tell — uniform three-beat cadence reads as machine-written. PDF metadata is clean (MiKTeX pdfTeX, empty Author/Title, no AI tokens in 69 PDFs, no generator-term leakage). | New `resume_reference.md` §7a + verification step 7; `critical_rules.md` rule 7a; `critique_framework.md` mechanics row now scores cadence (max 1 pt, style only) | | 2026-08-25 | **Cadence monoculture across every generated package.** Audit of all 18 packages in `output/` (368 bullets) found **45% used the same "X, Y and Z" triple**; the SBB package hit **85% — 11 of 13 bullets, every Swisscom and Bosch line** — plus two adjacent bullets both opening "Build and…". Not a truth defect: every bullet was accurate and the corpus is clean on AI vocabulary (0 cliché hits in 35 documents) and prose em-dashes (0.08/bullet). It is a *rhythm* tell — uniform three-beat cadence reads as machine-written. PDF metadata is clean (MiKTeX pdfTeX, empty Author/Title, no AI tokens in 69 PDFs, no generator-term leakage). | New `resume_reference.md` §7a + verification step 7; `critical_rules.md` rule 7a; `critique_framework.md` mechanics row now scores cadence (max 1 pt, style only) |
## Maintenance resume point
For the authorized project-review fixes, read [FIX_PROGRESS.md](FIX_PROGRESS.md) before continuing. Both agent skill directories route to the shared workflows in `resume_builder/reference/workflows/`.
+140
View File
@@ -0,0 +1,140 @@
# Review remediation — 2026-09-11
User authorized all recommendations in `output/Project_Review_2026-09-11/project_review.tex`.
Work sequentially; update this file after each verified fix and before stopping.
Do not rewrite submitted application archives or invent missing evidence.
## Resume point
**Follow-up review corrections implemented and verified.** The previous DONE labels below
describe the first implementation. This checklist records the subsequent corrections:
- [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.
## User rulings — 2026-09-11
- **Aker BP application date: 2026-07-29** (confirmed; the CLAUDE.md cell saying 07-30 was wrong).
- **Aker BP Adjacent-despite-79:** user does not remember and considers it immaterial, role closed
and rejected — *"mach wie du es am saubersten findest"*. Resolved as: keep the recorded class,
record an honest override reason saying the rationale is unrecoverable, and do **not** reclassify.
Silent reclassification was the review's own objection.
- **F18/F19 document layout changes: authorized.**
## Checklist
| Finding | Status | Scope / verification |
|---|---|---|
| F01 | DONE | Six shared workflows; both agent entrypoints route identically; parity/link regression test |
| F02 | DONE | 26 registry records, preserved original tables, common current view, interview events independent of outcome |
| F03 | DONE | Quota gone (tracker is uncapped, `cohort_tracker.py` is a shim); Aker BP date and override recorded; validation enforces unique IDs, ISO dates, score range and gate/class consistency |
| F04 | DONE | Three-level rule hierarchy in AGENTS.md + CLAUDE.md; deliverable vs QA separated; `resume_reference.md` Capgemini contradiction removed |
| F05 | DONE | Per-claim forbidden wording enforced, identity/employer/placeholder/exclusion checks, `document_exclusions` given a canonical home, PASS no longer claims to be a truth certificate |
| F06 | DONE | LaTeX comments, markup, accent macros and umlauts normalized; negations respected; German `und` in the cadence hint; advisory warnings labelled as never a gate |
| F07 | DONE | Overstatement narrowed (BS-1, zero-downtime, retrieval, evaluation, PyTorch context); undervaluation fixed (BS-6 reached no bundle at all; Linux/Ansible/Bash missing from taxonomy) |
| F08 | DONE | `evidence_schema` + per-claim `evidence` block on all 26; 8 dates recovered from existing text, 18 written `unrecorded` rather than guessed; coverage reported as an advisory hint, never a gate |
| F09 | DONE | `12+ years` -> `over ten years` with the reasoning inline; `cv_template.tex` banner-marked as the academic legacy path |
| F10 | DONE | US figures renamed `*_orientation_only`; the Bavarian formula runs foreign-INTO-German, so inverting it is not a recognised conversion. German grades stay canonical |
| F11 | DONE | Requisition-keyed; same-req locations merge; shared titles warn instead of deleting. **Live run recovered 57 roles (758→815)**, including a second RUAG *Software Engineer C5I* req that was invisible |
| F12 | DONE | Whole-word place matching (Bernau no longer reads as Bern); ~25 Swiss towns incl. Thun and Zimmerwald; Danish cities; pan-regional scope no longer beats an explicit onsite/one-country restriction. All 14 review cases correct |
| F13 | DONE | Identifying query params kept in the key; read and write paths share `lookup_decision`. **Live run recovered 7 reposted RUAG decisions**, labelled RELATED rather than applied as identical |
| F14 | DONE | Buckets renamed to keyword relevance with an explicit 'NOT a fit judgement'; German titles and platform/DevOps vocabulary added. Kdo Cy *ICT-System Engineer (DevSecOps/Platform Automation)* went 0→10; *Senior Dateningenieur* 1→3, level with its English twin |
| F15 | DONE | Taleo deadline moved out of `posted` (a closing date was making roles look freshly published); `unit`, `compensation`, `contact` and `date_provenance` carried as fields through to the report and JSON. Named contacts now surface — directly serving the channel rule |
| F16 | DONE | Restricted runs write `<date>.only-x.md` and leave the daily report intact; seen-state committed only after the report is on disk, and never by a restricted run; unique temp names for atomic writes |
| F17 | DONE | Build sweep 46/48; `package_manifest.py` records per package what a rebuild would really do; 7 unclassified packages registered with a safe default; ` oday` removed from the cover-letter template. Historical packages untouched |
| F18 | DONE | Rendered-line budget replaces the source-macro count; `page_metrics.py` measures page-one geometry. Demonstrated on a rebuilt copy of the NVIDIA CV: experience moved **55.4% → 44.0%**, 47 → 39 page-one lines, still 2 pages, 0 boxes, validator PASS |
| F19 | DONE | Education entries split (degree on the entry line, institution below) after the RUAG model; grades/thesis kept only where concretely relevant; template carries the rule as a comment |
**Sequencing note:** F11–F16 all touch `scout.py`. Fix behaviours in place and keep each
finding's diff separately verifiable. **Do not start the ~2400-line file split F16 suggests**
mid-remediation — it would destroy the ability to attribute a regression to a specific fix.
## Verification log
- Review baseline: 9 existing tests passed; 43/45 LaTeX builds succeeded. See the report for limitations.
- F01: moved current workflows into `resume_builder/reference/workflows/`; preserved skill frontmatter
and Claude invocation support. `python -m unittest discover -s tests -v`: 10/10 pass.
- F01 limitation: external `quick_validate.py` could not start because PyYAML is absent; repository
tests validate frontmatter and both routing links.
- F02: imported 26 applications from the newer CLAUDE table and seven structured cohort records.
Originals preserved in `job_scout/state/history/application_status_before_2026-09-11.json`.
- F03: `application_tracker.py summary` runs uncapped and reports "interview reached: 1" for SBB
despite the withdrawal — the F02 separation working. Aker BP written through
`records.transaction()`, so validation and view regeneration ran; `--check` reports views current.
- F04: `resume_reference.md` said "Capgemini/Bundeswehr: include only when directly relevant",
contradicting the standing never-include rule. Corrected, and the two rules separated.
- F05/F06 verification — **the counter-proof document is the evidence**:
- All six counter-proofs from the report are now caught (9 errors on one synthetic document).
- `% Never claim Terraform` (a comment) and an honest "no Terraform experience" — English and
German — correctly produce **no** finding.
- **All six packages built under current rules scan clean: SBB, NVIDIA, RUAG, Kdo Cy, Citadel,
Aker BP — 0 errors each.** Four false positives of my own making were found and fixed along the
way (title collision across employers, education institution vs employment entry, umlauts,
LaTeX accent macros). Each is now a regression test.
- 13 older packages DO report findings (Apple 9, Microsoft Principal FDE 8, …). These look like
genuine historical violations under today's stricter rules — "petabyte scale", "own the AWS data
platform", uncontextualised PyTorch/TensorFlow. **Not fixed: F17 governs historical archives and
they must not be silently rewritten.** Feed this list into F17.
- `tests/test_document_scanner.py` added: 8 must-fail, 4 must-not-fail, plus a guard that current
packages stay clean. Full suite 31/31 pass.
- F07: raw history preserved; only selection rules and safe wording changed. The NVIDIA/RUAG
"line could not be stopped" phrasing is left in the submitted letters and recorded as an OPEN
ITEM in `experience_bosch.md` - plausible, unevidenced, to be confirmed before reuse.
- F08: `unrecorded` is deliberately a real value. Failing the build on it would push a future
session to invent dates to get a green run.
- F10: no stale `final_grade_us_4pt` references remain anywhere in the repo.
- F11–F14 validated on a live 37-company run: 815 matches vs 758 the day before, the delta being
requisitions the title dedupe had been deleting. Seven employers reported shared-title collisions
that are now kept and flagged (Grafana 5, Axpo 6, AWS 3, RUAG 2, …).
- F14 note: the score floor on RUAG/SBB/BKW/PostFinance/Kdo Cy was a workaround for missing German
vocabulary. The vocabulary is now the fix; the floors can probably be reduced later, but they were
left in place to keep this diff attributable.
- F15/F16 validated on a live 37-company run: named contacts with phone numbers now appear in
report rows, NATO/Taleo closing dates render as Bewerbungsfrist rather than Posted, and
`--only=kdocy` wrote `2026-09-11.only-kdocy.md` while leaving the full report untouched.
- F16 context: the daily-report clobber is not hypothetical. It happened twice in this session
(2026-09-10 and 09-11) and both times the full run had to be repeated by hand.
- F17 build sweep RUN this session: 48 sources, 46 built, 2 failed —
`Infineon_AI_Engineer` cover letter (font expansion: the font is not scalable) and
`cv_template.tex` (unfilled placeholders, expected for the academic legacy template).
Overfull boxes: Microsoft ISE 25.33pt, Isovalent 0.88pt. Both are historical; not touched.
- F17 findings across 21 application packages: **17 are date-dependent** (` oday` in the cover
letter, so a rebuild restamps them), **1 has an incomplete source distribution** (NATO ships no
`resume.cls` and only builds because a copy exists elsewhere on this machine), 1 has no PDF.
The repo carries **three different `resume.cls` variants** (13/7/1 copies), which is exactly why
copying the newest one into NATO would not be a safe repair.
- F17 classification: 7 packages were unregistered; added to `historical_revalidate`, with a
recorded default that anything unlisted is treated that way. Being recent is not being verified —
none was re-read for this.
- F18/F19 validated by measurement, not assertion: `page_metrics.py` reproduced the review's own
figures (NVIDIA 55.4%, SBB 50.6%, RUAG 50.3%) before any change, which is what makes the
after-figure meaningful. The submitted NVIDIA package was NOT modified - the demonstration was
built from a copy.
- The percentage is a DIAGNOSTIC, not a target. No pixel goal, no forced page-two filling.
## Completion rules
DONE requires implementation plus relevant checks. Record partial work and exact next action.
Missing factual evidence must remain explicitly unresolved, never silently promoted to verified.
Historical packages are preserved; fixes apply to active sources and future generation unless
specifically noted.
+25 -2
View File
@@ -28,7 +28,7 @@ source documents
- Target Staff/Senior Data Engineering first. - Target Staff/Senior Data Engineering first.
- Use ML Platform/MLOps, data platform, analytics, and semiconductor profiles only where direct evidence covers the actual requirements. - Use ML Platform/MLOps, data platform, analytics, and semiconductor profiles only where direct evidence covers the actual requirements.
- Treat direct LLM engineering, formal model evaluation, Terraform-heavy SRE, and similar missing required experience as gates rather than keyword opportunities. - Treat direct LLM engineering, formal model evaluation, Terraform-heavy SRE, and similar missing required experience as gates rather than keyword opportunities.
- Build application cohorts with roughly 70% core, 20% adjacent, and 10% deliberate stretch roles. - Record fit and outcomes without application quotas; a fit classification is a preparation signal.
- Add a warm-channel action for core applications. - Add a warm-channel action for core applications.
The default document is a two-page International Tech resume for US-tech/FAANG-style roles in Europe. A Swiss/DACH policy overlay is available for employers that expect local conventions. Cover letters are conditional, not automatic. The default document is a two-page International Tech resume for US-tech/FAANG-style roles in Europe. A Swiss/DACH policy overlay is available for employers that expect local conventions. Cover letters are conditional, not automatic.
@@ -66,14 +66,37 @@ Validate a generated document:
python resume_builder/helpers/validate_resume_system.py --document output/Role/resume.tex python resume_builder/helpers/validate_resume_system.py --document output/Role/resume.tex
``` ```
Track the active 7/2/1 cohort: Track the uncapped application record (use `summary --all` for older applications too):
```powershell ```powershell
python resume_builder/helpers/cohort_tracker.py summary python resume_builder/helpers/cohort_tracker.py summary
``` ```
`job_scout/state/applications.json` owns current status and event history. The old cohort JSON and
`resume_builder/reference/current_applications.md` are generated views. Update status without erasing interview history:
```powershell
python resume_builder/helpers/cohort_tracker.py update --id <application-id> --date YYYY-MM-DD --event interview_held --note "Interview completed"
python resume_builder/helpers/cohort_tracker.py update --id <application-id> --date YYYY-MM-DD --outcome withdrawn_by_candidate --note "Reason"
python resume_builder/helpers/application_records.py --check
```
Use `application_records.py --render` to recover stale generated views. A classification that differs
from its numeric rubric needs `--fit-override-reason`; it is never silently relabelled.
Character counts are available only as readability diagnostics. There are no fixed character bands, equal-length bullet rules, or page-fill quotas. 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 ## 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. 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.
+307 -34
View File
@@ -38,6 +38,8 @@ import os
import re import re
import ssl import ssl
import sys import sys
import tempfile
from contextlib import contextmanager
import time import time
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import lru_cache from functools import lru_cache
@@ -58,10 +60,19 @@ API_FETCH_WORKERS = 8
CH_LOCATION_KEYWORDS = [ CH_LOCATION_KEYWORDS = [
"switzerland", "zurich", "zürich", "basel", "bern", "geneva", "genf", "switzerland", "zurich", "zürich", "basel", "bern", "geneva", "genf",
"lausanne", "zug", "rüschlikon", "stäfa", "schweiz", "suisse", "lausanne", "zug", "rüschlikon", "stäfa", "schweiz", "suisse",
# Added 2026-09-11 (F12). These were being rejected outright when the adapter gave no
# country: Zimmerwald is the Kdo Cy site he applied to, Thun is where he lives.
"thun", "zimmerwald", "baar", "olten", "winterthur", "luzern", "lucerne",
"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",
] ]
DK_LOCATION_KEYWORDS = [ DK_LOCATION_KEYWORDS = [
"denmark", "copenhagen", "danmark", "københavn", "koebenhavn", "denmark", "copenhagen", "danmark", "københavn", "koebenhavn",
# Added 2026-09-11 (F12): the two other Danish cities that actually hire here.
"aarhus", "århus", "odense", "aalborg", "ålborg", "lyngby", "roskilde",
] ]
NO_LOCATION_KEYWORDS = [ NO_LOCATION_KEYWORDS = [
@@ -84,6 +95,23 @@ GLOBAL_SCOPE_KEYWORDS = [
] ]
POSITIVE_KEYWORDS = { POSITIVE_KEYWORDS = {
# German/Swiss job titles (added 2026-09-11, F14). Without these, every German-language
# posting scored near zero regardless of fit, which is why RUAG, SBB, BKW, PostFinance
# and Kdo Cy all needed a _score_floor to stay visible at all. The floor is a workaround;
# these terms are the actual fix. Weights mirror their English counterparts.
# Platform/DevOps vocabulary that was simply absent (F14). The Kdo Cy
# "ICT-System Engineer (DevSecOps / Platform Automation)" scored ZERO before this -
# a posting whose every requirement line matches his canonical evidence.
"devsecops": 3, "platform engineer": 3, "platform automation": 3, "platform": 1,
"automation": 1, "infrastructure as code": 2, "observability": 2,
"site reliability": 3, "system engineer": 1, "ict": 1,
"dateningenieur": 2, "data engineer (m/w/d)": 2, "systemingenieur": 2,
"software-entwickler": 2, "softwareentwickler": 2, "entwickler": 1,
"plattform": 2, "plattform-engineer": 3, "datenplattform": 3,
"automatisierung": 2, "betrieb": 1, "datenanalyse": 2, "datenmodellierung": 2,
"schnittstellen": 1, "datenqualitaet": 2, "datenqualität": 2,
"künstliche intelligenz": 3, "kuenstliche intelligenz": 3, "maschinelles lernen": 3,
"cloud-plattform": 2, "containerisierung": 2, "informatiker": 1,
"genai": 3, "generative ai": 3, "llm": 3, "large language model": 3, "genai": 3, "generative ai": 3, "llm": 3, "large language model": 3,
"applied ai": 3, "applied ml": 3, "ai engineer": 3, "ml engineer": 3, "applied ai": 3, "applied ml": 3, "ai engineer": 3, "ml engineer": 3,
"mlops": 3, "ai platform": 3, "ml platform": 3, "mlops": 3, "ai platform": 3, "ml platform": 3,
@@ -718,7 +746,9 @@ def fetch_greenhouse(args):
"title": j.get("title", ""), "title": j.get("title", ""),
"location": loc_blob, "location": loc_blob,
"url": j.get("absolute_url"), "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], "description": desc[:2500],
}) })
return jobs return jobs
@@ -771,7 +801,8 @@ def fetch_smartrecruiters(args):
content = data.get("content", []) or [] content = data.get("content", []) or []
for p in content: for p in content:
loc = p.get("location") or {} 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"): if loc.get("remote"):
parts.append("Remote") parts.append("Remote")
if loc.get("hybrid"): if loc.get("hybrid"):
@@ -783,6 +814,7 @@ def fetch_smartrecruiters(args):
"id": str(p.get("id")), "id": str(p.get("id")),
"title": p.get("name", ""), "title": p.get("name", ""),
"location": loc_str, "location": loc_str,
"country": country,
"url": f"https://jobs.smartrecruiters.com/{company}/{p.get('id')}", "url": f"https://jobs.smartrecruiters.com/{company}/{p.get('id')}",
"posted": p.get("releasedDate", ""), "posted": p.get("releasedDate", ""),
"description": " ".join(filter(None, [dept, func])), "description": " ".join(filter(None, [dept, func])),
@@ -1146,15 +1178,19 @@ def _parse_taleo_jobsearch_html(html, host, career_section):
deadline = _unq(m.group(5)) deadline = _unq(m.group(5))
body = _unq(m.group(6)) body = _unq(m.group(6))
grade = _unq(m.group(7)) grade = _unq(m.group(7))
# List view exposes application deadline only (not a posted-on date). We store # List view exposes an application DEADLINE only, never a posted-on date. It used to
# the date part in `posted` so the report surfaces the closing date. # be stored in `posted`, which made a role closing soon look freshly published -
# backwards for a freshness signal (F15). It now goes to `deadline`, and `posted`
# stays empty because this board genuinely does not publish one.
deadline_short = deadline.split(",")[0].strip() if deadline else "" deadline_short = deadline.split(",")[0].strip() if deadline else ""
jobs.append({ jobs.append({
"id": job_no, "id": job_no,
"title": title, "title": title,
"location": loc, "location": loc,
"url": f"{detail_base}?job={job_no}&lang=en", "url": f"{detail_base}?job={job_no}&lang=en",
"posted": deadline_short, "posted": "",
"deadline": deadline_short,
"date_provenance": "taleo list view exposes a closing date only; no publication date",
"description": f"{body} {grade}".strip()[:500], "description": f"{body} {grade}".strip()[:500],
}) })
if jobs: if jobs:
@@ -1323,6 +1359,13 @@ def fetch_prospective(args):
"url": (j.get("links") or {}).get("directlink", ""), "url": (j.get("links") or {}).get("directlink", ""),
"posted": (j.get("start_date") or "")[:10], "posted": (j.get("start_date") or "")[:10],
"deadline": deadline, "deadline": deadline,
"date_provenance": "prospective start_date = publication; end_date = application deadline",
# Carried as fields, not just folded into description: these decide level, pay and
# whether a cold submit is even necessary (F15).
"unit": unit,
"compensation": f"Lohnklasse {lohnklasse}" if lohnklasse else "",
"contact": re.sub(r"\s+", " ", html.unescape(
re.sub(r"<[^>]+>", " ", str(szas.get("sza_contact", ""))))).strip()[:300],
"description": re.sub(r"\s+", " ", desc)[:1200], "description": re.sub(r"\s+", " ", desc)[:1200],
}) })
return jobs return jobs
@@ -1725,7 +1768,35 @@ ADAPTERS = {
} }
def location_eligibility(loc_text, policy="default"): # An explicit onsite marker or a single-country restriction beats a stray pan-regional word.
_ONSITE = re.compile(r"\b(?:on-?site|in-?office|vor ort|presence required|100% office)\b", re.I)
_COUNTRY_ONLY = re.compile(
r"\b(?:only|nur|exclusively|must (?:be|reside) in|residents? of|based in)\b", re.I)
def _place_match(low, keywords):
"""Whole-word place matching.
Substring matching accepted "Bernau bei Berlin" as Bern, which put German roles into a
Swiss-only result set. Hyphens and slashes count as boundaries so "Bern/Zurich" still hits.
"""
for keyword in keywords:
if re.search(r"(?<![\w])" + re.escape(keyword) + r"(?![\w])", low):
return True
return False
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. """Decide whether a posting's location is in-scope.
Returns (eligible, in_ch, is_remote, is_relocation). Returns (eligible, in_ch, is_remote, is_relocation).
@@ -1745,15 +1816,33 @@ def location_eligibility(loc_text, policy="default"):
if not loc_text: if not loc_text:
return False, False, False, False return False, False, False, False
low = loc_text.lower() low = loc_text.lower()
in_ch = any(k in low for k in CH_LOCATION_KEYWORDS) # An adapter's country code is stronger than an ambiguous town name. In free text,
in_dk = any(k in low for k in DK_LOCATION_KEYWORDS) # recognize country names; short codes count only as comma/slash-separated fields.
in_no = any(k in low for k in NO_LOCATION_KEYWORDS) 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 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 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) europe_scope = any(k in low for k in EUROPE_SCOPE_KEYWORDS)
global_scope = any(k in low for k in GLOBAL_SCOPE_KEYWORDS) and not is_us_only global_scope = any(k in low for k in GLOBAL_SCOPE_KEYWORDS) and not is_us_only
# Pan-regional scope counts even without the word "remote" (e.g. QuantCo "Europe"). # Pan-regional scope counts even without the word "remote" (e.g. QuantCo "Europe"),
is_europe_or_global = (europe_scope or global_scope) and not is_us_only # 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 = "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 is_ch_remote = in_ch and has_remote
if policy == "norway_only": if policy == "norway_only":
@@ -1788,6 +1877,15 @@ def _kw_in(kw, text):
def score_job(job, title_only=False): def score_job(job, title_only=False):
"""Keyword RELEVANCE for search ranking - explicitly not a fit score (F14).
It measures how many profile keywords a posting echoes. It does not know his evidence,
the hard requirements, the level, the language or the commute, and a higher number must
never be read as a better application. Compare: "Senior AI Engineer, Generative AI / LLM
/ RAG" scores 12 against "Senior Data Engineer" at 3, while the second is much closer to
what he can actually evidence. German titles score near zero regardless of fit, which is
why pre-filtered boards carry a _score_floor.
"""
# Title carries the real signal; the JD body is full of company boilerplate (every # Title carries the real signal; the JD body is full of company boilerplate (every
# Kraken post mentions crypto/blockchain/trading, every cloud post mentions python). # Kraken post mentions crypto/blockchain/trading, every cloud post mentions python).
# So title matches score at full weight and body-only matches at half (min 1) — enough # So title matches score at full weight and body-only matches at half (min 1) — enough
@@ -1814,9 +1912,17 @@ def score_job(job, title_only=False):
def _atomic_write_json(path, data): def _atomic_write_json(path, data):
"""Write JSON via temp + os.replace so a crash mid-write cannot truncate state files.""" """Write JSON via temp + os.replace so a crash mid-write cannot truncate state files."""
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp") # Unique temp name (F16): a fixed ".tmp" let two concurrent writers share one temp file
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") # and os.replace the wrong content into place.
os.replace(tmp, path) descriptor, tmp_name = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".", suffix=".tmp")
tmp = Path(tmp_name)
try:
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
stream.write(json.dumps(data, indent=2, ensure_ascii=False))
os.replace(tmp, path)
except BaseException:
tmp.unlink(missing_ok=True)
raise
def load_seen(): def load_seen():
@@ -1899,11 +2005,20 @@ def _decision_key(url):
if m: if m:
return f"{host}#{m.group(1)}" return f"{host}#{m.group(1)}"
# No known ID shape: fall back to the path with cosmetic variation stripped. # No known ID shape: fall back to the path with cosmetic variation stripped. The QUERY
# is kept (F13): dropping it made /vacancy?job=1 and /vacancy?job=2 the same key, so one
# decision silently covered two different requisitions.
path = parts.path.rstrip("/") path = parts.path.rstrip("/")
if path.endswith("/apply"): if path.endswith("/apply"):
path = path[: -len("/apply")] path = path[: -len("/apply")]
return f"{host}#{path.lower()}" query = urllib.parse.parse_qs(parts.query)
# Only identifying parameters; tracking noise must not fragment the key.
keep = {k: v for k, v in query.items()
if k.lower() in {"job", "jobid", "id", "req", "reqid", "requisition", "gh_jid", "vacancy"}}
suffix = ""
if keep:
suffix = "?" + "&".join(f"{k}={sorted(v)[0]}" for k, v in sorted(keep.items()))
return f"{host}#{path.lower()}{suffix}"
def load_decisions(): def load_decisions():
@@ -1958,6 +2073,40 @@ def find_decision_url(decisions, url):
return None return None
@lru_cache(maxsize=1)
def _raw_decisions():
"""Raw decision log for the report path's slug fallback (cached per run)."""
return load_decisions()
def lookup_decision(dec_index, decisions, url):
"""Resolve a decision for a report row, using the SAME identity rules as the write path.
The 2026-09-04 fix taught `find_decision_url` (used by --decide) that RUAG regenerates the
UUID on every repost, but the REPORT path kept doing a bare `_decision_key` lookup. Result:
a reposted RUAG requisition came back as undecided although the decision was in the log -
which is exactly what happened to `Lead-Architekt Network- und Richtfunk` on 2026-09-10,
days after 63 RUAG rule-outs had been written.
A slug match is reported as RELATED, not as the same requisition: a repost can differ
materially, so an old rule-out must not be applied to it automatically.
"""
hit = dec_index.get(_decision_key(url))
if hit is not None:
return hit
existing = find_decision_url(decisions, url)
if existing is None:
return None
related = dict(decisions[existing])
related["matched_by"] = "slug"
related["matched_url"] = existing
return related
def is_same_decision(decision):
return decision is not None and decision.get("matched_by") != "slug"
def save_decisions(decisions): def save_decisions(decisions):
_atomic_write_json(DECISIONS_FILE, decisions) _atomic_write_json(DECISIONS_FILE, decisions)
@@ -2053,25 +2202,31 @@ def write_report(path, results, errors, new_only, include_weak, stats=None, tota
if not include_weak and weak: if not include_weak and weak:
lines.append(f"\n_Hiding {len(weak)} weak/noise roles (score < 2). Use --include-weak to show._") 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 _decision_key(r["url"]) in decisions) n_decided = sum(1 for r in results if is_same_decision(
lookup_decision(decisions, _raw_decisions(), r["url"])))
if n_decided: if n_decided:
shown = "hidden" if hide_decided else "tagged inline" shown = "hidden" if hide_decided else "tagged inline"
lines.append(f"_{n_decided} role(s) already in the decision log ({shown}; " lines.append(f"_{n_decided} role(s) already in the decision log ({shown}; "
f"see state/decisions.json)._") f"see state/decisions.json)._")
buckets = [("Strong fit (score >= 6)", strong), # F14: this number is SEARCH RELEVANCE - how many profile keywords the posting echoes.
("Medium fit (score 2-5)", medium)] # It is not a fit assessment and must never be presented as one: "Senior AI Engineer"
# with GenAI/LLM/RAG scores 12 while "Senior Data Engineer" scores 3, although the
# second sits far closer to his actual evidence. Fit is decided on the full JD.
buckets = [("High keyword relevance (score >= 6) - NOT a fit judgement", strong),
("Medium keyword relevance (score 2-5)", medium)]
if include_weak: if include_weak:
buckets.append(("Weak / noise (score < 2)", weak)) buckets.append(("Low keyword relevance (score < 2) - German titles land here", weak))
for bucket_name, bucket in buckets: for bucket_name, bucket in buckets:
shown = [r for r in bucket shown = [r for r in bucket
if not (hide_decided and _decision_key(r["url"]) in decisions)] if not (hide_decided and
is_same_decision(lookup_decision(decisions, _raw_decisions(), r["url"])))]
if not shown: if not shown:
continue continue
lines.append(f"\n## {bucket_name} - {len(shown)} role(s)\n") lines.append(f"\n## {bucket_name} - {len(shown)} role(s)\n")
for r in shown: for r in shown:
d = decisions.get(_decision_key(r["url"])) d = lookup_decision(decisions, _raw_decisions(), r["url"])
new_tag = " [NEW]" if r["is_new"] else "" new_tag = " [NEW]" if r["is_new"] else ""
decided_tag = f" — 🗂 {d['decision'].upper()}" if d else "" decided_tag = f" — 🗂 {d['decision'].upper()}" if d else ""
loc_tag = ("CH" if r["in_ch"] else loc_tag = ("CH" if r["in_ch"] else
@@ -2083,10 +2238,26 @@ def write_report(path, results, errors, new_only, include_weak, stats=None, tota
lines.append(f"- Posted: {r['posted']}") lines.append(f"- Posted: {r['posted']}")
if r.get("deadline"): if r.get("deadline"):
lines.append(f"- **Bewerbungsfrist: {r['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"):
lines.append(f"- Vergütung: {r['compensation']}")
if r.get("contact"):
# Channel is the rationed variable: a published, reachable name means a cold
# submit is a choice rather than the only option.
lines.append(f"- **Ansprechperson: {r['contact']}**")
lines.append(f"- URL: {r['url']}") lines.append(f"- URL: {r['url']}")
if d: if d:
note = f" — {d['note']}" if d.get("note") else "" note = f" — {d['note']}" if d.get("note") else ""
lines.append(f"- 🗂 Decision: **{d['decision']}**{note} ({d.get('date','')})") if d.get("matched_by") == "slug":
lines.append(
f"- 🗂 RELATED decision (same slug, **new requisition id** — a repost can "
f"differ, so re-read before reusing): **{d['decision']}**{note} "
f"({d.get('date','')}) · matched `{d.get('matched_url','')}`")
else:
lines.append(f"- 🗂 Decision: **{d['decision']}**{note} ({d.get('date','')})")
if r["pos"]: if r["pos"]:
lines.append(f"- Positive: {', '.join(r['pos'])}") lines.append(f"- Positive: {', '.join(r['pos'])}")
if r["neg"]: if r["neg"]:
@@ -2118,12 +2289,38 @@ def _fetch_company(cid, display, adapter, args):
return cid, display, adapter, args, None, msg, time.perf_counter() - t0 return cid, display, adapter, args, None, msg, time.perf_counter() - t0
def _requisition_key(cid, job):
"""Stable per-requisition identity within one employer.
Prefers a board requisition id parsed out of the URL (the same patterns the decision log
uses), then the adapter's own id, then the URL. Employer-scoped so two boards that number
requisitions the same way can never collide.
"""
url = (job.get("url") or "").strip()
if url:
for host_pat, id_pat in _JOB_ID_PATTERNS:
try:
parts = urllib.parse.urlsplit(url)
except ValueError:
break
host = (parts.netloc or "").lower()
if host_pat and not re.search(host_pat, host):
continue
match = re.search(id_pat, parts.path + (f"?{parts.query}" if parts.query else ""))
if match:
return f"{cid}#{match.group(1)}"
identifier = job.get("id")
return f"{cid}#{identifier}" if identifier else f"{cid}#{url or job.get('title', '')}"
def _process_company(cid, display, args, jobs, seen, today, last_scrape): def _process_company(cid, display, args, jobs, seen, today, last_scrape):
"""Filter, score, and update seen for one company's job list. """Filter, score, and update seen for one company's job list.
Returns (results, stat_dict, warnings) where warnings is a list of (display, msg). Returns (results, stat_dict, warnings) where warnings is a list of (display, msg).
""" """
warnings = [] warnings = []
req_seen: dict[str, dict] = {}
title_collisions: dict[str, int] = {}
scraped = len(jobs) scraped = len(jobs)
prev = (last_scrape.get(cid) or {}).get("scraped", 0) prev = (last_scrape.get(cid) or {}).get("scraped", 0)
if scraped == 0: if scraped == 0:
@@ -2160,15 +2357,28 @@ def _process_company(cid, display, args, jobs, seen, today, last_scrape):
for j in jobs: for j in jobs:
jid = str(j.get("id") or j.get("url")) jid = str(j.get("id") or j.get("url"))
ok, in_ch, is_remote, is_relocation = location_eligibility( 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: if not ok:
continue continue
# Collapse the same role posted once per remote country (title differs only # Requisition identity is the primary key (F11). Title alone is NOT identity: two
# by a "| Country | Remote" suffix) — dedupe on the title before the first "|". # different reqs called "Senior Data Engineer" in Bern and Zurich used to collapse
# into one, and different teams hiring the same title vanished silently.
req_key = _requisition_key(cid, j)
if req_key in req_seen:
# Same requisition seen again - the case the old rule existed for, a role
# republished once per location. Merge the location instead of dropping it.
first = req_seen[req_key]
extra = (j.get("location") or "").strip()
if extra and extra.lower() not in first["location"].lower():
first["location"] = f"{first['location']} / {extra}"
continue
norm_title = re.sub(r"\s+", " ", (j.get("title") or "").split("|")[0]).strip().lower() norm_title = re.sub(r"\s+", " ", (j.get("title") or "").split("|")[0]).strip().lower()
if norm_title in title_seen: if norm_title in title_seen:
continue # Same title, DIFFERENT requisition. Keep it and flag it; removing it here is
# what lost real roles.
title_collisions.setdefault(norm_title, 0)
title_collisions[norm_title] += 1
title_seen.add(norm_title) title_seen.add(norm_title)
eligible += 1 eligible += 1
is_new = jid not in company_seen is_new = jid not in company_seen
@@ -2180,18 +2390,31 @@ def _process_company(cid, display, args, jobs, seen, today, last_scrape):
score = floor score = floor
if score >= 2: if score >= 2:
match += 1 match += 1
results.append({ row = {
"company": display, "company_id": cid, "company": display, "company_id": cid,
"title": j["title"], "location": j["location"], "title": j["title"], "location": j["location"],
"url": j["url"], "posted": j.get("posted", ""), "url": j["url"], "posted": j.get("posted", ""),
"updated_at": j.get("updated_at", ""),
"deadline": j.get("deadline", ""), "deadline": j.get("deadline", ""),
"date_provenance": j.get("date_provenance", ""),
"unit": j.get("unit", ""),
"compensation": j.get("compensation", ""),
"contact": j.get("contact", ""),
"score": score, "pos": pos, "neg": neg, "score": score, "pos": pos, "neg": neg,
"in_ch": in_ch, "remote": is_remote, "relocation": is_relocation, "in_ch": in_ch, "remote": is_remote, "relocation": is_relocation,
"is_new": is_new, "is_new": is_new,
}) }
results.append(row)
req_seen[req_key] = row
if is_new: if is_new:
company_seen[jid] = {"title": j["title"], "first_seen": today} company_seen[jid] = {"title": j["title"], "first_seen": today}
if title_collisions:
detail = ", ".join(f"{title!r} x{count + 1}" for title, count in sorted(title_collisions.items())[:4])
warnings.append((
display,
f"{len(title_collisions)} title(s) shared by different requisitions, all KEPT "
f"(check for real duplicates): {detail}"))
stat = { stat = {
"company": display, "scraped": scraped, "eligible": eligible, "company": display, "scraped": scraped, "eligible": eligible,
"match": match, "newest": newest, "error": False, "match": match, "newest": newest, "error": False,
@@ -2199,7 +2422,7 @@ def _process_company(cid, display, args, jobs, seen, today, last_scrape):
return results, stat, warnings return results, stat, warnings
def main(): def _main():
today = datetime.now(timezone.utc).strftime("%Y-%m-%d") today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
# Record a decision and exit: --decide "<url>" <status> [note words...] # Record a decision and exit: --decide "<url>" <status> [note words...]
@@ -2314,7 +2537,9 @@ def main():
"scraped": stat["scraped"], "date": today, "display": display, "scraped": stat["scraped"], "date": today, "display": display,
} }
finally: finally:
save_seen(seen) # Seen-state is NOT committed here (F16). It used to be, which meant a failed report
# write left roles marked as seen and a `--new-only` retry hid exactly the hits that
# were never delivered. It is committed after the report is on disk.
save_last_scrape(new_last_scrape) save_last_scrape(new_last_scrape)
_close_browser() _close_browser()
@@ -2326,19 +2551,36 @@ def main():
all_results.sort(key=lambda r: (-r["score"], r["company"], r["title"])) all_results.sort(key=lambda r: (-r["score"], r["company"], r["title"]))
REPORTS_DIR.mkdir(parents=True, exist_ok=True) REPORTS_DIR.mkdir(parents=True, exist_ok=True)
report_path = REPORTS_DIR / f"{today}.md" # Run scope in the filename (F16). A partial run used to overwrite the full daily report:
# `--only=kdocy` replaced a 37-company report with seven rows, and the full run had to be
# repeated by hand. Only an unrestricted run owns the plain `<date>` name.
scope = []
if only:
scope.append(f"only-{only}")
if new_only:
scope.append("new-only")
if hide_decided:
scope.append("undecided")
stem = today if not scope else f"{today}.{'.'.join(scope)}"
if scope:
print(f"Restricted run: writing {stem}.md/.json, leaving the full daily report intact.",
file=sys.stderr)
report_path = REPORTS_DIR / f"{stem}.md"
write_report(report_path, all_results, errors, new_only, include_weak, write_report(report_path, all_results, errors, new_only, include_weak,
stats=stats, total_secs=total_secs, stats=stats, total_secs=total_secs,
decisions=dec_index, hide_decided=hide_decided) decisions=dec_index, hide_decided=hide_decided)
# Plain data dump of every eligible role, unfiltered by keyword score — fit judgment # Plain data dump of every eligible role, unfiltered by keyword score — fit judgment
# for these is done in conversation against the profile, not by the keyword scorer. # for these is done in conversation against the profile, not by the keyword scorer.
json_path = REPORTS_DIR / f"{today}.json" json_path = REPORTS_DIR / f"{stem}.json"
dump = [{ dump = [{
"company": r["company"], "title": r["title"], "location": r["location"], "company": r["company"], "title": r["title"], "location": r["location"],
"url": r["url"], "posted": r["posted"], "deadline": r.get("deadline", ""), "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"], "is_new": r["is_new"],
"decision": dec_index.get(_decision_key(r["url"])), "decision": lookup_decision(dec_index, decisions, r["url"]),
"keyword_signal": {"score": r["score"], "pos": r["pos"], "neg": r["neg"]}, "keyword_signal": {"score": r["score"], "pos": r["pos"], "neg": r["neg"]},
"loc_flags": { "loc_flags": {
"in_ch": r["in_ch"], "remote": r["remote"], "relocation": r["relocation"], "in_ch": r["in_ch"], "remote": r["remote"], "relocation": r["relocation"],
@@ -2346,6 +2588,13 @@ def main():
} for r in all_results] } for r in all_results]
_atomic_write_json(json_path, dump) _atomic_write_json(json_path, dump)
# 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.
# 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"]) n_new = sum(1 for r in all_results if r["is_new"])
print(f"\nReport written: {report_path}", file=sys.stderr) print(f"\nReport written: {report_path}", file=sys.stderr)
print(f"Total matches: {len(all_results)} ({n_new} new) | " print(f"Total matches: {len(all_results)} ({n_new} new) | "
@@ -2385,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__": if __name__ == "__main__":
main() main()
+230 -71
View File
@@ -12,66 +12,64 @@
"adjacent": "Evidence Fit 60-74, no required Gap, normally at most one serious preferred gap", "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" "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.",
"generated_from": "job_scout/state/applications.json",
"applications": [ "applications": [
{ {
"company": "Google", "company": "NVIDIA",
"role": "Software Engineer III, Business Home", "role": "Senior DevOps Engineer, Zurich (JR2024256)",
"application_date": "2026-07-27", "application_date": "2026-09-05",
"fit_class": "core", "fit_class": "stretch",
"evidence_fit": 89.0, "evidence_fit": 64.0,
"hard_gate": "pass", "hard_gate": "fail",
"channel": "weak", "channel": "weak",
"outcome": "rejected_no_interview" "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), Line truncated
"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": "Aker BP ASA", "company": "RUAG MRO Holding AG (Business Area C5I)",
"role": "Data Product Architect - AI-ready Data Products", "role": "AI Engineer C5I (w/m/d), Thun",
"application_date": "2026-07-29", "application_date": "2026-08-27",
"fit_class": "adjacent", "fit_class": "stretch",
"evidence_fit": 79.0, "evidence_fit": 74.0,
"hard_gate": "pass", "hard_gate": "fail",
"channel": "weak", "channel": "weak",
"outcome": "rejected_no_interview", "outcome": "applied",
"submitted": "2026-07-29", "id": "app-ca0990d47bd0",
"rejection_date": "2026-08-27" "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",
"company": "SBB", "events": [
"role": "Data Engineer (m/w/d), Asset Management Infrastrukturanlagen, Bern", {
"job_id": "103755", "type": "legacy_status_import",
"application_date": "2026-08-25", "recorded_at": "2026-09-11",
"fit_class": "core", "source": "CLAUDE.md / application_cohort.json",
"evidence_fit": 79.0, "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).",
"hard_gate": "pass", "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)."
"channel": "weak", },
"outcome": "withdrawn_by_candidate", {
"submitted": "2026-08-25", "type": "applied",
"open_risk": "Anforderungsniveau K is a capped GAV band, figures not public; seat may read lateral or below current Staff + Component Owner scope.", "date": "2026-08-27"
"interview": { }
"stage": "Stage 2 of 4 - Virtuelles Kennenlernen mit HR und Fuehrungskraft", ],
"invited": "2026-08-28", "cohort_id": "evidence-first-2026-01"
"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", "company": "Schweizer Armee - Kommando Cyber",
@@ -85,28 +83,189 @@
"submitted": "2026-08-27", "submitted": "2026-08-27",
"rejection_date": "2026-09-10", "rejection_date": "2026-09-10",
"days_to_disposition": 14, "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." "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": "RUAG MRO Holding AG (Business Area C5I)", "company": "Citadel Securities",
"role": "AI Engineer C5I (w/m/d), Thun", "role": "Platform Engineer, Research Platform Engineering (Zurich/NY/Miami)",
"application_date": "2026-08-27", "application_date": "2026-08-26",
"fit_class": "stretch", "fit_class": "adjacent",
"evidence_fit": 74.0, "evidence_fit": 71.0,
"hard_gate": "fail", "hard_gate": "pass",
"channel": "weak", "channel": "weak",
"outcome": "applied" "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": "NVIDIA", "company": "SBB",
"role": "Senior DevOps Engineer, Zurich (JR2024256)", "role": "Data Engineer (m/w/d), Asset Management Infrastrukturanlagen, Bern",
"application_date": "2026-09-05", "job_id": "103755",
"fit_class": "stretch", "application_date": "2026-08-25",
"evidence_fit": 64.0, "fit_class": "core",
"hard_gate": "fail", "evidence_fit": 79.0,
"hard_gate": "pass",
"channel": "weak", "channel": "weak",
"outcome": "applied" "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 <https://company.sbb.ch/de/jobs-karriere/arbeitgeberin/lohn.html>) 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 ovLine truncated
"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"
},
{
"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"
} }
], ]
"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."
} }
+660
View File
@@ -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), Line truncated
"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 <https://company.sbb.ch/de/jobs-karriere/arbeitgeberin/lohn.html>) 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 ovLine truncated
"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."
]
}
+16 -9
View File
@@ -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", "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" "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": { "https://job.bkw.com/offene-stellen/business-controller-energy-mobility-telecom-m-w-d/447ae323-707b-4145-bbbe-91f8d4b32f7b": {
"company": "BKW (Bern)", "company": "BKW (Bern)",
"title": "Business Controller Energy, Mobility & Telecom (m/w/d)", "title": "Business Controller Energy, Mobility & Telecom (m/w/d)",
@@ -1842,7 +1835,7 @@
"company": "", "company": "",
"title": "", "title": "",
"decision": "skip", "decision": "skip",
"note": "JD read 2026-09-04. Repost of the pulled Data Lakehouse / Senior Data Platform Engineer req with the disqualifier moved into the title. Required: mehrjährige Erfahrung im Aufbau und Betrieb von ON PREMISE Data Warehouse Lösungen, plus MSSQL, Talend, SAP S/4HANA. claims.json has 0 for MSSQL, Talend and SAP; his stack is AWS cloud-native. Settles the 2026-08-31 downgrade.", "note": "JD read 2026-09-04. Repost of the pulled Data Lakehouse / Senior Data Platform Engineer req with the disqualifier moved into the title. Required: mehrjährige Erfahrung im Aufbau und Betrieb von ON PREMISE Data Warehouse Lösungen, plus MSSQL, Talend, SAP S/4HANA. claims.json has 0 for MSSQL, Talend and SAP; his stack is AWS cloud-native. Settles the 2026-08-31 downgrade. *** REASONING CORRECTED 2026-09-11 - THE DECISION STANDS, TWO OF ITS PREMISES DID NOT. *** Reading the primary Arbeitszeugnisse showed: (1) 'MSSQL 0 canonical' was WRONG - Fraunhofer's SCEDAS ran on MICROSOFT SQL SERVER with C# and Entity Framework (Arbeitszeugnis 31.10.2019); (2) 'his stack is AWS cloud-native' was too narrow - the Swisscom Zwischenzeugnis (Okt 2025) documents KOMPONENTENVERANTWORTUNG for the ETL pipelines that load business-critical Fulfillment data into the TERADATA DATA WAREHOUSE (DWH), which is data-warehouse operation, and Teradata is typically on-premise. What genuinely remains 0 is TALEND and SAP S/4HANA, and the required line is 'Aufbau UND Betrieb' - he has documented Betrieb, not Aufbau. The skip is therefore still defensible, but it was better than its written justification. RULE FOR FUTURE DWH POSTINGS: he HAS documented data-warehouse experience (Teradata, as component owner) and MSSQL exposure. Do not repeat 'no DWH' or 'MSSQL 0'. Screen such roles on Aufbau-vs-Betrieb and on the named vendor stack instead.",
"date": "2026-09-04" "date": "2026-09-04"
}, },
"https://www.bis.org/vacancy/jr100472": { "https://www.bis.org/vacancy/jr100472": {
@@ -1933,7 +1926,7 @@
"company": "", "company": "",
"title": "", "title": "",
"decision": "skip", "decision": "skip",
"note": "STALE SHORTLIST from 2026-07-06, cleared 2026-09-04. The req was pulled from the board between the 08-31 and 09-04 scrapes and reposted as Senior Data Engineer (MSSQL) mit Data Lakehouse Perspektive. The 07-06 shortlist was a title-match prior; the JD is on-prem MSSQL/Talend/SAP S/4HANA, 0 canonical each. See the successor req's own decision row.", "note": "STALE SHORTLIST from 2026-07-06, cleared 2026-09-04. The req was pulled from the board between the 08-31 and 09-04 scrapes and reposted as Senior Data Engineer (MSSQL) mit Data Lakehouse Perspektive. The 07-06 shortlist was a title-match prior; the JD is on-prem MSSQL/Talend/SAP S/4HANA, 0 canonical each. See the successor req's own decision row. *** REASONING CORRECTED 2026-09-11 - THE DECISION STANDS, TWO OF ITS PREMISES DID NOT. *** Reading the primary Arbeitszeugnisse showed: (1) 'MSSQL 0 canonical' was WRONG - Fraunhofer's SCEDAS ran on MICROSOFT SQL SERVER with C# and Entity Framework (Arbeitszeugnis 31.10.2019); (2) 'his stack is AWS cloud-native' was too narrow - the Swisscom Zwischenzeugnis (Okt 2025) documents KOMPONENTENVERANTWORTUNG for the ETL pipelines that load business-critical Fulfillment data into the TERADATA DATA WAREHOUSE (DWH), which is data-warehouse operation, and Teradata is typically on-premise. What genuinely remains 0 is TALEND and SAP S/4HANA, and the required line is 'Aufbau UND Betrieb' - he has documented Betrieb, not Aufbau. The skip is therefore still defensible, but it was better than its written justification. RULE FOR FUTURE DWH POSTINGS: he HAS documented data-warehouse experience (Teradata, as component owner) and MSSQL exposure. Do not repeat 'no DWH' or 'MSSQL 0'. Screen such roles on Aufbau-vs-Betrieb and on the named vendor stack instead.",
"date": "2026-09-04" "date": "2026-09-04"
}, },
"https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite/job/Switzerland-Zurich/Senior-DevOps-Engineer_JR2024256": { "https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite/job/Switzerland-Zurich/Senior-DevOps-Engineer_JR2024256": {
@@ -2418,5 +2411,19 @@
"decision": "skip", "decision": "skip",
"note": "USER DECISION 2026-09-10: requires networking knowledge he does not have. Consistent with the standing canonical gap - Netzwerk 0, Firewall 0 - which also ruled out RUAG Principal System Engineering C5I, RUAG System Engineer C5I and failed the NVIDIA Senior DevOps hard gate. Not a JD read by Claude; the user read it and made the call. Deadline was 2026-09-16.", "note": "USER DECISION 2026-09-10: requires networking knowledge he does not have. Consistent with the standing canonical gap - Netzwerk 0, Firewall 0 - which also ruled out RUAG Principal System Engineering C5I, RUAG System Engineer C5I and failed the NVIDIA Senior DevOps hard gate. Not a JD read by Claude; the user read it and made the call. Deadline was 2026-09-16.",
"date": "2026-09-10" "date": "2026-09-10"
},
"https://jobs.admin.ch/offene-stellen/ict-system-engineer-devsecops-platform-automation/2924a114-9fa7-4037-afa0-5f0c59eefbc5": {
"company": "Schweizer Armee - Kdo Cy",
"title": "ICT-System Engineer (DevSecOps / Platform Automation), Bern",
"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"
} }
} }
@@ -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, Line truncated
"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 sLine truncated
},
"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"
}
]
}
}
@@ -1,5 +1,10 @@
# Session: Bank for International Settlements — Senior Data & Analytics Engineer – Artificial Intelligence # 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 ## JD Info
- **File:** JDs/bis_senior_data_analytics_ai.txt - **File:** JDs/bis_senior_data_analytics_ai.txt
@@ -0,0 +1,383 @@
\documentclass[11pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[ngerman]{babel}
\usepackage{lmodern,microtype}
\usepackage[margin=23mm]{geometry}
\usepackage{enumitem,booktabs,tabularx,longtable,array}
\usepackage[hidelinks]{hyperref}
\usepackage{xurl}
\usepackage[version=4]{mhchem}
\setlength{\parindent}{0pt}
\setlength{\parskip}{5pt}
\setlist{nosep,leftmargin=1.4em}
\emergencystretch=2em
\widowpenalty=10000
\clubpenalty=10000
\newcommand{\file}[1]{{\small\nolinkurl{#1}}}
\newcommand{\finding}[2]{\subsection*{#1 --- #2}}
\newcommand{\source}[2]{\footnote{#1. Quelle #2 im Quellenverzeichnis; abgerufen am 11.09.2026.}}
\begin{document}
\begin{center}{\LARGE Projektreview und Markttauglichkeit der Bewerbungsunterlagen}\end{center}
\section{Gesamturteil und Prüfgrundlage}
Die aktuellen Bewerbungsunterlagen haben eine solide Basis für den Schweizer und europäischen Tech-Arbeitsmarkt: gut lesbare, einspaltige A4-Dokumente, nachvollziehbare Berufschronologie, konkrete technische Arbeit und deutlich bessere Abgrenzung eigener Verantwortung als in älteren Paketen. Ein grundlegender Designwechsel ist nicht erforderlich. Die grössten Verbesserungsmöglichkeiten liegen in der Konsistenz des Generierungssystems, der Verlässlichkeit seiner Prüfungen und der Gewichtung der ersten Lebenslaufseite.
\textbf{Das System ist derzeit weniger zuverlässig, als seine PASS-Meldungen und detaillierten Bewertungszahlen nahelegen.} Zwei Agenten können unterschiedliche Anweisungen erhalten. Bereits aufgehobene Regeln wirken weiter im Code. Der Validator akzeptiert nachweislich Aussagen, die den zentralen Evidenzregeln widersprechen. Im Job-Scout können passende Stellen verschwinden, bevor eine inhaltliche Bewertung stattfindet.
Die Recherche stützt das bestehende Grundformat, aber keinen Anspruch auf ein wissenschaftlich nachgewiesenes Erfolgsoptimum. Für die Schweiz, Dänemark und Norwegen existieren unterschiedliche Bewerbungskonventionen; Arbeitgebervorgaben können diese wiederum übersteuern. Besonders die generelle Behandlung von Anschreiben und die starke Orientierung an einem US-Tech-Kurzformat sollten länderspezifischer werden.
\textbf{Stand: 11. September 2026.} Geprüft wurden Projektanweisungen, Konfiguration, Skills beider Agentenverzeichnisse, Referenzen, kanonische Claims, aktive Wissensbasis und Rollen-Bundles, die fünf projektspezifischen Python-Dateien einschliesslich Tests sowie generierte Dokumente und Tracking-Dateien. Historische Rohquellen wurden zur Einordnung herangezogen; eine erneute Extraktion sämtlicher Originaldokumente war nicht Teil dieses Reviews.
\begin{tabularx}{\linewidth}{@{}p{.34\linewidth}X@{}}
\toprule Prüfung & Ergebnis und Aussagegrenze\\\midrule
Vorhandene Tests & 9 Tests bestanden mit \file{python -m unittest discover -s tests -v}. Das deckt die neu gefundenen Fehler nicht ab.\\
Systemvalidator & Standardlauf PASS, 0 Warnungen. Zusätzliche Gegenproben zeigen erhebliche Lücken.\\
LaTeX-Build & 43 vorhandene Output-Quellen plus zwei professionelle Templates: 43 von 45 erfolgreich, zwei Fehler. Je zwei Durchläufe mit pdfLaTeX, ohne Shell-Escape, Build-Produkte im temporären Verzeichnis.\\
Visuelle Stichprobe & SBB, NVIDIA, RUAG, Aker BP: je beide CV-Seiten; zusätzlich NVIDIA- und RUAG-Anschreiben. Insgesamt zehn Seiten geprüft.\\
Scout & 37 konfigurierte Unternehmen, 18 Adaptertypen. Statische Prüfung und gezielte Mock-Gegenproben; kein vollständiger Live-Funktionstest aller Portale.\\
Änderungsumfang & Keine Bewerbungen, Claims, Produktionsskripte oder eingereichten Archive geändert. Bericht und temporäre Prüfarbeitsdateien erstellt.\\\bottomrule
\end{tabularx}
\textbf{Prioritäten:} P1 bezeichnet Risiken für Wahrheitsgehalt, Stellenauswahl oder verbindliche Entscheidungen. P2 umfasst Reproduzierbarkeit, Wartbarkeit und wirksame Dokumentverbesserungen. P3 bezeichnet kleinere Bereinigungen. Die Einstufung ist eine Review-Priorisierung, kein berechneter Risikoscore.
\section{Widersprüche in Regeln, Zustand und Workflow}
\finding{F01 / P1}{Die Agenten verwenden unterschiedliche Bewerbungsprozesse}
\textbf{Fundstellen:} \file{.agents/skills/} und \file{.claude/skills/}, besonders jeweils \file{make-resume/SKILL.md} und \file{critique/SKILL.md}.
Alle sechs korrespondierenden Skills unterscheiden sich inhaltlich. Die Claude-Fassung von \file{make-resume} verlangt weiterhin feste Bullet-Zeilenvarianten und Zeichenbudgets; die neuere Fassung setzt auf natürliche Länge, Claims und Evidenz. Auch die Behandlung eines fehlenden Anschreibens, geschützter Abschnitte und der Bewertung unterscheidet sich. Damit hängt das Ergebnis vom verwendeten Agenten ab, obwohl beide dasselbe Projekt bearbeiten.
\textbf{Folge:} Veraltete Regeln können wieder eingeführt werden, ohne dass ein Codefehler nötig ist. Besonders problematisch sind alte Schreibanweisungen, die neuere Genauigkeitskontrollen umgehen oder Relevanz zugunsten eines Seitenfüllziels verschieben.
\textbf{Empfehlung:} Ein gemeinsamer, versionierter Prozess mit kleinen agentenspezifischen Einstiegspunkten. Die Einstiegspunkte sollen dieselben Referenzen laden, nicht vollständige Kopien enthalten. Ein Konsistenztest sollte diese Referenzen und die gültige Prozessversion prüfen. Die Dateikarte in \file{AGENTS.md} verweist ausserdem auf \file{.Codex/skills/}, während die tatsächlich verwendeten Skills anderswo liegen.
\finding{F02 / P1}{Bewerbungsstatus ist mehrfach und widersprüchlich gespeichert}
\textbf{Fundstellen:} aktive Sessions in \file{AGENTS.md}, \file{CLAUDE.md}, Kohortenstatus und Scout-Entscheidungen.
SBB steht in AGENTS noch als bevorstehendes Interview am 9. September; die aktuelleren Einträge dokumentieren das Gespräch und den Rückzug am 11. September. Kdo Cy steht dort noch als eingereicht, obwohl der neuere Tracker eine Absage am 10. September enthält. Auch Microsoft ISE und Equinor haben in CLAUDE bereits Absagen, während AGENTS auf Antwort wartet.
Das ist operativ relevant: Ein neuer Agent könnte eine Vorbereitung für ein abgeschlossenes Gespräch anbieten, falsche Conversion-Zahlen nennen oder eine Stelle erneut priorisieren. Lange Session-Zellen mischen zudem aktuelle Erkenntnisse, frühere Schätzungen und inzwischen überholte nächste Schritte. Die SBB-Gehaltskenntnis darf beispielsweise nicht auf andere Arbeitgeber in Bern oder Thun verallgemeinert werden.
\textbf{Empfehlung:} Eine strukturierte Bewerbung pro Requisition, mit aktuellem Zustand und separatem Ereignisverlauf. AGENTS- und CLAUDE-Übersichten daraus erzeugen. Ein Rückzug nach einem Interview muss zugleich \emph{aktueller Status: zurückgezogen} und \emph{Interview erreicht: ja} ausdrücken können. Historische Notizen erhalten, aber aus der aktuellen Handlungsanweisung entfernen.
\finding{F03 / P1}{Die aufgehobene Quote blockiert weiterhin Bewerbungen}
\textbf{Fundstelle:} \file{resume_builder/helpers/cohort_tracker.py:59}, insbesondere Zeile 61; ausserdem README und ältere Skill-Anweisungen.
Seit dem 5. September gelten keine Bewerbungsquoten mehr. Der Tracker verweigert dennoch die elfte Bewerbung mit \emph{cohort already contains ten applications}. Diese Sperre wurde mit einem synthetischen Datensatz reproduziert. Auch Hinweise auf volle Fit-Kategorien sind noch vorhanden.
\textbf{Empfehlung:} Die operative Obergrenze entfernen; historische Kohortenbezeichnungen dürfen bestehen bleiben. Zugleich eindeutige Bewerbungs-IDs, gültige Datumswerte, Score-Bereiche und idempotente Updates prüfen. Der aktuelle Schreibweg schützt weder ausreichend vor Duplikaten noch vor widersprüchlichen Nachträgen. Eine atomare Speicherung allein löst dabei keine konkurrierenden Updates.
\textbf{Weitere Bewertungsinkonsistenz:} Aker BP wird trotz 79 Punkten und bestandenem Hard Gate als Adjacent geführt, obwohl die numerische Einteilung Core nahelegt. Das kann eine sachlich gewollte Entscheidung sein. Es braucht dann einen expliziten Override-Grund und eine Rubrikversion, keine stillschweigende Umklassifizierung.
\finding{F04 / P2}{Regelhierarchie und Auslieferungsauftrag sind nicht eindeutig}
AGENTS verlangt ausschliesslich TEX-Ausgaben und lokale Kompilierung durch Dennis, fordert an anderer Stelle aber selbst Kompilierung und Sichtprüfung; Konfiguration und Skills beschreiben PDF-Auslieferungen. Alte und neue Referenzen widersprechen sich zudem bei Capgemini, der Bedeutung von Zeichenbudgets und verpflichtenden Bildungsdetails.
\textbf{Empfehlung:} Drei Ebenen unterscheiden: unveränderliche Evidenzregeln, aktuelle persönliche Präferenzen und markt-/arbeitgeberspezifische Darstellung. Technische QA und auszuliefernde Dateitypen separat festlegen. Dieser Review liefert entsprechend der TEX-Vorgabe den Bericht als TEX; die eigene Kompilierung dient ausschliesslich der QA.
\section{Evidenzprüfung und Wissensbasis}
\finding{F05 / P1}{PASS ist keine belastbare Wahrheitsprüfung}
\textbf{Fundstelle:} \file{resume_builder/helpers/validate_resume_system.py:121}, Funktion \file{scan_document}.
Der Dokumentcheck sucht im Wesentlichen globale verbotene Textmuster, bestimmte Skill-Namen und wenige Titelmuster. Er prüft nicht systematisch die erlaubten Verben und Verbote jedes einzelnen Claims, Eigentumsumfang, Zertifikatskontext, Arbeitgeberdaten, Identität, Zahlen oder die Zuordnung zwischen Bullet und Claim.
Folgende absichtlich falschen beziehungsweise unzulässigen Gegenproben bestanden ohne Fehler:
\begin{itemize}
\item vollständiger Besitz einer Data-Mesh-Plattform und des gesamten ML-Lebenszyklus;
\item Capgemini trotz ausdrücklicher persönlicher Ausschlussregel;
\item produktives Training mit PyTorch und TensorFlow trotz begrenzter kanonischer Evidenz;
\item produktive Arbeit mit nicht entsprechend belegten Skills wie Groovy und Snowflake;
\item falsche E-Mail, falscher Beginn der Staff-Rolle und eine erfundene Kostensenkung;
\item ein stehengebliebener \file{[GENERATE: Bullet]}-Platzhalter.
\end{itemize}
Das sind Tests des Validators, keine Behauptung, alle diese Fehler stünden in aktuellen Lebensläufen. Sie zeigen, dass sein grünes Ergebnis den zentralen Qualitätsanspruch nicht absichert.
\textbf{Empfehlung:} Jedes Dokument bekommt ein strukturiertes Inhaltsmanifest mit Claim-IDs, Skill-Evidenz und beabsichtigter Formulierung. Deterministisch prüfbar sind Identität, Daten, bekannte Metriken, Pflichtkontext, ausgeschlossene Inhalte und Referenzintegrität. Die semantische Frage, ob ein Satz unzulässige Gesamtverantwortung suggeriert, braucht weiterhin eine gesonderte inhaltliche Prüfung. Beide Resultate getrennt ausgeben; keinen automatischen Wahrheitsnachweis versprechen.
\finding{F06 / P2}{Der Textscan erzeugt Fehlalarme und sprachabhängige Lücken}
Ein Kommentar wie \file{\% Never claim Terraform experience} wird als unzulässiger Skill gewertet. Dasselbe gilt für die ehrliche Aussage, keine Terraform-Erfahrung zu haben. Umgekehrt umgeht ein durch LaTeX-Auszeichnung aufgeteilter Skillname den Stringvergleich. In einer SBB-Quelldatei wird bereits davor gewarnt, verbotene Namen auch nur in Kommentaren zu erwähnen: Der Workflow arbeitet um den Prüfer herum.
Die Bullet-Analyse verarbeitet nur bestimmte einzeilige Quellmuster. Die Dreierlisten-Heuristik in Zeile 140 erkennt englisches \emph{and}, nicht das entsprechende deutsche \emph{und}. Gegenproben zeigen diesen Unterschied; andere Warnungen wie wiederholte Verben funktionieren teilweise auch deutsch. Daraus folgt kein allgemeines Urteil, der gesamte Prüfer könne kein Deutsch.
\textbf{Empfehlung:} Kommentare und sichtbaren Text unterscheiden, LaTeX normalisieren und die relevanten Strukturen robust extrahieren. Verneinungen benötigen Kontext oder manuelle Freigabe. Stilheuristiken als unverbindliche Hinweise ausgeben. Für eine Grenze von ungefähr 50 Prozent Dreierlisten als Qualitäts- oder KI-Erkennungsregel wurde keine belastbare externe Grundlage gefunden. Belegte Verben und natürliche Sprache sind wichtiger als künstliche Variation.
Die Schema-Prüfung hat zusätzliche Lücken: Eine fehlende Identität führt nach der Fehlermeldung zu einem \file{KeyError}; ein unmögliches Datum wie \file{2035-99} und ein nicht existierender Quellenverweis wurden akzeptiert. Hier sind kleine negative Regressionstests besonders wirksam.
\finding{F07 / P1}{Korrekturen fehlen in aktiven Schreibquellen}
\begin{longtable}{@{}p{.30\linewidth}p{.64\linewidth}@{}}
\toprule Fundstelle & Widerspruch und notwendige Behandlung\\\midrule
\endhead
\file{bundles/bundle_ml_ai_engineer.md:74,81} & PyTorch wird unter aufzunehmenden Skills genannt; weiter unten steht korrekt, dass der Zertifikatskontext nötig ist. Die generische Liste kann genau diesen Kontext verlieren.\\
\file{experience/experience_swisscom.md:166--189} & Retrieval-Begriffe stehen als Skills/Keywords und Evaluation als Schreibempfehlung, obwohl Retrieval-Implementierung und breitere LLM-Evaluationsverantwortung nicht belegt sind. Datenbereitschaft und konfigurierte Agenten enger beschreiben.\\
\file{bundles/bundle_data_platform.md:69} & Aus kontinuierlichem Fabrikbetrieb wird eine Zero-Downtime-Deployment-Anforderung abgeleitet. Das ist ohne zusätzlichen Beleg eine stärkere Behauptung.\\
\file{experience/experience_bosch.md}, BS-1 & Vollständiges Ersetzen manueller Waferinspektion und unbeaufsichtigter Betrieb reichen weiter als die kanonisch belegte Bildklassifikation und qualitative Reduktion manueller Arbeit. Varianten zurück auf den bestätigten Umfang führen.\\
\file{reference/resume_reference.md:62} und Capgemini-Erfahrungsdatei & Bedingtes Aufnehmen von Capgemini widerspricht der persönlichen Ausschlussregel. Rohhistorie erhalten, aktive Auswahlregeln korrigieren.\\
\file{experience/experience_bosch.md} und Plattform-Bundle & Die neuere BS-6-Evidenz zu Linux/Ansible ist nicht vollständig in den normalen Auswahlpfaden angekommen. Das System verliert dadurch echte Stärken.\\
\file{support/skills_taxonomy.md} & Veralteter Hinweis auf unbekanntes IBM-Abschlussjahr trotz vorhandener verifizierter Datierung; unterschiedliche Einordnung von PyTorch. Auch Linux/Bash fehlen in der erwarteten systematischen Abbildung.\\
\bottomrule
\end{longtable}
Alle verkürzten Pfade in dieser Tabelle liegen unter \file{resume_builder/}. Die kritische Beobachtung ist die Richtung des Fehlers: Es entstehen sowohl Übertreibungen als auch unnötige Unterbewertungen. Das NVIDIA-Anschreiben spricht beispielsweise davon, dass die Produktionslinie für ein Deployment nicht gestoppt werden konnte; RUAG verwendet eine ähnliche Stillstandsaussage. Diese Aussagen sind aus dem bisher kanonisch dokumentierten 24/7-Kontext allein nicht ableitbar. Sie sollten bestätigt oder enger formuliert werden, nicht vorschnell als nachweislich falsch gelten.
Weitere Widersprüche betreffen die Priorität des Plattform-Bundles und die Platzierung des Security-Champion-Themas. Solche Prioritäten sollten aus einer gemeinsamen Rollenmatrix kommen. Nach einer Claim-Korrektur ist ein gezielter Abhängigkeitslauf über alle aktiven Varianten sinnvoll; unveränderliche Originalquellen und bereits versandte Dokumente dürfen dabei nicht stillschweigend umgeschrieben werden.
\finding{F08 / P2}{Das Evidenzmodell ist noch nicht durchgehend maschinenprüfbar}
Von 26 kanonischen Claims besitzt nur einer die jeweils geprüften strukturierten Felder \file{source}, \file{last_verified} und \file{ownership}. Andere Claims enthalten durchaus Evidenz und Einschränkungen in Freitext; die Zählung bedeutet ausdrücklich nicht, dass 25 Claims unbelegt seien. Sie erklärt aber, weshalb automatische Referenz- und Aktualitätsprüfungen derzeit schwer möglich sind.
Auch Skills, die innerhalb bestätigter Tätigkeiten vorkommen, sind nicht vollständig als eigenständige Skill-Einträge normalisiert. Das kollidiert mit der Regel, jeder ausgegebene Skill müsse vorher kanonisch registriert sein. Ein sinnvoller nächster Schritt ist ein kleines Schema für Quellenanker, Gültigkeitszeitraum, Kontext, Eigentumsumfang und erlaubte Ableitungen. Die Claim-ID verbindet die Daten; zusätzliche parallele Freitextregister würden den Pflegeaufwand weiter erhöhen.
\finding{F09 / P1}{Vorlagen enthalten bereits riskante Aussagen}
\textbf{Fundstelle:} \file{resume_builder/templates/resume_template.tex:36}. Die fest eingetragene Aussage \emph{12+ years of production software experience} folgt nicht aus der dargestellten Chronologie. Mai 2015 bis September 2026 sind rund elf Jahre und vier Monate verstrichene Zeit; daraus darf ohnehin nicht ungeprüft eine lückenlose Netto-Berufserfahrung berechnet werden.
\textbf{Empfehlung:} Die Jahreszahl entfernen oder aus bestätigten, nicht doppelt gezählten Zeiträumen ableiten. Auch eine automatisch gesetzte Thesis-Note und unveränderliche Bildungsdetails sollten keine allgemeine Industrievorlage steuern. Die ältere akademische \file{cv_template.tex} enthält weiterhin mehrseitige Zielvorgaben, feste Zeilenbudgets und unaufgelöste Platzhalter-/Asset-Erwartungen. Sie sollte als akademischer Legacy-Pfad klar abgegrenzt oder aktualisiert werden. Ein europäischer beruflicher CV ist nicht automatisch ein akademischer Lang-CV.
\finding{F10 / P2}{Die US-GPA-Umrechnung ist kein universeller Karrierefakt}
\textbf{Fundstelle:} \file{resume_builder/canonical/claims.json}, Bildungsangaben mit \emph{US = 5 -- G}. Die daraus resultierenden GPA-Werte setzen ein bestimmtes Zielnotensystem voraus. Die angeführte modifizierte bayerische Formel wird beispielsweise von der TUM für die Umrechnung ausländischer Noten in das deutsche System erläutert; auch dort ist der Rechner nur eine Orientierung. Daraus folgt keine allgemein akzeptierte inverse US-Umrechnung.\source{Technische Universität München: Bayerische Formel}{Q16}
\textbf{Empfehlung:} Bestätigte deutsche Originalnoten mit Skalenkontext verwenden. Nur wenn ein konkreter Empfänger eine Umrechnung verlangt, dessen Methode dokumentieren. Keine pauschale US-GPA als gleichrangigen kanonischen Abschlussfakt führen. Für die primären europäischen Zielmärkte bringt diese Umrechnung ohnehin wenig.
\section{Job-Scout: Fehler mit Einfluss auf die Stellenauswahl}
\finding{F11 / P1}{Titel-Deduplizierung entfernt verschiedene Requisitionen}
\textbf{Fundstelle:} \file{job_scout/scout.py:2168}, innerhalb \file{_process_company}. Pro Unternehmen wird auf dem normalisierten Titel vor dem ersten senkrechten Strich dedupliziert. Zwei verschiedene Requisitionen namens Senior Data Engineer in Bern und Zürich ergaben in der Gegenprobe nur einen Treffer. Auch unterschiedliche Teams mit demselben Titel sind betroffen.
Der Wunsch, mehrfach veröffentlichte Remote-Standorte zusammenzuführen, ist sinnvoll. Der Titel beweist aber keine Identität. \textbf{Lösung:} Arbeitgeber plus stabile Requisitions-ID als Primärschlüssel; mehrere Standorte nur bei derselben Requisition zusammenführen. Titelähnlichkeit darf einen Hinweis auf mögliche Duplikate erzeugen, keine irreversible Entfernung.
\finding{F12 / P1}{Ortsfilter verlieren Schweizer Stellen und erkennen falsche Remote-Eignung}
\textbf{Fundstelle:} \file{job_scout/scout.py:1728}, \file{location_eligibility}, sowie Adapter-Normalisierung.
Die isolierte Filterfunktion akzeptiert Bern, lehnt aber unter anderem Thun, Zimmerwald, Baar und Olten ohne Länderzusatz ab. Bernau in Deutschland wird wegen des Teilstrings Bern akzeptiert. Texte mit Europe können als Remote eingeordnet werden, obwohl London/Onsite oder eine Beschränkung auf Deutschland danebensteht. Aarhus und Odense werden ebenfalls nicht wie Kopenhagen erkannt.
Nicht jeder Adapter ist gleich betroffen: Manche ergänzen das Land. Ein konkreter Mock des SmartRecruiters-Pfads zeigt jedoch, dass \file{city=Baar, country=CH} auf den String Baar reduziert und danach verworfen wird. Damit ist das Problem mehr als eine abstrakte Schwäche des Ortsvokabulars.
\textbf{Lösung:} Länderkennung, Ort, Arbeitsmodell, erlaubte Beschäftigungsländer und Umzugspflicht als getrennte Felder erhalten. Unbekanntes Arbeitsmodell bleibt unbekannt. Europa-Remote allein belegt keine Anstellungsmöglichkeit in der Schweiz. Norwegen ist derzeit nur über ausgewählte Unternehmens-Overrides abgedeckt; die gewollten Ausnahmen sollten als explizite Suchpräferenz gepflegt werden.
\finding{F13 / P2}{Entscheidungs-IDs und Wiedererkennung sind nicht einheitlich}
\textbf{Fundstelle:} \file{job_scout/scout.py:1873}, \file{_decision_key}, sowie RUAG-Fallback und Report-Index.
Der generische Fallback verliert Query-Parameter: Zwei URLs \file{/vacancy?job=1} und \file{/vacancy?job=2} ergeben denselben Schlüssel. Bei gemeinsam genutzten ATS-Hosts fehlt teilweise der Arbeitgeber im Namensraum; eine Kollision wäre möglich, falls IDs nicht global eindeutig sind. Das ist ein strukturelles Risiko, kein Nachweis einer bereits eingetretenen SmartRecruiters-Kollision.
Für RUAG erkennt die interaktive Entscheidungssuche in der Gegenprobe einen bekannten Slug mit neuer UUID wieder; der Report-Lookup findet die alte Entscheidung nicht. \textbf{Lösung:} Ein gemeinsamer Identitätsresolver für Schreiben und Lesen. Neue Requisitionen mit ähnlichem Slug als verwandt markieren; nicht automatisch alte Ablehnungen übertragen, da eine Neuausschreibung sachlich anders sein kann.
\finding{F14 / P1}{Der Scout nennt Keyword-Interesse fälschlich Fit}
\textbf{Fundstelle:} \file{job_scout/scout.py:1790}, \file{score_job}, und Report-Kategorien.
Im Titeltest erreicht Senior Data Engineer drei Punkte, Senior AI Engineer mit Generative AI, LLM und RAG dagegen zwölf. Senior Dateningenieur erreicht nur einen Punkt. Das entspricht weder automatisch Dennis' stärkster Evidenz noch einer belastbaren Eignungsbewertung. Bei vorgefilterten Boards wird teilweise nur der Titel gewertet; harte Anforderungen im JD können so unberücksichtigt bleiben.
\textbf{Lösung:} Den Wert ehrlich als Suchrelevanz kennzeichnen. Anschliessend separate Signale für Evidenz, zwingende Anforderungen, Sprache, Standort, Niveau und praktische Attraktivität berechnen beziehungsweise als unbekannt markieren. Deutsche Berufsbezeichnungen ergänzen. Die verbindliche Fit-Prüfung bleibt am vollständigen JD; ein höheres Keyword-Ergebnis darf keine bevorzugte Bewerbung suggerieren.
\finding{F15 / P2}{Datums- und Kontaktinformationen verlieren ihre Bedeutung}
\textbf{Fundstellen:} Taleo-Parser ab \file{job_scout/scout.py:1110}, Datumsparser und Ergebnisobjekt in \file{_process_company}.
Der Taleo-Parser schreibt in der Gegenprobe eine Bewerbungsfrist vom 30. September in \file{posted}. Dadurch kann eine Stelle fälschlich als besonders frisch erscheinen. Andere Adapter liefern relative Daten oder Aktualisierungszeitpunkte, die nicht einheitlich mit Veröffentlichungsdaten vergleichbar sind.
Der Prospective-Pfad liest nützliche Angaben wie Einheit, Lohnklasse und Ansprechpartner in die Beschreibung ein. Der finale Ergebnisdatensatz verwirft die Beschreibung und führt diese Angaben nicht strukturiert fort. Damit gehen genau die Informationen verloren, die für Niveau, Gehalt und den Kontaktansatz wichtig sind.
\textbf{Lösung:} \file{published_at}, \file{updated_at}, \file{deadline} und \file{first_seen_at} trennen, inklusive Rohwert und Herkunft. Ansprechpartner, Vergütungshinweis und Organisationseinheit als eigene Felder führen; JD-Inhalt oder einen stabilen lokalen Verweis mitsamt Abrufdatum erhalten.
\finding{F16 / P2}{Report- und Zustandswrites können Informationen verlieren}
\textbf{Fundstellen:} \file{job_scout/scout.py:2202--2340}, Speicherroutinen und breite Exception-Behandlung in Adaptern.
Alle Läufe desselben Tages schreiben auf denselben Markdown-/JSON-Namen. Ein eingeschränkter Lauf mit \file{--only} oder \file{--new-only} kann damit den vollständigen Tagesreport überschreiben. Gesehen-Markierungen werden gespeichert, bevor der Report erfolgreich geschrieben ist. Bei einem Schreibfehler kann ein Wiederholungslauf mit New-only deshalb gerade die noch nicht ausgelieferten Treffer ausblenden.
Ein fester temporärer Dateiname bei atomaren Writes verhindert keine konkurrierenden Schreibkonflikte. Breite \file{except Exception: continue}-Blöcke können einzelne kaputte Karten stillschweigend auslassen; Pagination-Limits und vollständige Abdeckung sind nicht überall erkennbar.
\textbf{Lösung:} Eindeutige Run-ID mit Suchumfang, unveränderliche Reports und erst nach erfolgreicher Report-Erstellung committed Seen-State. Konkurrenzschutz oder bewusst nur ein schreibender Prozess. Pro Adapter Mengen, Abbruchgrund und Vollständigkeitsstatus ausgeben. Die rund 2.400 Zeilen Scout-Code nach Adapter, Normalisierung, Matching und Persistenz aufzuteilen wäre anschliessend sinnvoll; ein grosses neues Framework ist dafür unnötig.
\section{Builds, historische Pakete und sichtbarer Output}
\finding{F17 / P2}{Nicht alle Pakete sind aus den vorhandenen Quellen reproduzierbar}
\begin{itemize}
\item \file{output/NATO_AI_ENGINEER/e2e_nato_ai_engineer_resume.tex}: Build scheitert wegen fehlender \file{resume.cls}. Ein vorhandenes PDF verdeckt die unvollständige Quelldistribution.
\item \file{output/Infineon_AI_Engineer/e2e_infineon_ai_engineer_cover_letter.tex}: Im geprüften pdfLaTeX-Setup Fehler bei der Schriftexpansion, weil eine verwendete Schrift nicht skalierbar ist.
\item Microsoft-ISE-Resume: Overfull-Box von rund 25,3 pt; Isovalent-Resume: kleine Overfull-Box von rund 0,88 pt. Die vier aktuellen Stichproben bauten ohne solche Boxen.
\end{itemize}
Historische Klassenkopien sind nicht automatisch schädliche Duplikate: Sie können den damaligen Build einfrieren. Sie sollten über eine Version oder einen Hash nachvollziehbar sein. Beim NATO-Paket einfach die neueste Klasse hineinzukopieren wäre keine sichere Reparatur, weil alte Makros abweichen können.
Mehrere PDF-Kopien unterscheiden sich binär. Bei Kdo Cy ist der extrahierte Text dennoch identisch; das ist kein belegter Inhaltsfehler. Beim NATO-Resume unterscheiden sich dagegen die Skills und die enthaltenen Stationen zwischen E2E- und Abgabe-PDF. Es fehlt ein eindeutig maschinenlesbarer Verweis, welche Quelle die tatsächlich eingereichte Fassung erzeugt hat. Keine dieser historischen Fassungen sollte stillschweigend ersetzt werden.
\textbf{Empfehlung:} Pro Abgabe ein Manifest mit Requisition, Dokumenttyp, Quellen-/Klassenhash, festem Datum, finalem Dateinamen und Status. \file{\today} in historischen Anschreiben macht den erneuten Build datumsabhängig und sollte bei neuen Paketen durch das explizite Bewerbungsdatum ersetzt werden. Das Wiederverwendungsregister klassifiziert 14 Pakete, während 21 Bewerbungsordner vorhanden sind; unbekannte Pakete benötigen einen klaren Revalidierungsdefault.
\finding{F18 / P2}{Die erste Seite zeigt zu spät die stärksten Belege}
Die vier visuell geprüften Lebensläufe sind gut lesbar, technisch ruhig und ohne Überlappungen. Das A4-Format, die Schriftgrösse und die klare Chronologie funktionieren. Die Textausleitung zeigte in diesen PDFs keine Ersatzzeichen und eine nachvollziehbare Reihenfolge. Das ist ein nützlicher technischer Check, aber keine Zertifizierung für jedes ATS.
\begin{tabularx}{\linewidth}{@{}lrrX@{}}
\toprule Paket & Seiten & Beginn Erfahrung & Beobachtung\\\midrule
SBB & 2 & 50,6\% & Viel Platz für Profil/Skills; lange Bildungszeile.\\
NVIDIA & 2 & 55,4\% & Bosch/Linux-Evidenz erst auf Seite zwei.\\
RUAG & 2 & 50,3\% & Bildungsblock bereits besser aufgeteilt.\\
Aker BP & 2 & 50,5\% & Starker Skills-Vorlauf vor Berufserfahrung.\\\bottomrule
\end{tabularx}
Die Prozentwerte bezeichnen die vertikale Position der Überschrift zur Berufserfahrung relativ zur Seitenhöhe, nicht ein wissenschaftliches Layoutziel. Sie machen das gemeinsame Problem sichtbar: Der erste konkrete Berufseintrag beginnt erst etwa auf halber Seite. Fünf oder sechs Skills-Kategorien entsprechen durch Umbrüche wesentlich mehr gedruckten Zeilen. Eine Regel für vier bis sechs Quellmakros kontrolliert daher nicht die tatsächliche Dichte.
\textbf{Empfehlung:} Skills auf die entscheidenden belegten Anforderungen reduzieren und Wiederholungen zu den Experience-Bullets entfernen. Direkt nach dem kurzen Profil entweder die Erfahrung oder einen sehr kompakten Kompetenzblock zeigen. Die Belegreihenfolge innerhalb einer Station nach der Zielrolle priorisieren; die Arbeitgeberchronologie beibehalten. Kein fixes Pixelziel und kein erzwungenes Auffüllen der zweiten Seite.
\finding{F19 / P2}{Einige Formulierungen und Details schwächen die Wirkung}
Die langen Bildungszeilen mit Abschluss, Institution und rechtsbündigem Datum erzeugen bei SBB, NVIDIA und Aker BP auffällige Wortabstände beziehungsweise Umbrüche. Die RUAG-Variante mit getrennten Angaben ist ein brauchbares Vorbild. Abschlussnote und Thesis sollten bei erfahrenen Industrieprofilen nur bleiben, wenn sie konkrete Relevanz tragen.
Bei NVIDIA nimmt die Erklärung fehlender Erfahrung relativ viel Raum im Anschreiben ein. Eine klar benannte, entscheidende Lücke mit anschliessender belastbarer Anschlussfähigkeit wäre meist wirksamer als ein eigener längerer Defizitblock. Ehrlichkeit bleibt verbindlich; sie erfordert keine vorgezogene Selbstabsage. Bei RUAG könnten die echte Verbindung zu Thun und der Offiziershintergrund früher erscheinen. Deutsche Bullets sollten eine konsistente Perspektive erhalten; eine Mischung aus drittpersonalen Verbformen und anderen Stilen wirkt unnötig distanziert.
Ein enger, evidenztreuer Formulierungsrahmen wäre beispielsweise: \emph{Governed data products within Swisscom's company-wide Data Mesh} statt eines ganzen selbst gebauten Data Mesh. Bei Bosch sollte die konkret bestätigte Bildklassifikation beziehungsweise Integration beschrieben werden, statt die gesamte Waferinspektion als ersetzt darzustellen. Diese Beispiele sind Richtungen für eine spätere Bearbeitung, keine neu validierten Ersatzbullets.
Die Metadaten zur Wortzahl sind nicht überall reproduzierbar: NVIDIA nennt 279, während eine einfache Whitespace-Zählung des extrahierten Briefkörpers etwa 300 Wörter ergibt; RUAG liegt mit rund 299 nahe an den dokumentierten 295. Unterschiedliche Zählregeln können kleine Abweichungen erklären. Der relevante Befund ist fehlende Zählkonvention, nicht ein Verstoss gegen eine universelle optimale Wortzahl. Beide Briefe passen auf eine gut lesbare Seite.
\section{Vertiefte Recherche: Was passt zu den Zielmärkten?}
\subsection{Evidenzstärke und sinnvolle Grenzen}
Für die Frage nach dem grössten Bewerbungserfolg sind drei Quellenarten zu unterscheiden: Arbeitgebervorgaben zeigen, was ein konkreter Prozess akzeptiert oder verlangt. Nationale Arbeitsmarktstellen und Berufsverbände beschreiben verbreitete Konventionen. Experimente können Wirkungen untersuchen, gelten aber nur eingeschränkt ausserhalb ihrer untersuchten Population.
Die recherchierten Quellen liefern keine belastbare Rangfolge, mit der eine bestimmte Schrift, eine exakt zweizeilige Zusammenfassung oder ein fester Bullet-Rhythmus Dennis' Interviewwahrscheinlichkeit maximiert. Solche Regeln können gute redaktionelle Hilfen sein; als wissenschaftliches Erfolgsversprechen wären sie überzogen. Der Bericht empfiehlt deshalb einen gut begründeten Standard mit wenigen situativen Varianten.
\subsection{Schweiz: Das bestehende Grundformat beibehalten}
SECO/arbeit.swiss empfiehlt einen klaren, umgekehrt chronologischen Lebenslauf und nennt bis zu drei Seiten. Ein Foto ist dort eine mögliche Ergänzung. Das unterstützt einen zweiseitigen CV für das vorliegende Erfahrungsprofil; es begründet weder eine Pflicht zur dritten Seite noch einen starren Einseiter.\source{SECO: Bewerbungstipps}{Q1}
Für Schweizer Industrie-, Infrastruktur- und öffentliche Arbeitgeber sollten konkrete Verantwortung, Betriebsumfeld, Zusammenarbeit und verständliche Rollenbezeichnungen schnell erkennbar sein. Die tatsächlichen Component-/Application-Owner-Titel sind dabei wertvoll, müssen aber durch den eigenen Umfang erklärt werden. Arbeitsort, wahrheitsgemässe Sprachangaben und vorhandener Aufenthaltsstatus können praktische Einordnung erleichtern. Diese Gewichtung ist eine aus Quellen und Profil abgeleitete Empfehlung, keine allgemeine rechtliche Dokumentpflicht.
\textbf{Die Arbeitgeberregel hat Vorrang.} SBB erklärt das Motivationsschreiben ausdrücklich für freiwillig und bevorzugt PDFs. Der Lebenslauf soll separat und mit möglichst wenig Grafikelementen hochgeladen werden; weitere Unterlagen können gebündelt werden. Das SBB-Paket ohne Anschreiben war deshalb prozessgerecht.\source{SBB: Bewerbungstipps}{Q2}
Wo ein Anschreiben sinnvoll ist, empfiehlt die UZH eine Seite mit konkretem Bezug zur Stelle und nachvollziehbarer Motivation, statt den Lebenslauf zu wiederholen. Für Dennis kann das bei einem Branchenwechsel, einem besonderen Bezug zum Arbeitgeber oder einer erklärungsbedürftigen Anschlussfähigkeit echten Nutzen haben.\source{Universität Zürich: Anschreiben}{Q3}
Bei persönlichen Daten widersprechen sich selbst seriöse Schweizer Ratgeber. Der ETH-Welcome-Center-Leitfaden führt traditionelle Angaben zu Geburtsdatum und Familie auf, bezeichnet das Foto aber als optional; arbeit.swiss formuliert weniger eng. Das sind Konventionsempfehlungen, kein Beweis, dass mehr private Angaben bessere Chancen erzeugen. Die bewusste Präferenz gegen Geburtsdatum, Familienstand und Kinder sollte bestehen bleiben. Ein Foto ist ebenfalls kein notwendiger Standard.\source{ETH Zürich: How to apply for a job}{Q4}
Für international rekrutierende Tech-Unternehmen in der Schweiz bleibt das International-Tech-Profil passend: belegte technische Beiträge, klare Ergebnisse, wenig administrative Zusatzinformation. Die Sprache sollte sich nach Ausschreibung, Team und ausdrücklicher Vorgabe richten. Das englische SBB-Dokument war eine bewusste Entscheidung und wird deshalb nicht allein wegen des deutschen JDs als Fehler bewertet.
\subsection{Dänemark: Kurzprofil und Motivation systematisch mitdenken}
Workindenmark empfiehlt ein zugeschnittenes persönliches Profil von etwa vier bis acht Zeilen mit den wichtigsten Kompetenzen. Für internationale Bewerbende kann die Motivation für Dänemark relevant sein. Eine globale Zwei-bis-drei-Zeilen-Regel sollte deshalb kein hartes Hindernis sein.\source{Workindenmark: A Good Personal Profile Counts, aktualisiert 25.03.2026}{Q5}
Die gleiche staatliche Plattform rät dazu, unbekannte Arbeitgeber und die tatsächliche Funktion verständlich einzuordnen und Verantwortung, Zusammenarbeit sowie Ergebnisse zu beschreiben. Das passt gut zu Dennis' Laufbahn: Schweizer Unternehmensnamen und interne Owner-Titel erklären sich im Ausland nicht von selbst. Ein knapper Kontextsatz ist oft wertvoller als zusätzliche Technologiebegriffe.\source{Workindenmark: Work Experience, aktualisiert 12.03.2026}{Q6}
Das Anschreiben wird dort als häufig ebenso wichtig wie der CV beschrieben: höchstens eine Seite, individuell, mit Motivation und Beispielen fachlicher und sozialer Kompetenz. Für eine dänische Bewerbung sollte deshalb der Standard lauten: den erwarteten Bewerbungsumfang prüfen und ein spezifisches Schreiben ernsthaft einplanen. Die bestehende globale Tendenz zum Weglassen eines optionalen Briefs ist hier zu grob. Eine ausdrückliche Arbeitgebervorgabe gegen Anschreiben übersteuert diesen Standard.\source{Workindenmark: Your Cover Letter, aktualisiert 12.03.2026}{Q7}
Der Ingenieurverband IDA empfiehlt in seinem aktuellen Ratgeber gewöhnlich ein bis zwei Seiten und ein klares, angepasstes Kompetenzprofil. Das stützt zwei Seiten als brauchbaren Ausgangspunkt für diese technische Laufbahn.\source{IDA: How to make a good CV}{Q8}
Workindenmark beschreibt persönliche Informationen und Interessen als üblich. Daraus folgt keine Notwendigkeit, die persönliche Datenschutzpräferenz aufzugeben. Falls zutreffend, können eine konkrete Dänemark-Motivation, reale Sprachlernbemühungen oder relevante Interessen die persönliche Anschlussfähigkeit zeigen, ohne Familienangaben einzuführen. Relocation-Bereitschaft darf nur genannt werden, wenn sie tatsächlich besteht.\source{Workindenmark: Personal Details in Your CV, aktualisiert 12.03.2026}{Q9}
\subsection{Norwegen: Fachliches Profil und spezifische Bewerbung}
NAV empfiehlt eine übersichtliche Berufschronologie, genaue Tätigkeits- und Zeitangaben und verständliche Sprachkenntnisse. Die aktuelle Darstellung ist damit grundsätzlich vereinbar. Referenzen können auf Anfrage verfügbar sein; Namen sollten nicht ohne vorherige Abstimmung veröffentlicht werden.\source{NAV/Arbeidsplassen: Slik skriver du en god CV}{Q10}
Für technische Profile ist Tekna besonders einschlägig. Der Verband empfiehlt ein zugeschnittenes Kompetenzprofil von ungefähr vier bis sechs Zeilen, konkrete Verantwortung und Ergebnisse, umgekehrte Chronologie und im Regelfall maximal zwei Seiten. Foto ist eine persönliche Entscheidung; weniger relevante ältere Erfahrung darf zusammengefasst werden. Das spricht für eine kleine Norwegen-Variante des bestehenden CVs, nicht für ein völlig neues Design.\source{Tekna: How to write a good CV}{Q11}
NAV beschreibt das Bewerbungsschreiben als kurze, spezifische Begründung mit passenden Beispielen, vorzugsweise innerhalb einer Seite. Auch hier ist ein pauschaler internationaler Skip-Default zu wenig differenziert. Ein Anruf ist sinnvoll, wenn echte offene Fragen bestehen; Fragen, die schon in der Anzeige beantwortet sind, rechtfertigen ihn nicht.\source{NAV/Arbeidsplassen: Tips til jobbsøknaden}{Q12}
Für Aker BP, Equinor oder NATO sollten tatsächliche Motivation, Arbeitssprache und Standortmodell jeweils separat geprüft werden. Eine internationale Organisation in Norwegen kann ein eigenes Bewerbungsformat haben. Norwegische Marktgewohnheiten ersetzen dann nicht deren Portalvorgaben. Ungeklärte Umzugsbereitschaft ist eine offene praktische Frage, kein automatisch vorhandener Vorteil.
\subsection{Empfohlene Profilmatrix}
\begin{tabularx}{\linewidth}{@{}>{\raggedright\arraybackslash}p{.22\linewidth}>{\raggedright\arraybackslash}X>{\raggedright\arraybackslash}X@{}}
\toprule Kontext & Standard & Bewusste Abweichung\\\midrule
Schweiz, internationaler Tech-Arbeitgeber & Zwei Seiten, belegte technische Ergebnisse, kompakte Skills, Englisch sofern passend. & Brief nur bei Anforderung oder konkretem Mehrwert.\\
Schweiz, lokale Industrie und Infrastruktur & Zwei Seiten, verständliche Verantwortung, Sprachen und praktische Einordnung. & Dossier und Brief nach Arbeitgeber; dritte Seite nur bei relevantem Bedarf.\\
Dänemark & Zwei Seiten, stärkeres persönliches Fachprofil, Arbeitgeberkontext und Zusammenarbeit. & Individuellen Brief und Landesmotivation prüfen; Arbeitgeberausnahmen beachten.\\
Norwegen & Zwei Seiten, klare Schlüsselqualifikationen, Aufgaben und belegte Ergebnisse. & Kurzes spezifisches Schreiben; internationaler Arbeitgeber kann eigene Regeln haben.\\
Akademische Bewerbung & Separater akademischer CV-Prozess. & Publikationen und Lehre nach Anforderung, keine Industrie-Seitenregel erzwingen.\\\bottomrule
\end{tabularx}
\subsection{ATS: Textstruktur und Portalprüfung sind wichtiger als Mythen}
Greenhouse nennt komplexe Tabellen, Spalten, Grafiken und Informationen in Kopf-/Fusszeilen als mögliche Ursachen unvollständiger Verarbeitung. Der Anbieter nennt ausserdem eine Grenze von 2,5 MB für das Parsing. Ein Parsingfehler bedeutet laut Dokumentation nicht automatisch Ablehnung: Das Dokument kann angehängt bleiben und Daten müssen manuell korrigiert werden.\source{Greenhouse: Unsuccessful resume parse, aktualisiert 02.03.2026}{Q13}
Die aktuellen einspaltigen, textbasierten PDFs sind daher ein vernünftiger Standard. Eine LaTeX-Kontaktzeile im normalen Dokumentkörper ist dabei nicht mit einer Word-Kopfzeile gleichzusetzen. Die vorhandene Schrift ist kein nachgewiesenes ATS-Problem. Ein Wechsel zu einer anderen Schrift allein verspricht keinen belegten Vorteil.
Microsoft verlangt ausdrücklich, die aus dem CV übernommenen Profilinformationen zu prüfen und gegebenenfalls zu korrigieren. Eine kurze Kontrolle der Portal-Felder vor der Abgabe ist deshalb konkreter und verlässlicher als ein selbst vergebener ATS-Score.\source{Microsoft Careers: Hiring FAQs}{Q14}
\textbf{Empfohlene QA:} sichtbaren PDF-Text kopieren, Reihenfolge und Sonderzeichen prüfen, Kontaktinformationen und Datumszuordnung kontrollieren, Links testen und im tatsächlichen Portal die extrahierten Felder ansehen. DOCX nur dann zusätzlich erzeugen, wenn ein Arbeitgeber es verlangt oder das konkrete Portal mit dem PDF Probleme hat. Keywords müssen aus belegten Tätigkeiten stammen; unbekannte Technologien werden durch Aufnahme in eine Skills-Liste nicht zu Erfahrung.
\section{Was tatsächlich mehr Erfolg verspricht}
\subsection{Formulierungsqualität hilft; feste Stilrezepte sind nicht belegt}
Eine grosse randomisierte Studie von Wiles, Munyikwa und Horton untersuchte algorithmische Schreibunterstützung bei knapp 500.000 Neueintritten auf einem Online-Arbeitsmarkt. Die unterstützte Gruppe wurde relativ rund acht Prozent häufiger eingestellt; die Arbeitgeberzufriedenheit sank nicht. Das ist relevante Evidenz dafür, dass verständlichere Darstellung die Erkennung vorhandener Fähigkeiten verbessern kann. Es ist keine Prognose von acht Prozentpunkten mehr Erfolg für einen Schweizer Staff Engineer und kein Test eines bestimmten CV-Layouts.\source{Wiles, Munyikwa und Horton: Algorithmic Writing Assistance on Jobseekers' Resumes Increases Hires; NBER 30886, später Management Science 2025}{Q15}
Für die Formulierung folgt daraus als redaktionelle Empfehlung: klare Handlung, eigenes Objekt beziehungsweise eigener Umfang, technischer oder organisatorischer Kontext und ein bestätigtes Ergebnis. Nicht jeder Bullet braucht jede Komponente oder eine Zahl. Ein glaubwürdiges qualitatives Ergebnis ist besser als eine erfundene Prozentzahl. Zusammenarbeit und begrenzte Verantwortung können gerade im Grossunternehmen Kompetenz zeigen.
Bei einem Staff-Profil sollten vorhandene Belege für Entscheidungen, Standards, Abstimmung über Schnittstellen und dauerhafte Verantwortung sichtbarer werden. Der aktuelle Technologieumfang allein erklärt den Staff-Mehrwert nicht immer. Fehlende Nachweise hierfür sollten gezielt erhoben werden; sie dürfen nicht aus dem Titel hergeleitet werden.
\subsection{Kontaktstrategie: sinnvoll, aber keine Erfolgsgarantie}
Randomisierte LinkedIn-Experimente zu schwächeren beruflichen Verbindungen liefern Evidenz, dass Netzwerkstruktur berufliche Mobilität beeinflussen kann. Sie testen jedoch Empfehlungen neuer Verbindungen, nicht die Behauptung, ein Anruf bei jeder veröffentlichten Kontaktperson sei zwingend besser als eine direkte Bewerbung.\source{MIT-Bericht zur Science-Studie von Rajkumar et al., veröffentlicht 15.09.2022}{Q17}
Die neue projektinterne Kontaktpriorität ist als Arbeitsstrategie nachvollziehbar: Offene Fragen zu Lohnklasse, Scope, Präsenz und Team können früh geklärt werden. Ein kurzer Kaltanruf ist aber noch keine Empfehlung und macht den Kanal nicht automatisch stark. Als Verbesserungsvorschlag sollten Kontaktversuch, tatsächliches Gespräch, hilfreiche Information und echte Empfehlung getrennt erfasst werden. Vergebliche Kontaktversuche sollten eine passende Bewerbung kurz vor Fristende nicht unbegrenzt blockieren; eine solche Ausnahme wäre eine ausdrücklich zu beschliessende Strategieänderung, keine bereits umgesetzte Regel.
Die eigene Kohorte erlaubt noch keine belastbaren kausalen Aussagen: sieben erfasste Bewerbungen, durchgehend schwacher Kanal, unterschiedliche Rollen und teilweise noch offene Ergebnisse. SBB erreichte ein Interview und endete später durch Rückzug. Diese Beobachtung darf weder als Beweis für optimale Dokumente noch als Beweis gegen Kontaktaufnahme gelten. Der Rückzug ist zudem eine andere Ergebnisart als eine Arbeitgeberabsage.
\subsection{Fit, praktische Attraktivität und Ungewissheit auseinanderhalten}
Die getrennte Betrachtung von Evidence Fit, Document Quality und Channel Strength ist ein klarer Fortschritt und sollte bleiben. Die Zahlen sind aber interne Rubrikergebnisse, keine kalibrierten Einstellungswahrscheinlichkeiten. Unterschiede wie 92 versus 93 sind ohne nachvollziehbare Einzelbewertung und stabile Rubrik kaum entscheidungsrelevant.
Zusätzlich sollten bekannte Ausschlussgründe, ungeklärte Bedingungen und Präferenzen getrennt werden. Ein bestätigtes zwingendes Kriterium ohne passende Evidenz bleibt ein echtes Gate. Eine noch unbekannte Lohnklasse oder Präsenzregel ist zunächst eine zu klärende Frage. Ein formaler Titel unter Staff kann je nach tatsächlichem Scope unterschiedlich attraktiv sein; weder automatisch verwerfen noch schönrechnen.
Der stärkste operative Hebel ist wahrscheinlich, vor viel Dokumentarbeit die tatsächliche Passung bei Verantwortung, Vergütung, Arbeitsort und zwingenden Anforderungen zu klären. Das ist eine aus dem Projektverlauf abgeleitete Einschätzung, kein experimentell gemessener Effekt. Die bislang dokumentierten Absagen erlauben insbesondere keine sichere Zuschreibung an Schriftart, ATS oder einzelne fehlende Keywords.
\section{Umsetzungsreihenfolge und Abnahmekriterien}
\textbf{Zuerst die Verlässlichkeit reparieren.} Vor einer grossen optischen Überarbeitung sollten die folgenden kleinen, klar abgrenzbaren Änderungen erfolgen:
\begin{enumerate}
\item \textbf{Regel- und Statusbereinigung:} gemeinsame Skill-Referenzen, aktuelle Session-Ansicht, keine operative Zehn-Bewerbungen-Sperre. Abnahme: beide Agenteneinstiege laden dieselbe Prozessversion; eine elfte Bewerbung lässt sich ohne Quote erfassen.
\item \textbf{Konkrete Evidenzlecks schliessen:} 12+-Jahre-Text entfernen, Capgemini-Ausschluss konsistent machen, PyTorch-Kontext sichern, Zero-Downtime-/Inspektionsvarianten prüfen, BS-6 in Auswahlpfade aufnehmen. Abnahme: jede aktive Variante verweist auf ihren gültigen Claim; historische Abgaben bleiben erhalten.
\item \textbf{Negative Validator-Tests ergänzen:} falsche Identität, erfundene Metrik, Zertifikatskontext, Kommentare, Verneinungen, LaTeX-Auszeichnung und ungültige Datumswerte. Abnahme: eindeutige Fehler werden erkannt, erlaubte Gegenbeispiele nicht blockiert; semantisch offene Punkte erscheinen gesondert.
\item \textbf{Scout-Verluste verhindern:} Requisitions-ID statt Titel, Länderfelder erhalten, Remote-Ungewissheit respektieren, Datumsarten trennen. Abnahme: beide gleichnamigen Teststellen bleiben sichtbar; Baar/CH bleibt zulässig, Bernau/DE wird nicht als Schweiz erkannt.
\item \textbf{Dokumentgewichtung verbessern:} Skills kürzen, Berufserfahrung früher zeigen, Bildungszeilen sauber trennen, Länderprofil wählen. Abnahme: gerenderte Sichtprüfung und Textausleitung; keine neue Behauptung nur zur schöneren Seitengestaltung.
\end{enumerate}
\textbf{Danach die strukturellen Verbesserungen:} ein kleines Claim-/Dokumentschema, Bewerbung als Zustand plus Ereignisse, reproduzierbare Abgabemanifeste, robuste Scout-Persistenz und gezielte Adaptertests. Ein leichter automatisierter Check sollte mindestens die vorhandenen Tests, Schemas, Referenzintegrität und einen Build der gepflegten professionellen Templates ausführen. Die dafür benötigte Python-/TeX-Umgebung gehört dokumentiert.
\textbf{Erst danach breiter optimieren:} zusätzliche belegte Auswirkungen und Staff-Verantwortung sammeln, die Länderprofile verfeinern und den Bewerbungstrichter nach Ergebnisarten auswerten. Dabei nicht auf eine neue Quote oder auf immer mehr eigene Scores ausweichen. Eine kleine Anzahl überprüfbarer Regeln ist nützlicher als viele überlappende Dokumente mit Ausnahmen.
\textbf{Bewusst beibehalten:} Genauigkeit vor Wirkung; keine erfundenen Kennzahlen; keine Skills aus reiner Keyword-Nachfrage; kanonische Eigentumsgrenzen; zwei gut lesbare Seiten als Standard; konditionales Anschreiben mit Länder-/Arbeitgeberkontext; keine automatischen privaten Zusatzangaben; unveränderte Archive bereits versandter Bewerbungen.
\section{Reproduktionshinweise und Quellen}
Die Prüfarbeitsdateien liegen unter \file{tmp/project_review/}. \file{probe.py} enthält die isolierten Gegenproben; \file{probe_results.json} deren Resultate. \file{supplement.py} und \file{supplement_results.json} enthalten zusätzliche Adapter- und PDF-Prüfungen. \file{build_review.py}, \file{build_results.json} und \file{copy_comparisons.json} dokumentieren die isolierten Builds und Kopienvergleiche. Diese temporären Dateien gehören nicht zum Bewerbungsoutput und sind kein neuer kanonischer Evidenzbestand.
Der technische Review belegt die beschriebenen lokalen Gegenbeispiele. Er ersetzt weder einen vollständigen Live-Test aller Jobportale noch eine erneute Quellenprüfung jedes historischen Resume-Satzes. Die Recherche priorisierte Behörden, Arbeitgeber, Berufsverbände, Hochschulen und Originalforschung. Undatierte Ratgeber werden als aktuelle abrufbare Empfehlungen behandelt, nicht als neu publizierte Studien. Nicht zugängliche oder inzwischen leere Stellenseiten wurden nicht als tragende Evidenz verwendet.
\small
\begin{enumerate}[label=Q\arabic*.,leftmargin=2.7em,itemsep=6pt]
\item SECO / arbeit.swiss. \emph{Bewerbungstipps}. Schweizer Arbeitsmarktverwaltung; Konventionen und Dossieraufbau. \url{https://www.arbeit.swiss/de/stellensuchende/bewerbungstipps}
\item SBB. \emph{Bewerbungstipps}. Direkte Arbeitgebervorgaben, einschliesslich freiwilligem Motivationsschreiben und Upload-Struktur. \url{https://company.sbb.ch/de/jobs-karriere/jobs/bewerbungstipps.html}
\item Universität Zürich, Career Services. \emph{Anschreiben}. Hochschulberatung; Inhalt, Umfang und Nutzen. \url{https://www.careerservices.uzh.ch/de/ratgeber/bewerbung/bewerbungsdossier/Anschreiben.html}
\item ETH Zürich, Welcome Center. \emph{How to apply for a job}. Undatierte, teilweise traditionelle Dossierempfehlungen; im Bericht als widersprechende Konvention eingeordnet. \url{https://ethz.ch/en/the-eth-zurich/working-teaching-and-research/welcome-center/job-search-and-application/how-to-apply-for-a-job.html}
\item Workindenmark. \emph{A Good Personal Profile Counts}. Aktualisiert 25.03.2026. \url{https://www.workindenmark.dk/job-search-in-denmark/your-cv/a-good-personal-profile-counts}
\item Workindenmark. \emph{Work Experience}. Aktualisiert 12.03.2026. \url{https://www.workindenmark.dk/job-search-in-denmark/your-cv/work-experience}
\item Workindenmark. \emph{Your Cover Letter}. Aktualisiert 12.03.2026. \url{https://www.workindenmark.dk/job-search-in-denmark/your-cover-letter}
\item IDA. \emph{How to make a good CV}. Dänischer Ingenieurverband; berufsspezifische Beratung, keine kausale Wirkungsstudie. \url{https://ida.dk/en/career-and-legal-advice/job-search/how-to-make-a-good-cv}
\item Workindenmark. \emph{Personal Details in Your CV}. Aktualisiert 12.03.2026. \url{https://www.workindenmark.dk/job-search-in-denmark/your-cv/personal-details-in-your-cv}
\item NAV / Arbeidsplassen. \emph{Slik skriver du en god CV}. Norwegische Arbeitsmarktverwaltung. \url{https://arbeidsplassen.nav.no/slik-skriver-du-en-god-cv}
\item Tekna. \emph{How to write a good CV}. Norwegischer technisch-wissenschaftlicher Berufsverband. \url{https://www.tekna.no/en/career/cv-and-cover-letter/how-to-write-a-good-cv/}
\item NAV / Arbeidsplassen. \emph{Tips til jobbsøknaden}. Hinweise zu Schreiben und sinnvoller Kontaktaufnahme. \url{https://arbeidsplassen.nav.no/tips-til-jobbsoknaden}
\item Greenhouse Support. \emph{Unsuccessful resume parse}. Aktualisiert 02.03.2026. Technische Herstellerdokumentation; nicht stellvertretend für alle ATS. \url{https://support.greenhouse.io/hc/en-us/articles/200989175-Unsuccessful-resume-parse}
\item Microsoft Careers. \emph{Hiring FAQs}. Direkte Information zur Prüfung und Korrektur extrahierter Bewerberdaten. \url{https://careers.microsoft.com/v2/global/en/hiringfaqs.html}
\item Wiles, Emma; Munyikwa, Zanele; Horton, John J. \emph{Algorithmic Writing Assistance on Jobseekers' Resumes Increases Hires}. NBER Working Paper 30886 (2023, Revision Oktober 2023); veröffentlichte Fassung Management Science 71(12), 2025, S. 10144--10164. \url{https://www.nber.org/papers/w30886}
\item Technische Universität München. \emph{Bayerische Formel}. Zweck, Eingangsparameter und nicht verbindlicher Rechner. \url{https://www.tum.de/studium/bewerbung/infoportal-bewerbung/bayerische-formel}
\item MIT News, Peter Dizikes. \emph{A causal test of the strength of weak ties}. 15.09.2022; institutioneller Bericht zur randomisierten LinkedIn-Studie von Rajkumar et al. in Science, DOI 10.1126/science.abl4476. \url{https://news.mit.edu/2022/weak-ties-linkedin-employment-0915}
\end{enumerate}
\end{document}
@@ -122,3 +122,36 @@ Python, Kafka, AWS (S3 · Glue · Athena · Redshift · Airflow · CloudFormatio
- Listing all tools in paragraph form - Listing all tools in paragraph form
- Mentioning Bundeswehr unless specifically relevant (leadership angle for management-adjacent roles) - Mentioning Bundeswehr unless specifically relevant (leadership angle for management-adjacent roles)
- Overplaying test automation background - Overplaying test automation background
## Infrastructure and Linux operations (BS-6) — added 2026-09-11, review F07
BS-6 was absent from every bundle although it has been canonical since 2026-08-27, so genuine strengths never reached the selection paths.
- **Linux administration and automation at Bosch (2020–2022):** Linux servers carried the ML platform, Docker hosts, backend services and the Ansible control path.
- **Ansible beyond a single project:** configuration management and infrastructure automation, including self-written extensions (a credential plugin with local caching).
- **Use it when** the JD names Linux, Ansible, configuration management, IaC or platform operations — exactly the DevOps/SRE-flavoured reqs where this profile reads thin without it.
- **Currency limit, always state it:** employer-side Linux/Ansible ended Dec 2022; `PP-3` carries the current half and is a **personal** self-hosted Debian server. Never present PP-3 as employer work or as physical-datacenter evidence.
- **GitOps remains forbidden:** playbooks lived in Git but ran push-based from Jenkins/GitLab with no reconciliation agent. The substance is writable as “version-controlled infrastructure automation with Ansible from Git, executed through Jenkins/GitLab pipelines”.
## Data-warehouse and mentoring evidence (added 2026-09-11 from the Arbeitszeugnisse)
Both were documented all along and reached no bundle, so the selection paths could not see them.
Same failure mode as BS-6 (Linux/Ansible), found the same week.
- **Data warehouse (SW-2).** The Swisscom Zwischenzeugnis documents *Komponentenverantwortung* for
the ETL pipelines that load business-critical Fulfillment data from Oracle into the **Teradata
Data Warehouse (DWH)**. Use this whenever a JD names data warehousing — it was previously
screened as absent, which cost a correct answer on at least one posting.
**Scope limit:** this is documented **Betrieb** (operation) of a DWH, not **Aufbau**
(construction). A required line saying *"Aufbau und Betrieb von Data-Warehouse-Lösungen"* is only
half met — say which half.
- **Microsoft SQL Server (FC-1).** Fraunhofer's SCEDAS ran on MSSQL with C# and Entity Framework.
Historical and single-project; do not present it as data-platform depth.
- **Mentoring (BS-7).** *"Fachliche Betreuung von Studierenden, auch im Rahmen von
Abschlussarbeiten"* — documented technical supervision, including thesis supervision. He also
served as **Zweitprüfer (second examiner) and supervisor for one Bachelor thesis** at Bosch
(user-confirmed 2026-09-11) — a formal role carrying assessment responsibility, which is
materially stronger than the team-role evidence the profile otherwise falls back on. Use it when
a JD asks about developing other engineers (SBB asked explicitly and the answer had to fall back
on Security Champion and the Java community). **Technical supervision and formal examining, NOT
people management. Exactly one thesis as second examiner — do not pluralise.**
+25 -1
View File
@@ -66,9 +66,10 @@
| 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-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-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 | | 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 24/7 semiconductor production — zero-downtime constrained deployment" | Production-grade containerization under hardest constraints | | 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** | | 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 | | 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 |
| BS-2 | Data services | "Built multi-language data services (Python/Java/C#) over OracleDB and Hadoop/ImpalaSQL — platform-layer data access for semiconductor analysis teams" | Enterprise DB + Hadoop infrastructure | | BS-2 | Data services | "Built multi-language data services (Python/Java/C#) over OracleDB and Hadoop/ImpalaSQL — platform-layer data access for semiconductor analysis teams" | Enterprise DB + Hadoop infrastructure |
| BS-3 | App Owner | "Application Owner for semiconductor analytics platform — SLOs, reliability, vendor management, on-call coverage" | Platform SLA ownership | | BS-3 | App Owner | "Application Owner for semiconductor analytics platform — SLOs, reliability, vendor management, on-call coverage" | Platform SLA ownership |
| FC-1 | CI/CD initiative | "Independently introduced Jenkins CI/CD pipeline with quality gates at Fraunhofer CML — first build automation adopted by the research team" | Initiative: built CI/CD from zero | | FC-1 | CI/CD initiative | "Independently introduced Jenkins CI/CD pipeline with quality gates at Fraunhofer CML — first build automation adopted by the research team" | Initiative: built CI/CD from zero |
@@ -140,3 +141,26 @@ Kubernetes, Docker, AWS (S3 · Glue · Athena · Redshift · CloudFormation), Ka
- Claiming SRE/pure DevOps title (you were a data engineer with platform ownership) - Claiming SRE/pure DevOps title (you were a data engineer with platform ownership)
- Overstating Terraform/Helm experience (not confirmed — do not claim) - Overstating Terraform/Helm experience (not confirmed — do not claim)
- Mentioning SCEDAS, maritime research, BDD, or RPA - Mentioning SCEDAS, maritime research, BDD, or RPA
## Data-warehouse and mentoring evidence (added 2026-09-11 from the Arbeitszeugnisse)
Both were documented all along and reached no bundle, so the selection paths could not see them.
Same failure mode as BS-6 (Linux/Ansible), found the same week.
- **Data warehouse (SW-2).** The Swisscom Zwischenzeugnis documents *Komponentenverantwortung* for
the ETL pipelines that load business-critical Fulfillment data from Oracle into the **Teradata
Data Warehouse (DWH)**. Use this whenever a JD names data warehousing — it was previously
screened as absent, which cost a correct answer on at least one posting.
**Scope limit:** this is documented **Betrieb** (operation) of a DWH, not **Aufbau**
(construction). A required line saying *"Aufbau und Betrieb von Data-Warehouse-Lösungen"* is only
half met — say which half.
- **Microsoft SQL Server (FC-1).** Fraunhofer's SCEDAS ran on MSSQL with C# and Entity Framework.
Historical and single-project; do not present it as data-platform depth.
- **Mentoring (BS-7).** *"Fachliche Betreuung von Studierenden, auch im Rahmen von
Abschlussarbeiten"* — documented technical supervision, including thesis supervision. He also
served as **Zweitprüfer (second examiner) and supervisor for one Bachelor thesis** at Bosch
(user-confirmed 2026-09-11) — a formal role carrying assessment responsibility, which is
materially stronger than the team-role evidence the profile otherwise falls back on. Use it when
a JD asks about developing other engineers (SBB asked explicitly and the answer had to fall back
on Security Champion and the Java community). **Technical supervision and formal examining, NOT
people management. Exactly one thesis as second examiner — do not pluralise.**
@@ -71,14 +71,16 @@
## S4: Skills Guide ## S4: Skills Guide
**Bold tools (resume Technical Skills section):** **Bold tools (resume Technical Skills section):**
Python, Kubernetes, Docker, AWS (S3 · Glue · Athena · Airflow), Kafka, PyTorch · Scikit-learn Python, Kubernetes, Docker, AWS (S3 · Glue · Athena · Airflow), Kafka, Scikit-learn
*(PyTorch and TensorFlow/Keras are **certification-context-only** in `claims.json` — list them only beside the IBM AI Engineering certificate, never as production tooling.)*
**Must-include skills (ATS match):** **Must-include skills (ATS match):**
- Python, MLOps, ML inference deployment - Python, MLOps, ML inference deployment
- Docker, Kubernetes, Ansible - Docker, Kubernetes, Ansible
- AWS (S3, Glue, Athena, Redshift, Airflow), Apache Iceberg - AWS (S3, Glue, Athena, Redshift, Airflow), Apache Iceberg
- Apache Kafka - Apache Kafka
- PyTorch, Scikit-learn, Pandas, NumPy - Scikit-learn, Pandas, NumPy
- PyTorch / TensorFlow **only with certification context** — see the note above; a bare ATS listing is the exact way the context gets lost
- CI/CD (GitLab, Jenkins) - CI/CD (GitLab, Jenkins)
**Nice-to-have (include if JD mentions):** **Nice-to-have (include if JD mentions):**
+297 -29
View File
@@ -36,7 +36,8 @@
"marital status", "marital status",
"gender", "gender",
"children" "children"
] ],
"language_note": "GERMAN (native) and ENGLISH (fluent) ONLY. The Swisscom Zwischenzeugnis of October 2025 states he also uses FRENCH and ITALIAN professionally on a regular basis - the user reviewed that line on 2026-09-11 and confirmed it is an ERROR IN THE DOCUMENT. Do not add French or Italian to any CV, cover letter or application form on the strength of that sentence, and do not re-open the question the next time someone reads the Zeugnis. Practical warning: if that Zeugnis is submitted alongside an application, a reader may take the sentence at face value and ask about French."
}, },
"education": [ "education": [
{ {
@@ -49,12 +50,12 @@
"thesis_institution": "Tongji University, Shanghai", "thesis_institution": "Tongji University, Shanghai",
"thesis_title": "Development of a Web-Based Remote Fault Diagnosis System", "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": "1.6 (German scale, 1.0 best / 4.0 lowest pass) -- user-confirmed 2026-08-26",
"final_grade_us_4pt": "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.",
"thesis_grade": "1.0", "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_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_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.", "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", "id": "EDU-BENG",
@@ -63,8 +64,7 @@
"start": "2009-10", "start": "2009-10",
"end": "2012-10", "end": "2012-10",
"final_grade": "2.4 (German scale, 1.0 best / 4.0 lowest pass) -- user-confirmed 2026-08-26", "final_grade": "2.4 (German scale, 1.0 best / 4.0 lowest pass) -- user-confirmed 2026-08-26",
"final_grade_us_4pt": "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.", "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."
"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."
} }
], ],
"employment": [ "employment": [
@@ -98,10 +98,19 @@
"start": "2020-02", "start": "2020-02",
"end": "2022-12", "end": "2022-12",
"title_history": [ "title_history": [
{"title": "Data Engineer, Data Analysis", "start": "2020-02", "end": "2021-01"}, {
{"title": "Senior Data Engineer, Data Analysis", "start": "2021-01", "end": "2022-12"} "title": "Data Engineer, Data Analysis",
"start": "2020-02",
"end": "2021-01"
},
{
"title": "Senior Data Engineer, Data Analysis",
"start": "2021-01",
"end": "2022-12"
}
], ],
"title_history_source": "LinkedIn-confirmed, recorded in experience_bosch.md; user-confirmed 2026-08-27 while reviewing the RUAG C5I resume. Added because claims.json previously carried only the combined official_title with no promotion date, which made a Swisscom-style 'Beforderung <date>' line unwritable from the canonical file alone." "title_history_source": "LinkedIn-confirmed, recorded in experience_bosch.md; user-confirmed 2026-08-27 while reviewing the RUAG C5I resume. Added because claims.json previously carried only the combined official_title with no promotion date, which made a Swisscom-style 'Beforderung <date>' line unwritable from the canonical file alone. RE-CONFIRMED BY THE USER 2026-09-11 after the Arbeitszeugnis was read: the promotion and its timing are correct, and happened through a RE-GRADING (Neueinstufung). The Zeugnis does not contradict this - it simply names DEPARTMENTS ('Ingenieur' in Fertigungstechnologie 1/2/3) rather than job titles, and never uses the words 'Senior' or 'Data Analysis'. Do NOT expect this Zeugnis to corroborate the title string; do not 'correct' the title against it.",
"official_title_provenance": "NOT from the Arbeitszeugnis - that document names departments only. The title string comes from the user and from LinkedIn. Corrected 2026-09-11; an earlier note wrongly claimed the Zeugnis as its source."
}, },
{ {
"id": "FRAUNHOFER", "id": "FRAUNHOFER",
@@ -148,10 +157,28 @@
"last_verified": "2026-08-27" "last_verified": "2026-08-27"
} }
], ],
"evidence_schema": {
"added": "2026-09-11",
"why": "Review finding F08. Only one of 26 claims carried structured evidence fields, so recency and reference checks could not run automatically. This block defines the fields; `evidence` on each claim carries the values.",
"fields": {
"source": "Where the claim comes from (user confirmation, a document, a URL), or 'unrecorded'.",
"ownership": "Whose work this is: individual | component | contributor | team-role | personal-project, or 'unrecorded'."
},
"rules": [
"'unrecorded' is a legitimate, queryable value. It means nobody wrote it down - NOT that the claim is unevidenced. Most claims carry evidence in their free-text scope.",
"Never infer a date, a source or an ownership class to fill a field. Ask the user and record the answer.",
"Filling these fields does not weaken the free-text scope, allowed_verbs or forbidden lists; those remain authoritative for wording."
],
"employer_attribution_note": "last_verified was REMOVED on 2026-09-11 at the user's instruction: \"wofuer brauchst du ueberhaupt ein Datum? Es reicht doch wenn es passend zum Arbeitgeber zugeordnet ist.\" That is sound - every claim carries an employer through its ID prefix (SW/BS/FC/VZ/GN/PP/BW), and for closed employers the facts do not move, so a recency date measured nothing. What remains is provenance: `source` and `ownership`. Do not reintroduce a date field without a reason that is not recency-for-its-own-sake."
},
"claims": [ "claims": [
{ {
"id": "SW-1", "id": "SW-1",
"scope": "Primary engineer for pipelines in owned Fulfillment and Product Analysis domains; contributor to the wider company migration.", "evidence": {
"source": "Swisscom Zwischenzeugnis, Oktober 2025 (primary document, read 2026-09-11)",
"ownership": "individual within his domains; contributor company-wide"
},
"scope": "Migration of legacy ETL pipelines and build-and-operate of cloud-native applications on AMAZON AWS (S3, Glue, Athena, Redshift, Apache Airflow) - verbatim from the Zwischenzeugnis. Primary engineer for pipelines in his owned Fulfillment and Product Analysis domains; contributor to the wider company migration.",
"allowed_verbs": [ "allowed_verbs": [
"migrated", "migrated",
"implemented", "implemented",
@@ -167,7 +194,11 @@
}, },
{ {
"id": "SW-2", "id": "SW-2",
"scope": "Component Owner for business-critical Fulfillment ETL pipelines, including operation, data quality, governance, incidents and on-call obligations.", "evidence": {
"source": "Swisscom Zwischenzeugnis, Oktober 2025 (primary document, read 2026-09-11)",
"ownership": "component - Komponentenverantwortung, documented"
},
"scope": "Komponentenverantwortung (component ownership) for the ETL pipelines that move business-critical Fulfillment data out of Oracle databases into the TERADATA DATA WAREHOUSE (DWH) and make it available for analysis - verbatim from the Zwischenzeugnis. Includes quality, security, data protection and compliance per Security and Data Governance, root-cause analysis on application problems, and 2nd/3rd-level support plus on-call (Rufbereitschaft).",
"allowed_verbs": [ "allowed_verbs": [
"own", "own",
"operate", "operate",
@@ -178,7 +209,11 @@
}, },
{ {
"id": "SW-3", "id": "SW-3",
"scope": "Build and operate Python data applications on Kubernetes with GitLab CI/CD within the team environment.", "evidence": {
"source": "Swisscom Zwischenzeugnis, Oktober 2025 (primary document, read 2026-09-11)",
"ownership": "individual - implementation and operation"
},
"scope": "Implementation, administration and operation of Python applications on KUBERNETES CLUSTERS, and automation of CI/CD processes in GITLAB - verbatim from the Zwischenzeugnis. Also drives automation of technical processes and workflows, and shapes and prioritises backlog items with the Product Owner.",
"allowed_verbs": [ "allowed_verbs": [
"build", "build",
"deploy", "deploy",
@@ -188,6 +223,10 @@
}, },
{ {
"id": "SW-4", "id": "SW-4",
"evidence": {
"source": "Swisscom Zwischenzeugnis, Oktober 2025 (primary document, read 2026-09-11)",
"ownership": "individual"
},
"scope": "Deliver data products, dashboards and analyses with internal B2B stakeholders and product owners; not external consulting or strategic-account delivery.", "scope": "Deliver data products, dashboards and analyses with internal B2B stakeholders and product owners; not external consulting or strategic-account delivery.",
"allowed_verbs": [ "allowed_verbs": [
"deliver", "deliver",
@@ -204,6 +243,10 @@
}, },
{ {
"id": "SW-5", "id": "SW-5",
"evidence": {
"source": "unrecorded",
"ownership": "unrecorded"
},
"scope": "Mandatory rotating team Security Champion role for 2025/2026 only; not an award, certification or multi-year distinction.", "scope": "Mandatory rotating team Security Champion role for 2025/2026 only; not an award, certification or multi-year distinction.",
"allowed_verbs": [ "allowed_verbs": [
"serve" "serve"
@@ -218,16 +261,24 @@
}, },
{ {
"id": "SW-6", "id": "SW-6",
"scope": "Hands-on PySpark use at Swisscom; scale and performance metrics are not verified.", "evidence": {
"source": "user confirmation 2026-09-11",
"ownership": "individual - hands-on use"
},
"scope": "Hands-on PySpark use at Swisscom (user-confirmed 2026-09-11).",
"allowed_verbs": [ "allowed_verbs": [
"use", "use",
"develop", "develop",
"process" "process"
], ],
"metrics": "unverified" "metrics": "unverified - no scale or performance figures exist for this work. That is the ONLY caveat; it does not qualify the PySpark experience itself, which is production-current in skills. The scope line used to carry this caveat as well, which read as doubt about the skill rather than about the numbers."
}, },
{ {
"id": "SW-7", "id": "SW-7",
"evidence": {
"source": "unrecorded",
"ownership": "unrecorded"
},
"scope": "Build governed data products and onboard sources within Swisscom's company-wide Data Mesh; never claim ownership or construction of the shared mesh/platform. Atlassian Compass is the metadata/catalogue platform used for data-product metadata and lineage (user-confirmed 2026-07-29); Dennis uses it as a practitioner and does not administer or own the tooling.", "scope": "Build governed data products and onboard sources within Swisscom's company-wide Data Mesh; never claim ownership or construction of the shared mesh/platform. Atlassian Compass is the metadata/catalogue platform used for data-product metadata and lineage (user-confirmed 2026-07-29); Dennis uses it as a practitioner and does not administer or own the tooling.",
"allowed_verbs": [ "allowed_verbs": [
"build", "build",
@@ -251,6 +302,10 @@
}, },
{ {
"id": "SW-8", "id": "SW-8",
"evidence": {
"source": "unrecorded",
"ownership": "unrecorded"
},
"scope": "Configured domain-grounded LLM assistants in a Swisscom-owned web interface by selecting available models and supplying curated knowledge. Separate exposure includes LiteLLM API use, custom GPTs, Copilot and Kiro.", "scope": "Configured domain-grounded LLM assistants in a Swisscom-owned web interface by selecting available models and supplying curated knowledge. Separate exposure includes LiteLLM API use, custom GPTs, Copilot and Kiro.",
"allowed_verbs": [ "allowed_verbs": [
"configured", "configured",
@@ -269,7 +324,11 @@
}, },
{ {
"id": "BS-1", "id": "BS-1",
"scope": "Designed and executed integration of containerized ML inference into a 24/7 semiconductor production environment; model-development ownership is not established. Toolchain includes Docker, Kubernetes and Ansible - Ansible use at Bosch was intensive (user-confirmed 2026-08-27) and extended to configuration management and infrastructure automation beyond this single integration, making it standalone Infrastructure-as-Code evidence.", "evidence": {
"source": "Bosch Arbeitszeugnis, Dresden 31.12.2022 (primary document, read 2026-09-11); operating constraint user-clarified 2026-09-11",
"ownership": "individual - integration and operation; model development is NOT his"
},
"scope": "Designed and executed integration of containerized ML inference into a 24/7 semiconductor production environment; model-development ownership is not established. Toolchain includes Docker, Kubernetes and Ansible - Ansible use at Bosch was intensive (user-confirmed 2026-08-27) and extended to configuration management and infrastructure automation beyond this single integration, making it standalone Infrastructure-as-Code evidence. OPERATING CONSTRAINT, user-clarified 2026-09-11: the fab runs continuously, and a defect in the software could in the worst case halt production - an outcome that must never occur. That is a FAILURE-CONSEQUENCE constraint (blast radius of a bug), NOT a deployment-window constraint. Write it as 'a defect could have stopped production' or 'software whose failure would have halted a continuously operating fab'. ZEUGNIS WORDING (primary, 2026-09-11): 'Erarbeitung und Durchfuehrung von Integrationsstrategien fuer den Einsatz von Machine Learning Modellen in der 24/7 Fertigungsumgebung' - devising AND executing integration strategies for ML models in the 24/7 manufacturing environment. This corroborates the integration-and-operation framing and the 24/7 context directly.",
"allowed_verbs": [ "allowed_verbs": [
"integrated", "integrated",
"containerized", "containerized",
@@ -278,13 +337,19 @@
], ],
"forbidden": [ "forbidden": [
"trained the image classification model", "trained the image classification model",
"owned the full ML lifecycle" "owned the full ML lifecycle",
"a production line that could not be stopped for a deployment",
"could not be stopped for a deployment"
], ],
"metrics": "qualitative reduction in manual classification is source-backed; exact amount unverified" "metrics": "qualitative reduction in manual classification is source-backed; exact amount unverified"
}, },
{ {
"id": "BS-2", "id": "BS-2",
"scope": "Developed data services in Python, Java and C# over Oracle and Hadoop/Impala for internal analysis teams. Data types worked on (user-confirmed 2026-08-02): semiconductor fab sensor and process data -- defect management records, wafer inspection images, and electrical parameters from Process Control Monitoring (PCM). Relevant as genuine industrial sensor/asset-data experience.", "evidence": {
"source": "Bosch Arbeitszeugnis, Dresden 31.12.2022 (primary document, read 2026-09-11)",
"ownership": "individual - development"
},
"scope": "Developed data services in Python, Java and C# over Oracle and Hadoop/Impala for internal analysis teams. Data types worked on (user-confirmed 2026-08-02): semiconductor fab sensor and process data -- defect management records, wafer inspection images, and electrical parameters from Process Control Monitoring (PCM). Relevant as genuine industrial sensor/asset-data experience. Bosch development was PRIMARILY PYTHON, with some Java and KOTLIN (backend, Ansible plugins, extensions) and C# for Spotfire plugins (user-confirmed 2026-09-11). ZEUGNIS WORDING (primary): 'Entwicklung von Applikationen zur Datenanalyse in C#, Python und Java' and 'Integration von Software-Komponenten in die IT-Architektur der Halbleiterproduktion'. NOTE: the Zeugnis names C#, Python and Java only - KOTLIN rests on the user's statement of 2026-09-11 and is not corroborated by this document. Record it as user-confirmed, not Zeugnis-confirmed.",
"allowed_verbs": [ "allowed_verbs": [
"developed", "developed",
"built" "built"
@@ -293,7 +358,11 @@
}, },
{ {
"id": "BS-3", "id": "BS-3",
"scope": "Confirmed Application Owner responsibilities for analytics applications and upstream pipelines, including SLOs, vendors, training and documentation.", "evidence": {
"source": "Bosch Arbeitszeugnis, Dresden 31.12.2022 (primary document, read 2026-09-11)",
"ownership": "application owner - documented"
},
"scope": "Confirmed Application Owner responsibilities for analytics applications and upstream pipelines, including SLOs, vendors, training and documentation. Named systems (user-confirmed 2026-09-11): TECHNICAL Application Owner for the KLA Defect Management System (DMS) and for TIBCO Spotfire - see BS-5. ZEUGNIS WORDING (primary): 'Application Owner fuer Softwaresysteme zur Datenanalyse in der Halbleiterfertigung' plus 'Kommunikation mit den Anbietern der Softwaresysteme sowie den internen Kunden'.",
"allowed_verbs": [ "allowed_verbs": [
"served", "served",
"owned", "owned",
@@ -304,7 +373,11 @@
}, },
{ {
"id": "BS-4", "id": "BS-4",
"scope": "Built an ELK/Kafka anomaly-detection proof of concept and monitoring; not a company-wide observability platform.", "evidence": {
"source": "Bosch Arbeitszeugnis, Dresden 31.12.2022 (primary document, read 2026-09-11)",
"ownership": "individual - proof of concept"
},
"scope": "Built an ELK/Kafka anomaly-detection proof of concept and monitoring; not a company-wide observability platform. ZEUGNIS WORDING (primary): 'Durchfuehrung eines POCs fuer das Logfile-Management mit ELK-Stack' - the PoC framing is confirmed by the document itself, not just by policy.",
"allowed_verbs": [ "allowed_verbs": [
"built", "built",
"implemented", "implemented",
@@ -318,7 +391,11 @@
}, },
{ {
"id": "BS-5", "id": "BS-5",
"scope": "Co-owned the TIBCO Spotfire environment, built C# extensions and co-presented at TIBCO Analytics Forum 2022.", "evidence": {
"source": "unrecorded",
"ownership": "unrecorded"
},
"scope": "Co-owned the TIBCO Spotfire environment, built C# extensions and co-presented at TIBCO Analytics Forum 2022. He was TECHNICAL Application Owner for Spotfire and for the KLA Defect Management System (DMS) (user-confirmed 2026-09-11) - technical ownership, not people management or vendor-side ownership.",
"allowed_verbs": [ "allowed_verbs": [
"co-owned", "co-owned",
"built", "built",
@@ -328,6 +405,10 @@
}, },
{ {
"id": "BS-6", "id": "BS-6",
"evidence": {
"source": "unrecorded",
"ownership": "unrecorded"
},
"scope": "Administered and automated the Linux server estate underpinning fab analytics and ML workloads at Bosch (2020-2022): ML platform hosts, Docker hosts, backend services and the Ansible control path. Developed Ansible extensions, including a credential plugin that retrieved secrets from a password keystore and cached them locally to cut lookup volume and improve scalability and performance. Playbooks were version-controlled in Git and executed push-style from Jenkins/GitLab pipelines. User-confirmed 2026-08-27.", "scope": "Administered and automated the Linux server estate underpinning fab analytics and ML workloads at Bosch (2020-2022): ML platform hosts, Docker hosts, backend services and the Ansible control path. Developed Ansible extensions, including a credential plugin that retrieved secrets from a password keystore and cached them locally to cut lookup volume and improve scalability and performance. Playbooks were version-controlled in Git and executed push-style from Jenkins/GitLab pipelines. User-confirmed 2026-08-27.",
"allowed_verbs": [ "allowed_verbs": [
"administered", "administered",
@@ -348,7 +429,11 @@
}, },
{ {
"id": "FC-1", "id": "FC-1",
"scope": "Set up Jenkins CI/CD and contributed to SCEDAS development and maintenance.", "evidence": {
"source": "Fraunhofer Arbeitszeugnis, 31.10.2019 (primary document, read 2026-09-11)",
"ownership": "individual - development and CI setup"
},
"scope": "SCEDAS crew-scheduling software: software development in C#, Entity Framework and MICROSOFT SQL SERVER; introduced and configured Jenkins and built the build jobs for automatic SCEDAS deployment; bugfixing and support. Verbatim from the Arbeitszeugnis.",
"allowed_verbs": [ "allowed_verbs": [
"set up", "set up",
"developed", "developed",
@@ -358,7 +443,11 @@
}, },
{ {
"id": "FC-2", "id": "FC-2",
"scope": "Contributed ML/NLP components to ARTUS; no publication or model-training ownership.", "evidence": {
"source": "Fraunhofer Arbeitszeugnis, 31.10.2019 (primary document, read 2026-09-11)",
"ownership": "contributor - project work, not ownership"
},
"scope": "ARTUS research project: development work in SPEECH RECOGNITION and machine learning for an automatic TRANSCRIPTION SYSTEM FOR SEA RESCUE (Seenotrettung). Contribution only - no publication and no model-training ownership. Separately, co-authored a RESEARCH PROPOSAL on predicting optimal MAINTENANCE TIMING (Instandsetzungszeitpunkte) using machine learning - genuine predictive-maintenance exposure, at proposal stage, not a delivered system.",
"allowed_verbs": [ "allowed_verbs": [
"contributed", "contributed",
"implemented", "implemented",
@@ -372,7 +461,11 @@
}, },
{ {
"id": "FC-3", "id": "FC-3",
"scope": "Developed containerized microservices for the MISSION research platform.", "evidence": {
"source": "Fraunhofer Arbeitszeugnis, 31.10.2019 (primary document, read 2026-09-11)",
"ownership": "individual - development"
},
"scope": "MISSION research project: developed MICROSERVICES for a data-exchange platform using EXPRESS.JS, JAVASCRIPT, DOCKER and SQLITE. Verbatim from the Arbeitszeugnis. Also contributed to a study on new IT technologies for PERFORMANCE OPTIMIZATION AND MONITORING in the maritime domain.",
"allowed_verbs": [ "allowed_verbs": [
"developed", "developed",
"built" "built"
@@ -381,7 +474,11 @@
}, },
{ {
"id": "VZ-1", "id": "VZ-1",
"scope": "Contributed Python and C++ engineering to a distributed video-transcoding backend. Named broadcasters are product context, not direct delivery claims.", "evidence": {
"source": "user statement 2026-09-11 for the language split. NOT corroborated by the Vizrt Reference Letter, which describes testing responsibilities only and never mentions the transcoding backend, Python or C++.",
"ownership": "contributor"
},
"scope": "Contributed Python and C++ engineering to a distributed video-transcoding backend. Named broadcasters are product context, not direct delivery claims. Language split (user-confirmed 2026-09-11): PYTHON for the wrapper, C++ for the transcoding engine (FFmpeg). See feedback_cpp_emphasis - do not oversell C++; Python and Java are the stronger languages.",
"allowed_verbs": [ "allowed_verbs": [
"developed", "developed",
"engineered", "engineered",
@@ -396,7 +493,11 @@
}, },
{ {
"id": "VZ-2", "id": "VZ-2",
"scope": "Developed automated audio/video integration tests and connected quality gates to CI/CD.", "evidence": {
"source": "Vizrt Reference Letter, Bergen 09.05.2018 (primary document, read 2026-09-11)",
"ownership": "individual - test automation"
},
"scope": "Developed automated test suites and connected them as quality gates in the CI/CD pipeline. ZEUGNIS WORDING (primary): took part in planning new features and code changes with a focus on testing, worked on shortening the feedback loop and time to market while meeting quality expectations, and developed automated test suites to improve quality and long-term maintainability. Part of the Coder team.",
"allowed_verbs": [ "allowed_verbs": [
"developed", "developed",
"automated", "automated",
@@ -406,7 +507,11 @@
}, },
{ {
"id": "GN-1", "id": "GN-1",
"scope": "Introduced BDD through a proof of concept and held technical responsibility for test automation, training and Jenkins jobs.", "evidence": {
"source": "Generali Zwischenzeugnis/Endzeugnis, 30.06.2017 (primary document, read 2026-09-11)",
"ownership": "individual - technical responsibility for the test suite"
},
"scope": "Introduced Behaviour-Driven Development through a proof of concept and held TECHNICAL RESPONSIBILITY for test automation in the PIA-Postkorb / SE-Projekt Workflow project. Named tooling from the Arbeitszeugnis: SERENITY-BDD, SELENIUM and JBEHAVE for automated UI tests, with the BDD build jobs administered in JENKINS. Also: requirements gathering and estimation for new test cases, presenting BDD to the Java community and inside the project, and training and enabling project members to work with BDD.",
"allowed_verbs": [ "allowed_verbs": [
"introduced", "introduced",
"owned", "owned",
@@ -417,7 +522,11 @@
}, },
{ {
"id": "GN-2", "id": "GN-2",
"scope": "Developed UIPath RPA proofs of concept and acted as an internal contact.", "evidence": {
"source": "Generali Zwischenzeugnis/Endzeugnis, 30.06.2017 (primary document, read 2026-09-11)",
"ownership": "individual - PoC development and internal advisory"
},
"scope": "Robotic Process Automation: developed proofs of concept with UIPATH, presented a live demo for advisory and demonstration purposes, and acted as an internal point of contact for group companies at GDIS. Evaluated version 8.7 of the IBM OPERATIONAL DECISION MANAGEMENT (ODM) Decision Center during the trainee programme.",
"allowed_verbs": [ "allowed_verbs": [
"developed", "developed",
"served" "served"
@@ -426,7 +535,11 @@
}, },
{ {
"id": "GN-3", "id": "GN-3",
"scope": "Developed Java/J2EE workflow application features and contributed to integration proofs of concept.", "evidence": {
"source": "Generali Zwischenzeugnis/Endzeugnis, 30.06.2017 (primary document, read 2026-09-11)",
"ownership": "individual - development; architecture was a contribution"
},
"scope": "Application development in the PIA-Postkorb / SE-Projekt Workflow project; software development in the JAVA/J2EE environment, defect fixing and implementing new requirements. Named additions from the Arbeitszeugnis: migrated web services to a new deployment procedure (XLDEPLOY), CONTRIBUTED TO DESIGN AND ARCHITECTURE DECISIONS for the Aufgabenservices, and built into the Dispatcher proof of concept with APACHE CAMEL and SPRING BOOT. The Spring Boot exposure is PoC-scale and dates to 2015-2017 - it does not support a current Spring Boot claim.",
"allowed_verbs": [ "allowed_verbs": [
"developed", "developed",
"contributed", "contributed",
@@ -436,6 +549,10 @@
}, },
{ {
"id": "BW-1", "id": "BW-1",
"evidence": {
"source": "User confirmation 2026-07-10; thiessen_linkedin_profile.md",
"ownership": "Individual service and completed training."
},
"scope": "Completed officer candidate training and officer school during six years of service in the German Armed Forces (Bundeswehr), leaving service as Second Lieutenant. This establishes military and organisational context only; no technical military role, command scope, deployment, NATO service or clearance is established.", "scope": "Completed officer candidate training and officer school during six years of service in the German Armed Forces (Bundeswehr), leaving service as Second Lieutenant. This establishes military and organisational context only; no technical military role, command scope, deployment, NATO service or clearance is established.",
"ownership": "Individual service and completed training.", "ownership": "Individual service and completed training.",
"allowed_verbs": [ "allowed_verbs": [
@@ -456,6 +573,10 @@
}, },
{ {
"id": "PP-1", "id": "PP-1",
"evidence": {
"source": "unrecorded",
"ownership": "unrecorded"
},
"scope": "PERSONAL PROJECT (user-supplied 2026-08-25), not professional work. A self-hosted, single-user investing/signal platform Dennis built for himself: ingests daily US-equity prices, fundamentals and sentiment (LLM-assisted); runs one long-only cross-sectional momentum book (top-quintile residual 12-1 momentum, ATR stop/trail, max 15 concurrent names) through scheduled scan and backtest pipelines; surfaces gated setups in a web dashboard plus Telegram alerts. Legitimate use: evidence of self-directed trading-domain fluency and of an end-to-end ingestion/backtest/alerting build outside work. Full-ownership verbs ARE correct here -- unlike his employer work, this is genuinely solo.", "scope": "PERSONAL PROJECT (user-supplied 2026-08-25), not professional work. A self-hosted, single-user investing/signal platform Dennis built for himself: ingests daily US-equity prices, fundamentals and sentiment (LLM-assisted); runs one long-only cross-sectional momentum book (top-quintile residual 12-1 momentum, ATR stop/trail, max 15 concurrent names) through scheduled scan and backtest pipelines; surfaces gated setups in a web dashboard plus Telegram alerts. Legitimate use: evidence of self-directed trading-domain fluency and of an end-to-end ingestion/backtest/alerting build outside work. Full-ownership verbs ARE correct here -- unlike his employer work, this is genuinely solo.",
"allowed_verbs": [ "allowed_verbs": [
"built", "built",
@@ -472,6 +593,10 @@
}, },
{ {
"id": "PP-2", "id": "PP-2",
"evidence": {
"source": "unrecorded",
"ownership": "unrecorded"
},
"scope": "Udacity 'AI for Trading' Nanodegree, completed self-study (user-supplied 2026-08-25). Covers quantitative trading fundamentals -- factor construction, alpha signals, backtesting. Pairs with PP-1 as the formal half of self-directed domain grounding.", "scope": "Udacity 'AI for Trading' Nanodegree, completed self-study (user-supplied 2026-08-25). Covers quantitative trading fundamentals -- factor construction, alpha signals, backtesting. Pairs with PP-1 as the formal half of self-directed domain grounding.",
"allowed_verbs": [ "allowed_verbs": [
"completed" "completed"
@@ -485,6 +610,10 @@
}, },
{ {
"id": "PP-3", "id": "PP-3",
"evidence": {
"source": "unrecorded",
"ownership": "unrecorded"
},
"scope": "PERSONAL PROJECT (user-supplied 2026-08-27), not employer work. A self-hosted Debian server Dennis has operated for ~10 years and administers and hardens himself, running nginx, a mail server, Nextcloud, a VPN server, Docker services and Bitwarden. This is the primary evidence for hands-on Linux administration, system hardening and security configuration, and it is current and continuous - unlike the Bosch Linux work, which ended in 2022. User states he genuinely enjoys this kind of work, including physical infrastructure.", "scope": "PERSONAL PROJECT (user-supplied 2026-08-27), not employer work. A self-hosted Debian server Dennis has operated for ~10 years and administers and hardens himself, running nginx, a mail server, Nextcloud, a VPN server, Docker services and Bitwarden. This is the primary evidence for hands-on Linux administration, system hardening and security configuration, and it is current and continuous - unlike the Bosch Linux work, which ended in 2022. User states he genuinely enjoys this kind of work, including physical infrastructure.",
"allowed_verbs": [ "allowed_verbs": [
"operate", "operate",
@@ -502,6 +631,32 @@
"using it as evidence of datacenter or physical-infrastructure experience" "using it as evidence of datacenter or physical-infrastructure experience"
], ],
"metrics": "none; ~10 years of continuous operation is the only quantity, and it is self-reported" "metrics": "none; ~10 years of continuous operation is the only quantity, and it is self-reported"
},
{
"id": "BS-7",
"evidence": {
"source": "Bosch Arbeitszeugnis, Dresden 31.12.2022, for the general supervision; user confirmation 2026-09-11 for the Zweitpruefer role on one Bachelor thesis",
"ownership": "individual - technical supervision and formal second-examiner role"
},
"scope": "Fachliche Betreuung von Studierenden, auch im Rahmen von Abschlussarbeiten - technical supervision of students at Bosch, including their theses (verbatim from the Arbeitszeugnis, which uses the plural for both). NAMED INSTANCE, user-confirmed 2026-09-11: he acted as ZWEITPRUEFER (second examiner) AND supervisor for ONE Bachelor thesis by a student at the company. Zweitpruefer is a FORMAL role carrying assessment responsibility, not informal mentoring - that is what makes this stronger than the team-role evidence (Security Champion, Java community) the profile otherwise falls back on. LIMITS: exactly ONE thesis is confirmed as second-examiner work - do not pluralise it; the number of students supervised more generally is not recorded; and none of this is people management or line responsibility.",
"allowed_verbs": [
"supervised",
"mentored",
"guided",
"supported",
"served as second examiner",
"assessed"
],
"forbidden": [
"managed a team",
"line management",
"led engineers",
"supervised multiple theses",
"academic appointment",
"lecturer",
"taught a course"
],
"metrics": "one Bachelor thesis as Zweitpruefer/supervisor is confirmed; the wider number of students supervised is unverified"
} }
], ],
"skills": [ "skills": [
@@ -592,7 +747,9 @@
{ {
"name": "JavaScript", "name": "JavaScript",
"evidence": "production-historical-limited", "evidence": "production-historical-limited",
"output": "allowed-with-context" "output": "allowed-with-context",
"context": "Fraunhofer 2018-2019 (FC-3): Express.js microservices for the MISSION data-exchange platform.",
"note": "Evidence upgraded from bare 'production-historical-limited' to a named project on 2026-09-11, from the Fraunhofer Arbeitszeugnis. Does NOT license TypeScript, which stays forbidden - that distinction ruled out RUAG Senior Software Engineer C5I."
}, },
{ {
"name": "LiteLLM", "name": "LiteLLM",
@@ -702,6 +859,95 @@
"evidence": "pending-user-clarification", "evidence": "pending-user-clarification",
"output": "forbidden", "output": "forbidden",
"note": "RESOLVED 2026-08-27, and the answer is NO. User confirmed: Ansible playbooks lived in Git, but execution was PUSH-based out of Jenkins/GitLab pipelines - there was no pull-based reconciliation agent (Argo CD, Flux). That is version-controlled, CI-driven infrastructure automation, not GitOps. The word stays forbidden in every document. The SUBSTANCE is writable and should be written: 'versionskontrollierte Infrastrukturautomatisierung mit Ansible aus Git, ausgefuehrt ueber Jenkins-/GitLab-Pipelines'. The Kdo Cy JD names 'Pull-GitOps' explicitly, so precision here is an asset, not a loss." "note": "RESOLVED 2026-08-27, and the answer is NO. User confirmed: Ansible playbooks lived in Git, but execution was PUSH-based out of Jenkins/GitLab pipelines - there was no pull-based reconciliation agent (Argo CD, Flux). That is version-controlled, CI-driven infrastructure automation, not GitOps. The word stays forbidden in every document. The SUBSTANCE is writable and should be written: 'versionskontrollierte Infrastrukturautomatisierung mit Ansible aus Git, ausgefuehrt ueber Jenkins-/GitLab-Pipelines'. The Kdo Cy JD names 'Pull-GitOps' explicitly, so precision here is an asset, not a loss."
},
{
"name": "Groovy",
"evidence": "none",
"output": "forbidden",
"note": "Named in the NVIDIA JD alongside Bash and Python. Bash was confirmed and added 2026-09-05; Groovy was explicitly NOT - it stays unverified. Registered as a skill 2026-09-11 (review finding F05) so the document scanner can actually catch it: it was previously absent from claims.json entirely, which meant 'forbidden' existed only in prose and nothing enforced it."
},
{
"name": "Snowflake",
"evidence": "none",
"output": "forbidden",
"note": "Repeatedly named in target JDs (SBB, Snowflake Inc. reqs). The honest substitution is Teradata, Oracle, Redshift and Athena/Iceberg - see the SBB session file. Registered 2026-09-11 (review finding F05) for the same reason as Groovy. Note this forbids CLAIMING the skill; naming Snowflake as an employer or as a JD requirement in prose is not a claim, and the negation guard in the scanner allows an honest 'no Snowflake experience'."
},
{
"name": "Kotlin",
"evidence": "production-historical",
"output": "allowed-with-context",
"context": "Bosch 2020-2022: backend services, Ansible plugin work and extensions, alongside Python as the primary language and some Java.",
"note": "Added 2026-09-11 on user confirmation. Was absent from claims.json entirely, so it was unwritable under the evidence-first rule - the FOURTH real skill found missing this way, after Ansible and Linux (both 2026-08-27) and Bash (2026-09-05). Same systemic lesson: ask about the stack explicitly rather than trusting this file to be complete. (Groovy and Snowflake, registered the same day, are NOT instances of this pattern - they are skills he does NOT have, registered as `forbidden` so the document scanner could catch them.)"
},
{
"name": "Microsoft SQL Server",
"evidence": "production-historical",
"output": "allowed-with-context",
"context": "Fraunhofer 2018-2019 (FC-1): SCEDAS ran on Microsoft SQL Server with Entity Framework.",
"note": "Added 2026-09-11 from the Fraunhofer Arbeitszeugnis. IMPORTANT: the RUAG 'Senior Data Engineer (MSSQL)' rule-out of 2026-09-04 asserted 'MSSQL 0 canonical'. That premise was WRONG. The rule-out itself may still stand on its other required lines (multi-year ON-PREMISE data-warehouse construction, Talend, SAP S/4HANA - all genuinely 0), but the MSSQL half of the reasoning must not be repeated. One project database in 2018-2019 is also not data-warehouse experience; state it as what it is."
},
{
"name": "Entity Framework",
"evidence": "production-historical",
"output": "allowed-with-context",
"context": "Fraunhofer 2018-2019 (FC-1), with C# on SCEDAS.",
"note": "Added 2026-09-11 from the Arbeitszeugnis."
},
{
"name": "Express.js",
"evidence": "production-historical",
"output": "allowed-with-context",
"context": "Fraunhofer 2018-2019 (FC-3): microservices for the MISSION data-exchange platform.",
"note": "Added 2026-09-11 from the Arbeitszeugnis."
},
{
"name": "SQLite",
"evidence": "production-historical",
"output": "allowed-with-context",
"context": "Fraunhofer 2018-2019 (FC-3), MISSION platform.",
"note": "Added 2026-09-11 from the Arbeitszeugnis."
},
{
"name": "Selenium",
"evidence": "production-historical",
"output": "allowed-with-context",
"context": "Generali 2015-2017 (GN-1): automated UI tests with Serenity-BDD, Selenium and JBehave.",
"note": "Added 2026-09-11 from the Arbeitszeugnis."
},
{
"name": "Serenity-BDD / JBehave",
"evidence": "production-historical",
"output": "allowed-with-context",
"context": "Generali 2015-2017 (GN-1): the BDD test-automation stack he held technical responsibility for.",
"note": "Added 2026-09-11 from the Arbeitszeugnis."
},
{
"name": "Apache Camel",
"evidence": "production-historical-limited",
"output": "allowed-with-context",
"context": "Generali 2015-2017 (GN-3): Dispatcher proof of concept, with Spring Boot.",
"note": "Added 2026-09-11 from the Arbeitszeugnis. PoC scale - do not present as integration-platform ownership."
},
{
"name": "Spring Boot",
"evidence": "production-historical-limited",
"output": "allowed-with-context",
"context": "Generali 2015-2017 (GN-3): Dispatcher proof of concept, with Apache Camel.",
"note": "Added 2026-09-11 from the Arbeitszeugnis. This CONFIRMS what was previously only asserted in the SBB interview brief - Spring Boot exposure is real but PoC-scale and a decade old. It does NOT support a current Spring Boot claim, and it does not license Angular, which remains absent."
},
{
"name": "IBM Operational Decision Management (ODM)",
"evidence": "production-historical-limited",
"output": "allowed-with-context",
"context": "Generali trainee programme 2015: evaluated Decision Center 8.7.",
"note": "Added 2026-09-11 from the Arbeitszeugnis. Evaluation only."
},
{
"name": "XLDeploy",
"evidence": "production-historical-limited",
"output": "allowed-with-context",
"context": "Generali 2015-2017 (GN-3): migrated web services onto a new deployment procedure.",
"note": "Added 2026-09-11 from the Arbeitszeugnis."
} }
], ],
"global_forbidden_output_patterns": [ "global_forbidden_output_patterns": [
@@ -712,5 +958,27 @@
"customer-embedded delivery", "customer-embedded delivery",
"formal model evaluation", "formal model evaluation",
"3 consecutive years" "3 consecutive years"
] ],
"document_exclusions": [
{
"term": "Capgemini",
"scope": "every resume, CV and cover letter",
"reason": "Standing user preference: six months only (Nov 2014 - May 2015); the user does not want the short-stay impression. The resulting gap in the visible chronology is INTENTIONAL and must never be 'fixed' or explained away. The visible record starts at Generali, May 2015, which is also what any 'over ten years' tenure wording is measured against.",
"applies_to_documents_only": true,
"note": "This is a DOCUMENT rule, not a conversation rule. If an interviewer asks directly what the first civilian role was, naming it plainly is correct - declining to name a former employer reads worse than a six-month stay.",
"source": "User-confirmed 2026-08-27 after it regressed into the RUAG C5I draft; given a canonical home 2026-09-11 (review finding F05), having previously lived only in memory and config.md."
}
],
"open_discrepancies_2026_09_11": {
"note": "All six discrepancies found on 2026-09-11 are resolved; see resolved_2026_09_11. Kept as a record of what reading the primary documents actually turned up.",
"items": [],
"resolved_2026_09_11": {
"D1-bosch-title": "CLOSED. User re-confirmed the promotion and timing; the Zeugnis names departments, not titles. official_title_provenance corrected - it never came from that document.",
"D2-bosch-promotion-date": "CLOSED with D1. The 2021-01 re-grading stands.",
"D3-bosch-part-time": "CLOSED by user decision: part-time from 2021-09 is NOT recorded and does not belong in any document. Note for completeness: tenure wording such as 'over ten years' measures ELAPSED time and is unaffected. It would only matter if a document ever claimed full-time-equivalent years, which none does.",
"D4-swisscom-languages": "CLOSED. The user reviewed the sentence and confirmed it is an error in the Zeugnis. DE/EN only stands. See identity.language_note.",
"D6-vizrt-end-date": "CLOSED by user decision: end 2018-05 stands. The letter, written 2018-05-09, evidently stated a planned end date.",
"D5-swisscom-dwh": "CLOSED. User ruling: the RUAG role itself is not revisited (and a second cold application to RUAG is blocked anyway while 18027 is pending), but the reasoning IS corrected. Both RUAG decision-log rows now carry the correction. Standing rule recorded there: he has documented data-warehouse experience (Teradata, component owner) and MSSQL exposure - screen DWH roles on Aufbau-vs-Betrieb and the named vendor stack, never on 'no DWH' or 'MSSQL 0'."
}
}
} }
@@ -2,22 +2,68 @@
"schema_version": 1, "schema_version": 1,
"policy": "Generated output is never a source for a new application. Regenerate from canonical claims and normalized experience files.", "policy": "Generated output is never a source for a new application. Regenerate from canonical claims and normalized experience files.",
"unsafe_do_not_reuse": [ "unsafe_do_not_reuse": [
{"folder": "output/Apple_Data_Engineer", "reasons": ["fabricated LangChain", "unsupported petabyte scale", "overstated platform and agentic ownership"]}, {
{"folder": "output/Infineon_AI_Engineer", "reasons": ["fabricated LangChain"]}, "folder": "output/Apple_Data_Engineer",
{"folder": "output/Equinor_AI_Manager", "reasons": ["unsupported model serving and evaluation", "inflated responsible-AI and security ownership"]}, "reasons": [
{"folder": "output/Microsoft_ISE_Senior_SWE", "reasons": ["unsupported model-evaluation skill", "customer-delivery framing exceeds verified evidence"]}, "fabricated LangChain",
{"folder": "output/Kraken_SRE_AI_Agents", "reasons": ["Security Champion scope inflation", "stretch-role framing requires complete regeneration"]}, "unsupported petabyte scale",
{"folder": "output/Kraken_AI_Infrastructure", "reasons": ["pre-canonical package", "agent-infrastructure bridges require revalidation"]} "overstated platform and agentic ownership"
]
},
{
"folder": "output/Infineon_AI_Engineer",
"reasons": [
"fabricated LangChain"
]
},
{
"folder": "output/Equinor_AI_Manager",
"reasons": [
"unsupported model serving and evaluation",
"inflated responsible-AI and security ownership"
]
},
{
"folder": "output/Microsoft_ISE_Senior_SWE",
"reasons": [
"unsupported model-evaluation skill",
"customer-delivery framing exceeds verified evidence"
]
},
{
"folder": "output/Kraken_SRE_AI_Agents",
"reasons": [
"Security Champion scope inflation",
"stretch-role framing requires complete regeneration"
]
},
{
"folder": "output/Kraken_AI_Infrastructure",
"reasons": [
"pre-canonical package",
"agent-infrastructure bridges require revalidation"
]
}
], ],
"historical_revalidate": [ "historical_revalidate": [
"output/Google_Senior_Data_Engineer", "output/AkerBP_Data_Product_Architect",
"output/Snowflake_Observe_Enterprise",
"output/QuantCo_Cloud_Engineer",
"output/NATO_AI_ENGINEER",
"output/BIS", "output/BIS",
"output/Isovalent_DataEngineer", "output/CitadelSecurities_PlatformEngineer",
"output/Google_Business_Home",
"output/Google_Senior_Data_Engineer",
"output/Infineon", "output/Infineon",
"output/Microsoft_Principal_FDE_SWE" "output/Isovalent_DataEngineer",
"output/KdoCy_DevOps_Data_Platform",
"output/Microsoft_Principal_FDE_SWE",
"output/NATO_AI_ENGINEER",
"output/NVIDIA_SrDevOps_Zurich",
"output/QuantCo_Cloud_Engineer",
"output/RUAG_AI_Engineer_C5I",
"output/SBB_DataEngineer_AssetMgmt",
"output/Snowflake_Observe_Enterprise"
], ],
"paused_no_package": ["output/Google_FDE_GenAI"] "paused_no_package": [
"output/Google_FDE_GenAI"
],
"classification_default": "Any package folder NOT listed in this file defaults to historical_revalidate: never reuse a bullet from it without re-checking the claim against claims.json first. Recorded 2026-09-11 (review finding F17), which found 8 unclassified folders against 14 classified ones. The seven application packages among them were added to historical_revalidate; being recent is not the same as being verified, and none of them was re-read for this classification."
} }
@@ -0,0 +1,527 @@
{
"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": "922759ac4bc7a52c",
"e2e_akerbp_data_product_architect_resume.tex": "d0a0f9942a882567"
},
"class_files": {
"resume.cls": "b3a032a850cd0d6d"
},
"requires_class": [
"article",
"resume"
],
"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": "34abad48e3a82634",
"e2e_apple_data_engineer_resume.tex": "c3594ddcb32131da"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "4f037d7fcfd1ccfc",
"e2e_bis_senior_data_analytics_ai_resume.tex": "4ecc6a98ab82ea2b"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "4f2bf629635d3413",
"e2e_citadel_securities_platform_engineer_resume.tex": "5f2ac36737162b29"
},
"class_files": {
"resume.cls": "b3a032a850cd0d6d"
},
"requires_class": [
"article",
"resume"
],
"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": "390a7ea37836a93d",
"e2e_equinor_ai_manager_resume.tex": "f57087d53a777328"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "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"
},
"requires_class": [
"article",
"resume"
],
"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": "966fd9ff789fb4a4",
"e2e_google_senior_data_engineer_resume.tex": "1e3e56ab426172ab"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "4149a4a2ed2f619f",
"e2e_infineon_doctoral_resume.tex": "090946510d744691"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "a8cccec6b8d685b9",
"e2e_infineon_ai_engineer_resume.tex": "b7957e35ee342bca"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "33c6a2943f288893",
"e2e_isovalent_data_engineer_resume.tex": "b8edf89223ef8fb2"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "29d114255a1310cc",
"e2e_kdocy_devops_data_platform_resume.tex": "de3fcb42d1c5c478"
},
"class_files": {
"resume.cls": "b3a032a850cd0d6d"
},
"requires_class": [
"article",
"resume"
],
"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": "32eb97aedefb1702",
"e2e_kraken_ai_infra_resume.tex": "14350c33e1dbc18e"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "bf215661bc0e6936",
"e2e_kraken_sre_ai_agents_resume.tex": "b8d1a3cfae03186a"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "72ea729b7bd5c8da",
"e2e_microsoft_ise_resume.tex": "f61845e8087b5696"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "8095827a769ece44",
"e2e_microsoft_principal_fde_swe_resume.tex": "5be341c9dcccca28"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "863c590ea8a987c1",
"e2e_nato_ai_engineer_resume.tex": "e3b80997712dd9d7"
},
"class_files": {},
"requires_class": [
"moderncv",
"resume"
],
"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": "2252b98f7a7e0ca0",
"e2e_nvidia_sr_devops_resume.tex": "69787364df7d6f02"
},
"class_files": {
"resume.cls": "b3a032a850cd0d6d"
},
"requires_class": [
"article",
"resume"
],
"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": "f8ef588da7fe1232",
"e2e_quantco_cloud_engineer_resume.tex": "a8ebc1b56f623949"
},
"class_files": {
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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": "2b3d966dc0590b08",
"e2e_ruag_ai_engineer_c5i_resume.tex": "2cf799344c3d91c6"
},
"class_files": {
"resume.cls": "b52a6f7123ae9067"
},
"requires_class": [
"article",
"resume"
],
"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": "cd4e8a18fa0a235a"
},
"class_files": {
"resume.cls": "b3a032a850cd0d6d"
},
"requires_class": [
"resume"
],
"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": "f0d01764e943776f",
"e2e_snowflake_observe_enterprise_resume.tex": "cde2a2326fa2913e"
},
"class_files": {
"cv.cls": "30bf0ca5b3d0b718",
"resume.cls": "721ed8d5e80fcc54"
},
"requires_class": [
"moderncv",
"resume"
],
"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"
}
}
}
+17 -4
View File
@@ -26,7 +26,7 @@ Dennis worked across multiple data domains within the fab — these are domain s
This domain expertise is rare in data engineering candidates and is a strong differentiator for semiconductor company JDs (ASML, Infineon, TSMC, GlobalFoundries, etc.). Flag this domain when targeting semi roles. This domain expertise is rare in data engineering candidates and is a strong differentiator for semiconductor company JDs (ASML, Infineon, TSMC, GlobalFoundries, etc.). Flag this domain when targeting semi roles.
**CL framing:** "At Bosch Semiconductor in Dresden, I worked at the intersection of data engineering and semiconductor manufacturing analytics — owning the data pipelines and applications that drove defect management, parameter testing analysis, and process optimization across 300mm wafer production. My most impactful project was containerizing ML inference for automated image-based defect classification, turning a manual quality inspection process into a fully automated, 24/7 production system." **CL framing:** "At Bosch Semiconductor in Dresden, I worked at the intersection of data engineering and semiconductor manufacturing analytics — owning the data pipelines and applications that drove defect management, parameter testing analysis, and process optimization across 300mm wafer production. My most impactful project was containerizing ML inference for automated image-based defect classification, substantially reducing a manual quality-inspection workload in a 24/7 production environment." *(Do not write “fully automated” or “eliminated”: canonical BS-1 supports a qualitative reduction in manual classification, exact amount unverified.)*
--- ---
@@ -36,11 +36,24 @@ This domain expertise is rare in data engineering candidates and is a strong dif
**User's role:** Primary developer / strategy owner ("Erarbeitung und Durchführung" = designed AND executed) **User's role:** Primary developer / strategy owner ("Erarbeitung und Durchführung" = designed AND executed)
**Status:** Deployed to production **Status:** Deployed to production
**Context:** 300mm semiconductor fab runs continuously. Manual image classification for wafer defect detection created a bottleneck for line engineers in the Defect Management domain. Dennis designed and implemented the strategy to containerize and orchestrate ML inference into the live production pipeline — enabling fully automated defect classification with no manual intervention. > **RESOLVED 2026-09-11 by the user** (was an open item from review F07). The real constraint is
> a FAILURE CONSEQUENCE, not a deployment window: *"Bei einem Fehler (also Defekt in der Software)
> kann es zu einem Produktionsstopp im worst-case kommen, was natürlich nie passieren darf."*
>
> The submitted NVIDIA cover letter says *"a production line that could not be stopped for a
> deployment"* — that is a **different and inaccurate** claim. It describes a scheduling
> constraint; the truth is about the blast radius of a bug. **That exact phrasing is now
> `forbidden` on BS-1** and must not be reused. The letter itself is not rewritten (F17).
>
> **Write it as:** "integrated containerized ML inference into a continuously operating
> semiconductor fab, where a software defect could have halted production" — which is a stronger
> statement than the one that was sent, and true.
**Context:** 300mm semiconductor fab runs continuously. Manual image classification for wafer defect detection created a bottleneck for line engineers in the Defect Management domain. Dennis designed and implemented the strategy to containerize and orchestrate ML inference into the live production pipeline, automating image-based defect classification and substantially reducing manual inspection work. **Scope limit (canonical BS-1):** the reduction is source-backed but its size is unverified — do not claim elimination, “no manual intervention” or unattended operation, and never claim model development.
**Bullet variants:** **Bullet variants:**
- **2L:** Containerized and orchestrated ML inference (Docker, Kubernetes, Ansible) into 24/7 semiconductor production pipelines, enabling fully automated image-based defect classification and significantly reducing manual inspection workload for line engineers. - **2L:** Containerized and orchestrated ML inference (Docker, Kubernetes, Ansible) into 24/7 semiconductor production pipelines, automating image-based defect classification and reducing manual inspection workload for line engineers.
- **3L:** Designed and executed the ML inference integration strategy for Bosch's 24/7 semiconductor fab — containerizing defect-detection models with Docker, orchestrating via Kubernetes and Ansible, and embedding automated image classification into the Defect Management pipeline; eliminated manual wafer inspection bottleneck and enabled continuous, unattended quality monitoring across active 300mm production lines. - **3L:** Designed and executed the ML inference integration strategy for Bosch's 24/7 semiconductor fab — containerizing defect-detection models with Docker, orchestrating via Kubernetes and Ansible, and embedding automated image classification into the Defect Management pipeline; reduced the manual wafer-inspection bottleneck on active 300mm production lines. *(Earlier wording said “eliminated” and “unattended” — both exceed canonical BS-1.)*
- **1L:** Containerized ML inference (Docker, K8s, Ansible) for automated image-based defect classification in 24/7 semiconductor fab. - **1L:** Containerized ML inference (Docker, K8s, Ansible) for automated image-based defect classification in 24/7 semiconductor fab.
**Key skills:** Docker, Kubernetes, Ansible, ML deployment, ML inference, containerization, MLOps, image classification, defect detection, production ML, semiconductor manufacturing **Key skills:** Docker, Kubernetes, Ansible, ML deployment, ML inference, containerization, MLOps, image classification, defect detection, production ML, semiconductor manufacturing
@@ -3,7 +3,7 @@
### Cross-Position Section ### 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. **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:** **Resume inclusion guidance:**
- 2-page resume: **Omit entirely** - 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 - Cover letter: Not relevant to mention
@@ -163,8 +163,9 @@
**Metadata/catalogue platform (user-confirmed 2026-07-29):** Swisscom uses **Atlassian Compass** as the metadata platform for data products, including lineage. Dennis uses it as a practitioner to document data-product metadata and lineage. **Do NOT claim** he administers, rolled out or owns Compass. This is a genuine named catalogue/lineage tool and satisfies "or similar metadata/catalogue platform" JD phrasing — but it is **not** a substitute claim for Purview, Collibra or Alation, which remain unevidenced and forbidden. **Metadata/catalogue platform (user-confirmed 2026-07-29):** Swisscom uses **Atlassian Compass** as the metadata platform for data products, including lineage. Dennis uses it as a practitioner to document data-product metadata and lineage. **Do NOT claim** he administers, rolled out or owns Compass. This is a genuine named catalogue/lineage tool and satisfies "or similar metadata/catalogue platform" JD phrasing — but it is **not** a substitute claim for Purview, Collibra or Alation, which remain unevidenced and forbidden.
**Key skills:** Data Mesh, data products, metadata management, data lineage, Atlassian Compass, data catalog, data governance, AWS, Glue, Athena, CloudFormation, AWS CLI, CI/CD, agentic data foundation, grounded retrieval **Key skills:** Data Mesh, data products, metadata management, data lineage, Atlassian Compass, data catalog, data governance, AWS, Glue, Athena, CloudFormation, AWS CLI, CI/CD, agentic data foundation
**ATS keywords:** Data Mesh, data products, metadata management, AWS, Glue, Athena, CloudFormation, CI/CD, data governance, grounded retrieval, agentic AI foundation **NOT a skill here:** *grounded retrieval* was listed until 2026-09-11 but no retrieval implementation is evidenced — SW-8 records it as explicitly unknown. Describe the data side (governed, documented, lineage-tracked data that agents can consume), never retrieval work.
**ATS keywords:** Data Mesh, data products, metadata management, AWS, Glue, Athena, CloudFormation, CI/CD, data governance, agentic AI foundation
**Reframing notes:** **Reframing notes:**
- ML/AI: MED — evidence for data readiness and grounded enterprise data, not agent architecture or retrieval ownership - ML/AI: MED — evidence for data readiness and grounded enterprise data, not agent architecture or retrieval ownership
- Data Platform/Infra: HIGH — Data Mesh + metadata + AWS IaC is core platform signal - Data Platform/Infra: HIGH — Data Mesh + metadata + AWS IaC is core platform signal
@@ -186,7 +187,7 @@
**Key skills:** LLM application configuration, model selection, AI agents, domain knowledge bases, question answering, migration assistance, data mapping **Key skills:** LLM application configuration, model selection, AI agents, domain knowledge bases, question answering, migration assistance, data mapping
**ATS keywords:** LLM-powered applications, AI agents, model selection, knowledge grounding, question answering, workflow automation, data mapping **ATS keywords:** LLM-powered applications, AI agents, model selection, knowledge grounding, question answering, workflow automation, data mapping
**Reframing notes:** **Reframing notes:**
- ML/AI: HIGH for LLM-specific roles; lead with applied LLM delivery, then connect reliable data foundations and evaluation discipline. - ML/AI: HIGH for LLM-specific roles; lead with applied LLM *configuration*, then connect reliable data foundations. **Do not invoke evaluation discipline** — no LLM evaluation responsibility is evidenced and *formal model evaluation* is a globally forbidden pattern.
- Accuracy: Never say “trained an LLM” unless model weights were fine-tuned. Prefer “grounded,” “configured,” or “provided with a domain-specific knowledge base.” - Accuracy: Never say “trained an LLM” unless model weights were fine-tuned. Prefer “grounded,” “configured,” or “provided with a domain-specific knowledge base.”
- Production: Use “internal” or “prototype” only if true; otherwise omit deployment-status language until verified. - Production: Use “internal” or “prototype” only if true; otherwise omit deployment-status language until verified.
@@ -0,0 +1,140 @@
"""Authoritative application state and generated, read-only projections.
Run with --check to detect stale views, or --render after editing the registry.
Outcome events describe what happened; current outcome never erases an interview.
"""
from __future__ import annotations
import argparse
from contextlib import contextmanager
from datetime import date
import json
import math
import os
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
REGISTRY = ROOT / "job_scout/state/applications.json"
VIEW = ROOT / "resume_builder/reference/current_applications.md"
COHORT = ROOT / "job_scout/state/application_cohort.json"
OUTCOMES = {"open", "applied", "paused", "not_proceeding", "role_closed", "closed_no_response", "withdrawn_by_candidate", "rejected_no_interview", "rejected_after_interview", "interviewing", "offer", "accepted"}
def valid_date(value: str) -> str:
if not isinstance(value, str) or date.fromisoformat(value).isoformat() != value:
raise ValueError("expected ISO date YYYY-MM-DD")
return value
def validate(data: dict) -> None:
ids = set()
for record in data["applications"]:
identifier = record.get("id")
if not identifier or identifier in ids:
raise ValueError("application IDs must be nonempty and unique")
ids.add(identifier)
if record.get("outcome") not in OUTCOMES:
raise ValueError(f"{identifier}: invalid outcome")
for key in ("application_date", "rejection_date", "withdrawn"):
if record.get(key):
valid_date(record[key])
for event in record.get("events", []):
if event.get("date"):
valid_date(event["date"])
score = record.get("evidence_fit")
if score is not None and (isinstance(score, bool) or not isinstance(score, (float, int)) or not math.isfinite(score) or not 0 <= score <= 100):
raise ValueError(f"{identifier}: evidence_fit must be finite and between 0 and 100")
if record.get("hard_gate") == "fail" and record.get("fit_class") not in {"stretch", "no-go"}:
raise ValueError(f"{identifier}: failed gate requires Stretch or No-go")
if score is not None and record.get("hard_gate") == "pass" and record.get("fit_class") in {"core", "adjacent"}:
expected = "core" if score >= 75 else "adjacent" if score >= 60 else "stretch"
if record["fit_class"] != expected and not record.get("fit_override_reason"):
raise ValueError(f"{identifier}: classification differs from score; record fit_override_reason")
@contextmanager
def transaction():
"""Serialize local writers; fail clearly rather than overwrite another update."""
lock = REGISTRY.with_suffix(".lock")
try:
descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
except FileExistsError as exc:
raise ValueError(f"Application registry is locked: {lock}. Check for an active writer before removing a stale lock.") from exc
try:
with os.fdopen(descriptor, "w") as stream:
stream.write(str(os.getpid()))
data = load()
yield data
validate(data)
atomic_write(REGISTRY, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
render(data)
finally:
lock.unlink(missing_ok=True)
def atomic_write(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".", suffix=".tmp")
try:
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream:
stream.write(text)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
Path(temporary).unlink(missing_ok=True)
def load() -> dict:
return json.loads(REGISTRY.read_text(encoding="utf-8"))
def interview_reached(record: dict) -> bool:
return bool(record.get("interview")) or any(
event.get("type") in {"interview_invited", "interview_held"}
for event in record.get("events", [])
)
def projections(data: dict) -> dict[Path, str]:
lines = ["# Current applications", "", "Generated from `job_scout/state/applications.json`; do not edit this view.",
"Use current outcome and next action below. Imported legacy notes are historical, not current instructions.", "",
"| Application | Current outcome | Interview reached | Next action |", "|---|---|---|---|"]
for record in data["applications"]:
cells = [record["label"], record["outcome"], "yes" if interview_reached(record) else "not recorded", record["next_action"]]
lines.append("| " + " | ".join(str(cell).replace("|", " / ").replace("\n", " ") for cell in cells) + " |")
lines.extend(["", "## Current targeting notes", "", *["- " + note for note in data.get("targeting_notes", [])], ""])
cohort = dict(data["cohort_metadata"])
cohort["generated_from"] = "job_scout/state/applications.json"
cohort["applications"] = [record for record in data["applications"] if record.get("cohort_id") == cohort["cohort_id"]]
return {VIEW: "\n".join(lines), COHORT: json.dumps(cohort, indent=2, ensure_ascii=False) + "\n"}
def render(data: dict, check: bool = False) -> list[str]:
validate(data)
stale = []
for path, expected in projections(data).items():
if not path.exists() or path.read_text(encoding="utf-8") != expected:
stale.append(str(path.relative_to(ROOT)))
if not check:
atomic_write(path, expected)
return stale
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--check", action="store_true")
mode.add_argument("--render", action="store_true")
args = parser.parse_args()
stale = render(load(), check=args.check)
if args.check and stale:
print("STALE: " + ", ".join(stale))
return 1
print("Application views are current.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,119 @@
"""Record applications without quotas; preserve outcome events and interview history."""
from __future__ import annotations
import argparse
import hashlib
import application_records as records
FIT_CLASSES = ("core", "adjacent", "stretch", "no-go")
CHANNELS = ("strong", "moderate", "weak")
def summary(data: dict, all_applications: bool = False) -> None:
cohort_id = data["cohort_metadata"]["cohort_id"]
applications = [row for row in data["applications"] if all_applications or row.get("cohort_id") == cohort_id]
print(f"Application record: {'all' if all_applications else cohort_id} ({len(applications)} applications; no quota)")
for field, values in (("fit_class", FIT_CLASSES), ("channel", CHANNELS)):
for value in values:
print(f" {field} {value}: {sum(row.get(field) == value for row in applications)}")
unknown = sum(row.get(field) is None for row in applications)
if unknown:
print(f" {field} unrecorded: {unknown}")
outcomes = sorted({row["outcome"] for row in applications})
print(" outcomes: " + ", ".join(f"{value}={sum(row['outcome'] == value for row in applications)}" for value in outcomes))
print(f" interview reached (including later withdrawal/rejection): {sum(records.interview_reached(row) for row in applications)}")
def add_application(data: dict, args: argparse.Namespace) -> None:
records.valid_date(args.date)
identity = args.id or "app-" + hashlib.sha256(f"{args.company.casefold()}|{args.job_id or args.role.casefold()}|{args.date}".encode()).hexdigest()[:12]
candidate = {
"id": identity, "company": args.company, "role": args.role,
"label": f"{args.company} — {args.role}", "application_date": args.date,
"fit_class": args.fit_class, "evidence_fit": args.evidence_fit,
"hard_gate": args.hard_gate, "channel": args.channel, "outcome": args.outcome,
"cohort_id": data["cohort_metadata"]["cohort_id"], "job_id": args.job_id,
"url": args.url, "fit_override_reason": args.fit_override_reason,
"rubric_version": args.rubric_version,
"next_action": args.next_action, "current_summary": args.outcome,
"events": [{"type": args.outcome, "date": args.date}],
}
for existing in data["applications"]:
same_job = args.job_id and existing.get("job_id") == args.job_id and existing["company"].casefold() == args.company.casefold()
same_url = args.url and existing.get("url") == args.url
same_application = existing["company"].casefold() == args.company.casefold() and existing["role"].casefold() == args.role.casefold() and existing.get("application_date") == args.date
if existing["id"] == identity or same_job or same_url or same_application:
if all(existing.get(key) == value for key, value in candidate.items() if key not in {"events", "id", "current_summary", "next_action", "outcome"}):
return
raise ValueError(f"Application already exists ({existing['id']}); use update instead")
data["applications"].append(candidate)
records.validate(data)
def update_application(data: dict, args: argparse.Namespace) -> None:
records.valid_date(args.date)
matches = [row for row in data["applications"] if row["id"] == args.id]
if not matches:
raise ValueError(f"Unknown application ID: {args.id}")
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") 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}
if args.note:
event["note"] = args.note
if args.outcome:
event["outcome"] = args.outcome
if event not in row.setdefault("events", []):
row["events"].append(event)
if args.outcome:
row["outcome"] = args.outcome
row["current_summary"] = args.note or args.outcome
row["next_action"] = args.next_action or ("Await response." if args.outcome in {"applied", "interviewing", "open"} else "Review recorded outcome before further action.")
elif args.next_action:
row["next_action"] = args.next_action
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
view = commands.add_parser("summary")
view.add_argument("--all", action="store_true")
add = commands.add_parser("add")
for field in ("company", "role", "date"):
add.add_argument("--" + field, required=True)
add.add_argument("--id")
add.add_argument("--job-id")
add.add_argument("--url")
add.add_argument("--fit-class", choices=FIT_CLASSES, required=True)
add.add_argument("--evidence-fit", type=float, required=True)
add.add_argument("--channel", choices=CHANNELS, required=True)
add.add_argument("--hard-gate", choices=("pass", "fail", "unknown"), required=True)
add.add_argument("--outcome", choices=sorted(records.OUTCOMES), default="applied")
add.add_argument("--fit-override-reason")
add.add_argument("--rubric-version", default="critique-framework-2026-09-11")
add.add_argument("--next-action", default="Await response.")
update = commands.add_parser("update")
update.add_argument("--id", required=True)
update.add_argument("--date", required=True, help="Date of this event")
update.add_argument("--outcome", choices=sorted(records.OUTCOMES))
update.add_argument("--event", choices=("interview_invited", "interview_held", "assessment_passed", "contact_attempted", "contact_conversation", "referral_received", "note"))
update.add_argument("--note")
update.add_argument("--next-action")
args = parser.parse_args()
try:
if args.command == "summary":
summary(records.load(), args.all)
return
with records.transaction() as data:
(add_application if args.command == "add" else update_application)(data, args)
summary(data)
except (ValueError, OSError) as error:
parser.exit(2, f"ERROR: {error}\n")
if __name__ == "__main__":
main()
+3 -79
View File
@@ -1,81 +1,5 @@
#!/usr/bin/env python3 """Compatibility entry point; application_tracker owns the uncapped registry workflow."""
"""Record and summarize the controlled ten-application cohort.""" from application_tracker import main
from __future__ import annotations if __name__ == '__main__':
import argparse
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
TRACKER = ROOT / "job_scout" / "state" / "application_cohort.json"
FIT_CLASSES = ("core", "adjacent", "stretch")
CHANNELS = ("strong", "moderate", "weak")
def load() -> dict:
return json.loads(TRACKER.read_text(encoding="utf-8"))
def save(data: dict) -> None:
TRACKER.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def summary(data: dict) -> None:
applications = data["applications"]
# Slot caps were dropped 2026-09-05 (application_strategy.md). This is a record, not a
# budget: report counts, never counts-against-a-target, so nothing reads as a remaining slot.
print(f"Application record: {data['cohort_id']} ({len(applications)} applications)")
for fit_class in FIT_CLASSES:
count = sum(item["fit_class"] == fit_class for item in applications)
print(f" {fit_class}: {count}")
for channel in CHANNELS:
count = sum(item["channel"] == channel for item in applications)
print(f" channel {channel}: {count}")
outcomes: dict[str, int] = {}
for item in applications:
outcome = item.get("outcome", "open")
outcomes[outcome] = outcomes.get(outcome, 0) + 1
print(" outcomes: " + ", ".join(f"{key}={value}" for key, value in sorted(outcomes.items())))
def main() -> None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("summary")
add = subparsers.add_parser("add")
add.add_argument("--company", required=True)
add.add_argument("--role", required=True)
add.add_argument("--date", required=True)
add.add_argument("--fit-class", choices=FIT_CLASSES, required=True)
add.add_argument("--evidence-fit", type=float, required=True)
add.add_argument("--channel", choices=CHANNELS, required=True)
add.add_argument("--hard-gate", choices=("pass", "fail"), required=True)
add.add_argument("--outcome", default="open")
args = parser.parse_args()
data = load()
if args.command == "add":
if len(data["applications"]) >= 10:
raise SystemExit("cohort already contains ten applications")
if args.hard_gate == "fail" and args.fit_class != "stretch":
raise SystemExit("a hard-gate failure must be recorded as stretch")
data["applications"].append(
{
"company": args.company,
"role": args.role,
"application_date": args.date,
"fit_class": args.fit_class,
"evidence_fit": args.evidence_fit,
"hard_gate": args.hard_gate,
"channel": args.channel,
"outcome": args.outcome,
}
)
save(data)
summary(data)
if __name__ == "__main__":
main() main()
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""Build a reproducibility manifest for every application package (review finding F17).
A package folder that contains a PDF looks finished. It is not necessarily reproducible:
`output/NATO_AI_ENGINEER/` ships no `resume.cls` at all and only builds on a machine where
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.
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
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
OUTPUT = ROOT / "output"
MANIFEST = ROOT / "resume_builder" / "canonical" / "package_manifest.json"
REGISTER = ROOT / "resume_builder" / "canonical" / "historical_outputs.json"
TODAY_MACRO = re.compile(r"\\today")
# Ship with MiKTeX/TeX Live, so their absence from a package folder is not a missing file.
# Only project-local classes (resume.cls, cv.cls) have to travel with the package.
CTAN_CLASSES = {"article", "report", "book", "letter", "moderncv", "scrartcl", "scrlttr2",
"memoir", "extarticle", "europasscv", "europecv", "altacv"}
def digest(path: Path) -> str:
# 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, 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")}
needs = set()
uses_today = []
for name in sources:
text = (folder / name).read_text(encoding="utf-8", errors="replace")
text = re.sub(r"(?<!\\)%.*$", "", text, flags=re.M)
match = re.search(r"\\documentclass(?:\[[^\]]*\])?\{([^}]+)\}", text)
if match:
needs.add(match.group(1))
if TODAY_MACRO.search(text):
uses_today.append(name)
problems = []
for required in sorted(needs):
# Classes that ship with a TeX distribution are not part of the package's own source
# distribution; only project-local classes (resume.cls, cv.cls) have to travel with it.
if required in CTAN_CLASSES:
continue
if f"{required}.cls" not in classes:
problems.append(
f"source distribution incomplete: requires {required}.cls, which is not in the "
f"folder. A build here only succeeds if a copy happens to exist elsewhere on the "
f"machine. Do NOT repair by copying the newest class in - the repo carries "
f"several class variants and older macros differ.")
if uses_today:
problems.append(
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.")
entry = {
"sources": sources,
"class_files": classes,
"requires_class": sorted(needs),
"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(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"):
for item in register.get(key, []):
classified.add(item["folder"] if isinstance(item, dict) else item)
packages = {}
for folder in sorted(OUTPUT.iterdir()):
if not folder.is_dir() or not any(folder.glob("*.tex")):
continue
# Only APPLICATION packages. A folder holding a report or analysis document is not a
# 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, record_pdfs)
entry["reuse_classification"] = (
"recorded" if f"output/{folder.name}" in classified else "UNCLASSIFIED")
if f"output/{folder.name}" not in classified:
entry["problems"].append(
"not classified in historical_outputs.json. Default until someone classifies "
"it: treat as historical_revalidate - never reuse a bullet from it without "
"re-checking the claim against claims.json first.")
packages[folder.name] = entry
return {
"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": 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()
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:
errors = check_manifest(recorded, manifest)
if errors:
for error in errors:
print("ERROR: " + error)
return 1
print("Package manifest is current.")
return 0
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():
print(f" {name}")
for problem in entry["problems"]:
print(f" - {problem[:150]}")
return 0
if __name__ == "__main__":
sys.exit(main())
+59
View File
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
"""Measure where the first real employment evidence starts on page one (review finding F18).
Usage: python resume_builder/helpers/page_metrics.py <resume.pdf> [more.pdf ...]
Requires pdftotext (ships with MiKTeX).
The review reported ~50-55% for four packages. This reproduces that measurement from the
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 re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
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([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'<page width="([\d.]+)" height="([\d.]+)"', xml)
if page is None:
raise ValueError("No page geometry found in pdftotext output")
height = float(page.group(2))
words = [(float(m.group(2)), m.group(5).lower())
for m in re.finditer(
r'<word xMin="([\d.]+)" yMin="([\d.]+)" xMax="([\d.]+)" yMax="([\d.]+)">([^<]*)</word>',
xml)]
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,
"experience_pct": round(100 * first_y / height, 1) if first_y else None,
"page1_text_lines": lines,
"page1_words": len(words),
}
if __name__ == "__main__":
for argument in sys.argv[1:]:
pdf = Path(argument)
data = measure(pdf)
pct = data["experience_pct"]
print(f"{pdf.parent.name:<34} experience starts at "
f"{pct if pct is not None else '?':>5}% of page 1 | "
f"{data['page1_text_lines']:>3} text lines, {data['page1_words']:>4} words")
+237 -11
View File
@@ -12,6 +12,7 @@ import argparse
import json import json
import re import re
import sys import sys
from functools import lru_cache
from pathlib import Path from pathlib import Path
@@ -50,6 +51,16 @@ def canonical_checks(claims: dict, history: dict) -> tuple[list[str], list[str]]
if duplicates: if duplicates:
errors.append(f"duplicate {collection} ids: {', '.join(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", []) skills = claims.get("skills", [])
skill_names = [item.get("name") for item in skills] skill_names = [item.get("name") for item in skills]
duplicates = sorted({item for item in skill_names if skill_names.count(item) > 1}) duplicates = sorted({item for item in skill_names if skill_names.count(item) > 1})
@@ -87,11 +98,46 @@ def canonical_checks(claims: dict, history: dict) -> tuple[list[str], list[str]]
if duplicates: if duplicates:
errors.append(f"historical output folders classified twice: {', '.join(duplicates)}") errors.append(f"historical output folders classified twice: {', '.join(duplicates)}")
if claims["identity"].get("swiss_permit", "").startswith("UNVERIFIED"): # F08: evidence-metadata coverage is reported, never gated. "unrecorded" means nobody
# wrote it down - it does NOT mean the claim is unevidenced, and failing on it would
# push someone to invent dates to get a green run.
claim_list = claims.get("claims", [])
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 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.get("identity") or {}).get("swiss_permit", "").startswith("UNVERIFIED"):
warnings.append("Swiss permit type remains unverified; generators must ask before naming it") warnings.append("Swiss permit type remains unverified; generators must ask before naming it")
return errors, warnings return errors, warnings
UNCHECKED = (
"whether a sentence implies ownership or scale beyond the evidence (Scope Discipline)",
"whether each bullet is actually supported by the claim it was planned from",
"whether a number in the document is real - unverified metrics look identical to real ones",
"whether tone, emphasis and ordering suit this employer",
"anything in a language or phrasing the pattern lists do not cover",
)
def workflow_checks() -> list[str]: def workflow_checks() -> list[str]:
errors: list[str] = [] errors: list[str] = []
checks = { checks = {
@@ -105,7 +151,7 @@ def workflow_checks() -> list[str]:
"<= 3 lines white space", "<= 3 lines white space",
"20-21 variable bullets", "20-21 variable bullets",
], ],
ROOT / ".agents" / "skills" / "critique" / "SKILL.md": [ ROOT / "resume_builder" / "reference" / "workflows" / "critique.md": [
"Publications 10%", "Publications 10%",
"Eight-Dimension Scoring", "Eight-Dimension Scoring",
], ],
@@ -118,18 +164,188 @@ def workflow_checks() -> list[str]:
return errors return errors
def scan_document(path: Path, claims: dict) -> list[str]: _COMMENT = re.compile(r"(?<!\\)%.*$", re.M)
_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|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
# of literal umlauts, and leaving them in made "Universit\"at der Bundeswehr" unmatchable.
_ACCENT = re.compile(r"""\\["'`^~=.]\s*\{?([a-zA-Z])\}?""")
_SSHARP = re.compile(r"\\ss\b")
def _deaccent(raw: str) -> str:
"""Fold LaTeX accent macros to their base letter.
German packages write Universit\\"at der Bundeswehr M\\"unchen rather than literal
umlauts. Without this the employer/education comparison could not see the institution
name at all, which produced a false positive on several correct resumes.
"""
return _SSHARP.sub("ss", _ACCENT.sub(r"\1", raw))
def visible_text(raw: str) -> str:
"""Document text as a reader sees it.
Three fixes for the scanner, all from review finding F06:
* LaTeX COMMENTS are dropped. A line such as `% never claim Terraform` is a note to
the author, not a claim, and flagging it taught the workflow to write around the
checker instead of fixing the document.
* MARKUP is removed but its argument text kept, so `\\textbf{Snow}flake` can no longer
smuggle a forbidden skill past a plain substring comparison.
* 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)
@lru_cache(maxsize=512)
def _forbidden_phrase(phrase: str) -> re.Pattern:
"""Forbidden wording, tolerant of inserted qualifiers.
The canonical lists are literal ("built a Data Mesh"), but the regression this rule
exists to stop was worded "Built a decentralized Data Mesh" - one adjective defeated the
substring test. Allow up to two extra words between tokens. A false positive here costs
a rewording; a false negative ships a scope claim the evidence does not support.
"""
tokens = [re.escape(token) for token in phrase.lower().split()]
return re.compile(r"(?<!\w)" + r"\W+(?:\w+\W+){0,2}".join(tokens), re.I)
_FOLD = str.maketrans({"ä": "a", "ö": "o", "ü": "u", "ß": "ss",
"Ä": "a", "Ö": "o", "Ü": "u", "é": "e", "è": "e"})
def fold(text: str) -> str:
"""Umlaut-insensitive form. claims.json stores 'Universitat der Bundeswehr Munchen'
while documents render 'Universitaet der Bundeswehr Muenchen', and that mismatch made an
education institution look like the Bundeswehr employment entry on three correct resumes."""
return text.translate(_FOLD).lower()
def _mentions(haystack: str, term: str) -> bool:
return re.search(r"(?<!\w)" + re.escape(term.lower()) + r"(?!\w)", haystack) is not None
def _negated(haystack: str, term: str) -> bool:
"""True when every mention of the term sits behind a negation."""
hits = list(re.finditer(r"(?<!\w)" + re.escape(term.lower()) + r"(?!\w)", haystack))
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.
This is NOT a truth check and must never be reported as one. It cannot judge whether a
sentence implies ownership the evidence does not support; that stays a reading task.
See `UNCHECKED` and the summary printed by main().
"""
errors: list[str] = [] errors: list[str] = []
text = read_text(path) raw = read_text(path)
text = visible_text(raw)
lowered = text.lower() lowered = text.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", []): for pattern in claims.get("global_forbidden_output_patterns", []):
if pattern.lower() in lowered: if pattern.lower() in lowered:
errors.append(f"{path}: forbidden output pattern: {pattern}") errors.append(f"{path}: forbidden output pattern: {pattern}")
# Per-claim forbidden wording. 14 of 26 claims carry a `forbidden` list and NONE of it
# was enforced before: exactly the gap that let "owned the full ML lifecycle" pass.
for claim in claims.get("claims", []):
for phrase in claim.get("forbidden", []):
if not isinstance(phrase, str) or len(phrase) < 8:
continue # too short to match safely as a substring
if _forbidden_phrase(phrase).search(lowered):
errors.append(f"{path}: {claim.get('id','?')} forbidden wording: {phrase}")
for skill in claims.get("skills", []): for skill in claims.get("skills", []):
if skill.get("output") == "forbidden" and skill["name"].lower() in lowered: name, output = skill["name"], skill.get("output")
errors.append(f"{path}: forbidden or unverified skill: {skill['name']}") mentions = [part for part in contexts if _mentions(part, name)]
if "\\begin{rsubsection}" in lowered: if output == "forbidden" and any(not _negated(part, name) and not _employer_mention(part, name) for part in mentions):
for line_number, line in enumerate(text.splitlines(), 1): errors.append(f"{path}: forbidden or unverified skill: {name}")
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")
for term in (exclusion["term"] for exclusion in claims.get("document_exclusions", [])):
if _mentions(lowered, term):
errors.append(f"{path}: excluded from all documents by standing user preference: {term}")
# Scan the comment-stripped RAW text: _MARKUP treats "\item [GENERATE: x]" as a command
# with an optional argument and would swallow the placeholder whole.
# Templates are EXEMPT: an unfilled placeholder is what makes a template a template. The
# defect is a placeholder surviving into a generated document.
for match in ([] if template else _PLACEHOLDER.finditer(_COMMENT.sub("", raw))):
errors.append(f"{path}: unresolved placeholder left in the document: {match.group(0)!r}")
identity = claims.get("identity", {})
for field in ("email", "phone"):
value = identity.get(field)
if not value:
continue
compact = re.sub(r"[\s()-]", "", lowered)
if re.sub(r"[\s()-]", "", value.lower()) not in compact:
kind = "e-mail" if field == "email" else "phone number"
errors.append(f"{path}: canonical {kind} not found ({value}); check for a wrong contact line")
# Anchor the date check on the EMPLOYER NAME, never on the job title: titles collide
# across employers ("Research Software Engineer" contains "Software Engineer") and that
# produced a false positive on two real resumes. Employers excluded from documents are
# skipped outright - their absence is the point, not a defect.
# Employment dates belong on a CV. A cover letter naming an employer in prose is not
# claiming a date, so requiring one there is a false positive.
is_cv = "resume" in path.name.lower() or "cv" in path.name.lower() or "lebenslauf" in path.name.lower()
excluded = {item["term"].lower() for item in claims.get("document_exclusions", [])}
schooling_removed = fold(lowered)
for school in claims.get("education", []):
for key in ("institution", "thesis_institution"):
institution = fold(school.get(key) or "")
if len(institution) > 5:
schooling_removed = schooling_removed.replace(institution, " ")
for employer in (claims.get("employment", []) if is_cv else []):
name = (employer.get("employer") or "").lower()
if not name or any(term in name for term in excluded):
continue
short = re.split(r"\s*\(|,| gmbh| ag| asa", name)[0].strip()
# Education institutions are removed first: "Universitaet der Bundeswehr Muenchen" is a
# school, not the Bundeswehr employment entry, and matching it flagged three real
# resumes that were entirely correct.
if len(short) < 4 or not _mentions(schooling_removed, fold(short)):
continue
start = employer.get("start", "")
year = start.split("-")[0] if start else ""
if year and year not in text:
errors.append(
f"{path}: {employer.get('id','?')} ({employer.get('employer')}) appears but its "
f"canonical start year {year} is absent; verify the dates")
if "\\begin{rsubsection}" in raw.lower():
for line_number, line in enumerate(raw.splitlines(), 1):
if "\\begin{rSubsection}" not in line: if "\\begin{rSubsection}" not in line:
continue continue
if re.search(r"Production Ownership|AI-Ready|Customer-Embedded|Shipping ML", line, re.I): if re.search(r"Production Ownership|AI-Ready|Customer-Embedded|Shipping ML", line, re.I):
@@ -137,7 +353,10 @@ def scan_document(path: Path, claims: dict) -> list[str]:
return errors return errors
TRIPLE = re.compile(r"\w+, [^,]{3,45}, [^,]{3,45} and |\w+, [^,]{3,45} and ") # Advisory only. German documents use "und"; without it the cadence hint silently
# did nothing on every German package. No external evidence supports a hard ~50% limit,
# so this stays a hint and never an error (F06).
TRIPLE = re.compile(r"\w+, [^,]{3,45}, [^,]{3,45} (?:and|und) |\w+, [^,]{3,45} (?:and|und) ")
def _bullet_blocks(text: str) -> list[list[str]]: def _bullet_blocks(text: str) -> list[list[str]]:
@@ -217,14 +436,21 @@ def main() -> int:
errors.extend(scan_document(path, claims)) errors.extend(scan_document(path, claims))
warnings.extend(cadence_checks(path)) warnings.extend(cadence_checks(path))
if args.document:
print("Mechanical checks only. NOT verified automatically:")
for item in UNCHECKED:
print(f" - {item}")
print()
for warning in warnings: for warning in warnings:
print(f"WARN: {warning}") print(f"WARN (advisory, never a gate): {warning}")
for error in errors: for error in errors:
print(f"ERROR: {error}") print(f"ERROR: {error}")
if errors: if errors:
print(f"FAIL: {len(errors)} error(s), {len(warnings)} warning(s)") print(f"FAIL: {len(errors)} error(s), {len(warnings)} warning(s)")
return 1 return 1
print(f"PASS: canonical system valid ({len(warnings)} warning(s))") scope = "mechanical checks passed" if args.document else "canonical system valid"
print(f"PASS: {scope} ({len(warnings)} advisory warning(s)). "
"This is not a truth certificate - see the unchecked list above.")
return 0 return 0
@@ -45,8 +45,8 @@ literals the session had flagged as risky.
**What is kept: the recording, not the rationing.** Every application still records fit class, hard **What is kept: the recording, not the rationing.** Every application still records fit class, hard
gate, channel, date, stage and outcome. That record is what makes patterns visible instead of gate, channel, date, stage and outcome. That record is what makes patterns visible instead of
re-derived, and it produced every useful finding to date. `application_cohort.json` continues as a re-derived. `job_scout/state/applications.json` is the durable registry; `application_cohort.json` is
durable log; `cohort_tracker.py summary` remains a read-only view. There is no slot to consume and no a generated historical-cohort projection. `cohort_tracker.py summary` is a read-only view. There is no slot to consume and no
application is blocked for lack of one. application is blocked for lack of one.
**Fit class is now a preparation signal, not a spend limit.** An Evidence Fit of 64 does not mean **Fit class is now a preparation signal, not a spend limit.** An Evidence Fit of 64 does not mean
+9 -1
View File
@@ -1,6 +1,6 @@
# Cover Letter Reference # 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 ## 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. 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 ## Evidence Rules
1. Read `resume_builder/canonical/claims.json` first. 1. Read `resume_builder/canonical/claims.json` first.
@@ -0,0 +1,41 @@
# Current applications
Generated from `job_scout/state/applications.json`; do not edit this view.
Use current outcome and next action below. Imported legacy notes are historical, not current instructions.
| Application | Current outcome | Interview reached | Next action |
|---|---|---|---|
| NVIDIA - Senior DevOps Engineer, Zürich (req JR2024256) | applied | not recorded | **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. |
| RUAG C5I - AI Engineer C5I, Thun (application ID 18027) | applied | not recorded | 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). |
| Schweizer Armee - Kommando Cyber (Kdo Cy), DevOps Engineer III (Data Platform), Zimmerwald (Ref JRQ$540-19848) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| Citadel Securities — Platform Engineer, Research Platform Engineering, Zürich (also NY/Miami) | applied | not recorded | **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. |
| SBB - Data Engineer (m/w/d), Asset Management Infrastrukturanlagen, Bern (Job ID 103755) | withdrawn_by_candidate | yes | Closed. For future SBB roles assess Niveau L/Kader; do not generalize SBB bands to other employers. |
| AWS (AWS EMEA SARL, Switzerland Branch) — Senior Forward Deployed Engineer, Zürich (job 10504263) | not_proceeding | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| Google — Forward Deployed Engineer III, Google Cloud GTM (French, German), Zürich (req 78350205438567110) | not_proceeding | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| SDU Center for Energy Informatics, Odense DK — PhD, Theme 2 Predictive Maintenance & Asset Management of Smart Energy Networks (job 4159) | not_proceeding | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| 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) | 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. |
| Google — Senior Data Engineer (Merchant Data Science), Zürich/MV | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| Snowflake — Sr SWE, Enterprise (Observe by Snowflake), Zürich | closed_no_response | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| Isovalent (Cisco) Sr Data Engineer, Observability | role_closed | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| Google Zürich Sr SWE Infrastructure (Data Pipeline) | not_proceeding | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| Kraken AI Infrastructure | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| Infineon Doctoral | withdrawn_by_candidate | yes | Closed; no pending application action. Read recorded history before reopening. |
| Infineon AI Engineer | not_proceeding | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| Apple Data Engineer (ISE, Zurich) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| Google FDE GenAI (Zurich) | paused | not recorded | Likely abandon |
| Equinor — Manager / AI Architect, Agentic systems (Stavanger/Oslo/Rotvoll/Sandsli; req JR106747) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
| QuantCo Cloud Engineer (Europe/Zürich) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
## Current 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.
+69 -5
View File
@@ -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 | | 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 | | 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 | | 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 ## 2. Evidence Preflight
@@ -34,7 +38,7 @@ The reader must see these items in this order:
1. Name, professional identity, contact/location/work authorization. 1. Name, professional identity, contact/location/work authorization.
2. Optional 2--3 line summary. 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. 4. Professional Experience, reverse chronological.
5. Education and relevant certifications once. 5. Education and relevant certifications once.
@@ -59,7 +63,9 @@ Use relevance and evidence, not page-filling quotas. For Dennis's normal 2-page
- Swisscom: 4--5 bullets. - Swisscom: 4--5 bullets.
- Bosch: 3--4 bullets. - Bosch: 3--4 bullets.
- Fraunhofer, Vizrt and Generali: 0--1 bullet each. - Fraunhofer, Vizrt and Generali: 0--1 bullet each.
- Capgemini/Bundeswehr: include only when directly relevant. - **Capgemini: NEVER include.** Standing user preference (`config.md`); the resulting
Nov 2014 - May 2015 gap is intentional and must not be "fixed" or explained away.
- Bundeswehr: include only when directly relevant (defence, public-sector and security employers).
- Typical total: 11--14 bullets. This is a guide, not a requirement. - Typical total: 11--14 bullets. This is a guide, not a requirement.
A strong shorter resume is better than a full page padded with weak evidence. White space is acceptable when hierarchy and readability are strong. A strong shorter resume is better than a full page padded with weak evidence. White space is acceptable when hierarchy and readability are strong.
@@ -68,14 +74,14 @@ A strong shorter resume is better than a full page padded with weak evidence. Wh
The summary is optional. When used: 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. - State the demonstrated professional identity and two strongest proof points.
- Do not state the desired role as if it were prior experience. - Do not state the desired role as if it were prior experience.
- Avoid credential lists and unsupported scale claims. - Avoid credential lists and unsupported scale claims.
## 6. Skills ## 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. - List only canonical `allowed` or context-appropriate `allowed-with-context` skills.
- Certification-only skills stay in certification context. - Certification-only skills stay in certification context.
- Do not use Expert/Proficient/Familiar self-ratings. - Do not use Expert/Proficient/Familiar self-ratings.
@@ -157,3 +163,61 @@ Classify every JD requirement as:
- **Gap:** no reliable evidence. - **Gap:** no reliable evidence.
Never turn Adjacent into Direct by vocabulary substitution. A required, title-defining Gap is an application-fit problem, not a resume-writing problem. Never turn Adjacent into Direct by vocabulary substitution. A required, title-defining Gap is an application-fit problem, not a resume-writing problem.
---
## Page-one weighting and rendered density (review findings F18/F19, 2026-09-11)
**The problem, measured:** across four compiled packages the first real employment entry began
at **45-55% of page one** (NVIDIA 55.4%, SBB 50.6%, RUAG 50.3%, Aker BP 45.4%). A reader
reaching the middle of the page has seen a profile and a skills block, and no evidence yet.
Measure with `helpers/page_metrics.py <pdf>` rather than estimating.
These are **diagnostics, not targets.** There is no pixel goal and no rule that page two must
be filled. A package that reads well at 50% is fine; a skills block that pushed evidence there
is not.
### Skills: budget RENDERED lines, not source macros
The old rule counted `\skillline` macros, which does not control density: six categories with
long value lists wrap to **twelve or more rendered lines**. The NVIDIA package is the worked
example - six `\skillline` calls, one of them 240 characters.
- **Aim for roughly 4-8 rendered lines of skills**, verified in the compiled PDF.
- **Cut anything the experience bullets already prove.** A skills line repeating "Kubernetes,
Docker, GitLab CI/CD" when three bullets below say the same thing spends page-one space to
say it twice.
- **Keep what the JD actually gates on** plus the ATS literals that are genuinely evidenced.
Drop adjacent nice-to-haves; they read as padding and cost the reader's first screen.
- Certifications belong on one line, at the end, not spread across categories.
**Worked example, measured 2026-09-11.** The submitted NVIDIA CV was rebuilt from a copy with
this guidance applied - six skill lines cut to four by removing what the experience bullets
already prove, and the education entries split per F19. Result: the experience section moved
from **55.4% to 44.0%** of page one, page-one text lines from 47 to 39, still two pages, zero
overfull boxes, validator PASS. The submitted document itself was NOT modified (F17: archives
are evidence).
### Order: employer chronology stays, evidence order inside a position does not
- **Keep reverse-chronological employers.** Swiss and German readers expect it and breaking it
looks evasive.
- **Within a position, lead with the bullet closest to the target role**, not with the one that
happens to be most senior or most recent. The chronology is between employers, not inside one.
- After the short profile, the next thing should be **either the experience section or a very
compact competence block**. Not both at full length.
### Education (F19)
Long single-cell entries - degree, focus, institution, right-aligned date - produce visible
word-spacing and mid-word breaks. Seen in SBB, NVIDIA and Aker BP.
- **Split the entry**: degree on the entry line, institution (and focus) on the line below.
`RUAG_AI_Engineer_C5I` is the model that already does this well.
- **Grades and thesis stay only where they carry concrete relevance.** For an experienced
industry profile they usually do not; `claims.json` `grade_output_rule` already says grades
are off by default. The M.Eng. thesis is worth naming when the role touches condition
monitoring, diagnostics or industrial sensor data - as the SBB asset-management req did -
and otherwise not.
- Do not let an immutable template line force education detail into a document that does not
need it.
+3 -3
View File
@@ -5,7 +5,7 @@
Before generation, editing or critique: Before generation, editing or critique:
1. Read `resume_builder/canonical/claims.json` completely. 1. Read `resume_builder/canonical/claims.json` completely.
2. Read `config.md` and `AGENTS.md` corrections/status. 2. Read `config.md`, `AGENTS.md` corrections and `resume_builder/reference/current_applications.md`. Application status is authoritative in `job_scout/state/applications.json`; the Markdown view and old cohort JSON are generated projections. Historical events are not current instructions.
3. Run `python resume_builder/helpers/validate_resume_system.py`. 3. Run `python resume_builder/helpers/validate_resume_system.py`.
4. Treat canonical claims as higher authority than extractions, experience files, bundles, sessions or outputs. 4. Treat canonical claims as higher authority than extractions, experience files, bundles, sessions or outputs.
5. Never use a file under `output/` as source content for a new application. Consult `resume_builder/canonical/historical_outputs.json`. 5. Never use a file under `output/` as source content for a new application. Consult `resume_builder/canonical/historical_outputs.json`.
@@ -33,7 +33,7 @@ Read `resume_builder/reference/application_strategy.md` and `critique_framework.
## Audience and Cover-Letter Decisions ## 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. Apply `cl_reference.md` and record `Cover Letter Decision: YES/NO`. A deliberately omitted letter is a valid complete package.
@@ -88,4 +88,4 @@ After explicit approval:
## Progress and Recovery ## Progress and Recovery
Give short progress updates at major steps. Before ending or clearing context, update session Status and the Active Sessions pointer. If interrupted, add a Resume Point describing completed work and the exact next action. Give short progress updates at major steps. Before ending or clearing context, update session Status and the application registry through the tracker; regenerate its views. Do not duplicate status in AGENTS or CLAUDE. If interrupted, add a Resume Point describing completed work and the exact next action.
@@ -0,0 +1,59 @@
<!-- Shared workflow version: 2026-09-11 -->
# Critique an Application Package
## Startup
1. Read `resume_builder/reference/shared_ops.md`.
2. Read `resume_builder/canonical/claims.json` completely.
3. Read `config.md`, the session file, and the real verbatim JD.
4. Stop if the JD is reconstructed, incomplete or inferred.
5. Read `resume_builder/reference/critique_framework.md` and `resume_builder/support/ai_fingerprint_rules.md`.
6. Read the resume and optional cover letter. Never use either as evidence; verify against canonical claims and experience files.
## Preflight
Run:
```powershell
python resume_builder/helpers/validate_resume_system.py --document <resume.tex>
```
Add `--document <cover_letter.tex>` when a letter exists. Any failure is Tier 1 and prevents approval.
Compile the documents and inspect the PDFs. Extract resume text with `pdftotext` and confirm that employer, formal title and dates appear in the expected order.
## Critique
Follow `critique_framework.md` exactly:
1. Hard-gate audit.
2. Evidence Fit score, independent of writing.
3. Document Quality score, independent of role fit.
4. Channel Strength rating.
5. Competitive and reader-sequence assessment.
6. Canonical claim audit.
7. Tiered fixes.
8. Cover-letter decision/critique.
9. Mechanical verification.
Do not issue a single overall score. Do not label a high-quality document submit-ready when Evidence Fit fails a hard gate.
## Cover-Letter Handling
If the session records `Cover Letter Decision: NO`, do not penalize absence. If a letter exists, first assess whether it should exist under `cl_reference.md`.
## Save and Report
Save `output/<Folder>/critique_<name>.md`. Update the session with:
- Evidence Fit score and Core/Adjacent/Stretch classification.
- Hard-gate result.
- Document Quality score.
- Channel Strength.
- Tier 1 fixes.
- Critique status and date.
Present the fit verdict first, then the two scores and actionable fixes. Wait for explicit user approval before finalization or editing.
When approved, run the finalization checks from `shared_ops.md`. A package with a hard-gate NO-GO may be archived, but must not be described as recommended for submission.
@@ -0,0 +1,48 @@
<!-- Shared workflow version: 2026-09-11 -->
# Edit a Resume or Cover Letter
## Startup
1. Read `shared_ops.md` and `resume_builder/canonical/claims.json` completely.
2. Run the system validator.
3. Read the session, real JD, target `.tex`, critique/feedback, selected profile rules and relevant experience files.
4. Check `historical_outputs.json`. A package marked unsafe must be regenerated from canonical sources rather than lightly polished.
5. If the package was already submitted, preserve it as history. Create a clearly named new version only when the user requests a reusable corrected document.
## Diagnose
Classify requested changes as:
- Truth/provenance correction.
- Fit/framing correction.
- Information hierarchy or readability.
- Content selection.
- Mechanical/LaTeX.
- Blocked because evidence is absent.
Do not treat whitespace, equal bullet lengths or an exact character target as defects. Do not add filler. A critique cannot authorize an unsupported claim.
Present a concise edit plan and wait for explicit confirmation unless the user already approved specific edits.
## Execute
- Source every work claim from canonical IDs and experience files.
- Keep employer/title/date hierarchy conventional.
- Preserve natural bullet lengths and verified ownership scope.
- Remove unsupported skills rather than replacing them with synonyms.
- Update a cover letter only when its session decision is YES or the user explicitly requests it.
- If edits reveal a hard fit gate, update the Evidence Fit classification; writing improvements cannot remove the gate.
## Verify
Run the validator on every edited document, compile, inspect the PDF, and verify extracted text order. Compare before/after for:
- validator failures;
- page count;
- employer/title/date visibility;
- bullet count and relevance;
- duplicated sections;
- readability at normal size.
Update the session Edit History, Document Quality, status and next action. Mark the critique stale after material edits. Present results and wait for approval.
@@ -0,0 +1,43 @@
<!-- Shared workflow version: 2026-09-11 -->
# Decide and Generate a Cover Letter
## Startup
1. Read `shared_ops.md`, `resume_builder/canonical/claims.json`, `cl_reference.md`, the session, real JD and finished resume.
2. Run the validator on the resume.
3. Confirm that the session has a completed resume and a recorded Cover Letter Decision.
## Decision Gate
If the session says NO, explain the recorded reason and stop unless the user explicitly asks to override it. If no decision exists, apply `cl_reference.md` before writing and record YES/NO.
Do not generate a letter merely because an upload field exists or because older workflow expected a three-page package.
## Plan
State in one sentence what the letter adds beyond the resume. Select at most two evidence themes. Verify any named company/product/programme hook from a current primary source and save its URL in the session.
If the only possible content repeats resume bullets or paraphrases company news, change the decision to NO and explain why.
## Generate
- 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.
- Use only claims already present in both the canonical registry and resume.
- Never introduce a new tool, metric, customer, ownership claim or result.
## Verify
Run:
```powershell
python resume_builder/helpers/validate_resume_system.py --document <resume.tex> --document <cover_letter.tex>
```
Compile and inspect the letter. Confirm it is readable, within the employer's limit, and materially different from the resume. Update the session and present the letter for approval. Point to `/critique` next.
@@ -0,0 +1,86 @@
<!-- Shared workflow version: 2026-09-11 -->
# Generate a Resume
## Input
Accept a JD file, pasted JD, or URL. Obtain the real posting text before analysis. If it cannot be retrieved, ask the user to paste it; never reconstruct it.
## Phase 0 — Canonical and JD Preflight
1. Read `resume_builder/reference/shared_ops.md`.
2. Read `resume_builder/canonical/claims.json` completely.
3. Read `config.md`, `AGENTS.md`, `application_strategy.md`, `resume_reference.md`, and `critique_framework.md`.
4. Run `python resume_builder/helpers/validate_resume_system.py`.
5. Verify and save the verbatim JD; record source/date/status.
6. Create the output folder and session from `session_file_template.md`.
## Phase 1 — Fit Gate Before Writing
Build a requirement table with Required/Preferred and Direct/Adjacent/Gap/Constraint classifications. Cite canonical claim IDs for every Direct or Adjacent match.
Compute Evidence Fit and identify hard gates using `critique_framework.md`. Then assign:
- Core.
- Adjacent.
- Stretch.
- No-go.
Record Channel Strength and a warm-channel action for Core roles.
Stop and present the fit decision before resume planning when:
- a hard gate fails;
- the role is Stretch;
- a practical constraint is unresolved.
Proceed on a no-go only after the user explicitly overrides the stated reason. An override does not change the fit classification.
## Phase 2 — Audience and Content Plan
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 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.
- Normally 11--14 bullets total, with no page-fill quota.
For each proposed bullet show canonical ID, evidence type, scope, and why it belongs. Identify any impact metric that still needs user confirmation.
Apply `cl_reference.md` and propose `Cover Letter Decision: YES/NO` with one reason.
Present the plan and wait for explicit user confirmation before generation.
## Phase 3 — Generate
Copy `resume.cls` and `resume_template.tex` into the output folder. For a Swiss/DACH audience, also read `resume_template_dach.tex` as an audience-policy overlay; it is not a standalone document. Write fresh content from canonical claims and experience records; never copy a historical output.
Rules:
- Employer, formal/transparent title, dates and location come first.
- Do not use tailored themes as job titles.
- Mix natural bullet lengths.
- Use only verified metrics and outcomes.
- Preserve allowed verbs and ownership scope.
- Never add a skill to mirror the JD.
- Certifications appear once.
## Phase 4 — Validate and Inspect
Run:
```powershell
python resume_builder/helpers/validate_resume_system.py --document <resume.tex>
```
Fix every failure. Compile the LaTeX, inspect both pages, and run `pdftotext` to verify parsing and information order. Do not add filler to reduce whitespace.
Update the session with the as-built content, validation results, page count, cover-letter decision, and next action.
Present the finished resume and the original Evidence Fit classification. Wait for approval. If the cover-letter decision is YES, point to `/make-cl`; otherwise point directly to `/critique`.
@@ -0,0 +1,79 @@
<!-- Shared workflow version: 2026-09-11 -->
# Build or Update the Knowledge Base
## Authority Model
The pipeline is:
`source documents -> extractions -> canonical claims -> experience records/bundles -> generated applications`
Generated applications never flow backward into the KB.
Read `resume_builder/canonical/claims.json`, `config.md`, and all relevant extractions before changing normalized sources. Preserve raw source documents. A verified correction must update canonical claims first and then every dependent normalized file.
## Canonical Claim Record
For each employment item or achievement, record:
- stable ID;
- employer, title, dates and location;
- factual scope;
- individual vs shared ownership;
- allowed verbs;
- forbidden overstatements;
- metric/result and verification status;
- evidence source;
- last verified date.
When ownership is ambiguous, use contributing/shared scope and ask the user. Do not default to full ownership.
## Experience Files
Experience files explain facts and context. They may contain example phrasings, but those examples cannot override canonical scope and should not be optimized to fixed line counts.
Every achievement section must include canonical ID, source, ownership scope, status, verified result/metric state, relevant skills and role relevance.
## Skills Taxonomy
Use only these evidence levels:
- Production — current.
- Production — historical.
- Hands-on — current.
- Project / proof of concept.
- Certification / coursework.
- Unverified / never used.
Do not assign Expert/Proficient/Familiar labels. Add a skill to canonical claims before it can appear in a generated document.
## Role Bundles
Each bundle must contain:
1. Defensible positioning.
2. Good-fit roles and hard-gate cautions.
3. Achievement priorities.
4. Safe reframing rules.
5. Evidence-backed skills guidance.
6. Conditional cover-letter guidance.
Bundles may rank evidence; they may not broaden it. Title-defining gaps must be stated explicitly.
## Significance Context
Keep company/industry context separate from candidate impact. Mark external claims for re-verification at application time. Never attach generic scale, economics, customers or market trends to the candidate.
## Correction Propagation
After any correction, search canonical claims, config, extractions, experience files, bundles, support files, templates and active drafts. Historical submitted outputs remain unchanged but update `historical_outputs.json` when they become unsafe for reuse.
## Validation
Run:
```powershell
python resume_builder/helpers/validate_resume_system.py
```
Then review the diff for dates, titles, ownership, metrics and skill evidence. Present the changed claim records and affected downstream files to the user for confirmation.
@@ -0,0 +1,98 @@
<!-- Shared workflow version: 2026-09-11 -->
# Source Evidence Extraction
Use this skill when adding new evidence to `knowledge_base/extractions/`. Extraction preserves what a source supports; it does not tailor claims to a vacancy.
## Required inputs
1. Read `config.md` and the anti-fabrication rules in `AGENTS.md`.
2. Read the complete source material.
3. Check `resume_builder/canonical/claims.json` for existing facts and identifiers.
4. Inspect related extraction files so new evidence does not create a duplicate or contradiction.
Generated resumes, cover letters, critiques, and session files under `output/` are never evidence sources.
## Extraction procedure
### 1. Identify the source
Record:
- source filename and type;
- source date, if known;
- whether it is primary evidence, user-authored notes, or a secondary description;
- the person, employer, project, or publication it concerns.
### 2. Separate evidence from inference
For every potentially reusable fact, record:
- the source-supported statement;
- the exact supporting passage or precise location;
- any interpretation separately as `Inference`;
- confidence: `verified`, `user-confirmed`, `needs verification`, or `do not use`.
Never silently convert an inference into a verified claim.
### 3. Capture career facts precisely
When applicable, extract:
- exact employer, formal title, location, and start/end dates;
- ownership scope and collaborators;
- work performed, method, technology, and domain;
- outcome and beneficiary;
- metrics with units, baseline, period, and attribution;
- publication status, authorship position, and venue;
- current versus historical skill use;
- certification versus practical experience.
For large-company work, describe only the user's component, domain, pipeline, service, or contribution. Do not infer ownership of organization-wide platforms or transformations.
### 4. Record claim controls
For each major claim, include where useful:
- allowed verbs;
- verbs or framings that would overstate ownership;
- safe resume wording;
- known contradictions requiring resolution;
- whether a metric is verified, qualitative only, or still missing.
### 5. Write the extraction
Create or update a focused Markdown file in `knowledge_base/extractions/`. Use a clear structure such as:
```markdown
# Subject
## Source
## Verified facts
## Ownership and attribution
## Outcomes and metrics
## Skills and recency
## Safe claim seeds
## Inferences / open questions
## Conflicts with existing KB
```
Claim seeds are factual building blocks, not polished resume bullets. Do not add unsupported impact language for relevance.
## Publication rules
- Never infer `published`, `accepted`, or `under review` from a manuscript file.
- Preserve the user's author position.
- Institutional funding is not a personal award.
- Follow `config.md` provenance flags.
## Completion
Report:
- files created or updated;
- newly verified facts;
- unresolved contradictions or missing metrics;
- suggested next command: use `setup-build-kb` to promote reviewed evidence into the canonical registry, experience files, bundles, and support files.
Do not update canonical claims from an unreviewed extraction.
@@ -286,7 +286,7 @@ Each achievement has a **Significance** line (why it matters to any reader) and
| Role Type | Priority | Lead Verb | Framing Angle | | 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 |
--- ---
+6 -2
View File
@@ -60,12 +60,16 @@ Do not use Expert/Proficient/Familiar labels in resumes. Evidence and recency ar
| Domain-grounded assistants/custom GPTs | Hands-on — current | Configured with curated knowledge; no fine-tuning or formal evaluation | | Domain-grounded assistants/custom GPTs | Hands-on — current | Configured with curated knowledge; no fine-tuning or formal evaluation |
| Copilot, Kiro | Hands-on — current | AI-assisted engineering tools, not LLM product engineering | | Copilot, Kiro | Hands-on — current | AI-assisted engineering tools, not LLM product engineering |
| Linux administration | Professional 2020–2022 (BS-6) + personal and current (PP-3) | Employer-side use ended Dec 2022; PP-3 is a personal Debian server and must be named as such. Not evidence of physical-datacenter work |
| Ansible | Professional — intensive at Bosch (BS-6) | Configuration management and IaC, including self-written extensions. Does NOT license Terraform. **GitOps is forbidden** — push-based execution, no reconciliation agent |
| Bash / shell scripting | Professional-historical + personal-current (BS-6, PP-3) | Added to `claims.json` 2026-09-05. Does NOT license Groovy, nor systems/kernel-level claims |
## Certification-Only Signals ## Certification-Only Signals
| Skill | Evidence | | Skill | Evidence |
|---|---| |---|---|
| TensorFlow / Keras | IBM AI Engineering coursework | | TensorFlow / Keras | IBM AI Engineering coursework |
| PyTorch | Coursework/personal evidence only; verify before listing outside certification context | | PyTorch | **Certification-backed** (IBM AI Engineering, 4 Jun 2020, verify code 3ZBZFVAL6A34). `claims.json` output policy is `certification-context-only`: list only beside the certificate |
| Spark ML | Coursework context only unless professional evidence is added | | Spark ML | Coursework context only unless professional evidence is added |
| AI for Trading / quantitative ML | Udacity/WorldQuant Nanodegree | | AI for Trading / quantitative ML | Udacity/WorldQuant Nanodegree |
@@ -90,7 +94,7 @@ Do not use Expert/Proficient/Familiar labels in resumes. Evidence and recency ar
| iSAQB CPSA — Foundation | iSAQB | 2016; no expiry | | iSAQB CPSA — Foundation | iSAQB | 2016; no expiry |
| ITIL Foundation | PEOPLECERT / AXELOS | 2016; no expiry | | ITIL Foundation | PEOPLECERT / AXELOS | 2016; no expiry |
| AI for Trading Nanodegree | Udacity / WorldQuant | 2021 | | AI for Trading Nanodegree | Udacity / WorldQuant | 2021 |
| IBM AI Engineering Specialization | IBM / Coursera | Completion year not recorded | | IBM AI Engineering Specialization | IBM / Coursera | **Completed 4 Jun 2020**, no expiry, verify code 3ZBZFVAL6A34 (certificate read 2026-08-27). Explicitly non-credit — never present as an academic qualification |
The Swisscom Security Champion assignment is not a certification and does not belong in this table. The Swisscom Security Champion assignment is not a certification and does not belong in this table.
@@ -27,7 +27,12 @@ Bern, Switzerland $\vert$
\textit{City, country} \textit{City, country}
\vspace{1em} \vspace{1em}
\today % APPLICATION DATE -- replace with the literal date this letter is sent,
% e.g. 11 September 2026. Do NOT use \today (review finding F17, 2026-09-11):
% it makes the document date-dependent, so recompiling a historical letter silently
% restamps it with the current date. 17 existing packages already carry that defect
% and are deliberately left alone; new ones must not add to it.
[GENERATE: application date, literal]
\vspace{1em} \vspace{1em}
\textbf{Application for \textit{exact role title}} \textbf{Application for \textit{exact role title}}
+16
View File
@@ -1,6 +1,22 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% TEMPLATE: Academic CV (cv.cls) % TEMPLATE: Academic CV (cv.cls)
% FIXED sections contain actual content — copy verbatim during generation % FIXED sections contain actual content — copy verbatim during generation
% ============================================================================
% ACADEMIC LEGACY TEMPLATE -- NOT the industry path (review finding F09, 2026-09-11)
%
% This is the long-form ACADEMIC CV. It still uses fixed page targets, rendered-line
% budgets and 2L/3L bullet variants -- all of which the project dropped for industry
% documents, where natural bullet length and relevance govern selection.
%
% Use resume_template.tex (International Tech) or resume_template_dach.tex (Swiss/DACH)
% for every employer application. A European PROFESSIONAL CV is not an academic long CV;
% do not reach for this file just because a posting says "CV".
%
% Use this only for academic or research applications that genuinely want a long CV with
% publications. Its [GENERATE: ...] placeholders and asset expectations (GS.png, orcid.png)
% must all be resolved before output -- the document scanner now fails on a leftover
% placeholder.
% ============================================================================
% JD-DEPENDENT sections have [GENERATE: ...] placeholders — replace per JD % JD-DEPENDENT sections have [GENERATE: ...] placeholders — replace per JD
% %
% SETUP INSTRUCTIONS: % SETUP INSTRUCTIONS:
+12 -5
View File
@@ -33,14 +33,13 @@
\begin{document} \begin{document}
\begin{rSection}{Summary} \begin{rSection}{Summary}
Staff data engineer with 12+ years of production software experience across telecom, semiconductor manufacturing, broadcast technology and insurance. Owns business-critical pipeline components at Swisscom and previously integrated containerized ML inference into a continuously operating Bosch semiconductor fab. Staff data engineer with over ten years of production software experience across telecom, semiconductor manufacturing, broadcast technology and insurance. Owns business-critical pipeline components at Swisscom and previously integrated containerized ML inference into a continuously operating Bosch semiconductor fab.
\end{rSection} \end{rSection}
\begin{rSection}{Technical Skills} \begin{rSection}{Technical Skills}
\skillline{Languages}{Python, SQL; Java and C\# (professional, historical); C++ and JavaScript (limited historical use)} \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{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{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} \skillline{Certifications}{AWS Solutions Architect -- Associate; Data Engineering with AWS; iSAQB CPSA-F; ITIL Foundation}
\end{rSection} \end{rSection}
@@ -80,11 +79,19 @@ Staff data engineer with 12+ years of production software experience across tele
\end{rSection} \end{rSection}
% EDUCATION LAYOUT (review finding F19, 2026-09-11)
% Keep the degree on the entry line and the institution on the line below. Cramming
% degree + focus + institution into one cell with a right-aligned date produces visible
% word spacing and mid-word breaks - seen in the SBB, NVIDIA and Aker BP packages.
% Grades and thesis only where they carry concrete relevance; see claims.json
% grade_output_rule, which keeps grades off by default.
\begin{rSection}{Education} \begin{rSection}{Education}
\compactentry{M.Eng. Computer Aided Engineering (Software Design \& Engineering), Universität der Bundeswehr München}{Apr 2012 -- Oct 2013} \compactentry{M.Eng. Computer Aided Engineering}{Apr 2012 -- Oct 2013}
Thesis at Tongji University, Shanghai: \textit{Development of a Web-Based Remote Fault Diagnosis System}; grade 1.0. 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{rSection}
\end{document} \end{document}
+96
View File
@@ -0,0 +1,96 @@
from __future__ import annotations
import contextlib
import io
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "resume_builder/helpers"))
import application_records as records
import application_tracker as tracker
from argparse import Namespace
import copy
class ApplicationRecordTests(unittest.TestCase):
def test_generated_views_are_current(self):
self.assertEqual([], records.render(records.load(), check=True))
def test_imported_outcomes_preserve_interview_and_closed_state(self):
applications = records.load()["applications"]
self.assertEqual(len(applications), len({row["id"] for row in applications}))
sbb = next(row for row in applications if row["company"] == "SBB")
self.assertEqual("withdrawn_by_candidate", sbb["outcome"])
self.assertTrue(records.interview_reached(sbb))
for token in ("200040836", "JR106747", "JRQ$540-19848"):
row = next(row for row in applications if token in row["label"])
self.assertEqual("rejected_no_interview", row["outcome"])
def test_both_agents_link_to_one_view(self):
for name in ("AGENTS.md", "CLAUDE.md"):
text = (ROOT / name).read_text(encoding="utf-8")
section = text.split("## Active Sessions", 1)[1].split("## KB Corrections", 1)[0]
self.assertIn("resume_builder/reference/current_applications.md", section)
self.assertNotIn("| Session |", section)
def test_atomic_write_leaves_no_shared_temp_file(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "state.json"
records.atomic_write(path, '{"value": 1}\n')
self.assertEqual({"value": 1}, json.loads(path.read_text()))
self.assertEqual([path], list(Path(directory).iterdir()))
def args(self, **overrides):
fields = dict(id=None, company="Example", role="Engineer", date="2026-09-11", job_id="req-1", url=None,
fit_class="core", evidence_fit=79, hard_gate="pass", channel="weak", outcome="applied",
fit_override_reason=None, rubric_version="test", next_action="Await response.")
fields.update(overrides)
return Namespace(**fields)
def test_more_than_ten_applications_and_idempotent_retry(self):
data = records.load()
for index in range(11):
tracker.add_application(data, self.args(job_id=f"new-{index}", role=f"Engineer {index}"))
count = len(data["applications"])
tracker.add_application(data, self.args(job_id="new-10", role="Engineer 10"))
self.assertEqual(count, len(data["applications"]))
def test_bad_dates_scores_and_unexplained_classification_are_rejected(self):
for fields in ({"date":"2026-99-11"}, {"evidence_fit":101}, {"evidence_fit":float("nan")},
{"fit_class":"adjacent"}, {"hard_gate":"fail"}):
with self.subTest(fields=fields), self.assertRaises(ValueError):
tracker.add_application(records.load(), self.args(**fields))
def test_outcome_update_preserves_interview_event_and_is_idempotent(self):
data = records.load()
tracker.add_application(data, self.args())
row = data["applications"][-1]
invited = Namespace(id=row["id"], date="2026-09-12", event="interview_invited", outcome=None, note=None, next_action=None)
tracker.update_application(data, invited)
withdrawn = Namespace(id=row["id"], date="2026-09-13", event=None, outcome="withdrawn_by_candidate", note="Decision", next_action=None)
tracker.update_application(data, withdrawn)
tracker.update_application(data, withdrawn)
self.assertEqual(3, len(row["events"]))
self.assertTrue(records.interview_reached(row))
self.assertEqual("withdrawn_by_candidate", row["outcome"])
tracker.add_application(data, self.args())
self.assertEqual("withdrawn_by_candidate", row["outcome"])
def test_transaction_failure_does_not_write_registry(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "applications.json"
initial = json.dumps(records.load())
path.write_text(initial, encoding="utf-8")
with patch.object(records, "REGISTRY", path):
with self.assertRaises(ValueError), records.transaction() as data:
data["applications"][0]["outcome"] = "made-up"
self.assertEqual(initial, path.read_text(encoding="utf-8"))
self.assertFalse(path.with_suffix(".lock").exists())
path.with_suffix(".lock").write_text("another writer")
with self.assertRaises(ValueError), records.transaction():
pass
+108
View File
@@ -0,0 +1,108 @@
"""Negative regression tests for the document scanner (review findings F05 and F06).
Each case below is a counter-proof from `output/Project_Review_2026-09-11/project_review.tex`
that the scanner accepted before 2026-09-11. The point of these tests is that a green
validator run means something; they are not a claim that the scanner proves truthfulness.
See `validate_resume_system.UNCHECKED` for what deliberately stays a reading task.
"""
from __future__ import annotations
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "resume_builder" / "helpers"))
import validate_resume_system as validator # noqa: E402
CLAIMS = validator.load_json(validator.CLAIMS_PATH)
def scan(body: str) -> str:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "candidate.tex"
path.write_text(
"\\documentclass{resume}\n\\begin{document}\n"
"\\contactline{dennis@thiessen.io $\\vert$ +41 795 955 585}\n"
f"{body}\n\\end{{document}}\n",
encoding="utf-8")
return " | ".join(validator.scan_document(path, CLAIMS))
class ScannerCatchesTests(unittest.TestCase):
"""Things that must fail. Every one of these passed before the fix."""
def test_solo_ownership_of_a_company_scale_platform(self):
# The recorded regression wording carried an adjective the literal list missed.
self.assertIn("SW-7", scan(r"\item Built a decentralized Data Mesh for the group."))
def test_full_ml_lifecycle_ownership(self):
self.assertIn("BS-1", scan(r"\item I owned the full ML lifecycle end to end."))
def test_employer_excluded_from_all_documents(self):
self.assertIn("Capgemini", scan(r"\item Capgemini Deutschland GmbH, Nov 2014 -- May 2015."))
def test_certification_only_skill_without_its_context(self):
self.assertIn("PyTorch", scan(r"\item Trained production models in PyTorch."))
def test_forbidden_skill_split_by_markup(self):
# \textbf{Snow}flake defeated a plain substring comparison.
self.assertIn("Snowflake", scan(r"\item Delivered \textbf{Snow}flake pipelines."))
def test_unregistered_but_forbidden_skill(self):
self.assertIn("Groovy", scan(r"\item Maintained Groovy build jobs."))
def test_unresolved_placeholder(self):
self.assertIn("placeholder", scan(r"\item [GENERATE: Bullet]"))
def test_wrong_contact_details(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "wrong.tex"
path.write_text(
"\\documentclass{resume}\n\\begin{document}\n"
"\\contactline{wrong@example.com $\\vert$ +41 000 000 000}\n"
"\\end{document}\n", encoding="utf-8")
found = " | ".join(validator.scan_document(path, CLAIMS))
self.assertIn("e-mail", found)
self.assertIn("phone", found)
class ScannerAllowsTests(unittest.TestCase):
"""Things that must NOT fail. False positives taught the workflow to write around the checker."""
def test_latex_comment_is_not_a_claim(self):
self.assertEqual("", scan("% Never claim Terraform experience\n\\item Built pipelines."))
def test_honestly_denying_a_skill_is_not_claiming_it(self):
self.assertEqual("", scan(r"\item I have no Terraform experience and say so openly."))
def test_german_denial_is_not_claiming_it(self):
self.assertEqual("", scan(r"\item Keine Terraform-Erfahrung vorhanden."))
def test_education_institution_is_not_the_employment_entry(self):
# "Universitaet der Bundeswehr Muenchen" is a school, not the Bundeswehr employment
# record, and in German packages it is written with LaTeX accent macros.
body = r'\compactentry{M.Eng., Universit\"at der Bundeswehr M\"unchen}{Apr 2012 -- Oct 2013}'
self.assertNotIn("BUNDESWEHR", scan(body))
class ShippedDocumentsTests(unittest.TestCase):
"""The packages built under the current rules must stay clean."""
CURRENT = (
"SBB_DataEngineer_AssetMgmt", "NVIDIA_SrDevOps_Zurich", "RUAG_AI_Engineer_C5I",
"KdoCy_DevOps_Data_Platform", "CitadelSecurities_PlatformEngineer",
"AkerBP_Data_Product_Architect",
)
def test_current_packages_have_no_findings(self):
for folder in self.CURRENT:
for document in (ROOT / "output" / folder).glob("e2e_*resume.tex"):
with self.subTest(document=document.name):
self.assertEqual([], validator.scan_document(document, CLAIMS))
if __name__ == "__main__":
unittest.main()
+44 -1
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import importlib.util import importlib.util
import json import json
import sys
import re import re
import unittest import unittest
from pathlib import Path from pathlib import Path
@@ -62,10 +63,52 @@ class CanonicalSystemTests(unittest.TestCase):
"resume_builder/templates/resume_template.tex", "resume_builder/templates/resume_template.tex",
"resume_builder/templates/coverletter_template.tex", "resume_builder/templates/coverletter_template.tex",
): ):
self.assertEqual([], validator.scan_document(ROOT / relative, self.claims), relative) self.assertEqual([], validator.scan_document(ROOT / relative, self.claims, template=True), relative)
class PackageReproducibilityTests(unittest.TestCase):
"""F17: a PDF in a folder is not evidence that the package can be rebuilt."""
def test_manifest_is_current(self):
import subprocess
result = subprocess.run(
[sys.executable, str(ROOT / "resume_builder" / "helpers" / "package_manifest.py"),
"--check"], capture_output=True, text=True)
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
def test_cover_letter_template_does_not_use_today(self):
# \today makes a rebuild date-dependent: recompiling a historical letter
# silently restamps it with the current date.
template = (ROOT / "resume_builder" / "templates" / "coverletter_template.tex").read_text(
encoding="utf-8")
body = "\n".join(
line for line in template.splitlines() if not line.strip().startswith("%"))
self.assertNotIn("\\today", body)
def test_every_application_package_is_classified_for_reuse(self):
manifest = json.loads(
(ROOT / "resume_builder" / "canonical" / "package_manifest.json").read_text(
encoding="utf-8"))
unclassified = [name for name, entry in manifest["packages"].items()
if entry["reuse_classification"] == "UNCLASSIFIED"]
self.assertEqual([], unclassified)
class WorkflowPolicyTests(unittest.TestCase): class WorkflowPolicyTests(unittest.TestCase):
def test_agent_entrypoints_resolve_the_same_shared_workflow(self) -> None:
for entry in sorted((ROOT / ".agents/skills").glob("*/SKILL.md")):
other = ROOT / ".claude/skills" / entry.parent.name / "SKILL.md"
bodies = []
for path in (entry, other):
body = re.sub(r"\A---\n.*?\n---\n", "", path.read_text(encoding="utf-8"), count=1, flags=re.S)
bodies.append(body)
links = re.findall(r"\]\(([^)]+\.md)\)", body)
self.assertEqual(1, len(links), path)
workflow = (path.parent / links[0]).resolve()
self.assertEqual(ROOT / "resume_builder/reference/workflows" / (entry.parent.name + ".md"), workflow)
self.assertIn("Shared workflow version:", workflow.read_text(encoding="utf-8"))
self.assertEqual(*bodies)
def test_skills_use_minimal_valid_frontmatter(self) -> None: def test_skills_use_minimal_valid_frontmatter(self) -> None:
for skill_file in sorted((ROOT / ".agents/skills").glob("*/SKILL.md")): for skill_file in sorted((ROOT / ".agents/skills").glob("*/SKILL.md")):
text = skill_file.read_text(encoding="utf-8") text = skill_file.read_text(encoding="utf-8")
+158
View File
@@ -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('<page width="595" height="842">'
'<word xMin="0" yMin="100" xMax="60" yMax="111">Ten</word>'
'<word xMin="60" yMin="100" xMax="160" yMax="111">experience</word>'
'<word xMin="0" yMin="420" xMax="100" yMax="431">Professional</word>'
'<word xMin="101" yMin="420" xMax="160" yMax="431">Experience</word></page>')
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)
+65
View File
@@ -0,0 +1,65 @@
"""Scout selection regressions from the 2026-09-11 review (F11 onwards).
These are the cases where a bug silently removed or mis-ranked a role *before* any human
judgement happened, which is the expensive kind: a job that never reaches the report cannot
be evaluated at all.
"""
from __future__ import annotations
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "job_scout"))
import scout # noqa: E402
def process(jobs, cid="acme", display="Acme"):
results, stat, warnings = scout._process_company(
cid, display, {}, jobs, {}, "2026-09-11", {})
return results, stat, warnings
class RequisitionDedupeTests(unittest.TestCase):
"""F11: title is not identity."""
def test_two_requisitions_sharing_a_title_both_survive(self):
jobs = [
{"id": "R-1", "title": "Senior Data Engineer",
"location": "Bern, Switzerland", "url": "https://acme.example/jobs/R-1"},
{"id": "R-2", "title": "Senior Data Engineer",
"location": "Zurich, Switzerland", "url": "https://acme.example/jobs/R-2"},
]
results, _, warnings = process(jobs)
self.assertEqual(2, len(results), "a different requisition must never be dropped")
self.assertTrue(any("title(s) shared by different requisitions" in message
for _, message in warnings),
"the collision should be reported, not silently resolved")
def test_same_requisition_republished_per_location_is_merged(self):
jobs = [
{"id": "R-9", "title": "Platform Engineer | Switzerland | Remote",
"location": "Bern, Switzerland", "url": "https://acme.example/jobs/R-9"},
{"id": "R-9", "title": "Platform Engineer | Germany | Remote",
"location": "Munich, Germany", "url": "https://acme.example/jobs/R-9"},
]
results, _, _ = process(jobs)
self.assertEqual(1, len(results), "one requisition should stay one row")
self.assertIn("Bern", results[0]["location"])
def test_distinct_titles_are_unaffected(self):
jobs = [
{"id": "A", "title": "Data Engineer", "location": "Bern, Switzerland",
"url": "https://acme.example/jobs/A"},
{"id": "B", "title": "Platform Engineer", "location": "Bern, Switzerland",
"url": "https://acme.example/jobs/B"},
]
results, _, warnings = process(jobs)
self.assertEqual(2, len(results))
self.assertEqual([], [m for _, m in warnings if "title(s) shared" in m])
if __name__ == "__main__":
unittest.main()