fix(scrape): keep C++, C# and C job titles distinct in dedup and keys (#830)

norm() dropped "+" and "#", so "C++ Developer", "C# Developer" and
"C Developer" at one company normalized alike: /scrape counted a new
posting as already seen and /rank excluded it once the user had applied
to the other. slugify() did the same for canonical keys, so keep both
characters (spelled out as plus/sharp in keys). Keys stored under the old
rule are re-keyed by job_key.py --audit --migrate; the store collision
error now says so.
This commit is contained in:
Jallepalli Harsha Vardhan authored and GitHub committed 2026-11-10 19:00:25 +01:00
1 parent 5fe97b6ab7
commit 44f95de6e9
4 files changed
+20 -2

No files matched your search

+12
View File
@@ -70,6 +70,18 @@ class ScrapeStateTests(unittest.TestCase):
self.assertEqual(stored["legacy_key"], old["legacy_key"])
self.assertEqual(stored["fresh_builder"], fresh)
def test_c_sharp_and_c_plus_plus_titles_are_different_postings(self):
base = {
"company": "Acme", "first_seen": "2026-10-21", "posted_date": None, "deadline": None,
"fit": "high", "status": "new", "portal": "example-search", "source": "cli",
}
cpp = {**base, "title": "C++ Developer", "url": "https://jobs.test/1111111"}
self.assertEqual(len(self.run_tool("store", [cpp])["added"]), 1)
csharp = {**base, "title": "C# Developer", "url": "https://jobs.test/2222222"}
out = self.run_tool("candidates", [csharp])
self.assertEqual(out["selected"], [csharp])
self.assertEqual(len(self.run_tool("store", [csharp])["added"]), 1)
def test_store_creates_missing_history_and_rejects_partial_entries_without_writing(self):
good = {
"title": "Builder", "company": "Fresh", "url": "https://jobs.test/fresh",
+2
View File
@@ -105,6 +105,8 @@ def slugify(text: str) -> str:
decomposed = unicodedata.normalize("NFKD", str(text))
folded = decomposed.translate(_LATIN_FOLD_TABLE)
ascii_only = folded.encode("ascii", "ignore").decode("ascii")
# Spell out "+" and "#" so "C++", "C#" and "C" titles keep distinct keys.
ascii_only = ascii_only.replace("+", "plus").replace("#", "sharp")
return _NON_SLUG.sub("-", ascii_only.lower()).strip("-")
+2 -1
View File
@@ -146,7 +146,8 @@ def norm(text) -> str:
# 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")
# "+" and "#" carry identity: "C++", "C#" and "C" are different jobs.
if char.isalnum() or char in "+#" or unicodedata.category(char).startswith("M")
)
+4 -1
View File
@@ -117,7 +117,10 @@ def run(args) -> int:
raise ValueError("job has an invalid portal or source")
key = make_key(job["company"], job["title"], job["url"])
if key in seen:
raise ValueError(f"canonical key already exists for another posting: {key}")
raise ValueError(
f"canonical key already exists for another posting: {key} "
"(keys stored before a key-rule change: run python3 tools/job_key.py --audit --migrate)"
)
seen[key] = job
if selected:
args.state.parent.mkdir(parents=True, exist_ok=True)