redrob-ranker / src /traps.py
Dhruv Goyal
Redrob ranker demo - full hybrid bge-small (Docker/Streamlit)
e0a3391
Raw
History Blame Contribute Delete
4.44 kB
"""
Validity / trap gate - runs FIRST, highest ROI.
Two jobs:
1. honeypot detection: subtly *impossible* profiles (the spec's tier-0 set).
Only clear internal contradictions are flagged, so we never demote a real
candidate by accident (precision over recall - a missed honeypot just needs
to lose on merit; a false positive ejects a genuine top-100 pick).
2. keyword-stuffer detection: non-technical title + AI skills with no
corroborating work evidence (the JD's "Marketing Manager with every AI
keyword" trap).
Returns structured reasons so reasoning.py can explain and eval/ can score
trap-catch rate.
"""
from __future__ import annotations
from typing import Dict, List
from . import config, features
def honeypot_reasons(rec: dict) -> List[str]:
"""Return a list of impossibility reasons; empty list = not a honeypot."""
reasons: List[str] = []
yoe = rec["yoe"]
yoe_months = yoe * 12
# 1. "expert" proficiency but zero months of use.
for s in rec["skills"]:
if s["proficiency"] == "expert" and s["months"] == 0:
reasons.append("expert_skill_with_zero_months")
break
# NOTE: we deliberately do NOT flag "skill months > career months" - in this
# synthetic dataset skill durations are assigned independently of career length
# for normal profiles (~9% of the pool), so it is noise, not an impossibility.
# The exception (expert proficiency + huge duration on zero-experience) is
# already covered by rule 1 and the experience/timeline checks below.
# 3. a single role longer than the entire career.
if yoe > 0 and rec["career"]:
if max(c["months"] for c in rec["career"]) > yoe_months + 12:
reasons.append("role_duration_exceeds_career")
# 4. summed role months wildly exceed total experience (impossible overlap).
if yoe > 0 and rec["total_career_months"] > yoe_months + 36:
reasons.append("career_months_exceed_experience")
# 5/6. per-role date sanity: end<start, or duration longer than the span.
for c in rec["career"]:
if c["start"] and c["end"]:
if c["end"] < c["start"]:
reasons.append("end_date_before_start_date")
span = (c["end"] - c["start"]).days / 30.4
if c["months"] > span + 3:
reasons.append("role_duration_exceeds_date_span")
# 7. claims more experience than the earliest start date allows.
starts = [c["start"] for c in rec["career"] if c["start"]]
if starts and yoe > 0:
from datetime import date
ref = date.fromisoformat(config.REFERENCE_DATE)
implied_years = (ref - min(starts)).days / 365.25
if yoe > implied_years + 2.0:
reasons.append("experience_exceeds_timeline")
# de-dup while preserving order
seen, out = set(), []
for r in reasons:
if r not in seen:
seen.add(r)
out.append(r)
return out
def stuffer_info(rec: dict) -> Dict:
"""Detect keyword-stuffer. Returns {is_stuffer, ai_skills, evidence}.
A stuffer = non-technical current title + several AI skills + no corroborating
evidence in the work narrative and no genuinely backed AI skill.
"""
tclass = features.title_class(rec)
n_ai = features.ai_skill_count(rec)
work_ev = features.evidence_groups(rec)
backed = features.skill_evidence_groups(rec)
evidence_total = sum(work_ev.values())
held_tech_role = features.has_relevant_or_adjacent_role(rec)
# Stuffer = non-technical/off-domain person who has never held an ML/SWE role,
# lists several AI skills, shows only incidental (<3 hits) AI language in their
# actual work narrative, and has no platform-assessed AI skill to back it up.
is_stuffer = (
tclass in ("nontech", "offdomain")
and n_ai >= 3
and not held_tech_role
and evidence_total < 3
and len(backed) == 0
)
return {
"is_stuffer": is_stuffer,
"title_class": tclass,
"ai_skill_count": n_ai,
"evidence_total": evidence_total,
"held_tech_role": held_tech_role,
"backed_ai_skills": sorted(backed),
}
def assess(rec: dict) -> Dict:
"""Full trap assessment for one record."""
hp = honeypot_reasons(rec)
st = stuffer_info(rec)
return {
"is_honeypot": len(hp) > 0,
"honeypot_reasons": hp,
**st,
}