fix: review findings F03-F06 — quotas, rule hierarchy and a validator that checks

F03 — application records
- Quota already removed with F02's tracker; verified uncapped and that
  "interview reached" survives a later withdrawal.
- Aker BP: application date confirmed 2026-07-29 by the user, settling the
  one-day conflict. Its Adjacent-despite-79 classification is kept, NOT
  reclassified: the user does not remember the rationale and judged it
  immaterial on a closed role, and silently reclassifying to make the
  arithmetic tidy is the exact objection the review raised. Recorded as an
  explicit override reason with a rubric version instead.

F04 — rule hierarchy
- Three levels stated in AGENTS.md and CLAUDE.md: immutable evidence rules,
  standing user preferences, market/employer presentation.
- Deliverables separated from QA. The .tex stays the artefact of record; you
  still compile, into the application's own output/ folder, because the user
  cannot approve what he cannot open.
- `resume_reference.md` said "Capgemini/Bundeswehr: include only when directly
  relevant", contradicting the standing never-include rule. Split and fixed.

F05 — a green validator now means something
- Per-claim `forbidden` wording is enforced. 14 of 26 claims carried such a
  list and NONE of it was checked, which is why "owned the full ML lifecycle"
  passed. Matching tolerates inserted adjectives: the real regression read
  "Built a decentralized Data Mesh" and one word defeated the literal list.
- Added identity (e-mail/phone), employer-date, placeholder and
  document-exclusion checks. Capgemini's omission rule now has a canonical
  home in claims.json instead of living only in memory and config.md.
- Registered Groovy and Snowflake as forbidden skills. Both were absent from
  claims.json entirely, so "forbidden" existed in prose with nothing enforcing
  it.
- PASS no longer reads as a truth certificate: it prints what it did not check
  — scope implications, bullet-to-claim support, whether a number is real.

F06 — the scanner stopped fighting the documents
- LaTeX comments are no longer claims; `% never claim Terraform` was a finding
  before, which taught the workflow to write around the checker.
- Markup, accent macros and umlauts are normalized, so `\textbf{Snow}flake`
  cannot smuggle a forbidden skill past a substring test and
  `Universit\"at der Bundeswehr M\"unchen` is recognisably a school.
- Honest denials pass in English and German.
- The triple-list cadence hint knew only English "and", so it silently did
  nothing on every German package. Advisory only, and now labelled as such.

Verification
- The six counter-proofs from the report are caught: 9 errors on one synthetic
  document, where the old scanner found none.
- All six packages built under current rules scan clean — SBB, NVIDIA, RUAG,
  Kdo Cy, Citadel, Aker BP. Four false positives of my own were found and fixed
  on the way there; each is now a regression test.
- 13 older packages do report findings. Those look like genuine historical
  violations and are deliberately NOT fixed here: F17 governs archives and they
  must not be silently rewritten.
- `tests/test_document_scanner.py` added. Full suite 31/31 pass.

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 19:33:24 +02:00
1 parent afdef7eb94
commit fc5f3e49ba
8 files changed
+421 -31

No files matched your search

