diff --git a/CHANGELOG.md b/CHANGELOG.md index 6506e7b..2916980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `** 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). diff --git a/SKILL.md b/SKILL.md index a4ca8f8..3dae366 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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." --- diff --git a/modules/candidate-config/scripts/generate_templates.py b/modules/candidate-config/scripts/generate_templates.py index 08d143b..e678fb8 100644 --- a/modules/candidate-config/scripts/generate_templates.py +++ b/modules/candidate-config/scripts/generate_templates.py @@ -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 @@ -330,7 +330,7 @@ def main(): "{{RECRUITER_NAME}}", "{{RECRUITER_TITLE}}", "{{COMPANY_NAME}}", - "{{DATE_LETTER}}", + "{{DATE_LINE}}", "{{JOB_TITLE}}", "{{SUBJECT_LABEL}}", "{{GREETING}}", diff --git a/modules/cover-letter-generator/GUIDE.md b/modules/cover-letter-generator/GUIDE.md index 0f12e3b..c6d7f32 100644 --- a/modules/cover-letter-generator/GUIDE.md +++ b/modules/cover-letter-generator/GUIDE.md @@ -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) diff --git a/modules/cover-letter-generator/assets/Cover_letter_template.docx b/modules/cover-letter-generator/assets/Cover_letter_template.docx index ba6bd44..99684b2 100644 Binary files a/modules/cover-letter-generator/assets/Cover_letter_template.docx and b/modules/cover-letter-generator/assets/Cover_letter_template.docx differ diff --git a/modules/cover-letter-generator/references/letter_conventions.md b/modules/cover-letter-generator/references/letter_conventions.md index 926dbc7..086342f 100644 --- a/modules/cover-letter-generator/references/letter_conventions.md +++ b/modules/cover-letter-generator/references/letter_conventions.md @@ -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 diff --git a/modules/cover-letter-generator/scripts/fill_cover_letter.py b/modules/cover-letter-generator/scripts/fill_cover_letter.py index 7bd7770..eb4c5ce 100644 --- a/modules/cover-letter-generator/scripts/fill_cover_letter.py +++ b/modules/cover-letter-generator/scripts/fill_cover_letter.py @@ -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, @@ -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", @@ -404,7 +407,7 @@ def main(): "recruiter_name", "recruiter_title", "company_name", - "date_letter", + "date_line", "job_title", "greeting", "paragraph_1_intro", diff --git a/tests/_data/cover_letter.json b/tests/_data/cover_letter.json index 6257a04..9b8a90f 100644 --- a/tests/_data/cover_letter.json +++ b/tests/_data/cover_letter.json @@ -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:", diff --git a/tests/test_fill_cover_letter.py b/tests/test_fill_cover_letter.py index d37b8cf..2b49ee4 100644 --- a/tests/test_fill_cover_letter.py +++ b/tests/test_fill_cover_letter.py @@ -7,7 +7,9 @@ table) and a floating (anchored) signature. """ +import io import json +import re import zipfile import pytest @@ -15,6 +17,7 @@ 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" ) @@ -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) @@ -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)) diff --git a/tooling/build_samples.py b/tooling/build_samples.py index 85854aa..93df3f3 100644 --- a/tooling/build_samples.py +++ b/tooling/build_samples.py @@ -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:",