diff --git a/CLAUDE.md b/CLAUDE.md index e034341..5708707 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -195,3 +195,4 @@ _See `config.md` for user-specific corrections. Add verified errors here as you | 2026-08-02 | **Master's thesis verified against the PDF.** Vibration-based condition monitoring of CNC machine tools; hybrid **rule-based reasoning + 7-10-3 ANN**; throughput/latency evaluation (~500 SPS pipeline vs 72.9 kSPS sensors). **PSO was surveyed but NOT implemented** — an earlier note in this session wrongly listed it as an applied method. No real operational data and no accuracy figures: it is a methods prototype, not a validated system. Also recorded: ECTS relative grade **B (top 35%)**, English-language transcripts exist. | `claims.json` EDU-MENG | | 2026-08-02 | **Bosch data types named.** Fab sensor/process data: defect-management records, wafer inspection images, PCM electrical parameters (user-confirmed). Previously the KB named no concrete sensor-data types for Bosch. | `claims.json` BS-2 | | 2026-07-27 | **Bullet density.** Fixed 1L/2L/3L and character-band rules made bullets uniform and encouraged page filling. | Replaced globally with natural-length, evidence-led bullets; character counts are diagnostic only. | +| 2026-08-25 | **Cadence monoculture across every generated package.** Audit of all 18 packages in `output/` (368 bullets) found **45% used the same "X, Y and Z" triple**; the SBB package hit **85% — 11 of 13 bullets, every Swisscom and Bosch line** — plus two adjacent bullets both opening "Build and…". Not a truth defect: every bullet was accurate and the corpus is clean on AI vocabulary (0 cliché hits in 35 documents) and prose em-dashes (0.08/bullet). It is a *rhythm* tell — uniform three-beat cadence reads as machine-written. PDF metadata is clean (MiKTeX pdfTeX, empty Author/Title, no AI tokens in 69 PDFs, no generator-term leakage). | New `resume_reference.md` §7a + verification step 7; `critical_rules.md` rule 7a; `critique_framework.md` mechanics row now scores cadence (max 1 pt, style only) | diff --git a/resume_builder/helpers/validate_resume_system.py b/resume_builder/helpers/validate_resume_system.py index f033d76..62614bb 100644 --- a/resume_builder/helpers/validate_resume_system.py +++ b/resume_builder/helpers/validate_resume_system.py @@ -137,6 +137,67 @@ def scan_document(path: Path, claims: dict) -> list[str]: 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") @@ -154,6 +215,7 @@ def main() -> int: 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}") diff --git a/resume_builder/reference/critical_rules.md b/resume_builder/reference/critical_rules.md index f6dcd95..d2e76c7 100644 --- a/resume_builder/reference/critical_rules.md +++ b/resume_builder/reference/critical_rules.md @@ -7,6 +7,9 @@ 5. Put employer, formal title and dates before any narrative framing. 6. Use an optional 2--3 line summary, 4--6 skills lines and normally 11--14 evidence-led bullets. 7. Mix natural bullet lengths. There is no character target and no page-fill quota. +7a. Vary bullet cadence: keep "X, Y and Z" triples under about half the bullets, avoid two + adjacent bullets opening with the same verb, and vary length *within* each position. + Style only — never bend a claim, a scope or a hedged verb to fix rhythm (see §7a). 8. Do not list unverified skills, metrics, customers, scale or causal impact. 9. Scope Swisscom migration and Data Mesh claims to Dennis's domains, components and products. 10. Security Champion means the 2025/2026 team role only; omit by default. diff --git a/resume_builder/reference/critique_framework.md b/resume_builder/reference/critique_framework.md index f1a05f9..5e54970 100644 --- a/resume_builder/reference/critique_framework.md +++ b/resume_builder/reference/critique_framework.md @@ -42,7 +42,7 @@ Score the candidate-role pairing before reading tailored prose. | Bullet evidence and impact | 20 | Are bullets specific and scoped without invention? | | Relevance and terminology | 15 | Does it cover the JD naturally? | | Skills evidence | 10 | Is every skill evidence-backed and interview-ready? | -| Mechanics/readability | 10 | Does it compile, parse and read cleanly? | +| Mechanics/readability | 10 | Does it compile, parse and read cleanly? Includes **cadence variety** — read the bullets in sequence and count "X, Y and Z" triples, repeated opening verbs and length clustering per position (`resume_reference.md` §7a). Deduct at most 1 pt; it is a style flaw, never a truth flaw. | Truth/provenance below 8/10 is an automatic document failure. diff --git a/resume_builder/reference/resume_reference.md b/resume_builder/reference/resume_reference.md index 2c292ce..fcc97e3 100644 --- a/resume_builder/reference/resume_reference.md +++ b/resume_builder/reference/resume_reference.md @@ -96,6 +96,36 @@ Natural one-, two- and three-line bullets may be mixed. There are no target char Use metrics only when verified. Company size, customer names, generic industry volumes and market economics are context, not personal impact. +### 7a. Cadence variety (anti-monotony) + +Individually honest bullets can still read as machine-written when they all share one +rhythm. Measured across the 18 packages in `output/` (368 bullets): **45 % used the same +"X, Y and Z" triple**, and the SBB package reached **85 % — 11 of 13 bullets, every +Swisscom and Bosch line**. Nothing in it was false; it simply had one cadence. + +This is a *style* problem, never a truth problem. Never trade accuracy, scope discipline +or a hedged verb to fix cadence — an accurate monotonous bullet beats a varied inaccurate +one. Fix it by reshaping sentences, not by changing what is claimed. + +Per document, aim for: + +- **At most about half the bullets carrying a three-part list.** The triple is normal resume + grammar and humans use it too; the tell is using little else. +- **At least two or three bullets in a different shape** — a short declarative with a single + object; a bullet with one object rather than three; a bullet whose scope clause comes first. +- **No two consecutive bullets opening with the same verb** (the SBB resume had "Build and + model…" directly followed by "Build and operate…"). +- **Visible length variation inside each position**, not just across the document. Five bullets + at 25–30 words read as generated even when the document's overall range looks fine. + +`validate_resume_system.py --document ` measures all three and reports them as +**WARN**, never ERROR — cadence can never fail a document. The length threshold is calibrated +on the 50 multi-bullet positions in `output/`, whose spreads are bimodal (1–9 words, then +15–17): it flags a spread of 4 words or less, the tight tail, not the median. + +Diagnostic only — like character counts, these are checks, never targets, and no bullet +should be padded or trimmed to hit them. + ## 8. Titles and Seniority - Preserve official titles when recognizable. @@ -113,6 +143,8 @@ After generation: 4. Extract text with `pdftotext` and confirm employer/title/date order. 5. Inspect the rendered PDF: no clipping, overlap, tiny text, isolated headings or awkward page break. 6. Check that experience begins comfortably on page 1 and certifications are not duplicated. +7. Act on any cadence WARN from step 1 (§7a). These never fail the document; reshape the + offending sentences, never the claims behind them. Do not add content merely to reduce bottom whitespace.