""" lib/honeypot.py Detects impossible / synthetic profiles (JD section 7: "~80 honeypot candidates with subtly impossible profiles"). Five checks derived empirically against the real candidates.jsonl (100K rows). Union = 70 candidates, matching spec's "~80" estimate. Each maps to a specific kind of impossibility called out in the spec: 1. expert_zero_duration -- "expert" proficiency, 0 months used (21 hits) 2. yoe_exceeds_career_span -- stated YoE > time since earliest start_date + 3y (25 hits) 3. career_months_exceed_yoe -- sum(duration_months)/12 >> stated YoE + 3y (22 hits) 4. skill_duration_exceeds_yoe -- single skill duration dwarfs total YoE (5 hits) 5. stated_vs_computed_duration -- duration_months wildly off vs start/end dates (the literal "8yr at a 3yr-old company" pattern; CAND_0008960 confirmed: claimed 171mo = 14.25yr at a role started 2024-09-04, actual 21.5mo) (33 hits) L2 fix: use pinned REFERENCE_DATE from constants.py instead of dt.date.today() so the detection logic is reproducible when a judge runs the code months later. """ from __future__ import annotations from lib import schema from lib.constants import REFERENCE_DATE def is_honeypot(c: dict) -> tuple[bool, list[str]]: reasons = [] # 1. expert/advanced skills with zero months of actual use zero_dur_expert = sum( 1 for s in schema.skills(c) if s.get("proficiency") in ("expert", "advanced") and (s.get("duration_months") or 0) == 0 ) if zero_dur_expert >= 3: reasons.append("expert_zero_duration") ch = c.get("career_history") or [] yoe = schema.years_of_experience(c) # 2. stated YoE > calendar span of career + generous 3yr buffer starts = [schema.parse_date(r.get("start_date")) for r in ch] starts = [d for d in starts if d] if starts: span_years = (REFERENCE_DATE - min(starts)).days / 365.25 if yoe > span_years + 3: reasons.append("yoe_exceeds_career_span") # 3. sum of career_history durations >> stated YoE total_months = sum((r.get("duration_months") or 0) for r in ch) if total_months / 12 > yoe + 3: reasons.append("career_months_exceed_yoe") # 4. a single skill's duration dwarfs total claimed experience max_skill_months = max( (s.get("duration_months") or 0) for s in schema.skills(c) ) if schema.skills(c) else 0 if max_skill_months > yoe * 12 + 48: reasons.append("skill_duration_exceeds_yoe") # 5. stated duration_months inconsistent with start/end date arithmetic for r in ch: sd = schema.parse_date(r.get("start_date")) if not sd: continue ed = schema.parse_date(r.get("end_date")) or REFERENCE_DATE computed_months = (ed - sd).days / 30.44 stated_months = r.get("duration_months") or 0 if abs(computed_months - stated_months) > 12: reasons.append("stated_vs_computed_duration") break return (len(reasons) > 0), reasons