File size: 5,117 Bytes
e0a3391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"""
Shared feature extraction - term matching used by the trap gate, the scorer and
the reasoning generator so all three read the profile the same way.
"""
from __future__ import annotations

import re
from typing import Dict, List, Set

from . import config


def _term_pattern(term: str) -> str:
    """Word-boundary regex for one term. Short/ambiguous tokens (<=3 chars, e.g.
    'rag', 'ner', 'nlp', 'llm', 'e5') require both-side boundaries so they match
    real tokens, not substrings inside words like 'average' or 'owner'. Longer
    terms use a left boundary + prefix so morphological variants match
    (rank/ranking/ranked, embedding/embeddings, retrieval)."""
    esc = re.escape(term.strip())
    if len(term.strip()) <= 3:
        return r"\b" + esc + r"\b"
    left = r"\b" if term[0].isalnum() else ""
    return left + esc


def _compile_group(terms: List[str]) -> "re.Pattern":
    return re.compile("|".join(_term_pattern(t) for t in terms), re.IGNORECASE)


# Precompile once at import.
_EVIDENCE_PATTERNS = {g: _compile_group(t) for g, t in config.EVIDENCE_TERMS.items()}
_NICE_PATTERN = _compile_group(config.NICE_TO_HAVE_TERMS)


def _any_term(text: str, terms: List[str]) -> bool:
    return any(t in text for t in terms)


def title_class(rec: dict) -> str:
    """Classify the current title: 'relevant' | 'adjacent' | 'nontech' |
    'offdomain' | 'other'. Order matters: relevant wins over adjacent."""
    t = rec["title_lower"]
    if _any_term(t, config.RELEVANT_TITLE_TERMS):
        return "relevant"
    if _any_term(t, config.OFFDOMAIN_TITLE_TERMS):
        return "offdomain"
    if _any_term(t, config.NONTECH_TITLE_TERMS):
        return "nontech"
    if _any_term(t, config.ADJACENT_TITLE_TERMS):
        return "adjacent"
    return "other"


def evidence_groups(rec: dict) -> Dict[str, int]:
    """Count evidence hits per group across career descriptions + headline +
    summary (the *work narrative*), NOT the bare skills list. This is the
    'outcome evidence, not vocabulary' read that catches plain-language Tier-5s
    and ignores keyword stuffing in the skills array."""
    # Narrative text = headline + summary + every role description.
    work_text = " ".join(
        [rec["headline"].lower(), rec["summary"].lower()]
        + [c["description_lower"] for c in rec["career"]]
        + [c["title_lower"] for c in rec["career"]]
    )
    out: Dict[str, int] = {}
    for group, pat in _EVIDENCE_PATTERNS.items():
        out[group] = len(pat.findall(work_text))
    return out


def skill_evidence_groups(rec: dict) -> Set[str]:
    """Evidence groups that appear as *genuinely assessed* skills - an AI/IR skill
    with a real Redrob assessment score >= 60. Self-reported duration alone does
    NOT count as backing (stuffers fake duration); the platform assessment is the
    trustworthy signal per redrob_signals_doc."""
    backed: Set[str] = set()
    for s in rec["skills"]:
        if s["assessment"] is None or s["assessment"] < 60:
            continue
        nm = s["name_lower"]
        for group, pat in _EVIDENCE_PATTERNS.items():
            if pat.search(nm):
                backed.add(group)
    return backed


def _any_skill_is_ai(skill: dict) -> bool:
    """True if a single skill entry names an AI/ML/IR skill."""
    return any(pat.search(skill["name_lower"]) for pat in _EVIDENCE_PATTERNS.values())


def ai_skill_count(rec: dict) -> int:
    """Number of distinct AI/ML/IR-flavoured skills listed (any group)."""
    n = 0
    for s in rec["skills"]:
        nm = s["name_lower"]
        if any(pat.search(nm) for pat in _EVIDENCE_PATTERNS.values()):
            n += 1
    return n


def consulting_only(rec: dict) -> bool:
    """True if every company in the career history is an IT-services/consulting
    firm (JD explicit disqualifier) - and there is at least one job."""
    if not rec["companies"]:
        return False
    def is_consult(c):
        return any(f in c for f in config.CONSULTING_FIRMS)
    return all(is_consult(c) for c in rec["companies"])


def location_class(rec: dict) -> str:
    loc = rec["location_lower"]
    if _any_term(loc, config.PREFERRED_LOCATIONS):
        return "preferred"
    if _any_term(loc, config.WELCOME_LOCATIONS):
        return "welcome"
    return "far"


def has_relevant_or_adjacent_role(rec: dict) -> bool:
    """True if any role in the career history (or the current title) is a
    relevant or adjacent technical title - i.e. the person has actually held an
    ML/SWE-type role at some point, not just listed skills."""
    titles = [rec["title_lower"]] + [c["title_lower"] for c in rec["career"]]
    for t in titles:
        if _any_term(t, config.RELEVANT_TITLE_TERMS) or _any_term(t, config.ADJACENT_TITLE_TERMS):
            return True
    return False


def is_title_hopper(rec: dict) -> bool:
    """JD: switching companies every ~1.5y chasing titles. Flag short average
    tenure across several completed jobs."""
    completed = [c for c in rec["career"] if not c["is_current"] and c["months"] > 0]
    return len(completed) >= 3 and rec["avg_tenure_months"] < 18