Redrob-hackathon / README.md
Mohit0708's picture
Configure Hugging Face Space
bc32894
|
Raw
History Blame Contribute Delete
44.5 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade
metadata
title: Redrob Hackathon
emoji: πŸš€
colorFrom: blue
colorTo: green
sdk: gradio
sdk_version: 5.38.0
python_version: 3.11
app_file: app.py
pinned: false

Redrob Hackathon

Candidate ranking demo.

Redrob Intelligent Candidate Ranker

Submission for the Redrob Hackathon β€” Intelligent Candidate Discovery & Ranking Challenge

A CPU-only, fully deterministic, two-phase candidate ranking engine that selects the top 100 of 100,000 candidates against a Senior AI Engineer job description, with evidence-graph reasoning and honeypot-resistant safety layers.

Python CPU Only No LLM Calls No Network Deterministic Runtime


Table of Contents

  1. Quick Start
  2. TL;DR Summary
  3. Architecture Overview
  4. Phase 1 β€” precompute.py
  5. Phase 2 β€” rank.py
  6. Feature Catalog (β‰ˆ55 features, G1–G13)
  7. Scoring Formula
  8. Safety & Trap Handling
  9. Evidence-Graph Reasoning
  10. Compute Constraints & Compliance
  11. File Map
  12. Adversarial Test Suite
  13. Design Decisions & Tradeoffs
  14. Determinism & Reproducibility Guarantees
  15. Constraints Compliance Matrix
  16. Known Limitations

1. Quick Start

# Install dependencies
pip install -r requirements.txt

# Single-command end-to-end run (auto-runs precompute if features.parquet is missing)
python rank.py --candidates ./candidates.jsonl --out ./submission.csv

For faster iteration (e.g. while tuning scoring weights):

# Phase 1 β€” run once per JD or candidate-pool change (~196 s)
python precompute.py --candidates ./candidates.jsonl --out ./artifacts/features.parquet

# Phase 2 β€” run as many times as needed (~4 s)
python rank.py --candidates ./candidates.jsonl --out ./submission.csv \
               --artifacts ./artifacts/features.parquet

Validate the output:

# Format validation β€” mirrors the hackathon validator
python validate_submission.py submission.csv

# Adversarial test suite (10 cases: keyword stuffer, honeypot, strong candidate, etc.)
python test_adversarial.py

2. TL;DR Summary

Aspect Value
Inputs candidates.jsonl (100K candidates) + job_description.docx
Output submission.csv β€” exactly 100 rows, columns: candidate_id, rank, score, reasoning
Pipeline Two phases β€” precompute (196 s) writes features.parquet; rank (4 s) writes CSV
Compute CPU-only Β· ≀16 GB RAM Β· no network Β· no LLM API calls Β· deterministic
Features β‰ˆ55 JD-driven features across 13 groups (G1–G13)
Scoring Rule-based elite composite: Impact 42% + Ownership 33% + Search/JD-fit 15% + Behaviour 10%, modulated by hiring intent
Safety 6 multiplicative penalties (disqualifier, behavioral twin, LangChain-only, closed-source, title floor, honeypot) β€” design target: 0 honeypots in top 100
Reasoning Evidence-graph based β€” every cited keyword is verified against the candidate's career text; no hallucination
Determinism Fixed REFERENCE_DATE = 2026-06-17, hash-based template selection, zero RNG
Single command python rank.py --candidates ./candidates.jsonl --out ./submission.csv

3. Architecture Overview

The engine is split into two phases to keep the timed ranking step well under the 5-minute CPU-only budget. Phase 1 performs the expensive per-candidate feature extraction and writes a Parquet artifact (~1.3 GB). Phase 2 is a lightweight vectorized scoring pass plus reasoning generation that completes in approximately 4 seconds.

                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚           Phase 1 Β· precompute.py  (~196 s)            β”‚
                     β”‚                                                         β”‚
 job_description.docx──▢ JD Parser ──▢ Hiring Intent ──▢ Query Expansion     β”‚
                     β”‚      β”‚               β”‚                  β”‚              β”‚
                     β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
                     β”‚                            β–Ό                           β”‚
 candidates.jsonl ───▢  Canonical Profile ──▢ Feature Registry (G1–G13)      β”‚
                     β”‚  (career narrative,        β”‚         β”‚                 β”‚
                     β”‚   unified text blob)       β”‚         β”‚                 β”‚
                     β”‚                            β–Ό         β–Ό                 β”‚
                     β”‚                    TF-IDF+SVD    Honeypot Detector     β”‚
                     β”‚                    Embeddings    (~80 trap patterns)   β”‚
                     β”‚                         β”‚              β”‚               β”‚
                     β”‚                         β–Ό              β–Ό               β”‚
                     β”‚                    features.parquet (~1.3 GB)         β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                β”‚  (parquet handoff)
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚             Phase 2 Β· rank.py  (~4 s)                  β”‚
                     β”‚                                                         β”‚
                     β”‚  Load parquet ──▢ Elite Composite                      β”‚
                     β”‚                       β”‚                                 β”‚
                     β”‚       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
                     β”‚       β–Ό               β–Ό              β–Ό                  β”‚
                     β”‚   Additive      Multiplicative   Intent-                β”‚
                     β”‚   Boosts        Penalties        modulated weights      β”‚
                     β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
                     β”‚                       β–Ό                                 β”‚
                     β”‚             Top-100 + Sigmoid Stretch                  β”‚
                     β”‚                       β–Ό                                 β”‚
                     β”‚      Deterministic Tiebreak (14 sub-signals)           β”‚
                     β”‚                       β–Ό                                 β”‚
                     β”‚             Evidence-Graph Reasoning                    β”‚
                     β”‚                       β–Ό                                 β”‚
                     β”‚        submission.csv ──▢ validate_submission.py        β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This two-phase split also mirrors how the engine would work in production: Phase 1 runs nightly (or on candidate-ingest events) and refreshes the feature store; Phase 2 runs on every recruiter search query and must be sub-second. The hackathon submission demonstrates both halves at scale.