@@ -12,6 +12,7 @@ import argparse
import json
import re
import sys
from functools import lru_cache
from pathlib import Path
@@ -92,6 +93,15 @@ def canonical_checks(claims: dict, history: dict) -> tuple[list[str], list[str]]
return errors, warnings
UNCHECKED = (
"whether a sentence implies ownership or scale beyond the evidence (Scope Discipline)",
"whether each bullet is actually supported by the claim it was planned from",
"whether a number in the document is real - unverified metrics look identical to real ones",
"whether tone, emphasis and ordering suit this employer",
"anything in a language or phrasing the pattern lists do not cover",
)
def workflow_checks() -> list[str]:
errors: list[str] = []
checks = {
@@ -118,18 +128,172 @@ def workflow_checks() -> list[str]:
return errors
_COMMENT = re.compile(r"(?<!\\)%.*$", re.M)
_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)
# LaTeX accent macros: \"a, 'e, \`e, \^o, \~n, plus \ss. German documents use these instead
# of literal umlauts, and leaving them in made "Universit\"at der Bundeswehr" unmatchable.
_ACCENT = re.compile(r"""\\["'`^~=.]\s*\{?([a-zA-Z])\}?""")
_SSHARP = re.compile(r"\\ss\b")
def _deaccent(raw: str) -> str:
"""Fold LaTeX accent macros to their base letter.
German packages write Universit\\"at der Bundeswehr M\\"unchen rather than literal
umlauts. Without this the employer/education comparison could not see the institution
name at all, which produced a false positive on several correct resumes.
"""
return _SSHARP.sub("ss", _ACCENT.sub(r"\1", raw))
def visible_text(raw: str) -> str:
"""Document text as a reader sees it.
Three fixes for the scanner, all from review finding F06:
* LaTeX COMMENTS are dropped. A line such as `% never claim Terraform` is a note to
the author, not a claim, and flagging it taught the workflow to write around the
checker instead of fixing the document.
* MARKUP is removed but its argument text kept, so `\\textbf{Snow}flake` can no longer
smuggle a forbidden skill past a plain substring comparison.
* Whitespace is collapsed so a term split across a line break still matches.
"""
text = _deaccent(_COMMENT.sub("", raw))
text = _MARKUP.sub(" ", text)
text = text.replace("{", " ").replace("}", " ").replace("~", " ")
return re.sub(r"\s+", " ", text)
@lru_cache(maxsize=512)
def _forbidden_phrase(phrase: str) -> re.Pattern:
"""Forbidden wording, tolerant of inserted qualifiers.
The canonical lists are literal ("built a Data Mesh"), but the regression this rule
exists to stop was worded "Built a decentralized Data Mesh" - one adjective defeated the
substring test. Allow up to two extra words between tokens. A false positive here costs
a rewording; a false negative ships a scope claim the evidence does not support.
"""
tokens = [re.escape(token) for token in phrase.lower().split()]
return re.compile(r"(?<!\w)" + r"\W+(?:\w+\W+){0,2}".join(tokens), re.I)
_FOLD = str.maketrans({"ä": "a", "ö": "o", "ü": "u", "ß": "ss",
"Ä": "a", "Ö": "o", "Ü": "u", "é": "e", "è": "e"})
def fold(text: str) -> str:
"""Umlaut-insensitive form. claims.json stores 'Universitat der Bundeswehr Munchen'
while documents render 'Universitaet der Bundeswehr Muenchen', and that mismatch made an
education institution look like the Bundeswehr employment entry on three correct resumes."""
return text.translate(_FOLD).lower()
def _mentions(haystack: str, term: str) -> bool:
return re.search(r"(?<!\w)" + re.escape(term.lower()) + r"(?!\w)", haystack) is not None
def _negated(haystack: str, term: str) -> bool:
"""True when every mention of the term sits behind a negation."""
hits = list(re.finditer(r"(?<!\w)" + re.escape(term.lower()) + r"(?!\w)", haystack))
return bool(hits) and all(_NEGATION.search(haystack[max(0, hit.start() - 70):hit.start()]) for hit in hits)
def scan_document(path: Path, claims: dict) -> list[str]:
"""Deterministic, mechanically checkable properties of a generated document.
This is NOT a truth check and must never be reported as one. It cannot judge whether a
sentence implies ownership the evidence does not support; that stays a reading task.
See `UNCHECKED` and the summary printed by main().
"""
errors: list[str] = []
text = read_text(path)
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()
for pattern in claims.get("global_forbidden_output_patterns", []):
if pattern.lower() in lowered:
errors.append(f"{path}: forbidden output pattern: {pattern}")
# Per-claim forbidden wording. 14 of 26 claims carry a `forbidden` list and NONE of it
# was enforced before: exactly the gap that let "owned the full ML lifecycle" pass.
for claim in claims.get("claims", []):
for phrase in claim.get("forbidden", []):
if not isinstance(phrase, str) or len(phrase) < 8:
continue # too short to match safely as a substring
if _forbidden_phrase(phrase).search(lowered):
errors.append(f"{path}: {claim.get('id','?')} forbidden wording: {phrase}")
for skill in claims.get("skills", []):
if skill.get("output") == "forbidden" and skill["name"].lower() in lowered:
errors.append(f"{path}: forbidden or unverified skill: {skill['name']}")
if "\\begin{rsubsection}" in lowered:
for line_number, line in enumerate(text.splitlines(), 1):
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):
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):
errors.append(
f"{path}: {name} is certification-context-only and appears without that context")
for term in (exclusion["term"] for exclusion in claims.get("document_exclusions", [])):
if _mentions(lowered, term):
errors.append(f"{path}: excluded from all documents by standing user preference: {term}")
# Scan the comment-stripped RAW text: _MARKUP treats "\item [GENERATE: x]" as a command
# with an optional argument and would swallow the placeholder whole.
for match in _PLACEHOLDER.finditer(_COMMENT.sub("", raw)):
errors.append(f"{path}: unresolved placeholder left in the document: {match.group(0)!r}")
identity = claims.get("identity", {})
for field in ("email", "phone"):
value = identity.get(field)
if not value:
continue
compact = re.sub(r"[\s()-]", "", lowered)
if re.sub(r"[\s()-]", "", value.lower()) not in compact:
kind = "e-mail" if field == "email" else "phone number"
errors.append(f"{path}: canonical {kind} not found ({value}); check for a wrong contact line")
# Anchor the date check on the EMPLOYER NAME, never on the job title: titles collide
# across employers ("Research Software Engineer" contains "Software Engineer") and that
# produced a false positive on two real resumes. Employers excluded from documents are
# skipped outright - their absence is the point, not a defect.
excluded = {item["term"].lower() for item in claims.get("document_exclusions", [])}
schooling_removed = fold(lowered)
for school in claims.get("education", []):
for key in ("institution", "thesis_institution"):
institution = fold(school.get(key) or "")
if len(institution) > 5:
schooling_removed = schooling_removed.replace(institution, " ")
for employer in claims.get("employment", []):
name = (employer.get("employer") or "").lower()
if not name or any(term in name for term in excluded):
continue
short = re.split(r"\s*\(|,| gmbh| ag| asa", name)[0].strip()
# Education institutions are removed first: "Universitaet der Bundeswehr Muenchen" is a
# school, not the Bundeswehr employment entry, and matching it flagged three real
# resumes that were entirely correct.
if len(short) < 4 or not _mentions(schooling_removed, fold(short)):
continue
start = employer.get("start", "")
year = start.split("-")[0] if start else ""
if year and year not in text:
errors.append(
f"{path}: {employer.get('id','?')} ({employer.get('employer')}) appears but its "
f"canonical start year {year} is absent; verify the dates")
if "\\begin{rsubsection}" in raw.lower():
for line_number, line in enumerate(raw.splitlines(), 1):
if "\\begin{rSubsection}" not in line:
continue
if re.search(r"Production Ownership|AI-Ready|Customer-Embedded|Shipping ML", line, re.I):
@@ -137,7 +301,10 @@ def scan_document(path: Path, claims: dict) -> list[str]:
return errors
TRIPLE = re.compile(r"\w+, [^,]{3,45}, [^,]{3,45} and |\w+, [^,]{3,45} and ")
# Advisory only. German documents use "und"; without it the cadence hint silently
# did nothing on every German package. No external evidence supports a hard ~50% limit,
# so this stays a hint and never an error (F06).
TRIPLE = re.compile(r"\w+, [^,]{3,45}, [^,]{3,45} (?:and|und) |\w+, [^,]{3,45} (?:and|und) ")
def _bullet_blocks(text: str) -> list[list[str]]:
@@ -217,14 +384,21 @@ def main() -> int:
errors.extend(scan_document(path, claims))
warnings.extend(cadence_checks(path))
if args.document:
print("Mechanical checks only. NOT verified automatically:")
for item in UNCHECKED:
print(f" - {item}")
print()
for warning in warnings:
print(f"WARN: {warning}")
print(f"WARN (advisory, never a gate): {warning}")
for error in errors:
print(f"ERROR: {error}")
if errors:
print(f"FAIL: {len(errors)} error(s), {len(warnings)} warning(s)")
return 1
print(f"PASS: canonical system valid ({len(warnings)} warning(s))")
scope = "mechanical checks passed" if args.document else "canonical system valid"
print(f"PASS: {scope} ({len(warnings)} advisory warning(s)). "
"This is not a truth certificate - see the unchecked list above.")
return 0