mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
fix(rank): preserve Unicode identity in tracker matching (#486)
This commit is contained in:
1 parent
ba4f2397a3
commit
120f476a08
3 files changed
+54
-1
No files matched your search
@@ -84,6 +84,15 @@ per-file diff commands.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`/rank` tracker matching preserves Unicode company and role names**
|
||||
(`tools/rank_state.py`, `tests/test_rank_state.py`) - ASCII-only normalization
|
||||
collapsed distinct non-Latin roles to the same empty value and dropped
|
||||
entirely non-Latin companies from tracker exclusions. Match using Unicode
|
||||
case folding and NFC normalization, retaining letters, numbers, and combining
|
||||
marks while continuing to ignore punctuation and spacing. CLI regressions
|
||||
use the standard tracker header and cover distinct names, tracked matches,
|
||||
equivalent accent encodings, and the existing ASCII matching behavior.
|
||||
|
||||
- **The Python tools no longer crash on Windows when a posting, company, CV line or file
|
||||
name falls outside the ANSI code page** (`tools/rank_state.py`, `tools/job_key.py`,
|
||||
`tools/verify_pdf.py`, `tools/verify_layout.py`, `tools/convert_salary_excel.py`,
|
||||
|
||||
@@ -10,6 +10,7 @@ matches Step 4's existing rules exactly - the location_verdict legacy
|
||||
migration, the deadline null-is-not-a-correction rule, and verbatim
|
||||
strengths/gaps persistence.
|
||||
"""
|
||||
import csv
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -122,6 +123,42 @@ class Candidates(RankStateCase):
|
||||
self.assertEqual([row["key"] for row in out["selected"]], ["b"])
|
||||
self.assertEqual(out["excluded_by_tracker"], 1)
|
||||
|
||||
def test_tracker_exclusion_preserves_unicode_identity(self):
|
||||
# Use the standard /outcome header and the real candidates CLI.
|
||||
header = (
|
||||
"date,company,sector,role,role_type,channel,status,contact_person,"
|
||||
"fit_rating,notes,cv_file,cover_letter_file,source,deadline"
|
||||
).split(",")
|
||||
cases = (
|
||||
# Tracked company/role, candidate company/title, expected exclusion.
|
||||
("Acme", "设计师", "Acme", "工程师", False),
|
||||
("Acme", "工程师", "Acme", "工程师", True),
|
||||
("腾讯", "Engineer", "腾讯", "Engineer", True),
|
||||
("腾讯", "Engineer", "阿里巴巴", "Engineer", False),
|
||||
("Компания", "Инженер", "КОМПАНИЯ", "ИНЖЕНЕР", True),
|
||||
("Café", "Engineer", "Cafe\u0301", "Engineer", True),
|
||||
("Straße", "Engineer", "STRASSE", "Engineer", True),
|
||||
("कला Labs", "Engineer", "कल Labs", "Engineer", False),
|
||||
("Acme, Inc.", "SOC Analyst", "ACME_INC", "soc-analyst", True),
|
||||
)
|
||||
tracker = self.tmp / "tracker.csv"
|
||||
for company, role, candidate_company, title, excluded in cases:
|
||||
with self.subTest(tracked=(company, role), candidate=(candidate_company, title)):
|
||||
self.write_state({
|
||||
"candidate": entry(company=candidate_company, title=title),
|
||||
"other": entry(company="Other", title="Untracked role"),
|
||||
})
|
||||
with tracker.open("w", encoding="utf-8", newline="") as fh:
|
||||
writer = csv.DictWriter(fh, fieldnames=header)
|
||||
writer.writeheader()
|
||||
writer.writerow({"date": TODAY, "company": company, "role": role, "status": "applied"})
|
||||
out = self.run_tool("candidates", "--tracker", str(tracker))
|
||||
self.assertEqual(
|
||||
[row["key"] for row in out["selected"]],
|
||||
["other"] if excluded else ["candidate", "other"],
|
||||
)
|
||||
self.assertEqual(out["excluded_by_tracker"], int(excluded))
|
||||
|
||||
def test_focus_filters_on_title_company_and_stored_fit_notes(self):
|
||||
self.write_state(
|
||||
{
|
||||
|
||||
+8
-1
@@ -42,6 +42,7 @@ import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import unicodedata
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
@@ -97,7 +98,13 @@ def parse_iso(value) -> date | None:
|
||||
|
||||
|
||||
def norm(text) -> str:
|
||||
return re.sub(r"[^a-z0-9]", "", str(text or "").lower())
|
||||
"""Ignore case and separators without discarding non-Latin identity."""
|
||||
text = unicodedata.normalize("NFC", str(text or "").casefold())
|
||||
# Combining marks can distinguish names even after NFC (e.g. Indic vowels).
|
||||
return "".join(
|
||||
char for char in text
|
||||
if char.isalnum() or unicodedata.category(char).startswith("M")
|
||||
)
|
||||
|
||||
|
||||
def tracker_pairs(path: Path) -> set[tuple[str, str]]:
|
||||
|
||||
Reference in new issue
Block a user