4. Phase 1 β€” precompute.py

Goal: Convert raw candidate JSONL into a wide Parquet feature table that the ranker can score in seconds.

Pipeline

1. JD Parser (lib/jd_parser.py)

The full job description text is embedded as JD_FULL_TEXT inside jd_parser.py (extracted from job_description.docx). A pure-Python pattern-matching parser produces a structured JDUnderstanding dataclass containing required/preferred skills, red flags, YOE bands, seniority, domain, preferred locations, and behavioural expectations (notice period, product vs. services, production code requirement). No LLM call is made β€” this is regex, keyword dictionaries, and section detection. All downstream modules consume this one object, so changing the JD requires only re-running the parser, not rewriting feature code.

2. Hiring Intent (lib/hiring_intent.py)

Derives a high-level HiringIntent from the JD: philosophy (e.g. "product over research"), primary_need ("production_systems"), ownership_expectation (0–1), shipping_culture ("scrappy"), team_context ("founding" / "early" / "mature"), and depth_requirement ("specialist" / "generalist"). This intent modulates scoring weights in Phase 2 β€” a founding-team JD boosts ownership weight by ~25%; a specialist JD boosts search/JD-fit weight by ~18%.

3. Query Expansion (lib/query_expansion.py)

Takes the JD's "ideal candidate" text and expands it with synonyms and related terms (e.g. "RAG" β†’ "retrieval augmented generation, dense retrieval, vector search, semantic search"). This expanded text becomes the query against which candidate embeddings are compared, so a candidate who writes "dense retrieval" still matches a JD that says "RAG".

4. Canonical Profile (lib/candidate_profile.py)

Normalizes raw candidate JSON into a typed profile with computed convenience accessors. Critically, schema.unified_text_blob() builds the text used for TF-IDF and keyword matching from career_history descriptions, summary, and headline β€” but explicitly excludes the skills[] array. This is the single most important anti-stuffing decision: a candidate cannot inflate their match score by adding "RAG, Pinecone, FAISS" to their skills list, because that list is not part of the matching text. Skills are scored separately via the skill_coverage feature, which cross-checks each declared skill against the text blob.

5. Feature Registry (lib/feature_registry.py + lib/features.py)

A plugin-style registry where each of the β‰ˆ55 features is a registered FeatureSpec with name, group, description, default weight, extract function, intent-modulation map, and optional depends_on for interaction features. This makes ablation trivial (unregister one spec) and lets intent modulation be data-driven rather than hardcoded.

6. TF-IDF + SVD Embeddings (lib/embeddings.py)

Fits a TF-IDF vectorizer on the union of candidate text blobs and the expanded ideal-candidate text, then reduces to 100 dimensions via truncated SVD. Cosine similarity to the expanded query becomes the embedding_sim feature. TF-IDF+SVD was chosen over sentence-transformers deliberately β€” see Β§13 Design Decisions.

7. Honeypot Detector (lib/honeypot.py)

Flags candidates with internally-impossible profiles (e.g. 8 years of PyTorch experience when PyTorch 1.0 was released in October 2018, or a "Principal Engineer" title with only 1 year of total experience). Honeypots carry a 0.01Γ— multiplier in Phase 2 β€” they essentially cannot enter the top 100 unless the entire candidate pool is degenerate.

Output

A single Parquet file (~1.3 GB for 100K candidates) with one row per candidate. Columns are the β‰ˆ55 numeric features plus metadata columns (current_title, current_company, YOE) and JSON-encoded evidence columns prefixed with _ (e.g. _candidate_json, _disq_reasons, _behaviour_evidence, _tier5_evidence) that the Phase 2 reasoning engine requires.

Why Parquet? Columnar compression brings 100K rows Γ— ~70 columns to ~1.3 GB vs. ~6 GB for JSON; typed schema makes pd.read_parquet ~10Γ— faster than re-parsing JSONL; and preserved dtypes eliminate type-inference overhead on every run.


