Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ fix → patch (`0.3.x`); feature addition → minor (`0.x.0`).
> Ajouté → Added · Modifié → Changed · Corrigé → Fixed · Déprécié → Deprecated ·
> Supprimé → Removed · Sécurité → Security · Validé → Validated · Documentation → Documentation.

## [0.17.0] — 2026-06-06
## [0.18.0] — 2026-06-06

### Changed
- **Cover-letter template de-French-ified (minimal pass).** The place-and-date line was a hardcoded `{{SENDER_CITY}}, {{DATE_LETTER}}` composite — a continental/French tic (mandatory city + comma) baked into the layout. It is now a **single `{{DATE_LINE}}` slot the model composes in the target locale's convention** (e.g. `Paris, le 6 juin 2026`, `New York, June 6, 2026`, or a date-only line where the locale omits the city). The date was already model-supplied, so this only merges two values into one model-composed line — no script-owned formatting handed over. `sender_city` stays a slot for the sender address block. Body alignment (justify) left as-is: acceptable for the majority of the 11 default languages, and changing it would swap one bias for another. Regenerated `assets/Cover_letter_template.docx` accordingly.
- **Data contract:** `--data-json` now carries `date_line` instead of `date_letter`. Updated `fill_cover_letter.py` (placeholder map, required fields, docstring), `generate_templates.py` (template + placeholder list), the cover-letter `GUIDE.md`, the gallery (`tooling/build_samples.py`) and the test fixture. No migration: the value was always composed per letter by the model, never stored.

### Documentation
- **`letter_conventions.md`:** new `DATE_LINE` section (model composes place+date per locale; city optional for date-only locales) and a **"Layout & direction" known-limitation note** — the template is LTR; for an RTL target language reachable only via the "Other… (ISO code)" option (Arabic, Hebrew, Persian, Urdu…), the block layout is not mirrored (content correct, arrangement LTR). None of the 11 default interface languages is RTL, so this never affects them. Closes the cover-letter localization topic: text conventions were already agnostic (model-driven for any language); only the layout's continental tic needed neutralizing.



### Added
- **`manage_tracker.py --timezone <IANA>`** on every file-writing subcommand (`init`, `upsert`, `bulk`, `batch-status`, `reconcile`). The filename timestamp zone is now a parameter the model resolves from the candidate locale, instead of a hardcoded constant. New `resolve_timezone()` helper: validates the IANA name via `zoneinfo`, and on an unknown/malformed name warns on stderr and falls back to `Europe/Paris` — a filename op never aborts (robustness floor). No city→zone table (mapping a place to its zone is a model task, same reason LNG-1 dropped the language tables).
Expand Down
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: candidate-suite
version: 0.17.0
version: 0.18.0
updated: 2026-06-06
description: "All-in-one suite for preparing job applications and interviews, bundled as a single module: cover letter, interview prep, application summary, strategic playbook, one-page reference card, and application tracking, plus candidate-profile configuration. Single entry point: it shows a selection widget, then generates each deliverable through its sub-module's script. Use whenever the user wants to apply for a role, prepare an application or an interview, get tools for a job posting, configure the skill or update their CV or profile, generate a cover letter, produce a summary, playbook, or reference card, track applications or view their application dashboard (e.g. asking where their applications stand), or makes any open-ended request for job-application help. These are intents, not required wordings: match the user's intent regardless of the language the request is written in."
---
Expand Down
4 changes: 2 additions & 2 deletions modules/candidate-config/scripts/generate_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def add_common_body(doc):
run_company.font.size = Pt(11)
run_company.font.bold = True

