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

@@ -51,6 +51,16 @@ def canonical_checks(claims: dict, history: dict) -> tuple[list[str], list[str]]
if duplicates:
errors.append(f"duplicate {collection} ids: {', '.join(duplicates)}")
for collection in ("education", "employment"):
for item in claims.get(collection, []):
for period in [item, *item.get("title_history", [])]:
for field in ("start", "end"):
value = period.get(field)
if value == "present" and field == "end":
continue
if value is not None and (not isinstance(value, str) or not re.fullmatch(r"\d{4}-(?:0[1-9]|1[0-2])", value)):
errors.append(f"{item.get('id')}: invalid {field} date {value!r}; expected YYYY-MM")
skills = claims.get("skills", [])
skill_names = [item.get("name") for item in skills]
duplicates = sorted({item for item in skill_names if skill_names.count(item) > 1})
@@ -95,15 +105,26 @@ def canonical_checks(claims: dict, history: dict) -> tuple[list[str], list[str]]
missing_block = [c.get("id") for c in claim_list if not c.get("evidence")]
if missing_block:
errors.append("claims without an evidence block: " + ", ".join(map(str, missing_block)))
for claim in claim_list:
evidence = claim.get("evidence")
if not isinstance(evidence, dict):
errors.append(f"{claim.get('id')}: evidence must be an object")
continue
for field in ("source", "ownership"):
if not isinstance(evidence.get(field), str) or not evidence[field].strip():
errors.append(f"{claim.get('id')}: evidence.{field} must be a nonempty string (or 'unrecorded')")
source = evidence.get("source", "")
if isinstance(source, str) and re.fullmatch(r"[\w./-]+\.(?:md|pdf|tex|json|txt)", source) and not (ROOT / source).is_file():
errors.append(f"{claim.get('id')}: evidence source file does not exist: {source}")
undated = [c.get("id") for c in claim_list
if (c.get("evidence") or {}).get("last_verified") == "unrecorded"]
if isinstance(c.get("evidence"), dict) and c["evidence"].get("last_verified") == "unrecorded"]
if undated:
warnings.append(
f"{len(undated)} of {len(claim_list)} claims have last_verified 'unrecorded' "
f"({', '.join(map(str, undated[:6]))}{', ...' if len(undated) > 6 else ''}). "
"Record a real date when one is confirmed; never infer one to clear this hint.")
if claims["identity"].get("swiss_permit", "").startswith("UNVERIFIED"):
if (claims.get("identity") or {}).get("swiss_permit", "").startswith("UNVERIFIED"):
warnings.append("Swiss permit type remains unverified; generators must ask before naming it")
return errors, warnings
@@ -148,7 +169,9 @@ _MARKUP = re.compile(r"\\[a-zA-Z@]+\s*(?:\[[^\]]*\])?")
_PLACEHOLDER = re.compile(r"\[GENERATE[^\]]*\]|\bTODO\b|\bFIXME\b|\bXXX\b|\bLOREM IPSUM\b|<<[^>]*>>", re.I)
# "no Terraform experience", "keine Terraform-Erfahrung", "not worked with Terraform"
_NEGATION = re.compile(
r"\b(?:no|not|never|without|kein|keine|keinerlei|ohne|nicht)\b[^.;]{0,60}$", re.I)
r"\b(?:(?:no|without|keine?|keinerlei|ohne)\s+(?:(?:experience|erfahrung)\s+(?:with|in|mit)\s+)?"
r"|(?:not|never)\s+(?:used|worked with)\s+|nicht\s+(?:mit\s+)?)$", re.I)
_INLINE = re.compile(r"\\(?:textbf|textit|textnormal|textrm|textsf|texttt|emph|underline)\{([^{}]*)\}")
# LaTeX accent macros: \"a, 'e, \`e, \^o, \~n, plus \ss. German documents use these instead
@@ -179,6 +202,8 @@ def visible_text(raw: str) -> str:
* Whitespace is collapsed so a term split across a line break still matches.
"""
text = _deaccent(_COMMENT.sub("", raw))
while _INLINE.search(text):
text = _INLINE.sub(r"\1", text)
text = _MARKUP.sub(" ", text)
text = text.replace("{", " ").replace("}", " ").replace("~", " ")
return re.sub(r"\s+", " ", text)
@@ -218,6 +243,17 @@ def _negated(haystack: str, term: str) -> bool:
return bool(hits) and all(_NEGATION.search(haystack[max(0, hit.start() - 70):hit.start()]) for hit in hits)
def _employer_mention(text: str, name: str) -> bool:
"""Narrow exception for a company name, never for a bare technology list."""
if name.lower() != "snowflake":
return False
hits = list(re.finditer(r"\bsnowflake\b", text))
return bool(hits) and all(
re.search(r"\b(?:application to|applying to|position at|role at|bewerbung bei|bewerbung an)\s*$", text[max(0, hit.start()-60):hit.start()])
or re.match(r"\s+(?:inc\b|incorporated\b)", text[hit.end():])
for hit in hits)
def scan_document(path: Path, claims: dict, template: bool = False) -> list[str]:
"""Deterministic, mechanically checkable properties of a generated document.
@@ -229,9 +265,9 @@ def scan_document(path: Path, claims: dict, template: bool = False) -> list[str]
raw = read_text(path)
text = visible_text(raw)
lowered = text.lower()
# Second view with grouping braces closed up rather than spaced out, so a name split by
# markup -- extbf{Snow}flake -- cannot slip past the skill comparison.
dense = re.sub(r"\s+", " ", _MARKUP.sub(" ", _deaccent(_COMMENT.sub("", raw))).replace("{", "").replace("}", "")).lower()
# Keep context local to a sentence, bullet or skills row.
contexts = [visible_text(part).lower() for part in re.split(
r"\\(?:item|skillline|compactentry)\b|[.!?;]\s+|\n\s*\n", _COMMENT.sub("", raw))]
for pattern in claims.get("global_forbidden_output_patterns", []):
if pattern.lower() in lowered:
@@ -248,15 +284,11 @@ def scan_document(path: Path, claims: dict, template: bool = False) -> list[str]
for skill in claims.get("skills", []):
name, output = skill["name"], skill.get("output")
seen_plain = _mentions(lowered, name)
seen_dense = _mentions(dense, name)
if output == "forbidden" and (seen_plain or seen_dense) and not _negated(lowered if seen_plain else dense, name):
mentions = [part for part in contexts if _mentions(part, name)]
if output == "forbidden" and any(not _negated(part, name) and not _employer_mention(part, name) for part in mentions):
errors.append(f"{path}: forbidden or unverified skill: {name}")
if output == "certification-context-only" and _mentions(lowered, name):
window = " ".join(
lowered[max(0, m.start() - 200):m.start() + 200]
for m in re.finditer(re.escape(name.lower()), lowered))
if not re.search(r"certificat|zertifik|coursera|nanodegree|kurs|course|training|weiterbildung", window):
if output == "certification-context-only":
if any(not _negated(part, name) and not re.search(r"certificat|zertifik|coursera|nanodegree|\bkurs\b|course|weiterbildung", part) for part in mentions):
errors.append(
f"{path}: {name} is certification-context-only and appears without that context")