# How the AI-Job-Search Framework Evaluates Job Fit: A Deterministic Scoring Pipeline

> Discover how the AI-Job-Search framework evaluates job fit using a deterministic scoring pipeline. Learn about eligibility gates and weighted scores for skills experience behavior and career alignment.

- Repository: [Mads Lorentzen/ai-job-search](https://github.com/MadsLorentzen/ai-job-search)
- Tags: deep-dive
- Published: 2026-08-26

---

**The framework evaluates job fit through a deterministic, rule-based scoring pipeline that applies hard-stop eligibility gates before calculating weighted dimension scores across technical skills, experience, behavioral fit, and career alignment.**

The **ai-job-search** repository by MadsLorentzen implements a reproducible, code-driven approach to triage job postings using a fixed rubric defined in specification files rather than subjective judgment. This framework processes scraped job data through a strict evaluation protocol that combines hard filters with quantitative scoring to generate ranked shortlists.

## The Two-Phase Evaluation Architecture

The framework splits **job fit evaluation** into distinct gate-checking and scoring phases, ensuring only viable candidates proceed to quantitative analysis.

### Phase 1: Hard-Stop Gates (Eligibility and Language)

Before any numeric scoring occurs, the system runs **eligibility** and **language** gates defined in [`.claude/skills/job-application-assistant/04-job-evaluation.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/04-job-evaluation.md). These are deterministic filters that immediately exclude postings based on citizenship requirements, security clearances, residency restrictions, or unsupported languages.

- **Eligibility Gate**: Returns `FAIL` for jobs requiring attributes the candidate lacks (e.g., US citizenship when unavailable), immediately vetoing the posting.
- **Language Gate**: Returns `FAIL` if the posting requires a language not listed in the candidate profile, or `FLAG` to highlight potential mismatches while keeping the job in consideration.

Jobs that fail either gate receive no score and generate no application draft.

### Phase 2: Quantitative Scoring Dimensions

For postings that pass the gates, the framework evaluates **five dimensions** on a 0-100 scale:

- **Technical Skills** (0-100): Match between required technical stack and candidate capabilities
- **Experience Match** (0-100): Alignment of years and type of experience with role requirements
- **Behavioral/Culture Fit** (0-100): Compatibility with stated company values and working style
- **Career Alignment** (0-100): Progression toward candidate's stated long-term goals
- **Location & Logistics**: Pass/Fail/Flag verdict (unweighted but can veto shortlisting)

Unlike the weighted dimensions, **Location & Logistics** acts as a final veto gate rather than contributing to the numeric score.

## How Dimension Scores Are Calculated and Weighted

According to the specification in [`04-job-evaluation.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/04-job-evaluation.md), raw dimension scores combine using fixed weights to produce an overall fit percentage.

### The Weighting Formula

The framework applies these predetermined weights:

- **Technical**: 30%
- **Experience**: 25%
- **Behavioral**: 15%
- **Career**: 30%

The calculation follows this arithmetic:

```

Overall Score = (Technical × 0.30) + (Experience × 0.25) + (Behavioral × 0.15) + (Career × 0.30)

```

### Fit Band Classification

The weighted total maps to discrete **fit bands** that determine triage priority:

- **Strong Fit**: ≥ 75 points
- **Good Fit**: 60-74 points
- **Moderate Fit**: 45-59 points
- **Weak Fit**: 30-44 points
- **Poor Fit**: < 30 points

Only postings achieving "Strong Fit" or "Good Fit" typically receive application drafts, while "Moderate Fit" jobs may be flagged for secondary review.

## The `/rank` Command Execution Flow

The [`.claude/commands/rank.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/rank.md) file orchestrates the complete evaluation pipeline through seven discrete steps:

1. **Parse Input**: Determines which jobs to rank (defaults to `new` unprocessed postings)
2. **Load State**: Reads [`job_scraper/seen_jobs.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/job_scraper/seen_jobs.json) and exclusion tracker `job_search_tracker.csv` to avoid re-evaluating previously processed or applied positions
3. **Load Specifications**: Imports the evaluation rubric from [`04-job-evaluation.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/04-job-evaluation.md) and candidate data from [`01-candidate-profile.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/01-candidate-profile.md)
4. **Parallel Scoring**: Dispatches agents to fetch each posting URL, apply gates, extract relevant sections, and return JSON with dimension scores
5. **Aggregation**: Computes weighted scores, applies fit bands, enforces location/language vetoes, and adds urgency markers (🔥) for deadlines under 7 days
6. **Persistence**: Writes enriched data back to [`seen_jobs.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/seen_jobs.json) with fields including `rank_score`, `rank_verdict`, `location_verdict`, `language_gate`, `strengths`, and `gaps`
7. **Presentation**: Generates a markdown table showing shortlists, closing-soon opportunities, and excluded entries with clickable URLs

All calculations remain **pure data transforms** with no external API calls, ensuring cheap, repeatable execution.

## Implementation Example: Scoring Logic in Python

The core arithmetic implemented by the `/rank` command follows this deterministic pattern:

```python
import json
from pathlib import Path

# Load static weights and thresholds from the evaluation spec

WEIGHTS = {
    "technical": 0.30,
    "experience": 0.25,
    "behavioral": 0.15,
    "career": 0.30,
}

THRESHOLDS = {
    "strong": 75,
    "good": (60, 74),
    "moderate": (45, 59),
    "weak": (30, 44),
    "poor": (0, 29),
}

def weighted_score(dim_scores: dict) -> float:
    """Combine dimension scores using fixed weights from 04-job-evaluation.md."""
    return sum(dim_scores[d] * WEIGHTS[d] for d in WEIGHTS)

def fit_band(overall: float) -> str:
    """Map overall score to fit band for triage decisions."""
    if overall >= THRESHOLDS["strong"]:
        return "Strong Fit"
    lo, hi = THRESHOLDS["good"]
    if lo <= overall <= hi:
        return "Good Fit"
    lo, hi = THRESHOLDS["moderate"]
    if lo <= overall <= hi:
        return "Moderate Fit"
    lo, hi = THRESHOLDS["weak"]
    if lo <= overall <= hi:
        return "Weak Fit"
    return "Poor Fit"

# Example evaluation workflow

dim_scores = {
    "technical": 78,
    "experience": 65,
    "behavioral": 70,
    "career": 80,
}

overall = weighted_score(dim_scores)
band = fit_band(overall)

print(f"Score: {overall:.0f}/100 → {band}")

# Output: Score: 77/100 → Strong Fit

```

The gate-checking logic operates separately before scoring:

```python
def passes_gates(job: dict) -> bool:
    """Verify hard-stop gates before calculating fit scores."""
    if job.get("eligibility_gate") == "FAIL":
        return False
    if job.get("language_gate") == "FAIL":
        return False
    return True

```

## Summary

- The framework uses a **deterministic two-phase pipeline**: hard-stop gates first, then weighted dimension scoring.
- **Five dimensions** (Technical, Experience, Behavioral, Career, Location) evaluate job fit, with four contributing to a weighted 0-100 score.
- **Fixed weights** (30% Technical, 25% Experience, 15% Behavioral, 30% Career) ensure consistent evaluation across all postings.
- The **`/rank` command** in [`.claude/commands/rank.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/rank.md) orchestrates parallel agent processing, aggregation, and persistence to [`seen_jobs.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/seen_jobs.json).
- **Pure data transforms** ensure the scoring pipeline operates without external API dependencies, making job fit evaluation fast, cheap, and reproducible.

## Frequently Asked Questions

### How does the framework handle jobs that require security clearance or specific citizenship?

The **eligibility gate** defined in [`.claude/skills/job-application-assistant/04-job-evaluation.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/04-job-evaluation.md) immediately returns `FAIL` for postings requiring citizenship, permanent residency, or security clearances that the candidate lacks. These jobs are excluded from scoring and receive no application draft, functioning as a hard filter before any quantitative analysis begins.

### What happens if a job posting is in a language the candidate doesn't speak?

The **language gate** checks against the candidate's declared languages in [`01-candidate-profile.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/01-candidate-profile.md). If the posting requires an undeclared language, it receives a `FAIL` verdict and is excluded entirely. If the match is uncertain, it receives a `FLAG` status—keeping the job in consideration but marking it for manual review in the final output table.

### Can I adjust the weights for different types of job searches?

The weights are **hard-coded in the specification file** ([`04-job-evaluation.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/04-job-evaluation.md)) as Technical 30%, Experience 25%, Behavioral 15%, and Career 30%. To modify these, you would edit the `WEIGHTS` dictionary in the evaluation specification and ensure the `/rank` command loads the updated values when processing [`seen_jobs.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/seen_jobs.json).

### Where does the framework store the final job fit scores?

After evaluation, the framework enriches [`job_scraper/seen_jobs.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/job_scraper/seen_jobs.json) with structured fields including `rank_score` (the weighted 0-100 total), `rank_verdict` (the fit band classification), `location_verdict`, `language_gate`, plus arrays of `strengths` and `gaps` extracted during agent analysis. This file serves as the persistent state for tracking which jobs to pursue.