run_location = p_recipient.add_run("{{SENDER_CITY}}, {{DATE_LETTER}}")
run_location = p_recipient.add_run("{{DATE_LINE}}")
run_location.font.name = "Calibri"
run_location.font.size = Pt(11)
run_location.font.bold = False
Expand Down Expand Up @@ -330,7 +330,7 @@ def main():
"{{RECRUITER_NAME}}",
"{{RECRUITER_TITLE}}",
"{{COMPANY_NAME}}",
"{{DATE_LETTER}}",
"{{DATE_LINE}}",
"{{JOB_TITLE}}",
"{{SUBJECT_LABEL}}",
"{{GREETING}}",
Expand Down
3 changes: 2 additions & 1 deletion modules/cover-letter-generator/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,8 @@ If the signature is unavailable, the script handles the absence (placeholder ign

The `--data-json` must contain:
- All candidate data (sender_name, sender_street, etc.)
- Recipient data (recruiter_name, recruiter_title, company_name, sender_city, date_letter)
- Recipient data (recruiter_name, recruiter_title, company_name)
- **`date_line`** — the place-and-date line, **composed by the model in the target locale's convention** (e.g. `Paris, le 6 juin 2026` / `New York, June 6, 2026` / date-only `6 June 2026` where the locale omits the city); date formatting per `references/language_style_generic.md`. `sender_city` is still supplied for the **sender address block**.
- Job title (job_title)
- **Localized letter strings, produced by the model in the run language** — `subject_label` (objet label incl. its separator + trailing space, e.g. `Poste : ` / `Position: ` / `Betreff: `), `greeting` (salutation), `closing` (e.g. `Cordialement,` / `Sincerely,` / `Mit freundlichen Grüßen,`); see `references/letter_conventions.md`
- The 5 paragraphs (paragraph_1_intro, paragraph_2_current, paragraph_3_experience, paragraph_4_value, paragraph_5_closing)
Expand Down
Binary file modified modules/cover-letter-generator/assets/Cover_letter_template.docx
Binary file not shown.
19 changes: 18 additions & 1 deletion modules/cover-letter-generator/references/letter_conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,26 @@
where the language marks one), native idiom. The letter is consistently addressed to the
recipient / organization throughout.

## 6. Place-and-date line (`DATE_LINE` slot)

- The model composes the **whole line** in the target locale's convention — there is no
fixed "City, Date" composite in the template (that was a continental/French tic). Examples:
`Paris, le 6 juin 2026` (FR), `New York, June 6, 2026` (US EN), `6 giugno 2026` (IT),
or a **date-only** line where the locale conventionally omits the place (common in US EN).
- Date rendering (order, month name, separators) follows the generic resource. The sender's
city is available to the model but **need not** appear when the local convention is date-only.

## Layout & direction (known limitation)

- The neutral template is **left-to-right (LTR)**. None of the 11 default interface languages
is RTL, so this is a non-issue for them. For an RTL target language reachable only via the
**"Other… (ISO code)"** option (Arabic, Hebrew, Persian, Urdu…), the spatial layout is **not
mirrored** — the *content* is still correct in the target language, only the block arrangement
stays LTR. This is a **documented limitation**, not a silent failure; mirroring is out of scope.

## Named slots the model fills (in the target language)

`GREETING`, `SUBJECT`, the five body paragraphs (`§1`–`§5`), `CLOSING`.
`GREETING`, `SUBJECT`, `DATE_LINE`, the five body paragraphs (`§1`–`§5`), `CLOSING`.

