feat(resume): check bullet cadence variety, warn-only

An audit of all 18 packages in output/ (368 bullets) found one rhythm
running through every document: 45% of bullets used the same "X, Y and
Z" triple, and the SBB package reached 85% - 11 of 13 bullets, every
Swisscom and Bosch line - plus two adjacent bullets both opening
"Build and...".

This is a style finding, not a truth finding. Every bullet was accurate.
The corpus is clean on the axes that actually signal generated text: no
AI vocabulary (0 hits for leverage/spearheaded/robust/passionate and 56
others across 35 documents), prose em-dashes at 0.08/bullet, and PDF
metadata carrying nothing but MiKTeX pdfTeX with empty Author/Title
(0 AI tokens and 0 generator-term leaks across 69 PDFs). What is left is
cadence: ten bullets sharing one three-beat rhythm read as machine-made
even when nothing in them is false.

Guarded deliberately so it cannot do harm. cadence_checks() emits WARN
and never ERROR, the critique deduction caps at 1 point, and both the
reference and the docstring state that no claim, scope or hedged verb
may be bent to satisfy rhythm. An anti-monotony rule with teeth would be
worse than the problem - it would pressure a future run into loosening a
scoped claim to vary a sentence.

Thresholds are calibrated on the corpus, not guessed. Position length
spreads are bimodal (1-9 words, then 15-17), so the check flags a spread
of <=4, the tight tail at ~28% of positions; the first draft used <=6
and flagged the median. Also fixed the opening-verb extractor, which
read \textbf{Owned ...} as the word "textbf" and produced six false
positives on one document. Per-package warnings now run 0-3.

Docs: resume_reference.md 7a + verification step, critical_rules.md 7a,
critique_framework.md mechanics row, CLAUDE.md corrections log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtzyTKBcg6BWhD5qFegtK
This commit is contained in:
2026-08-25 09:38:19 +02:00
co-authored by Claude Opus 5
parent af7189a93d
commit 92a81a7dcb
5 changed files with 99 additions and 1 deletions
+1
View File
@@ -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 | **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-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-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) |
@@ -137,6 +137,67 @@ def scan_document(path: Path, claims: dict) -> list[str]:
return errors 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: def main() -> int:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--document", action="append", default=[], help="Generated .tex file to validate") 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}") errors.append(f"document not found: {path}")
else: else:
errors.extend(scan_document(path, claims)) errors.extend(scan_document(path, claims))
warnings.extend(cadence_checks(path))
for warning in warnings: for warning in warnings:
print(f"WARN: {warning}") print(f"WARN: {warning}")
@@ -7,6 +7,9 @@
5. Put employer, formal title and dates before any narrative framing. 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. 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. 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. 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. 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. 10. Security Champion means the 2025/2026 team role only; omit by default.
@@ -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? | | Bullet evidence and impact | 20 | Are bullets specific and scoped without invention? |
| Relevance and terminology | 15 | Does it cover the JD naturally? | | Relevance and terminology | 15 | Does it cover the JD naturally? |
| Skills evidence | 10 | Is every skill evidence-backed and interview-ready? | | 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. Truth/provenance below 8/10 is an automatic document failure.
@@ -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. 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 2530 words read as generated even when the document's overall range looks fine.
`validate_resume_system.py --document <file.tex>` 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 (19 words, then
1517): 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 ## 8. Titles and Seniority
- Preserve official titles when recognizable. - Preserve official titles when recognizable.
@@ -113,6 +143,8 @@ After generation:
4. Extract text with `pdftotext` and confirm employer/title/date order. 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. 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. 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. Do not add content merely to reduce bottom whitespace.