File size: 3,723 Bytes
1595f22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# %%writefile core/scoring.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, Any, List


def _safe_len(x) -> int:
    return len(x) if isinstance(x, list) else 0


def normalize_weights(weights: Dict[str, Any]) -> Dict[str, float]:
    w_must = float(weights.get("must_have", 0))
    w_nice = float(weights.get("nice_to_have", 0))
    w_exp = float(weights.get("experience", 0))
    w_soft = float(weights.get("soft_skills", 0))

    total = w_must + w_nice + w_exp + w_soft
    if total <= 0:
        return {"must_have": 0.6, "nice_to_have": 0.25, "experience": 0.15, "soft_skills": 0.0}

    return {
        "must_have": w_must / total,
        "nice_to_have": w_nice / total,
        "experience": w_exp / total,
        "soft_skills": w_soft / total
    }


def compute_coverage(matched: List[Any], missing: List[Any], partial: List[Any] | None = None) -> float:
    m = _safe_len(matched)
    miss = _safe_len(missing)
    p = _safe_len(partial) if partial is not None else 0

    denom = m + miss + p
    if denom == 0:
        return 0.0

    return (m + 0.5 * p) / denom


def experience_score(assessment: str) -> float:
    assessment = (assessment or "").strip().lower()
    if assessment == "below":
        return 0.0
    if assessment in ("meets", "exceeds"):
        return 1.0
    return 0.0


def clamp(x: float, lo: float, hi: float) -> float:
    return max(lo, min(hi, x))


def score_candidate(jd_rubric: Dict[str, Any], match_summary: Dict[str, Any]) -> Dict[str, Any]:
    weights = normalize_weights(jd_rubric.get("recommended_weights", {}))

    mh = match_summary.get("must_have_match", {}) or {}
    nh = match_summary.get("nice_to_have_match", {}) or {}
    exp = match_summary.get("experience_analysis", {}) or {}

    must_cov = compute_coverage(
        matched=mh.get("matched", []),
        missing=mh.get("missing", []),
        partial=mh.get("partial", [])
    )

    nice_cov = compute_coverage(
        matched=nh.get("matched", []),
        missing=nh.get("missing", []),
        partial=None
    )

    exp_sc = experience_score(exp.get("assessment", ""))

    # Soft skills scoring (optional). Keep 0 for student MVP unless you add logic later.
    soft_sc = 0.0

    base_total = (
        weights["must_have"] * must_cov +
        weights["nice_to_have"] * nice_cov +
        weights["experience"] * exp_sc +
        weights["soft_skills"] * soft_sc
    ) * 100.0

    # ✅ Bonus/Penalty based on Step-4 indicators (transparent & capped)
    positives = match_summary.get("positive_indicators", []) or []
    negatives = match_summary.get("negative_indicators", []) or []

    bonus_per_positive = 1.0
    penalty_per_negative = 1.5
    raw_adjustment = (len(positives) * bonus_per_positive) - (len(negatives) * penalty_per_negative)

    # Cap adjustment so it doesn't dominate scoring
    adjustment_cap = 8.0
    adjustment = clamp(raw_adjustment, -adjustment_cap, adjustment_cap)

    final_total = clamp(base_total + adjustment, 0.0, 100.0)

    return {
        "candidate_name": match_summary.get("candidate_name", ""),
        "total_score": round(final_total, 2),
        "breakdown": {
            "base_score": round(base_total, 2),
            "bonus_penalty_adjustment": round(adjustment, 2),
            "positive_count": len(positives),
            "negative_count": len(negatives),
            "must_have_coverage": round(must_cov, 3),
            "nice_to_have_coverage": round(nice_cov, 3),
            "experience_score": round(exp_sc, 3),
            "weights_normalized": weights
        }
    }