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:
@@ -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 25–30 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 (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
|
## 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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user