5. Phase 2 β€” rank.py

Goal: Load the Parquet, score every candidate, select the top 100, generate reasoning, write CSV, and self-validate.

Pipeline

1. Auto-precompute guard

If features.parquet does not exist, rank.py shells out to precompute.py automatically, making the single-command reproduction path work end-to-end on a fresh checkout.

2. Vectorized scoring (lib/scoring.py)

Both elite_score_vec(df) and final_score_vec(df) operate on the entire DataFrame at once via NumPy β€” there is no Python per-candidate loop in the hot path. The composite formula is detailed in Β§7.

3. Top-100 + sigmoid stretch

After computing raw_score, the top 100 are selected, min-max normalized to [0, 1], and passed through a stretched sigmoid:

stretched = 0.52 + 0.47 Γ— Οƒ(10 Γ— (norm βˆ’ 0.5))

This produces scores in approximately [0.52, 0.99] with a clear top-to-bottom falloff. The relative ordering is exactly preserved β€” this is a cosmetic presentation choice so Stage-4 reviewers see a confident distribution rather than a flat 0.71–0.74 cluster.

4. Deterministic tiebreak (14 sub-signals)

When two candidates have identical stretched scores after rounding to 6 decimal places, ties are broken by a 14-element lexicographic key: tier5_signature, pre_llm_x_ownership, impact_magnitude, ownership_hierarchy, production_strength, career_depth_ratio, evidence_strength, skill_coverage, title_relevance, cross_validation, education_tier, endorsement_signal, assessment_signal, and salary_compatibility. candidate_id ascending is appended as the final fallback per the spec.

5. Evidence-graph reasoning (lib/reasoning.py)

Generates the 1–2 sentence reasoning column. See Β§9.

6. Self-validation

After writing the CSV, rank.py runs validate_output() inline (a mirror of validate_submission.py) and exits non-zero on any failure. This surfaces format drift immediately rather than at hackathon-upload time.


6. Feature Catalog (β‰ˆ55 features, G1–G13)

Features are organized into 13 groups. Each feature is a float in roughly [0, 1], extracted from the canonical profile and the unified text blob.

G1 β€” JD Fit (7 features)

The most direct measure of skill, title, and seniority match against the JD.

Feature Description
skill_coverage Fraction of JD-required skills found in candidate's text
preferred_coverage Fraction of JD-preferred skills
domain_specialization Depth in the JD's primary domain (e.g. IR/RecSys)
skill_trust_avg Average endorsement-weighted proficiency across matched skills
title_relevance Similarity of current/past titles to JD's role title
seniority Seniority-band fit (junior / mid / senior / staff)
jd_skill_count Raw count of JD-required skills present

G2 β€” Impact & Ownership (4 features)

The most heavily weighted group in the elite composite. Captures whether the candidate has demonstrably owned systems and shipped outcomes.

Feature Description
ownership_hierarchy Evidence of owning systems end-to-end (init β†’ ship β†’ operate)
impact_magnitude Quantified outcome magnitude (e.g. "reduced latency 40%")
impact_signals Count of impact-claim patterns in career text
evidence_strength Composite of quantification, specificity, and recency of evidence

G3 β€” Production & Scale (3 features)

Distinguishes production-experienced engineers from research-only or demo-only candidates β€” a JD disqualifier category.

Feature Description
production_strength Composite of deployment verbs, scale nouns, traffic mentions
production_diversity Number of distinct production contexts (multiple employers, multiple systems)
scale_evidence Mentions of QPS, users, latency budgets, SLOs

G4 β€” Experience & Career (8 features)

Feature Description
yoe_band_score Fit to the JD's 5–9 year band (peak at 6–8, gentle decay outside)
career_depth_ratio Years in applied ML/AI roles Γ· total YOE
pre_llm_months Months of ML experience before November 2022 (ChatGPT release)
career_trajectory Upward / lateral / chaotic
company_quality Best company tier (Tier-1 FAANG-adjacent β†’ Tier-5 services)
company_quality_avg Weighted-average tier across career
career_stability Inverse of job-hopping frequency
promotion_velocity Titles per year (too high = title-chaser red flag)

G5 β€” Retrieval & Evaluation (3 features)

Specific to this JD's "evaluation frameworks for ranking systems" requirement.

Feature Description
retrieval_depth Depth of retrieval/IR vocabulary in career text
evaluation_experience Mentions of NDCG, MRR, MAP, A/B tests, offline-online correlation
system_design_evidence Design vocabulary (sharding, replication, index refresh, drift)

G6 β€” Behavioural (7 features)

Derived from the 23 Redrob signals. Multiplier-style signals that modify the composite rather than driving it.

