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:
dennisthiessenandClaude Opus 5 committed 2026-09-11 20:56:17 +02:00
1 parent eecdbdade6
commit b8c5141279
4 files changed
+137 -7

No files matched your search

+14 -7
View File
@@ -6,12 +6,15 @@ Do not rewrite submitted application archives or invent missing evidence.
## Resume point
F01–F17 complete. **Next: F18** — evidence earlier on page one and a rendered skills budget.
F18/F19 change how the CVs look; the user authorised this on 2026-09-11. They need compiling
and visual inspection, not blind patching, so budget for a build loop.
**All findings F01–F19 are complete.**
Remaining known gaps, none of them a finding: the visual page-by-page sampling was not repeated
(the F18 work measured page-one geometry from the compiled PDF instead), and the 13 older
packages listed below still carry genuine findings under today's rules - deliberately, because
F17 classifies archives as evidence rather than rewriting them.
Read this file and inspect `git log`/`git diff` before resuming; do not repeat completed work.
Checkpoints: `afdef7e` (F01+F02) · `fc5f3e4` (F03–F06) · `ce5b7f5` (F07) · `3519b2f` (F08–F10) · `afd8174` (F11–F13) · `ce8619b` (F14) · `c7e8d4e` (F15–F16) · plus F17.
Checkpoints: `afdef7e` (F01+F02) · `fc5f3e4` (F03–F06) · `ce5b7f5` (F07) · `3519b2f` (F08–F10) · `afd8174` (F11–F13) · `ce8619b` (F14) · `c7e8d4e` (F15–F16) · `eecdbda` (F17) · plus F18–F19.
## User rulings — 2026-09-11
@@ -43,8 +46,8 @@ Checkpoints: `afdef7e` (F01+F02) · `fc5f3e4` (F03–F06) · `ce5b7f5` (F07) ·
| F15 | DONE | Taleo deadline moved out of `posted` (a closing date was making roles look freshly published); `unit`, `compensation`, `contact` and `date_provenance` carried as fields through to the report and JSON. Named contacts now surface — directly serving the channel rule |
| F16 | DONE | Restricted runs write `<date>.only-x.md` and leave the daily report intact; seen-state committed only after the report is on disk, and never by a restricted run; unique temp names for atomic writes |
| F17 | DONE | Build sweep 46/48; `package_manifest.py` records per package what a rebuild would really do; 7 unclassified packages registered with a safe default; ` oday` removed from the cover-letter template. Historical packages untouched |
| F18 | TODO | Evidence earlier on page one; rendered skills budget |
| F19 | TODO | Education layout, phrasing guidance, reproducible word counts |
| F18 | DONE | Rendered-line budget replaces the source-macro count; `page_metrics.py` measures page-one geometry. Demonstrated on a rebuilt copy of the NVIDIA CV: experience moved **55.4% → 44.0%**, 47 → 39 page-one lines, still 2 pages, 0 boxes, validator PASS |
| F19 | DONE | Education entries split (degree on the entry line, institution below) after the RUAG model; grades/thesis kept only where concretely relevant; template carries the rule as a comment |
**Sequencing note:** F11–F16 all touch `scout.py`. Fix behaviours in place and keep each
finding's diff separately verifiable. **Do not start the ~2400-line file split F16 suggests**
@@ -107,7 +110,11 @@ mid-remediation — it would destroy the ability to attribute a regression to a
- F17 classification: 7 packages were unregistered; added to `historical_revalidate`, with a
recorded default that anything unlisted is treated that way. Being recent is not being verified —
none was re-read for this.
- Still not re-run: the visual page sampling (F18).
- F18/F19 validated by measurement, not assertion: `page_metrics.py` reproduced the review's own
figures (NVIDIA 55.4%, SBB 50.6%, RUAG 50.3%) before any change, which is what makes the
after-figure meaningful. The submitted NVIDIA package was NOT modified - the demonstration was
built from a copy.
- The percentage is a DIAGNOSTIC, not a target. No pixel goal, no forced page-two filling.
## Completion rules
+59
View File
@@ -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")
@@ -159,3 +159,61 @@ Classify every JD requirement as:
- **Gap:** no reliable evidence.
Never turn Adjacent into Direct by vocabulary substitution. A required, title-defining Gap is an application-fit problem, not a resume-writing problem.
---
## Page-one weighting and rendered density (review findings F18/F19, 2026-09-11)
**The problem, measured:** across four compiled packages the first real employment entry began
at **45-55% of page one** (NVIDIA 55.4%, SBB 50.6%, RUAG 50.3%, Aker BP 45.4%). A reader
reaching the middle of the page has seen a profile and a skills block, and no evidence yet.
Measure with `helpers/page_metrics.py <pdf>` rather than estimating.
These are **diagnostics, not targets.** There is no pixel goal and no rule that page two must
be filled. A package that reads well at 50% is fine; a skills block that pushed evidence there
is not.
### Skills: budget RENDERED lines, not source macros
The old rule counted `\skillline` macros, which does not control density: six categories with
long value lists wrap to **twelve or more rendered lines**. The NVIDIA package is the worked
example - six `\skillline` calls, one of them 240 characters.
- **Aim for roughly 4-8 rendered lines of skills**, verified in the compiled PDF.
- **Cut anything the experience bullets already prove.** A skills line repeating "Kubernetes,
Docker, GitLab CI/CD" when three bullets below say the same thing spends page-one space to
say it twice.
- **Keep what the JD actually gates on** plus the ATS literals that are genuinely evidenced.
Drop adjacent nice-to-haves; they read as padding and cost the reader's first screen.
- Certifications belong on one line, at the end, not spread across categories.
**Worked example, measured 2026-09-11.** The submitted NVIDIA CV was rebuilt from a copy with
this guidance applied - six skill lines cut to four by removing what the experience bullets
already prove, and the education entries split per F19. Result: the experience section moved
from **55.4% to 44.0%** of page one, page-one text lines from 47 to 39, still two pages, zero
overfull boxes, validator PASS. The submitted document itself was NOT modified (F17: archives
are evidence).
### Order: employer chronology stays, evidence order inside a position does not
- **Keep reverse-chronological employers.** Swiss and German readers expect it and breaking it
looks evasive.
- **Within a position, lead with the bullet closest to the target role**, not with the one that
happens to be most senior or most recent. The chronology is between employers, not inside one.
- After the short profile, the next thing should be **either the experience section or a very
compact competence block**. Not both at full length.
### Education (F19)
Long single-cell entries - degree, focus, institution, right-aligned date - produce visible
word-spacing and mid-word breaks. Seen in SBB, NVIDIA and Aker BP.
- **Split the entry**: degree on the entry line, institution (and focus) on the line below.
`RUAG_AI_Engineer_C5I` is the model that already does this well.
- **Grades and thesis stay only where they carry concrete relevance.** For an experienced
industry profile they usually do not; `claims.json` `grade_output_rule` already says grades
are off by default. The M.Eng. thesis is worth naming when the role touches condition
monitoring, diagnostics or industrial sensor data - as the SBB asset-management req did -
and otherwise not.
- Do not let an immutable template line force education detail into a document that does not
need it.
@@ -80,6 +80,12 @@ Staff data engineer with over ten years of production software experience across
\end{rSection}
% EDUCATION LAYOUT (review finding F19, 2026-09-11)
% Keep the degree on the entry line and the institution on the line below. Cramming
% degree + focus + institution into one cell with a right-aligned date produces visible
% word spacing and mid-word breaks - seen in the SBB, NVIDIA and Aker BP packages.
% Grades and thesis only where they carry concrete relevance; see claims.json
% grade_output_rule, which keeps grades off by default.
\begin{rSection}{Education}
\compactentry{M.Eng. Computer Aided Engineering (Software Design \& Engineering), Universität der Bundeswehr München}{Apr 2012 -- Oct 2013}
Thesis at Tongji University, Shanghai: \textit{Development of a Web-Based Remote Fault Diagnosis System}; grade 1.0.