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
@@ -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")
|
||||
|
||||
|
||||
Reference in new issue
Block a user