Feature Description
recency Days since last_active_date (decay function)
responsiveness recruiter_response_rate with response-time penalty
market_demand search_appearance_30d + saved_by_recruiters_30d normalized
github_activity GitHub score (-1 treated as missing, not penalized)
availability_score open_to_work_flag Γ— notice-period fit Γ— interview_completion_rate
interview_completion interview_completion_rate
platform_trust verified_email Γ— verified_phone Γ— linkedin_connected

G7 β€” Resume Quality (4 features)

Feature Description
quantified_outcomes Fraction of career entries with quantified impact
truthiness Claims-without-evidence detector (e.g. "led large scale" with no number)
keyword_stuffing_risk skills[] list vs text-blob mismatch
profile_completeness profile_completeness_score normalized

G8 β€” Safety (2 features)

Feature Description
disqualifier_penalty 0.05Γ— multiplier if any JD disqualifier fires
is_honeypot Boolean; carries a 0.01Γ— multiplier in scoring

G9 β€” Location (1 feature)

Feature Description
location_score Pune/Noida = 1.0 Β· other preferred cities = 0.8 Β· other India = 0.5 Β· outside India = 0.2

G10 β€” Career Narrative (3 features)

From lib/career_narrative.py β€” detects whether the career arc is coherent.

Feature Description
career_coherence Domain-progression consistency score
narrative_type "upward_specialist" / "lateral_generalist" / "chaotic" / "title_inflation"
narrative_suspicious List of suspicious-pattern flags (JSON-encoded)

G11 β€” Interaction Features (4 features)

Products of base features that capture compound signals a linear model would miss.

Feature Description
ownership_x_production Owners who also ship to production
skill_x_yoe Skills weighted by years of relevant experience
impact_x_domain High-impact work specifically in the JD's domain
trajectory_x_company Upward trajectory at high-tier companies

G12 β€” Key Differentiator Features (6 features)

These features each target a specific failure mode of the baseline composite that the core groups (G1–G11) do not fully address.

Feature Description
tier5_signature Recovers strong candidates (FAANG-adjacent, deep specialists) who don't keyword-match the JD but are objectively strong. Additive boost.
behavioral_twin_penalty Penalizes candidates with strong on-paper skills but behaviorally unavailable (low response rate, long notice). Response rate < 0.15 is a hard filter.
langchain_only_penalty Flags candidates whose AI experience is exclusively recent LangChain work without pre-LLM ML production experience. Direct JD disqualifier.
closed_source_penalty Flags candidates with 5+ years of entirely closed-source work and no external validation (papers, talks, OSS).
pre_llm_x_ownership Interaction: pre-LLM ML experience Γ— ownership. Rare and extremely strong founding-team signal.
salary_compatibility Fit between expected_salary_range and the role's implicit band.

G13 β€” Platform Signal Features (4 features)

These features leverage platform-specific signals not captured by the core career-text analysis.

Feature Description
assessment_signal Uses platform-validated skill_assessment_scores (not self-reported). Stronger signal than declared skills.
endorsement_signal Combines endorsements_received with skill-relevance filtering. "Endorsed on RAG" vs. "Endorsed on Excel" produce very different scores.
education_tier IIT/NIT/IIIT + CS/AI/ML field bonus. Mild tiebreaker only β€” not a primary ranking factor (avoids over-weighting pedigree).
cross_validation Count of dimensions (impact, ownership, production, depth, evaluation) all simultaneously strong (β‰₯ 0.50). Provides a +0.025 additive boost for candidates who are genuinely elite across the board.

7. Scoring Formula

Elite Composite (additive, intent-modulated weights)

elite = w_impact     Γ— impact_composite
      + w_ownership  Γ— ownership_composite
      + w_search     Γ— search_composite
      + w_behaviour  Γ— behaviour_composite

Base weights (NDCG@10-optimized β€” top-10 precision is 50% of total score):

Component Base Weight Sub-component Composition
Impact 0.42 impact_magnitude (0.38) + impact_signals (0.32) + production_diversity (0.30)
Ownership 0.33 ownership_hierarchy (0.45) + evidence_strength (0.55)
Search / JD-fit 0.15 skill_coverage (0.32) + title_relevance (0.23) + skill_trust_avg (0.20) + pre_llm_months (0.13) + retrieval_depth (0.12)
Behaviour 0.10 career_trajectory (0.22) + truthiness (0.22) + responsiveness (0.22) + yoe_band_score (0.12) + availability_score (0.12) + seniority (0.10)

Intent modulation (applied multiplicatively to base weights, then renormalized):

Hiring-intent signal Effect
ownership_expectation β‰₯ 0.8 (founding-team JD) ownership Γ—1.25 Β· impact Γ—0.95
primary_need = production_systems impact Γ—1.10 Β· search Γ—0.93
team_context ∈ {founding, early} ownership Γ—1.10 Β· behaviour Γ—0.82
depth_requirement = specialist search Γ—1.18 Β· behaviour Γ—0.88
shipping_culture = scrappy impact Γ—1.05 Β· ownership Γ—1.05

Final Score

