diff --git a/FIX_PROGRESS.md b/FIX_PROGRESS.md index fc36623..76e168b 100644 --- a/FIX_PROGRESS.md +++ b/FIX_PROGRESS.md @@ -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 `.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 diff --git a/resume_builder/helpers/page_metrics.py b/resume_builder/helpers/page_metrics.py new file mode 100644 index 0000000..275f5ff --- /dev/null +++ b/resume_builder/helpers/page_metrics.py @@ -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 [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'([^<]*)', + 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") diff --git a/resume_builder/reference/resume_reference.md b/resume_builder/reference/resume_reference.md index ca109f3..ec5c692 100644 --- a/resume_builder/reference/resume_reference.md +++ b/resume_builder/reference/resume_reference.md @@ -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 ` 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. diff --git a/resume_builder/templates/resume_template.tex b/resume_builder/templates/resume_template.tex index 7844eac..7a7ad28 100644 --- a/resume_builder/templates/resume_template.tex +++ b/resume_builder/templates/resume_template.tex @@ -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.