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
@@ -57,7 +57,9 @@ def update_application(data: dict, args: argparse.Namespace) -> None:
|
||||
row = matches[0]
|
||||
if row.get("application_date") and args.date < row["application_date"]:
|
||||
raise ValueError("event date precedes application date")
|
||||
outcome_dates = [event["date"] for event in row.get("events", []) if event.get("outcome") and event.get("date")]
|
||||
outcome_dates = [event["date"] for event in row.get("events", [])
|
||||
if (event.get("outcome") or event.get("type") in records.OUTCOMES) and event.get("date")]
|
||||
outcome_dates.extend(row[key] for key in ("withdrawn", "rejection_date") if row.get(key))
|
||||
if args.outcome and outcome_dates and args.date < max(outcome_dates):
|
||||
raise ValueError("older outcome cannot replace current state; record it as a historical note")
|
||||
event = {"type": args.event or args.outcome or "note", "date": args.date}
|
||||
|
||||
@@ -6,9 +6,9 @@ A package folder that contains a PDF looks finished. It is not necessarily repro
|
||||
one happens to be installed elsewhere, and its submitted PDF differs in content from the
|
||||
e2e source's PDF. The existing PDF hid both problems.
|
||||
|
||||
This records, per package: which sources exist, the hash of the class each one would compile
|
||||
against, whether a PDF is present, and whether the submission-named PDF still matches the
|
||||
e2e one. It does NOT rewrite anything - historical packages are evidence.
|
||||
Records source/class content hashes. Optional PDF snapshots detect changed local artifacts;
|
||||
missing PDFs are allowed because generated PDFs are deliberately not tracked in Git.
|
||||
Filename association is not proof that a PDF was built from the current source.
|
||||
|
||||
python resume_builder/helpers/package_manifest.py # write the manifest
|
||||
python resume_builder/helpers/package_manifest.py --check # fail if it is stale
|
||||
@@ -34,17 +34,19 @@ CTAN_CLASSES = {"article", "report", "book", "letter", "moderncv", "scrartcl", "
|
||||
|
||||
|
||||
def digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()[:16]
|
||||
# Git/Windows may change line endings without changing the TeX source.
|
||||
data = path.read_text(encoding="utf-8").encode("utf-8") if path.suffix in {".tex", ".cls"} else path.read_bytes()
|
||||
return hashlib.sha256(data).hexdigest()[:16]
|
||||
|
||||
|
||||
def describe(folder: Path) -> dict:
|
||||
sources = sorted(p.name for p in folder.glob("*.tex"))
|
||||
def describe(folder: Path, record_pdfs: bool = False) -> dict:
|
||||
sources = {p.name: digest(p) for p in sorted(folder.glob("*.tex"))}
|
||||
classes = {p.name: digest(p) for p in folder.glob("*.cls")}
|
||||
pdfs = sorted(p.name for p in folder.glob("*.pdf"))
|
||||
needs = set()
|
||||
uses_today = []
|
||||
for name in sources:
|
||||
text = (folder / name).read_text(encoding="utf-8", errors="replace")
|
||||
text = re.sub(r"(?<!\\)%.*$", "", text, flags=re.M)
|
||||
match = re.search(r"\\documentclass(?:\[[^\]]*\])?\{([^}]+)\}", text)
|
||||
if match:
|
||||
needs.add(match.group(1))
|
||||
@@ -68,20 +70,19 @@ def describe(folder: Path) -> dict:
|
||||
f"rebuild is date-dependent: {', '.join(uses_today)} use \\today, so recompiling a "
|
||||
f"historical package silently restamps it with the current date. New packages must "
|
||||
f"carry the explicit application date instead.")
|
||||
if sources and not pdfs:
|
||||
problems.append("no PDF present: nothing reviewable without a build")
|
||||
|
||||
return {
|
||||
entry = {
|
||||
"sources": sources,
|
||||
"class_files": classes,
|
||||
"requires_class": sorted(needs),
|
||||
"pdfs": pdfs,
|
||||
"uses_today": uses_today,
|
||||
"problems": problems,
|
||||
}
|
||||
if record_pdfs:
|
||||
entry["pdf_artifacts"] = {p.name: digest(p) for p in sorted(folder.glob("*.pdf"))}
|
||||
return entry
|
||||
|
||||
|
||||
def build() -> dict:
|
||||
def build(record_pdfs: bool = False) -> dict:
|
||||
register = json.loads(REGISTER.read_text(encoding="utf-8"))
|
||||
classified = set()
|
||||
for key in ("unsafe_do_not_reuse", "historical_revalidate", "paused_no_package"):
|
||||
@@ -96,7 +97,7 @@ def build() -> dict:
|
||||
# submission and has nothing to classify for reuse.
|
||||
if not any(folder.glob("*resume*.tex")) and not any(folder.glob("*cover*letter*.tex")):
|
||||
continue
|
||||
entry = describe(folder)
|
||||
entry = describe(folder, record_pdfs)
|
||||
entry["reuse_classification"] = (
|
||||
"recorded" if f"output/{folder.name}" in classified else "UNCLASSIFIED")
|
||||
if f"output/{folder.name}" not in classified:
|
||||
@@ -107,32 +108,57 @@ def build() -> dict:
|
||||
packages[folder.name] = entry
|
||||
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"generated_note": (
|
||||
"Generated by package_manifest.py (review finding F17). A PDF in a folder is not "
|
||||
"evidence that the package is reproducible. This file records what would actually "
|
||||
"happen on a rebuild. It is descriptive: nothing here authorises rewriting a "
|
||||
"evidence that the package is reproducible. Source and class hashes detect edits; "
|
||||
"optional pdf_artifacts are observations, not verified source-to-PDF mappings. "
|
||||
"Absent PDFs are allowed; present PDFs must match their recorded hash. "
|
||||
"Nothing here authorises rewriting a "
|
||||
"submitted document."),
|
||||
"packages": packages,
|
||||
}
|
||||
|
||||
|
||||
def check_manifest(recorded: dict, current: dict, output: Path = OUTPUT) -> list[str]:
|
||||
# Compare the portable source inventory separately from optional local build products.
|
||||
baseline = json.loads(json.dumps(recorded))
|
||||
errors = []
|
||||
for name, entry in baseline.get("packages", {}).items():
|
||||
for filename, expected in entry.pop("pdf_artifacts", {}).items():
|
||||
path = output / name / filename
|
||||
if path.exists() and digest(path) != expected:
|
||||
errors.append(f"changed PDF artifact: {name}/{filename}; inspect before recording a new baseline")
|
||||
if baseline != current:
|
||||
errors.append("source manifest is stale; inspect changes, then run package_manifest.py")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--check", action="store_true", help="fail if the manifest is stale")
|
||||
parser.add_argument("--record-pdfs", action="store_true", help="explicitly snapshot current local PDF hashes (not proof of source association)")
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest = build()
|
||||
text = json.dumps(manifest, indent=2, ensure_ascii=False) + "\n"
|
||||
if args.check and args.record_pdfs:
|
||||
parser.error("--check cannot refresh PDF baselines")
|
||||
recorded = json.loads(MANIFEST.read_text(encoding="utf-8")) if MANIFEST.exists() else {}
|
||||
manifest = build(record_pdfs=args.record_pdfs)
|
||||
if args.check:
|
||||
current = MANIFEST.read_text(encoding="utf-8") if MANIFEST.exists() else ""
|
||||
if current != text:
|
||||
print("ERROR: package manifest is stale; re-run package_manifest.py")
|
||||
errors = check_manifest(recorded, manifest)
|
||||
if errors:
|
||||
for error in errors:
|
||||
print("ERROR: " + error)
|
||||
return 1
|
||||
print("Package manifest is current.")
|
||||
return 0
|
||||
|
||||
MANIFEST.write_text(text, encoding="utf-8")
|
||||
if not args.record_pdfs:
|
||||
for name, entry in manifest["packages"].items():
|
||||
old = recorded.get("packages", {}).get(name, {})
|
||||
if "pdf_artifacts" in old:
|
||||
entry["pdf_artifacts"] = old["pdf_artifacts"]
|
||||
MANIFEST.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
flagged = {name: entry for name, entry in manifest["packages"].items() if entry["problems"]}
|
||||
print(f"{len(manifest['packages'])} packages described, {len(flagged)} with findings\n")
|
||||
for name, entry in flagged.items():
|
||||
|
||||
@@ -8,38 +8,38 @@ The review reported ~50-55% for four packages. This reproduces that measurement
|
||||
compiled PDF so a change can be checked rather than asserted. The percentage is a diagnostic,
|
||||
never a target: no pixel goal, no forced page-two filling.
|
||||
"""
|
||||
import io
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
PDFTOTEXT = Path(r"C:\Users\Dennis\AppData\Local\Programs\MiKTeX\miktex\bin\x64\pdftotext.exe")
|
||||
HEADINGS = ("professional experience", "berufserfahrung", "experience")
|
||||
|
||||
|
||||
def measure(pdf: Path) -> dict:
|
||||
executable = shutil.which("pdftotext")
|
||||
if not executable:
|
||||
raise RuntimeError("pdftotext is required on PATH (Poppler or MiKTeX)")
|
||||
with tempfile.TemporaryDirectory() as work:
|
||||
out = Path(work) / "bbox.xml"
|
||||
subprocess.run([str(PDFTOTEXT), "-bbox", "-f", "1", "-l", "1", str(pdf), str(out)],
|
||||
subprocess.run([executable, "-bbox", "-f", "1", "-l", "1", str(pdf), str(out)],
|
||||
capture_output=True, check=True)
|
||||
xml = out.read_text(encoding="utf-8", errors="replace")
|
||||
page = re.search(r'<page width="([\d.]+)" height="([\d.]+)"', xml)
|
||||
if page is None:
|
||||
raise ValueError("No page geometry found in pdftotext output")
|
||||
height = float(page.group(2))
|
||||
words = [(float(m.group(2)), m.group(5).lower())
|
||||
for m in re.finditer(
|
||||
r'<word xMin="([\d.]+)" yMin="([\d.]+)" xMax="([\d.]+)" yMax="([\d.]+)">([^<]*)</word>',
|
||||
xml)]
|
||||
first_y = None
|
||||
for index, (y, text) in enumerate(words):
|
||||
if text in ("professional", "berufserfahrung", "experience"):
|
||||
following = " ".join(w for _, w in words[index:index + 2])
|
||||
if any(h.startswith(following.split()[0]) for h in HEADINGS):
|
||||
first_y = y
|
||||
break
|
||||
lines = len({round(y, 1) for y, _ in words})
|
||||
rows = {}
|
||||
for y, text in words:
|
||||
rows.setdefault(round(y, 1), []).append(text)
|
||||
first_y = next((y for y, row in sorted(rows.items()) if " ".join(row).strip() in HEADINGS), None)
|
||||
lines = len(rows)
|
||||
return {
|
||||
"height": height,
|
||||
"experience_y": first_y,
|
||||
|
||||
@@ -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