#!/usr/bin/env python3 """Validate canonical resume data, generation sources, and generated documents. Usage: python resume_builder/helpers/validate_resume_system.py python resume_builder/helpers/validate_resume_system.py --document output/Acme/resume.tex """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] CLAIMS_PATH = ROOT / "resume_builder" / "canonical" / "claims.json" HISTORY_PATH = ROOT / "resume_builder" / "canonical" / "historical_outputs.json" def load_json(path: Path) -> dict: with path.open(encoding="utf-8") as handle: return json.load(handle) def read_text(path: Path) -> str: return path.read_text(encoding="utf-8") def canonical_checks(claims: dict, history: dict) -> tuple[list[str], list[str]]: errors: list[str] = [] warnings: list[str] = [] if claims.get("schema_version") != 1: errors.append("claims.json schema_version must be 1") if history.get("schema_version") != 1: errors.append("historical_outputs.json schema_version must be 1") for key in ("identity", "education", "employment", "claims", "skills"): if not claims.get(key): errors.append(f"claims.json missing non-empty {key}") for collection in ("education", "employment", "claims"): ids = [item.get("id") for item in claims.get(collection, [])] if None in ids: errors.append(f"{collection} contains an item without id") duplicates = sorted({item for item in ids if ids.count(item) > 1}) if duplicates: errors.append(f"duplicate {collection} ids: {', '.join(duplicates)}") 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}) if duplicates: errors.append(f"duplicate skills: {', '.join(duplicates)}") for skill in skills: if skill.get("output") not in { "allowed", "allowed-with-context", "certification-context-only", "forbidden", }: errors.append(f"invalid output policy for skill {skill.get('name')}") config = read_text(ROOT / "config.md") bundle_names = re.findall(r"\|\s*(bundle_[a-z0-9_]+\.md)\s*\|", config) for bundle_name in bundle_names: if not (ROOT / "resume_builder" / "bundles" / bundle_name).exists(): errors.append(f"configured bundle does not exist: {bundle_name}") expected_files = [ ROOT / "resume_builder" / "templates" / "resume_template.tex", ROOT / "resume_builder" / "reference" / "resume_reference.md", ROOT / "resume_builder" / "reference" / "critique_framework.md", ] for path in expected_files: if not path.exists(): errors.append(f"required workflow file missing: {path.relative_to(ROOT)}") history_folders: list[str] = [] for key in ("unsafe_do_not_reuse", "historical_revalidate"): for item in history.get(key, []): history_folders.append(item["folder"] if isinstance(item, dict) else item) duplicates = sorted({item for item in history_folders if history_folders.count(item) > 1}) if duplicates: errors.append(f"historical output folders classified twice: {', '.join(duplicates)}") if claims["identity"].get("swiss_permit", "").startswith("UNVERIFIED"): warnings.append("Swiss permit type remains unverified; generators must ask before naming it") return errors, warnings def workflow_checks() -> list[str]: errors: list[str] = [] checks = { ROOT / "resume_builder" / "templates" / "resume_template.tex": [ "Research Experience", "FLIPPED format", "Selected Publications", ], ROOT / "resume_builder" / "reference" / "resume_reference.md": [ "ALL variable bullets", "<= 3 lines white space", "20-21 variable bullets", ], ROOT / ".agents" / "skills" / "critique" / "SKILL.md": [ "Publications 10%", "Eight-Dimension Scoring", ], } for path, forbidden_phrases in checks.items(): text = read_text(path).lower() for phrase in forbidden_phrases: if phrase.lower() in text: errors.append(f"obsolete workflow rule remains in {path.relative_to(ROOT)}: {phrase}") return errors def scan_document(path: Path, claims: dict) -> list[str]: errors: list[str] = [] text = read_text(path) lowered = text.lower() for pattern in claims.get("global_forbidden_output_patterns", []): if pattern.lower() in lowered: errors.append(f"{path}: forbidden output pattern: {pattern}") 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): if "\\begin{rSubsection}" not in line: continue if re.search(r"Production Ownership|AI-Ready|Customer-Embedded|Shipping ML", line, re.I): errors.append(f"{path}:{line_number}: marketing theme used before formal employer/title") return errors TRIPLE = re.compile(r"\w+, [^,]{3,45}, [^,]{3,45} and |\w+, [^,]{3,45} and ") def _bullet_blocks(text: str) -> list[list[str]]: """Bullets grouped by rSubsection (one group per position).""" body = re.search(r"\\begin\{document\}(.*)\\end\{document\}", text, re.S) body_text = body.group(1) if body else text blocks = re.split(r"\\begin\{rSubsection\}", body_text)[1:] return [re.findall(r"\\item\s+(.+)", block) for block in blocks] def _visible_text(bullet: str) -> str: """Bullet text with LaTeX markup removed, so \\textbf{Owned ...} reads as 'Owned ...'.""" text = re.sub(r"\\[a-zA-Z]+\*?(\[[^]]*\])?", " ", bullet) return re.sub(r"[{}$\\]", " ", text) def _opening_word(bullet: str) -> str: match = re.search(r"[A-Za-z]+", _visible_text(bullet)) return match.group(0).lower() if match else "" def cadence_checks(path: Path) -> list[str]: """Bullet cadence diagnostics (resume_reference.md 7a). Warnings only, never errors. Uniform rhythm is a style flaw, not a truth flaw: no claim, scope or hedged verb may be bent to satisfy these. Reshape sentences. """ blocks = _bullet_blocks(read_text(path)) bullets = [b for block in blocks for b in block] if len(bullets) < 6: return [] warnings: list[str] = [] triples = [b for b in bullets if TRIPLE.search(b)] share = len(triples) / len(bullets) if share > 0.5: warnings.append( f"{path}: {len(triples)}/{len(bullets)} bullets ({share:.0%}) use an " f"'X, Y and Z' triple (reference 7a: under ~50%). Reshape sentences, not claims." ) for position, block in enumerate(blocks, 1): for index in range(1, len(block)): previous, current = _opening_word(block[index - 1]), _opening_word(block[index]) if previous and previous == current: warnings.append( f"{path}: position {position}, bullets {index}-{index + 1} both open " f"with '{previous}'" ) # Threshold calibrated on the 50 multi-bullet positions in output/: spreads are # bimodal (1-9, then 15-17). <=4 flags the tight tail (~28%), not the median. lengths = [len(_visible_text(b).split()) for b in block] if len(lengths) >= 4 and max(lengths) - min(lengths) <= 4: warnings.append( f"{path}: position {position} bullets cluster at {min(lengths)}-{max(lengths)} " f"words; vary length within the position, not just across the document." ) return warnings def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--document", action="append", default=[], help="Generated .tex file to validate") args = parser.parse_args() claims = load_json(CLAIMS_PATH) history = load_json(HISTORY_PATH) errors, warnings = canonical_checks(claims, history) errors.extend(workflow_checks()) for document in args.document: path = Path(document) if not path.is_absolute(): path = ROOT / path if not path.exists(): errors.append(f"document not found: {path}") else: errors.extend(scan_document(path, claims)) warnings.extend(cadence_checks(path)) for warning in warnings: print(f"WARN: {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))") return 0 if __name__ == "__main__": sys.exit(main())