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:
1 parent
925a2d74a3
commit
34299bce54
25 files changed
+1427
-322
No files matched your search
+77
-17
@@ -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()
|
||||
Reference in new issue
Block a user