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
This commit is contained in:
dennisthiessenandClaude Opus 5 committed 2026-09-11 19:54:17 +02:00
1 parent afd8174bbd
commit ce8619b8e3
2 files changed
+47 -10

No files matched your search

+33 -3
View File
@@ -93,6 +93,23 @@ GLOBAL_SCOPE_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,
"applied ai": 3, "applied ml": 3, "ai engineer": 3, "ml engineer": 3,
"mlops": 3, "ai platform": 3, "ml platform": 3,
@@ -1819,6 +1836,15 @@ def _kw_in(kw, text):
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
# 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
@@ -2130,10 +2156,14 @@ def write_report(path, results, errors, new_only, include_weak, stats=None, tota
lines.append(f"_{n_decided} role(s) already in the decision log ({shown}; "
f"see state/decisions.json)._")
buckets = [("Strong fit (score >= 6)", strong),
("Medium fit (score 2-5)", medium)]
# F14: this number is SEARCH RELEVANCE - how many profile keywords the posting echoes.
# 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:
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:
shown = [r for r in bucket