final = elite
      + 0.065 Γ— tier5_signature           # differentiator boost
      + 0.045 Γ— pre_llm_x_ownership       # differentiator boost
      + 0.035 Γ— (impact Γ— ownership)      # combo boost
      + 0.025 Γ— cross_validation          # platform signal boost
      Γ— disqualifier_penalty              # 1.0 or 0.05
      Γ— behavioral_twin_penalty           # 1.0, 0.50, or 0.0 (if RR < 0.15)
      Γ— langchain_only_penalty            # 1.0 or 0.55
      Γ— closed_source_penalty             # 1.0 or 0.60
      Γ— honeypot_multiplier               # 1.0 or 0.01
      Γ— title_floor(title_relevance)      # non-engineer β†’ cap 0.20 Β· irrelevant β†’ 0.05

Boosts are additive (rare +0.065–0.025 lift for true elite candidates). Penalties are multiplicative β€” a single disqualifier drops the score to approximately 5% of its elite composite. This asymmetry is intentional: small recoveries for nuanced signal misses, but hard kills for genuine disqualifiers and traps.

Score Stretch (cosmetic β€” ordering preserved exactly)

stretched   = 0.52 + 0.47 Γ— Οƒ(10 Γ— (norm βˆ’ 0.5))
final_score = round(stretched, 6)

This maps raw scores to approximately [0.52, 0.99] with a confident-looking top-to-bottom falloff. Relative ordering is exactly preserved; this does not affect NDCG, MAP, or P@10 (all rank-based, not score-based).

Tie-breaking (14-element lexicographic key)

Priority order when final_score values are equal after 6-decimal rounding:

Priority Signal
1 tier5_signature
2 pre_llm_x_ownership
3 impact_magnitude
4 ownership_hierarchy
5 production_strength
6 career_depth_ratio
7 evidence_strength
8 skill_coverage
9 title_relevance
10 cross_validation
11 education_tier
12 endorsement_signal
13 assessment_signal
14 salary_compatibility
Final fallback candidate_id ascending (per spec)

8. Safety & Trap Handling

The dataset is explicitly adversarial. redrob_signals_doc.md describes approximately 80 honeypots with subtly impossible profiles, plus keyword stuffers, behavioral twins, and plain-language Tier-5 candidates designed to evade naive keyword matchers. The safety stack below is designed to keep the honeypot rate in the top 100 at 0%.

8.1 Honeypot Detection (lib/honeypot.py)

Flags candidates with internally-impossible profiles:

  • YOE claims that predate the technology (e.g. 8 years of PyTorch when v1.0 shipped October 2018)
  • Title–YOE mismatch (Principal Engineer at 1 year YOE)
  • Career gaps that do not reconcile
  • Skill durations longer than total YOE

Penalty: 0.01Γ— multiplier β€” essentially disqualifying.

8.2 Disqualifier Penalty (features.disqualifier_penalty)

Fires a 0.05Γ— multiplier on any of the JD's explicit disqualifiers:

  • Pure research with no production deployment
  • Senior engineer with no production code in the last 18 months (architecture/tech-lead drift)
  • Career exclusively at consulting firms (TCS, Infosys, Wipro, Accenture, Cognizant, Capgemini)
  • Primary expertise in CV/speech/robotics without significant NLP/IR exposure

8.3 Behavioral Twin Penalty (features.behavioral_twin)

Targets candidates who look strong on paper but are unhireable in practice: strong skills, recent active date, high profile completeness β€” but very low recruiter_response_rate and a long notice period. Penalty: 0.50Γ— soft and 0.0Γ— hard when recruiter_response_rate < 0.15, aligning with the spec's explicit guidance on response rate.

8.4 LangChain-Only Penalty (features.langchain_only_recent)

Flags candidates whose AI experience is exclusively recent (≀12 months) LangChain tutorial work without substantial pre-LLM ML production experience. Penalty: 0.55Γ—.

8.5 Closed-Source Isolation Penalty (features.closed_source_isolation)

Flags candidates whose entire 5+ year career has been on closed-source proprietary systems with no external validation (papers, talks, OSS contributions). Penalty: 0.60Γ—.

8.6 Title Floor

Caps candidates whose title does not match the engineering track:

  • Non-engineering titles (HR Manager, Sales Lead, etc.) with AI keywords β†’ score capped at 0.20
  • Irrelevant titles with no engineering signal β†’ capped at 0.05

8.7 Keyword Stuffing Resistance

Two-layer defense:

  1. unified_text_blob excludes skills[] β€” adding "RAG, Pinecone, FAISS, NDCG, LoRA, BM25, Embeddings" to a skills list does nothing for matching text. Confirmed by test_adversarial.py :: keyword_stuffer.
  2. keyword_stuffing_risk feature explicitly measures skills-vs-text mismatch and feeds into the truthiness behavioural component.

9. Evidence-Graph Reasoning

The reasoning column is generated by lib/reasoning.py using an evidence-graph approach β€” every claim is backed by a verified node in the candidate's profile.

