feat: rebuild evidence-first application workflow

This commit is contained in:
2026-07-27 17:56:15 +02:00
parent c24892f381
commit fe5f24704f
57 changed files with 3815 additions and 3555 deletions
+78 -175
View File
@@ -1,198 +1,101 @@
#!/usr/bin/env python3
"""
Count rendered characters in LaTeX resume/CV bullets.
Strips LaTeX markup to show what a reader actually sees on the page.
"""Report readable length diagnostics for LaTeX resume bullets.
This helper has no target bands and no page-fill logic. Rendered character and
word counts are observations; they do not determine whether a bullet is good.
Usage:
python3 char_count.py "\\textbf{DFT} analysis of \\ce{TiO2} surfaces"
echo "bullet text" | python3 char_count.py
python3 char_count.py -f cv output/file.tex
python3 char_count.py --raw "bullet text" # just the number
python resume_builder/helpers/char_count.py "\\item Built ..."
python resume_builder/helpers/char_count.py output/Role/resume.tex
python resume_builder/helpers/char_count.py --raw "bullet text"
"""
from __future__ import annotations
import argparse
import re
import sys
import argparse
from pathlib import Path
def strip_latex(text):
"""Strip LaTeX markup to get rendered text."""
# Remove \item[] prefix
text = re.sub(r'\\item\s*(\[\s*\])?\s*', '', text)
# \href{url}{text} -> text
text = re.sub(r'\\href\{[^}]*\}\{([^}]*)\}', r'\1', text)
# \textbf{X} -> X
text = re.sub(r'\\textbf\{([^}]*)\}', r'\1', text)
# \textit{X} -> X
text = re.sub(r'\\textit\{([^}]*)\}', r'\1', text)
# \underline{X} -> X
text = re.sub(r'\\underline\{([^}]*)\}', r'\1', text)
# \emph{X} -> X
text = re.sub(r'\\emph\{([^}]*)\}', r'\1', text)
# \ce{X} -> X (subscript digits still count as 1 char each)
text = re.sub(r'\\ce\{([^}]*)\}', r'\1', text)
# Greek letters -> 1 char each
greeks = [
'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta',
'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'pi', 'rho', 'sigma',
'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega',
'Alpha', 'Beta', 'Gamma', 'Delta', 'Theta', 'Lambda', 'Sigma',
'Phi', 'Psi', 'Omega',
]
for g in greeks:
text = text.replace(f'$\\{g}$', 'G')
text = text.replace(f'\\{g}', 'G')
# $^\circ$ -> 1 char
text = re.sub(r'\$\^\{?\\circ\}?\$', 'D', text)
# $^\dagger$ -> 1 char
text = re.sub(r'\$\^\{?\\dagger\}?\$', 'D', text)
# Superscripts: $^{2}$ or $^2$ -> content
text = re.sub(r'\$\^\{([^}]*)\}\$', r'\1', text)
text = re.sub(r'\$\^(.)\$', r'\1', text)
# Subscripts: $_{2}$ or $_2$ -> content
text = re.sub(r'\$_\{([^}]*)\}\$', r'\1', text)
text = re.sub(r'\$_(.)\$', r'\1', text)
# \sim -> 1 char (~)
text = text.replace('$\\sim$', '~')
text = text.replace('\\sim', '~')
text = text.replace('\\textasciitilde', '~')
# $<$ $>$ -> 1 char
text = re.sub(r'\$([<>])\$', r'\1', text)
# --- -> em-dash (1 char but ~2x wide)
text = text.replace('---', '\u2014')
# -- -> en-dash (1 char)
text = text.replace('--', '\u2013')
# Remove remaining $ (math mode delimiters)
text = text.replace('$', '')
# Remove remaining \commands
text = re.sub(r'\\[a-zA-Z]+\s*', '', text)
# Remove remaining braces
text = text.replace('{', '').replace('}', '')
# Collapse multiple spaces
text = re.sub(r' +', ' ', text)
return text.strip()
def strip_latex(text: str) -> str:
"""Approximate reader-visible text without imposing layout targets."""
text = re.sub(r"\\item\s*(\[\s*\])?\s*", "", text)
text = re.sub(r"\\href\{[^}]*\}\{([^}]*)\}", r"\1", text)
for command in ("textbf", "textit", "underline", "emph", "ce"):
text = re.sub(rf"\\{command}\{{([^}}]*)\}}", r"\1", text)
text = re.sub(r"\$\^\{?\\circ\}?\$", "°", text)
text = re.sub(r"\$\^\{([^}]*)\}\$", r"\1", text)
text = re.sub(r"\$\^(.)\$", r"\1", text)
text = text.replace("$\\sim$", "~").replace("\\sim", "~")
text = text.replace("---", "").replace("--", "")
text = text.replace("\\&", "&").replace("\\%", "%")
text = re.sub(r"\\[A-Za-z]+\s*", "", text)
text = text.replace("$", "").replace("{", "").replace("}", "")
return re.sub(r"\s+", " ", text).strip()
def count_bold_chars(text):
"""Count characters inside \\textbf{} commands."""
return sum(len(m) for m in re.findall(r'\\textbf\{([^}]*)\}', text))
def count_em_dashes(text):
"""Count em-dashes (---) which render ~2x wide."""
return len(re.findall(r'---', text))
def classify_bullet(char_count, bold_chars, fmt):
"""Classify bullet into variant and check limits."""
if fmt == 'resume':
base = 119
penalty = 0.5
tiers = [
('1L', 105, 111, 117, None),
('2L', 189, 205, 218, 78),
]
else:
base = 91
penalty = 0.25
tiers = [
('1L', 88, 93, 101, None),
('2L', 168, 182, 190, 65),
('3L', 250, 268, 280, 65),
]
effective = base - (penalty * bold_chars)
for variant, lo, hi, hard_max, orphan in tiers:
if char_count <= hard_max:
if char_count < lo:
status = 'SHORT'
elif char_count <= hi:
status = 'OK'
else:
status = 'NEAR MAX'
return variant, status, lo, hi, hard_max, orphan, effective
return 'OVER', 'OVER LIMIT', 0, 0, 0, None, effective
def format_one(raw, fmt):
"""Format analysis for a single bullet."""
rendered = strip_latex(raw)
n = len(rendered)
bold = count_bold_chars(raw)
em = count_em_dashes(raw)
variant, status, lo, hi, hard_max, orphan, eff = classify_bullet(n, bold, fmt)
parts = [f" {n:3d} chars | {variant} {fmt.upper()} | {status} (target {lo}-{hi}, max {hard_max})"]
if bold:
parts.append(f" Bold: {bold} chars -> effective limit/line: {eff:.0f}")
if em:
parts.append(f" Em-dashes: {em} (each ~2x wide, budget +{em} extra)")
parts.append(f" Rendered: {rendered}")
return '\n'.join(parts), variant
def extract_items(text):
"""Extract \\item lines from .tex source."""
items = []
for line in text.split('\n'):
s = line.strip()
if s.startswith('\\item'):
items.append(s)
def extract_items(source: str) -> list[str]:
"""Extract item bodies, including wrapped source lines."""
items: list[str] = []
current: list[str] = []
for line in source.splitlines():
stripped = line.strip()
if stripped.startswith("\\item"):
if current:
items.append(" ".join(current))
current = [stripped]
elif current and not stripped.startswith("\\end{itemize}"):
current.append(stripped)
elif current:
items.append(" ".join(current))
current = []
if current:
items.append(" ".join(current))
return items
def main():
parser = argparse.ArgumentParser(
description='Count rendered characters in LaTeX resume/CV bullets')
parser.add_argument('input', nargs='?',
help='Bullet text or .tex file path')
parser.add_argument('-f', '--format', choices=['resume', 'cv'],
default='resume', help='Document format (default: resume)')
parser.add_argument('--raw', action='store_true',
help='Output only char count (for scripting)')
def diagnose(raw: str) -> tuple[str, int, int, int]:
rendered = strip_latex(raw)
words = re.findall(r"\b[\w+#./-]+\b", rendered, flags=re.UNICODE)
clauses = len(re.findall(r"[;:]|\s—\s", rendered)) + 1 if rendered else 0
return rendered, len(rendered), len(words), clauses
def format_report(raw: str) -> str:
rendered, characters, words, clauses = diagnose(raw)
note = ""
if words > 35 or clauses > 3:
note = "\n Review: potentially dense; check whether it contains multiple accomplishments."
return (
f" {words} words | {characters} rendered characters | {clauses} clause(s)\n"
f" Rendered: {rendered}{note}"
)
def main() -> None:
parser = argparse.ArgumentParser(description="Report LaTeX bullet length diagnostics")
parser.add_argument("input", nargs="?", help="Bullet text or .tex file")
parser.add_argument("--raw", action="store_true", help="Output rendered character count only")
args = parser.parse_args()
if args.input and args.input.endswith('.tex'):
with open(args.input) as f:
items = extract_items(f.read())
if args.input and args.input.endswith(".tex") and Path(args.input).is_file():
items = extract_items(Path(args.input).read_text(encoding="utf-8"))
if not items:
print("No \\item lines found.")
print("No \\item bullets found.")
return
total_lines = 0
print(f"Found {len(items)} bullets ({args.format} format):\n")
for i, item in enumerate(items, 1):
for index, item in enumerate(items, 1):
if args.raw:
print(len(strip_latex(item)))
print(diagnose(item)[1])
else:
report, variant = format_one(item, args.format)
print(f"Bullet {i}:")
print(report)
print()
if variant not in ('OVER',):
total_lines += int(variant[0])
if not args.raw:
print(f"Total rendered lines: {total_lines}")
elif args.input:
if args.raw:
print(len(strip_latex(args.input)))
else:
report, _ = format_one(args.input, args.format)
print(report)
else:
for line in sys.stdin:
line = line.strip()
if not line:
continue
if args.raw:
print(len(strip_latex(line)))
else:
report, _ = format_one(line, args.format)
print(report)
print()
print(f"Bullet {index}:\n{format_report(item)}\n")
return
raw = args.input if args.input is not None else sys.stdin.read().strip()
if not raw:
parser.error("provide bullet text, a .tex file, or stdin")
print(diagnose(raw)[1] if args.raw else format_report(raw))
if __name__ == '__main__':
if __name__ == "__main__":
main()
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Record and summarize the controlled ten-application cohort."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
TRACKER = ROOT / "job_scout" / "state" / "application_cohort.json"
FIT_CLASSES = ("core", "adjacent", "stretch")
CHANNELS = ("strong", "moderate", "weak")
def load() -> dict:
return json.loads(TRACKER.read_text(encoding="utf-8"))
def save(data: dict) -> None:
TRACKER.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def summary(data: dict) -> None:
applications = data["applications"]
print(f"Cohort: {data['cohort_id']} ({len(applications)}/10 applications)")
for fit_class in FIT_CLASSES:
count = sum(item["fit_class"] == fit_class for item in applications)
target = data["target_mix"][fit_class]
print(f" {fit_class}: {count}/{target}")
for channel in CHANNELS:
count = sum(item["channel"] == channel for item in applications)
print(f" channel {channel}: {count}")
outcomes: dict[str, int] = {}
for item in applications:
outcome = item.get("outcome", "open")
outcomes[outcome] = outcomes.get(outcome, 0) + 1
print(" outcomes: " + ", ".join(f"{key}={value}" for key, value in sorted(outcomes.items())))
def main() -> None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("summary")
add = subparsers.add_parser("add")
add.add_argument("--company", required=True)
add.add_argument("--role", required=True)
add.add_argument("--date", required=True)
add.add_argument("--fit-class", choices=FIT_CLASSES, required=True)
add.add_argument("--evidence-fit", type=float, required=True)
add.add_argument("--channel", choices=CHANNELS, required=True)
add.add_argument("--hard-gate", choices=("pass", "fail"), required=True)
add.add_argument("--outcome", default="open")
args = parser.parse_args()
data = load()
if args.command == "add":
if len(data["applications"]) >= 10:
raise SystemExit("cohort already contains ten applications")
if args.hard_gate == "fail" and args.fit_class != "stretch":
raise SystemExit("a hard-gate failure must be recorded as stretch")
data["applications"].append(
{
"company": args.company,
"role": args.role,
"application_date": args.date,
"fit_class": args.fit_class,
"evidence_fit": args.evidence_fit,
"hard_gate": args.hard_gate,
"channel": args.channel,
"outcome": args.outcome,
}
)
save(data)
summary(data)
if __name__ == "__main__":
main()
@@ -0,0 +1,170 @@
#!/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
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))
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())