Everything else is handled elsewhere: page layout and the signatory-name placement by the
template; date rendering, number/percent formatting, and punctuation spacing by the generic
Expand Down
9 changes: 6 additions & 3 deletions modules/cover-letter-generator/scripts/fill_cover_letter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
- sender_full_name
- recruiter_name, recruiter_title (use "Hiring Manager" / "Madame, Monsieur" if unknown)
- company_name
- date_letter (format: "27 May 2026" or "27 mai 2026")
- date_line (place-and-date line, composed by the model in the target
convention: e.g. "Paris, le 6 juin 2026", "New York, June 6, 2026", or a
date-only "6 June 2026" where the locale omits the city — see
letter_conventions.md; date formatting per language_style_generic.md)
- job_title
- greeting (salutation, e.g.: "Dear Jane Smith," or "Madame, Monsieur,")
- paragraph_1_intro, paragraph_2_current, paragraph_3_experience,
Expand Down Expand Up @@ -200,7 +203,7 @@ def build_replacements(data):
"{{RECRUITER_NAME}}": "recruiter_name",
"{{RECRUITER_TITLE}}": "recruiter_title",
"{{COMPANY_NAME}}": "company_name",
"{{DATE_LETTER}}": "date_letter",
"{{DATE_LINE}}": "date_line",
"{{JOB_TITLE}}": "job_title",
"{{GREETING}}": "greeting",
"{{SUBJECT_LABEL}}": "subject_label",
Expand Down Expand Up @@ -404,7 +407,7 @@ def main():
"recruiter_name",
"recruiter_title",
"company_name",
"date_letter",
"date_line",
"job_title",
"greeting",
"paragraph_1_intro",
Expand Down
2 changes: 1 addition & 1 deletion tests/_data/cover_letter.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"recruiter_name": "Jane Smith",
"recruiter_title": "Head of Talent",
"company_name": "Acme Financial Group",
"date_letter": "5 June 2026",
"date_line": "Paris, 5 June 2026",
"job_title": "Head of Engineering",
"greeting": "Dear Ms Smith,",
"subject_label": "Subject:",
Expand Down
42 changes: 42 additions & 0 deletions tests/test_fill_cover_letter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
table) and a floating (anchored) signature.
"""

import io
import json
import re
import zipfile

import pytest

from _helpers import REPO_ROOT, load_module, run_cli

CL_REL = "modules/cover-letter-generator/scripts/fill_cover_letter.py"
GT_REL = "modules/candidate-config/scripts/generate_templates.py"
TEMPLATE = (
REPO_ROOT / "modules/cover-letter-generator/assets/Cover_letter_template.docx"
)
Expand Down Expand Up @@ -59,6 +62,36 @@ def test_build_replacements_maps_keys_including_legacy_name():
assert cl.build_replacements({"company_name": None})["{{COMPANY_NAME}}"] == ""


def test_date_line_is_a_single_model_filled_slot():
# 0.18.0 — de-French-ification: the place-and-date line is one slot the model
# composes per locale, not a hardcoded "{{SENDER_CITY}}, {{DATE_LETTER}}" composite.
cl = load_module(CL_REL)
repl = cl.build_replacements({"date_line": "New York, June 6, 2026"})
assert repl["{{DATE_LINE}}"] == "New York, June 6, 2026"
assert "{{DATE_LETTER}}" not in repl # old continental composite is gone


@pytest.mark.needs_docx
def test_generated_template_placeholders_match_fill_map():
# Drift guard (the gap a generator-only rename would slip through): the set of
# {{...}} placeholders the template GENERATOR emits must equal the set
# fill_cover_letter knows how to replace — except {{SIGNATURE_IMAGE}}, inserted
# as a floating image, not via build_replacements.
gt = load_module(GT_REL)
cl = load_module(CL_REL)
buf = io.BytesIO()
gt.create_template(style="hybrid").save(buf)
gen = set(
re.findall(
r"\{\{[A-Z_0-9]+\}\}",
zipfile.ZipFile(buf).read("word/document.xml").decode(),
)
)
fill = set(cl.build_replacements({}).keys())
assert "{{DATE_LINE}}" in gen and "{{DATE_LETTER}}" not in gen # 0.18.0 rename
assert gen - {"{{SIGNATURE_IMAGE}}"} == fill # no orphan placeholder either way


@pytest.mark.needs_docx
def test_check_body_cap_detects_overflow():
cl = load_module(CL_REL)
Expand Down Expand Up @@ -110,6 +143,15 @@ def test_missing_required_field_exits_1(tmp_path):
assert proc.returncode == 1


@pytest.mark.needs_docx
def test_missing_date_line_is_required(tmp_path):
# 0.18.0: date_line replaced date_letter in the required-fields list.
d = _data()
del d["date_line"]
proc, _ = _run(tmp_path, d)
assert proc.returncode == 1


@pytest.mark.needs_docx
def test_body_over_cap_is_rejected(tmp_path):
proc, _ = _run(tmp_path, dict(_data(), paragraph_3_experience="x" * 3000))
Expand Down
2 changes: 1 addition & 1 deletion tooling/build_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def lorem_letter_data(signed):
"recruiter_name": "Jane Smith",
"recruiter_title": "Head of Talent",
"company_name": "Acme Financial Group",
"date_letter": "5 June 2026",
"date_line": "Paris, 5 June 2026",
"job_title": "Head of Engineering",
"greeting": "Dear Ms Smith,",
"subject_label": "Subject:",
Expand Down