- rank.py: get_weights(profile) — when the profile reads non-tech (sales/finance/HR/ops/support/…), tilt the weights to role(0.16)/industry(0.12)/semantic(0.12), drop skill(0.18). Tech weights unchanged. (The competency-skill-backfill was tried + REVERTED — uniform per-role competencies added no discrimination and Phase 2's real-cosine f_semantic already supplies the non-tech signal.) - curate.py: non-tech instruction — role/responsibility fit is the spine; a missing skill list is NOT a negative; a strong role+industry+responsibility+seniority match is a genuine ~80, not ~65. - test_contracts.py: allowlist the new keyword-handled non-tech vocab (active boards keyword-fold; numeric codes deferred since the active feed/recall paths don't use them). Regression (120 pairs): MAE 18.4 → 16.1 (all from non-tech: sales 17.1→13.0, ops 16.3→11.7), recall holds 0.92, tech unchanged. 31 tests pass.
99 lines
5.2 KiB
Python
99 lines
5.2 KiB
Python
"""Phase-0 contract checks — the guardrails that catch silent failures BEFORE they ship.
|
|
|
|
These assert the load-bearing contracts the engine-improvement plan depends on:
|
|
1. BOARDS ↔ NORMALIZERS alignment (an enabled board with no normalizer = silent drop).
|
|
2. The funnel shapes (sift fusion weights, embed text fns, SIFT_TOP_K) are where the plan expects.
|
|
3. overlap() returns None when a side has no skills (the floor-free contract).
|
|
4. The frontend↔backend EXACT-STRING vocab contract: every ScoutPrefs ROLE/INDUSTRY option the UI
|
|
offers must resolve to a coerce.py board code, else it silently degrades to a keyword fold.
|
|
(Cross-repo read of growqr-demo's scout.ts; skipped if the sibling repo isn't present.)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app.engine import search, normalize, sift, embed
|
|
from app.engine.board_adapters import coerce as C
|
|
from app.engine import skills as S
|
|
from app.config import get_settings
|
|
|
|
|
|
# ── 1. BOARDS ↔ NORMALIZERS alignment ────────────────────────────────────────
|
|
def test_boards_have_normalizers():
|
|
for board in search.BOARDS:
|
|
assert board in normalize.NORMALIZERS, f"BOARDS has '{board}' with no NORMALIZERS entry"
|
|
|
|
|
|
def test_enabled_boards_are_registered():
|
|
enabled = [b.strip() for b in get_settings().BOARDS_ENABLED.split(",") if b.strip()]
|
|
for b in enabled:
|
|
assert b in search.BOARDS, f"BOARDS_ENABLED lists '{b}' which is not a registered board"
|
|
assert b in normalize.NORMALIZERS, f"enabled board '{b}' has no normalizer"
|
|
|
|
|
|
# ── 2. Funnel shapes (guards the Phase-2 edits) ──────────────────────────────
|
|
def test_sift_fusion_weights_present_and_normalized():
|
|
assert hasattr(sift, "W_WHITEBOX") and hasattr(sift, "W_VIBE")
|
|
assert abs((sift.W_WHITEBOX + sift.W_VIBE) - 1.0) < 1e-9, "sift fusion weights must sum to 1"
|
|
|
|
|
|
def test_embed_text_builders_exist():
|
|
# the Phase-2 richer-embedding work edits these; assert they exist + return strings
|
|
assert callable(embed._job_text) and callable(embed._profile_text)
|
|
assert isinstance(embed._job_text({"title": "X", "organization": "Y"}), str)
|
|
assert isinstance(embed._profile_text({"title": "X"}, None), str)
|
|
|
|
|
|
def test_sift_top_k_configured():
|
|
assert isinstance(get_settings().SIFT_TOP_K, int) and get_settings().SIFT_TOP_K > 0
|
|
|
|
|
|
# ── 3. Floor-free skill contract ─────────────────────────────────────────────
|
|
def test_overlap_none_when_a_side_is_empty():
|
|
assert S.overlap([], ["python", "sql"]) is None, "empty user skills must yield None (not a floor)"
|
|
assert S.overlap(["python"], []) is None, "empty job skills must yield None (not a floor)"
|
|
# both present → a real dict with a score
|
|
o = S.overlap(["python", "sql"], ["python", "django"])
|
|
assert isinstance(o, dict) and "score" in o
|
|
|
|
|
|
# ── 4. Frontend↔backend EXACT-STRING vocab contract ──────────────────────────
|
|
# Values the UI sends that are intentionally NOT numeric board codes — they fold into the keyword.
|
|
# The ACTIVE boards (naukri-feed, linkedin-recall, foundit, indeed, workindia) all search by keyword +
|
|
# city NAME, so the non-tech vocab below works WITHOUT a code (the code maps are only for the off,
|
|
# code-based actors). Codes for these are deferred per plan; listed here so drift still fails loudly.
|
|
_ROLE_KEYWORD_ONLY = {"Open to any", "Finance", "HR", "Customer Support", "Supply Chain",
|
|
"Business Development", "Legal"}
|
|
_INDUSTRY_KEYWORD_ONLY = {"Any", "Open to any", "", "Banking", "Insurance", "Retail", "FMCG",
|
|
"Pharma", "Manufacturing", "Logistics"}
|
|
|
|
_SCOUT_TS = Path(__file__).resolve().parents[2] / "growqr-demo/frontend/src/scout/data/scout.ts"
|
|
|
|
|
|
def _extract_array(name: str, text: str) -> list[str]:
|
|
m = re.search(rf"{name}\s*[:=]\s*\[(.*?)\]", text, re.S)
|
|
if not m:
|
|
return []
|
|
return re.findall(r'"([^"]+)"', m.group(1))
|
|
|
|
|
|
@pytest.mark.skipif(not _SCOUT_TS.exists(), reason="growqr-demo frontend not present (cross-repo)")
|
|
def test_frontend_roles_resolve_in_backend():
|
|
text = _SCOUT_TS.read_text()
|
|
roles = _extract_array("ROLE_OPTIONS", text)
|
|
assert roles, "could not extract ROLE_OPTIONS from scout.ts — regex/contract drift"
|
|
unmapped = [r for r in roles if r not in C.ROLE_NAUKRI_DEPT and r not in _ROLE_KEYWORD_ONLY]
|
|
assert not unmapped, f"frontend ROLE options with no coerce.py code (silent keyword-fold): {unmapped}"
|
|
|
|
|
|
@pytest.mark.skipif(not _SCOUT_TS.exists(), reason="growqr-demo frontend not present (cross-repo)")
|
|
def test_frontend_industries_resolve_in_backend():
|
|
text = _SCOUT_TS.read_text()
|
|
inds = _extract_array("INDUSTRY_OPTIONS", text)
|
|
assert inds, "could not extract INDUSTRY_OPTIONS from scout.ts — regex/contract drift"
|
|
unmapped = [i for i in inds if i not in C.INDUSTRY_NAUKRI and i not in _INDUSTRY_KEYWORD_ONLY]
|
|
assert not unmapped, f"frontend INDUSTRY options with no coerce.py code (silent keyword-fold): {unmapped}"
|