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
This commit is contained in:
dennisthiessenandClaude Opus 5 committed 2026-09-11 23:57:06 +02:00
1 parent 925a2d74a3
commit 34299bce54
25 files changed
+1427 -322

No files matched your search

+77 -17
View File
@@ -39,6 +39,7 @@ import re
import ssl
import sys
import tempfile
from contextlib import contextmanager
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import lru_cache
@@ -65,7 +66,7 @@ CH_LOCATION_KEYWORDS = [
"st. gallen", "sankt gallen", "aarau", "baden", "emmen", "duebendorf", "dübendorf",
"frauenfeld", "biel", "bienne", "solothurn", "chur", "sion", "neuchâtel", "neuchatel",
"fribourg", "freiburg", "lugano", "bellinzona", "kloten", "wallisellen", "opfikon",
"lupfig", "villigen", "rotkreuz", "allschwil", "kaiseraugst", "payerne", "kastellaun",
"lupfig", "villigen", "rotkreuz", "allschwil", "kaiseraugst", "payerne",
]
DK_LOCATION_KEYWORDS = [
@@ -745,7 +746,9 @@ def fetch_greenhouse(args):
"title": j.get("title", ""),
"location": loc_blob,
"url": j.get("absolute_url"),
"posted": j.get("updated_at", ""),
"posted": j.get("published_at", ""),
"updated_at": j.get("updated_at", ""),
"date_provenance": "Greenhouse updated_at is an update, not a publication date",
"description": desc[:2500],
})
return jobs
@@ -798,7 +801,8 @@ def fetch_smartrecruiters(args):
content = data.get("content", []) or []
for p in content:
loc = p.get("location") or {}
parts = [loc.get("fullLocation") or loc.get("city") or ""]
country = (loc.get("country") or "").upper()
parts = [loc.get("fullLocation") or loc.get("city") or "", country]
if loc.get("remote"):
parts.append("Remote")
if loc.get("hybrid"):
@@ -810,6 +814,7 @@ def fetch_smartrecruiters(args):
"id": str(p.get("id")),
"title": p.get("name", ""),
"location": loc_str,
"country": country,
"url": f"https://jobs.smartrecruiters.com/{company}/{p.get('id')}",
"posted": p.get("releasedDate", ""),
"description": " ".join(filter(None, [dept, func])),
@@ -1781,7 +1786,17 @@ def _place_match(low, keywords):
return False
def location_eligibility(loc_text, policy="default"):
COUNTRY_NAMES = {
"CH": ("switzerland", "schweiz", "suisse", "svizzera", "ch"),
"DK": ("denmark", "danmark", "dk"), "NO": ("norway", "norge", "no"),
"DE": ("germany", "deutschland", "de"), "AT": ("austria", "österreich", "at"),
"FR": ("france", "frankreich", "fr"), "IT": ("italy", "italien", "it"),
"GB": ("united kingdom", "great britain", "uk", "gb"),
"US": ("united states", "usa", "us"), "NL": ("netherlands", "niederlande", "nl"),
}
def location_eligibility(loc_text, policy="default", country=None):
"""Decide whether a posting's location is in-scope.
Returns (eligible, in_ch, is_remote, is_relocation).
@@ -1801,9 +1816,23 @@ def location_eligibility(loc_text, policy="default"):
if not loc_text:
return False, False, False, False
low = loc_text.lower()
in_ch = _place_match(low, CH_LOCATION_KEYWORDS)
in_dk = _place_match(low, DK_LOCATION_KEYWORDS)
in_no = _place_match(low, NO_LOCATION_KEYWORDS)
# An adapter's country code is stronger than an ambiguous town name. In free text,
# recognize country names; short codes count only as comma/slash-separated fields.
countries = {country.upper()} if country else {
code for code, names in COUNTRY_NAMES.items() if any(
_place_match(low, [name]) if len(name) > 2 else
bool(re.search(r"(?:^|[,/])\s*" + name + r"\s*(?:$|[,/])", low)) for name in names)}
explicit_country = bool(countries)
excluded = set()
for code in countries:
for name in COUNTRY_NAMES.get(code, (code.lower(),)):
token = r"\b" + re.escape(name) + r"\b"
if re.search(r"(?:excluding|except|not in|außer|ausser|nicht in)\s+" + token, low) or re.search(token + r"\s+(?:excluded|not eligible|ausgeschlossen)", low):
excluded.add(code)
countries -= excluded
in_ch = "CH" in countries if explicit_country else _place_match(low, CH_LOCATION_KEYWORDS)
in_dk = "DK" in countries if explicit_country else _place_match(low, DK_LOCATION_KEYWORDS)
in_no = "NO" in countries if explicit_country else _place_match(low, NO_LOCATION_KEYWORDS)
is_us_only = any(p in low for p in US_ONLY_PATTERNS) and not in_ch
has_remote = bool(re.search(r"\bremote\b", low)) or "work from anywhere" in low
europe_scope = any(k in low for k in EUROPE_SCOPE_KEYWORDS)
@@ -1811,7 +1840,7 @@ def location_eligibility(loc_text, policy="default"):
# Pan-regional scope counts even without the word "remote" (e.g. QuantCo "Europe"),
# but NOT when the posting also says onsite or restricts residence to one country
# (F12: "Europe" beside "London / Onsite" used to read as remote-eligible).
restricted = bool(_ONSITE.search(low)) or (
restricted = "CH" in excluded or bool(_ONSITE.search(low)) or (
bool(_COUNTRY_ONLY.search(low)) and not (in_ch or in_dk))
is_europe_or_global = (europe_scope or global_scope) and not is_us_only and not restricted
is_ch_remote = in_ch and has_remote
@@ -2074,6 +2103,10 @@ def lookup_decision(dec_index, decisions, url):
return related
def is_same_decision(decision):
return decision is not None and decision.get("matched_by") != "slug"
def save_decisions(decisions):
_atomic_write_json(DECISIONS_FILE, decisions)
@@ -2169,8 +2202,8 @@ def write_report(path, results, errors, new_only, include_weak, stats=None, tota
if not include_weak and weak:
lines.append(f"\n_Hiding {len(weak)} weak/noise roles (score < 2). Use --include-weak to show._")
n_decided = sum(1 for r in results
if lookup_decision(decisions, _raw_decisions(), r["url"]) is not None)
n_decided = sum(1 for r in results if is_same_decision(
lookup_decision(decisions, _raw_decisions(), r["url"])))
if n_decided:
shown = "hidden" if hide_decided else "tagged inline"
lines.append(f"_{n_decided} role(s) already in the decision log ({shown}; "
@@ -2188,7 +2221,7 @@ def write_report(path, results, errors, new_only, include_weak, stats=None, tota
for bucket_name, bucket in buckets:
shown = [r for r in bucket
if not (hide_decided and
lookup_decision(decisions, _raw_decisions(), r["url"]) is not None)]
is_same_decision(lookup_decision(decisions, _raw_decisions(), r["url"])))]
if not shown:
continue
lines.append(f"\n## {bucket_name} - {len(shown)} role(s)\n")
@@ -2205,6 +2238,8 @@ def write_report(path, results, errors, new_only, include_weak, stats=None, tota
lines.append(f"- Posted: {r['posted']}")
if r.get("deadline"):
lines.append(f"- **Bewerbungsfrist: {r['deadline']}**")
if r.get("updated_at"):
lines.append(f"- Updated: {r['updated_at']}")
if r.get("unit"):
lines.append(f"- Einheit: {r['unit']}")
if r.get("compensation"):
@@ -2322,7 +2357,7 @@ def _process_company(cid, display, args, jobs, seen, today, last_scrape):
for j in jobs:
jid = str(j.get("id") or j.get("url"))
ok, in_ch, is_remote, is_relocation = location_eligibility(
j.get("location", ""), policy=policy
j.get("location", ""), policy=policy, country=j.get("country")
)
if not ok:
continue
@@ -2359,6 +2394,7 @@ def _process_company(cid, display, args, jobs, seen, today, last_scrape):
"company": display, "company_id": cid,
"title": j["title"], "location": j["location"],
"url": j["url"], "posted": j.get("posted", ""),
"updated_at": j.get("updated_at", ""),
"deadline": j.get("deadline", ""),
"date_provenance": j.get("date_provenance", ""),
"unit": j.get("unit", ""),
@@ -2386,7 +2422,7 @@ def _process_company(cid, display, args, jobs, seen, today, last_scrape):
return results, stat, warnings
def main():
def _main():
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
# Record a decision and exit: --decide "<url>" <status> [note words...]
@@ -2540,6 +2576,7 @@ def main():
dump = [{
"company": r["company"], "title": r["title"], "location": r["location"],
"url": r["url"], "posted": r["posted"], "deadline": r.get("deadline", ""),
"updated_at": r.get("updated_at", ""),
"date_provenance": r.get("date_provenance", ""), "unit": r.get("unit", ""),
"compensation": r.get("compensation", ""), "contact": r.get("contact", ""),
"is_new": r["is_new"],
@@ -2554,10 +2591,9 @@ def main():
# Report and dump are on disk, so the run genuinely delivered: now the roles may be
# marked seen. A restricted run must not mark anything seen at all - it did not look at
# the whole board, and doing so would hide those roles from the next full run.
if scope:
print("Restricted run: seen-state deliberately NOT updated.", file=sys.stderr)
else:
save_seen(seen)
# All returned matches have been delivered in the JSON, even for new-only or
# one-company runs. Unvisited companies remain unchanged in the loaded seen map.
save_seen(seen)
n_new = sum(1 for r in all_results if r["is_new"])
print(f"\nReport written: {report_path}", file=sys.stderr)
@@ -2598,5 +2634,29 @@ def main():
# ==============================================================================
@contextmanager
def writer_lock():
path = ROOT / "state" / "scout.lock"
path.parent.mkdir(parents=True, exist_ok=True)
try:
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
except FileExistsError:
raise SystemExit("Another scout writer is active. If a prior run crashed, verify it has stopped before removing state/scout.lock.")
try:
with os.fdopen(descriptor, "w") as stream:
stream.write(str(os.getpid()))
yield
finally:
path.unlink(missing_ok=True)
def main():
if any(arg in sys.argv for arg in ("--help", "-h")):
return _main()
with writer_lock():
_raw_decisions.cache_clear()
return _main()
if __name__ == "__main__":
main()