How It Works

  1. Fact extraction β€” Pull verified facts from the canonical profile: current title, current company, YOE, top-3 strongest features, disqualifier reasons (if any), honeypot status.
  2. Template selection β€” hash(candidate_id) % N picks one of N templates per reasoning "shape" (strong-fit / behavioral-concern / skill-adjacent / disqualifier-soft / filler). Hash-based selection is deterministic and varies reasoning across candidates.
  3. Slot filling β€” Fill template slots only with verified facts. If a fact is missing (e.g. no quantified outcomes), the slot is silently omitted rather than filled with a generic claim.
  4. Tech-keyword verification β€” Any technical term mentioned in the reasoning is checked against the unified text blob. If the term is not present in the candidate's actual career text, it is removed before emission. This is the hallucination guard.
  5. JD-connection check β€” The reasoning must reference at least one JD requirement (skill, disqualifier, or behavioural expectation). Pure generic praise ("great candidate, strong skills") is rejected and a new template is selected.

Guarantees

Property How It Is Achieved
No hallucination Every cited skill, employer, or experience is verified to exist in the candidate's profile
Variation Template selection is hash-based and slot-filling is per-candidate; 10 randomly-sampled reasonings will be substantively different
Rank consistency Template shape is chosen based on the candidate's feature profile, which correlates with rank
Honest concerns If a disqualifier fired or a behavioral twin penalty applied, the reasoning mentions it

10. Compute Constraints & Compliance

Constraint Limit Observed (100K candidates, 8-core CPU) Status
Total runtime ≀ 5 min wall-clock precompute 196 s + rank ~4 s = **200 s** βœ…
Memory ≀ 16 GB RAM ~1.3 GB peak (Parquet + scoring vectors) βœ…
Compute CPU only No GPU dependencies in requirements.txt βœ…
Network Off during ranking No HTTP/socket code anywhere in lib/ βœ…
Disk ≀ 5 GB intermediate ~1.3 GB Parquet βœ…
Determinism Required Fixed REFERENCE_DATE, hash templates, no RNG βœ…

11. File Map

redrob_ranker/
β”œβ”€β”€ README.md                    # This document
β”œβ”€β”€ architecture.png             # Architecture diagram
β”œβ”€β”€ requirements.txt             # pandas, numpy, scikit-learn, pyarrow
β”œβ”€β”€ submission_metadata.yaml     # Team identity, AI tools declaration, methodology
β”œβ”€β”€ precompute.py                # Phase 1 entry β€” extracts ~55 features β†’ Parquet
β”œβ”€β”€ rank.py                      # Phase 2 entry β€” scores, top-100, reasoning β†’ CSV
β”œβ”€β”€ validate_submission.py       # Format validator (mirrors hackathon validator)
β”œβ”€β”€ test_adversarial.py          # 10-case adversarial test suite
└── lib/
    β”œβ”€β”€ __init__.py
    β”œβ”€β”€ schema.py                # Canonical accessors (unified_text_blob, signals, etc.)
    β”œβ”€β”€ constants.py             # REFERENCE_DATE, role taxonomy
    β”œβ”€β”€ jd_parser.py             # JD β†’ JDUnderstanding dataclass
    β”œβ”€β”€ jd_requirements.py       # Hardcoded fallback JD requirements
    β”œβ”€β”€ hiring_intent.py         # JD β†’ HiringIntent dataclass
    β”œβ”€β”€ query_expansion.py       # JD ideal-text β†’ expanded query
    β”œβ”€β”€ candidate_profile.py     # Raw JSON β†’ CanonicalProfile
    β”œβ”€β”€ features.py              # ~55 feature extract functions (G1–G13)
    β”œβ”€β”€ feature_registry.py      # Plugin-style feature registry
    β”œβ”€β”€ embeddings.py            # TF-IDF + SVD embedder
    β”œβ”€β”€ retrieval.py             # BM25/dense retrieval (placeholder)
    β”œβ”€β”€ rrf.py                   # Reciprocal Rank Fusion helper
    β”œβ”€β”€ scoring.py               # elite_score_vec, final_score_vec, weights
    β”œβ”€β”€ reasoning.py             # Evidence-graph reasoning generator
    β”œβ”€β”€ evidence.py              # Evidence extraction utilities
    β”œβ”€β”€ evidence_graph.py        # Evidence graph data structure
    β”œβ”€β”€ career_narrative.py      # Career coherence / trajectory classifier
    β”œβ”€β”€ company_tier.py          # Company β†’ tier mapping
    β”œβ”€β”€ title_scoring.py         # Title similarity scoring
    β”œβ”€β”€ domain.py                # Skill/domain taxonomy
    β”œβ”€β”€ honeypot.py              # Honeypot detector
    └── failure_analyzer.py      # Diagnostic for low-score candidates

Per-file Role Summary

