From f84410decb81fc1f3c89e8f441d12e14213e30d7 Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Fri, 26 Jun 2026 16:07:50 +0530 Subject: [PATCH 1/7] Fix: Preserve punctuation in tokenization to prevent skill collisions (#1169) --- src/utils/recommender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/recommender.py b/src/utils/recommender.py index 45d4fa85..859f3f3f 100644 --- a/src/utils/recommender.py +++ b/src/utils/recommender.py @@ -79,7 +79,7 @@ def parse_skills(skills_string): return [SKILL_ALIASES.get(skill, skill) for skill in raw_skills] def _tokenize(text): - return re.findall(r"[a-z0-9]+", str(text).lower()) + return re.findall(r"[a-z0-9\+#\.-]+", str(text).lower()) def _project_text(project): parts = [ From 4eb77f2fa6e21665a55113411a877f3259769daf Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Mon, 27 Jul 2026 14:42:29 +0530 Subject: [PATCH 2/7] ci: install pytest and run full pytest suite instead of unittest --- .github/workflows/deploy.yml | 3 ++- .github/workflows/python-app.yml | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 80e23874..cdba1353 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,11 +24,12 @@ jobs: - name: Install dependencies run: | pip install -r requirements-dev.txt + pip install pytest # Run tests - name: Run tests run: | - python -m unittest tests/test_basic.py + PYTHONPATH=src pytest # Optional: Print success - name: Success message diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index da21d874..8647362c 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -29,7 +29,8 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install flake8 + if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi + pip install flake8 pytest - name: Lint code (non-blocking) run: | @@ -37,4 +38,4 @@ jobs: - name: Run tests run: | - python -m unittest tests/test_basic.py + PYTHONPATH=src pytest From ecf3a8f1f163b36fd5c76cfc469bbbfa8c0d8cfa Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Mon, 27 Jul 2026 15:07:46 +0530 Subject: [PATCH 3/7] ci: install pytest and run full pytest suite instead of unittest --- .github/workflows/deploy.yml | 3 ++- .github/workflows/python-app.yml | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 80e23874..cdba1353 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,11 +24,12 @@ jobs: - name: Install dependencies run: | pip install -r requirements-dev.txt + pip install pytest # Run tests - name: Run tests run: | - python -m unittest tests/test_basic.py + PYTHONPATH=src pytest # Optional: Print success - name: Success message diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index da21d874..8647362c 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -29,7 +29,8 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install flake8 + if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi + pip install flake8 pytest - name: Lint code (non-blocking) run: | @@ -37,4 +38,4 @@ jobs: - name: Run tests run: | - python -m unittest tests/test_basic.py + PYTHONPATH=src pytest From 137200babf525e38956a2a3fd6aa4bf425353231 Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Thu, 30 Jul 2026 21:32:46 +0530 Subject: [PATCH 4/7] Fix syntax error and malformed merge conflict in recommender.py --- src/utils/recommender.py | 56 +++++----------------------------------- 1 file changed, 7 insertions(+), 49 deletions(-) diff --git a/src/utils/recommender.py b/src/utils/recommender.py index a7d38098..fd31509d 100644 --- a/src/utils/recommender.py +++ b/src/utils/recommender.py @@ -105,16 +105,6 @@ def clear_caches(): "ai": "artificial intelligence", "k8s": "kubernetes", "tf": "tensorflow", - -# Common aliases and abbreviations for skills -# This improves recommendation accuracy by normalizing user input -SKILL_ALIASES = { - "js": "javascript", - "py": "python", - "html5": "html", - "css3": "css", - "c++": "cpp", - "web dev": "javascript", } def _normalize_skill(s: str) -> str: """Normalize a skill string: strip surrounding whitespace and lowercase.""" @@ -124,47 +114,17 @@ def _normalize_skill(s: str) -> str: # that reference SKILL_ALIASES continue to work without modification. SKILL_ALIASES = SKILL_SYNONYMS -def parse_skills(skills_string): - """ - Convert a raw skills string into a normalized, synonym-resolved lowercase list. - - Accepts either: - - A JSON array e.g. '["Python", "ReactJS"]' -> ["python", "react"] - - A comma-separated string e.g. "JS, TS, Node, " -> ["javascript", "typescript", "node.js"] - - Processing steps applied to every token: - 1. Strip surrounding whitespace. - 2. Convert to lowercase. - 3. Discard empty strings (handles trailing commas / double commas). - 4. Map through SKILL_SYNONYMS so abbreviations become canonical names. - """ +def parse_skill_entries(skills_string): + """Parse skills with optional per-skill proficiency levels.""" if not skills_string or not skills_string.strip(): return [] stripped = skills_string.strip() - # --- JSON array branch --- - -def parse_skill_entries(skills_string): - """Parse skills with optional per-skill proficiency levels.""" - stripped = skills_string.strip() - if stripped.startswith("["): try: parsed = json.loads(stripped) if isinstance(parsed, list): - tokens = [str(s).strip().lower() for s in parsed if str(s).strip()] - return [SKILL_SYNONYMS.get(token, token) for token in tokens] - except (json.JSONDecodeError, ValueError): - pass # fall through to comma-splitting - - # --- Comma-separated branch --- - tokens = [ - s.strip().lower() - for s in skills_string.split(",") - if s.strip() # skip blanks produced by trailing / consecutive commas - ] - return [SKILL_SYNONYMS.get(token, token) for token in tokens] entries = [] for item in parsed: if isinstance(item, dict): @@ -177,7 +137,7 @@ def parse_skill_entries(skills_string): if skill: entries.append( { - "skill": SKILL_ALIASES.get(skill, skill), + "skill": SKILL_SYNONYMS.get(skill, skill), "proficiency": ( proficiency if proficiency in ( @@ -195,19 +155,17 @@ def parse_skill_entries(skills_string): return [ { - "skill": SKILL_ALIASES.get(_normalize_skill(skill), _normalize_skill(skill)), + "skill": SKILL_SYNONYMS.get(_normalize_skill(skill), _normalize_skill(skill)), "proficiency": "Beginner", } for skill in skills_string.split(",") if skill.strip() ] - -def parse_skills(skills_string): - return [entry["skill"] for entry in parse_skill_entries(skills_string)] - - def parse_skills(skills_string): + """ + Convert a raw skills string into a normalized, synonym-resolved lowercase list. + """ return [entry["skill"] for entry in parse_skill_entries(skills_string)] _nlp_model = None From af9d0aedae2501d45fa282a858cb757aa6de6bb7 Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Thu, 30 Jul 2026 21:34:40 +0530 Subject: [PATCH 5/7] Fix syntax errors, math import, and broken tests --- src/routes/main_routes.py | 1 + src/utils/recommender.py | 2 +- tests/test_basic.py | 3 --- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/routes/main_routes.py b/src/routes/main_routes.py index f2bc28db..c0a8b178 100644 --- a/src/routes/main_routes.py +++ b/src/routes/main_routes.py @@ -27,6 +27,7 @@ from config import Config from utils.portfolio_analyzer import analyze_portfolio import os +import math from models import db, ProjectProgress, UserGameProgress _skill_validator = SkillProgressionValidator() diff --git a/src/utils/recommender.py b/src/utils/recommender.py index fd31509d..fe38acd2 100644 --- a/src/utils/recommender.py +++ b/src/utils/recommender.py @@ -708,7 +708,7 @@ def get_recommendations( }) # Sort projects in descending order so the # most relevant recommendations appear first. - scored_projects.sort(key=lambda item: (item["score"], item["project"].get("id", 0)), reverse=True) + scored_projects.sort(key=lambda item: (item["score"], -item["project"].get("id", 0)), reverse=True) selected_projects = ( scored_projects diff --git a/tests/test_basic.py b/tests/test_basic.py index 504914e1..7b29cfb5 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -354,9 +354,6 @@ def test_skill_synonyms_dict_has_minimum_entries(): f"Expected at least 10 synonym entries, found {len(SKILL_SYNONYMS)}" ) - - assert len(errors) == 4 - # ============================================================ # Flask Route Tests # ============================================================ From 94dd1d35fd2e720b633c5e9315aada2ab03f1fbc Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Thu, 30 Jul 2026 21:42:42 +0530 Subject: [PATCH 6/7] Mock _tfidf_similarity to fix tiebreaker test expectations --- tests/test_tiebreaker.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_tiebreaker.py b/tests/test_tiebreaker.py index c29861ba..70303124 100644 --- a/tests/test_tiebreaker.py +++ b/tests/test_tiebreaker.py @@ -1,4 +1,4 @@ -# tests/test_tiebreaker.py +# tests/test_tiebreaker.py # Regression tests for issue #711: # Equal-scored recommendations must use project id as a stable tie-breaker. @@ -25,9 +25,13 @@ def test_recommendations_are_deterministic(): assert ids1 == ids2, "Recommendation order changed between identical calls (issue #711)" -def test_equal_score_tiebreaker_uses_id(): +def test_equal_score_tiebreaker_uses_id(monkeypatch): """When projects tie on score, lower id must come first.""" - result = get_recommendations("python", "Beginner", "web", "low") + import utils.recommender as rec + monkeypatch.setattr(rec, "score_single_project", lambda *args, **kwargs: (1.0, {})) + monkeypatch.setattr(rec, "get_nlp_model", lambda: None) + monkeypatch.setattr(rec, "tfidf_similarity_score", lambda *args, **kwargs: 0.0) + result = rec.get_recommendations("python", "Beginner", "web", "low") recs = result["recommendations"] for i in range(len(recs) - 1): assert recs[i]["id"] <= recs[i + 1]["id"], ( From 572aae7ae968281dd2a4b011aecec9c201cdeda6 Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Thu, 30 Jul 2026 21:46:16 +0530 Subject: [PATCH 7/7] Fix sorting tuple for tie-breakers --- src/utils/recommender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/recommender.py b/src/utils/recommender.py index fe38acd2..4b3c0a0a 100644 --- a/src/utils/recommender.py +++ b/src/utils/recommender.py @@ -708,7 +708,7 @@ def get_recommendations( }) # Sort projects in descending order so the # most relevant recommendations appear first. - scored_projects.sort(key=lambda item: (item["score"], -item["project"].get("id", 0)), reverse=True) + scored_projects.sort(key=lambda item: (-item["score"], int(item["project"].get("id", 0)))) selected_projects = ( scored_projects