fix(F18-F19): get evidence onto page one earlier, measured not asserted
F18 - page-one weighting - Across four compiled packages the first employment entry began at 45-55% of page one. A reader reaching the middle of the page had seen a profile and a skills block, and no evidence. - The old skills rule counted `\skillline` MACROS, which does not control density: six categories with long value lists wrap to twelve or more rendered lines. Replaced with a rendered-line budget verified in the compiled PDF, plus the rule that a skills line repeating what three bullets below already prove is spending the first screen to say it twice. - Employer chronology stays reverse-chronological; the ordering freedom is WITHIN a position, where the bullet closest to the target role leads. - New helper `page_metrics.py` measures page-one geometry from the PDF via pdftotext -bbox. F19 - education and phrasing - Degree + focus + institution crammed into one cell with a right-aligned date produced visible word spacing and mid-word breaks in SBB, NVIDIA and Aker BP. Split the entry, as RUAG_AI_Engineer_C5I already did. Template carries the rule. - Grades and thesis stay only where concretely relevant. claims.json grade_output_rule already keeps grades off by default; the M.Eng. thesis earns its place on condition-monitoring or sensor-data roles and otherwise does not. Validated by measurement rather than assertion - page_metrics.py first reproduced the review's own figures (NVIDIA 55.4%, SBB 50.6%, RUAG 50.3%). That is what makes the after-figure meaningful. - The NVIDIA CV was rebuilt FROM A COPY with the guidance applied: experience moved 55.4% -> 44.0% of page one, page-one text lines 47 -> 39, still two pages, zero overfull boxes, validator PASS. - The submitted package itself was not modified. F17 classifies archives as evidence, not as drafts. The percentage is a diagnostic, not a target: no pixel goal and no rule that page two must be filled. All 19 review findings are now complete. 37/37 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
This commit is contained in:
1 parent
eecdbdade6
commit
b8c5141279
4 files changed
+137
-7
No files matched your search
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Measure where the first real employment evidence starts on page one (review finding F18).
|
||||
|
||||
Usage: python resume_builder/helpers/page_metrics.py <resume.pdf> [more.pdf ...]
|
||||
Requires pdftotext (ships with MiKTeX).
|
||||
|
||||
The review reported ~50-55% for four packages. This reproduces that measurement from the
|
||||
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 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:
|
||||
with tempfile.TemporaryDirectory() as work:
|
||||
out = Path(work) / "bbox.xml"
|
||||
subprocess.run([str(PDFTOTEXT), "-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)
|
||||
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})
|
||||
return {
|
||||
"height": height,
|
||||
"experience_y": first_y,
|
||||
"experience_pct": round(100 * first_y / height, 1) if first_y else None,
|
||||
"page1_text_lines": lines,
|
||||
"page1_words": len(words),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for argument in sys.argv[1:]:
|
||||
pdf = Path(argument)
|
||||
data = measure(pdf)
|
||||
pct = data["experience_pct"]
|
||||
print(f"{pdf.parent.name:<34} experience starts at "
|
||||
f"{pct if pct is not None else '?':>5}% of page 1 | "
|
||||
f"{data['page1_text_lines']:>3} text lines, {data['page1_words']:>4} words")
|
||||
Reference in new issue
Block a user