File Lines Role
precompute.py 517 Phase 1 orchestrator β€” JSONL β†’ Parquet
rank.py 332 Phase 2 orchestrator β€” Parquet β†’ CSV + reasoning
lib/features.py 1,244 All β‰ˆ55 feature extract functions
lib/reasoning.py 650 Evidence-graph reasoning templates and verifier
lib/candidate_profile.py 472 Canonical profile normalization
lib/evidence.py 536 Evidence extraction utilities
lib/jd_parser.py 655 JD β†’ structured requirements
lib/scoring.py 417 Elite composite + final score (vectorized)
lib/feature_registry.py 507 Plugin-style feature registry
lib/hiring_intent.py 292 JD β†’ hiring intent dataclass
lib/career_narrative.py 222 Career coherence classifier
lib/failure_analyzer.py 239 Low-score diagnostic
lib/jd_requirements.py 154 Hardcoded JD fallback
lib/query_expansion.py 175 Query expansion for embeddings
lib/company_tier.py 168 Company β†’ tier mapping
lib/domain.py 197 Skill / domain taxonomy
lib/evidence_graph.py 216 Evidence graph data structure
lib/rrf.py 147 Reciprocal Rank Fusion helper
lib/title_scoring.py 125 Title similarity
lib/embeddings.py 67 TF-IDF + SVD embedder
lib/schema.py 99 Canonical accessors
lib/honeypot.py 73 Honeypot detector
lib/constants.py 18 REFERENCE_DATE + role constants
test_adversarial.py 280 10 adversarial test cases
validate_submission.py 165 Format validator
Total ~7,970

12. Adversarial Test Suite

test_adversarial.py runs 10 hand-crafted candidates through the full feature-extraction and scoring pipeline, asserting that each is handled correctly. This is the regression suite β€” every code change must keep all 10 tests green.

python test_adversarial.py
# Expected: 10/10 tests pass, with per-test diagnostics showing which features fired and the resulting score.
# Test Name What It Validates Expected Outcome
1 keyword_stuffer Many AI skills in skills[], zero career context Low score; keyword_stuffing_risk high; excluded from top 100
2 hr_manager_with_ai_keywords Non-engineering title with RAG/LLM skills Title floor fires; score capped at 0.20
3 strong_candidate Ideal JD match β€” 6–8 YOE, shipped ranking systems, recently active Top-10 score; strong-fit reasoning template
4 career_chaos Random domain jumps every 6 months narrative_type = "chaotic"; behavioural penalty; excluded
5 title_inflation Junior with 2 YOE claiming "Architect" narrative_suspicious flag; promotion_velocity red flag; excluded
6 stable_upward Coherent upward trajectory at one company narrative_type = "upward_specialist"; small boost
7 production_owner_no_impact Owns systems but no quantified outcomes ownership high but impact_magnitude low; middling score
8 research_only Strong publications, no production deployment Disqualifier fires; 0.05Γ— multiplier; excluded
9 honeypot Impossible profile (8 years PyTorch, 1 YOE Principal) Honeypot flag; 0.01Γ— multiplier; excluded
10 interaction_features High ownership Γ— production Γ— impact simultaneously ownership_x_production and impact_x_domain high; cross_validation strong; top-5 score

13. Design Decisions & Tradeoffs

Why TF-IDF+SVD over sentence-transformers

The spec forbids network calls during ranking and caps runtime at 5 minutes on CPU. Sentence-transformers (all-MiniLM-L6-v2, ~80 MB) would require bundling the model binary or downloading at runtime (forbidden). Even with the model bundled, encoding 100K candidates at ~50 ms each on CPU amounts to ~5,000 seconds β€” far beyond the budget. TF-IDF+SVD encodes 100K candidates in ~15 seconds, is bit-for-bit deterministic, has zero binary footprint, and is more interpretable (you can inspect which terms drive a candidate's similarity to the JD). Query expansion closes most of the paraphrase-matching gap by explicitly including synonyms in the query text.

Why rule-based scoring over learned weights

A learned model (XGBoost, neural ranker) would require labeled training data. The hackathon provides none β€” only a JD and a candidate pool. Self-labeling using the elite composite as a pseudo-target is circular, and LLM-generated labels are forbidden during ranking.

Rule-based scoring with explicit, JD-derived weights has three key advantages:

  1. Explainable β€” every score component maps to a JD requirement, making Stage-4 manual review defensible.
  2. No training data needed β€” works out of the box on any JD.
  3. Reproducible β€” no random initialization, no train/test split, no hyperparameter sweep; same code + same data = same output, always.

The cost is brittleness β€” a learned model would generalize better to unseen JD patterns. For a single-JD hackathon submission, this tradeoff strongly favours rules.

Why skills[] is excluded from the matching text

The dataset contains explicit keyword-stuffer traps β€” candidates with skills: [RAG, Pinecone, FAISS, NDCG, LoRA, BM25, Embeddings] but no career context for any of them. Including skills[] in the TF-IDF text blob would let these candidates score artificially high on skill coverage and embedding similarity.

By excluding skills[] from the matching text and scoring skills separately via skill_coverage (which cross-checks each declared skill against the text blob), we get a robust signal: a candidate who declares "RAG" must also have RAG-like language in their career descriptions to receive credit.

Why the score stretch sigmoid

A raw composite of 0.7134, 0.7128, 0.7119, …, 0.6892 looks flat and uninformative β€” Stage-4 reviewers cannot distinguish rank 1 from rank 50 at a glance. The stretched sigmoid maps the same ordering to 0.987, 0.974, 0.962, …, 0.524, which reads as "confident top, gradual falloff" without changing relative ordering or affecting any rank-based metric (NDCG, MAP, P@10).

Why 14 sub-signals in the tiebreaker

Score ties after rounding to 6 decimal places are rare but possible, especially among bottom-50 filler candidates. The spec requires deterministic tie-breaking with candidate_id ascending as the final fallback. Prepending 14 ordered sub-signals ensures ties break in a principled order (Tier-5 signature first, then pre-LLMΓ—Ownership, then impact magnitude, etc.) rather than purely alphabetically, making the top-10 ordering more meaningful when ties occur near the cutoff.


14. Determinism & Reproducibility Guarantees

Source of Non-determinism How We Eliminate It
Calendar date (recency, decay) REFERENCE_DATE = 2026-06-17 pinned in lib/constants.py
Random number generation No random module used in any feature or scoring path
Hash randomization (PYTHONHASHSEED) Template selection uses hashlib.md5(candidate_id), not built-in hash()
Float summation order NumPy operations on fixed-shape arrays β€” order is deterministic
Sort stability Explicit ascending=[False, True] on ["score", "candidate_id"]
TF-IDF vocabulary order TfidfVectorizer builds vocabulary in a single pass; same input β†’ same vocab
SVD initialization TruncatedSVD uses random_state=0 explicitly
External API calls None β€” no HTTP/socket code anywhere in lib/

Verification: Running python rank.py --candidates ./candidates.jsonl --out ./submission.csv twice on the same machine produces byte-identical CSV output. validate_submission.py passes on both runs.


15. Constraints Compliance Matrix

Cross-referenced against submission_spec.md Β§3.

Constraint Spec Limit Our Submission How to Verify
Total runtime ≀ 5 min ~200 s (precompute + rank) time python rank.py …
Memory ≀ 16 GB RAM ~1.3 GB peak python -c "import resource; …"
Compute CPU only No GPU dependencies grep -i cuda requirements.txt β†’ empty
Network Off during ranking No HTTP/socket code grep -rE "requests|http|socket|urllib" lib/ β†’ empty
Disk ≀ 5 GB intermediate ~1.3 GB Parquet du -sh artifacts/
Output format CSV, 100 rows, 4 cols Exactly that python validate_submission.py submission.csv
Score monotonicity Non-increasing by rank Enforced + validated validate_output() in rank.py
Tie-breaking Deterministic 14 sub-signals + candidate_id asc See Β§7
Honeypot rate in top 100 < 10% Design target: 0% top["is_honeypot"].sum() printed by rank.py
AI tools declaration Required Declared in submission_metadata.yaml cat submission_metadata.yaml

16. Known Limitations

The following is an honest engineering disclosure of what this system does not handle well, so Stage-4 reviewers can probe knowingly.

  1. No dense paraphrase matching. TF-IDF+SVD with query expansion covers most synonym cases, but a candidate who describes RAG work as "context-augmented generation with vector stores" without using any canonical terms may under-score. A natural extension would add a small local sentence-transformer for top-100 re-ranking after the cheap TF-IDF pre-filter.

  2. Education tier is India-centric. The education_tier feature maps IIT/NIT/IIIT to tiers 1–2 and treats everything else as tier 3. For non-Indian candidates this is a weak signal. Acceptable for this JD (India-located role) but would need rework for global pools.

  3. Salary compatibility is heuristic. The salary_compatibility feature compares expected_salary_range to an implicit band derived from the JD's seniority and location. The band is hardcoded (not learned), so it may misjudge edge cases such as candidates willing to take a pay cut for equity.

  4. Reasoning templates are finite. Approximately 15 templates per "shape" (strong-fit, behavioral-concern, etc.) are shipped. For a top-100 submission this gives ~6–7 candidates per template, which the spec's "Variation" check accepts. A top-1000 submission would require more templates or a precomputed template library.

  5. No online learning. The system is entirely batch β€” precompute + rank. It does not adapt to recruiter feedback. This is intentional for an offline hackathon but would be the first addition in production via a click-through rate signal.

  6. Adversarial test coverage is hand-crafted. The 10 tests in test_adversarial.py cover the trap types described in redrob_signals_doc.md, but a determined adversary could craft candidates that evade all 10. A stronger suite would generate adversarial candidates programmatically via mutation fuzzing on real candidates.


End of README β€” Redrob Intelligent Candidate Ranker