# How the AI Job Search Framework Evaluates Job Fit Against Scoring Dimensions

> Discover how the AI Job Search Framework evaluates job fit using a three-stage pipeline, scoring dimensions, and actionable verdict bands. Optimize your search today.

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

---

**The AI Job Search Framework evaluates job fit through a three-stage pipeline that applies hard-filter eligibility gates, scores postings across five weighted dimensions, and aggregates results into actionable verdict bands ranging from "Strong Fit" to "Poor Fit."**

The MadsLorentzen/ai-job-search repository implements a transparent, reproducible evaluation system that separates mandatory exclusion criteria from quantitative fit assessment. This article breaks down exactly how the framework uses defined scoring dimensions to transform raw job postings into ranked, actionable opportunities.

## The Three-Stage Evaluation Pipeline

The evaluation logic is 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) and executed through the `/rank` command ([`.claude/commands/rank.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/rank.md)). The architecture strictly separates binary exclusion filters from numeric scoring to ensure only viable candidates enter the weighted calculation.

### Stage 1: Pre-Scoring Eligibility Gates

Before any numeric analysis occurs, the framework applies **Eligibility** and **Language** gates. These act as binary filters:

- **Eligibility Gate**: Validates mandatory requirements (citizenship, visa status, years of experience).
- **Language Gate**: Confirms language proficiency matches posting requirements.

If a posting fails either gate, the framework immediately excludes it from ranking. The veto reason persists in [`job_scraper/seen_jobs.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/job_scraper/seen_jobs.json) under `language_note` or `eligibility_note` fields for auditability.

### Stage 2: The Five Scoring Dimensions

Postings that pass the gates receive numeric scores across five dimensions. Four dimensions use a 0-100 scale weighted toward the final score, while Location operates as a hard veto:

| Dimension | Scale | Weight | Description |
|-----------|-------|--------|-------------|
| **Technical Skills Match** | 0-100 | 30% | Alignment between required/preferred skills and candidate capabilities |
| **Experience Match** | 0-100 | 25% | Functional requirement alignment with candidate's work history |
| **Behavioral / Culture Fit** | 0-100 | 15% | Compatibility with organizational culture and team dynamics |
| **Career Alignment & Motivation** | 0-100 | 30% | Role's potential to advance candidate's long-term career goals |
| **Location & Logistics** | PASS/FAIL/FLAG | Veto only | Hard filter for relocation requirements; excluded from weighted calculation |

The specific meaning of score buckets (80-100, 60-79, etc.) is rigidly defined in [`04-job-evaluation.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/04-job-evaluation.md) to ensure consistent interpretation across scoring agents.

### Stage 3: Aggregation and Verdict Assignment

The framework calculates a **weighted overall score** using the formula:

```python
overall = (
    scores["technical"] * 0.30 +
    scores["experience"] * 0.25 +
    scores["behavioral"] * 0.15 +
    scores["career"] * 0.30
)

```

This aggregates into five verdict bands:

- **Strong Fit** (≥75): Apply outright
- **Good Fit** (60-74): Apply with gap-addressing notes
- **Moderate Fit** (45-59): Consider; discuss with user
- **Weak Fit** (30-44): Usually skip unless strategic
- **Poor Fit** (<30): Skip

Any `FAIL` verdict from Location & Logistics removes the job from the shortlist regardless of other scores.

## Technical Implementation in the Codebase

The `/rank` command orchestrates this evaluation by dispatching agents to apply the rubric to each posting. Agents return structured data that the command persists and displays.

### Scoring Agent Output Format

When an agent evaluates a posting, it returns JSON matching this schema (example from the implementation):

```json
{
  "key": "job-12345",
  "status": "scored",
  "scores": {
    "technical": 78,
    "experience": 85,
    "behavioral": 70,
    "career": 65
  },
  "location_verdict": "PASS",
  "language_gate": "FLAG",
  "language_note": "Posting requires fluent Polish – candidate lists Polish at Basic",
  "strengths": [
    "Strong Python & ML experience matches required skills"
  ],
  "gaps": [
    "Missing exposure to cloud-native CI/CD tools"
  ]
}

```

### Weighted Calculation Logic

The explicit aggregation implementation from [`.claude/commands/rank.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/rank.md) follows this Python logic:

```python
weights = {"technical": 0.30, "experience": 0.25,
           "behavioral": 0.15, "career": 0.30}

def overall_score(scores):
    return sum(scores[dim] * weights[dim] for dim in weights)

# Example calculation

scores = {"technical": 78, "experience": 85,
          "behavioral": 70, "career": 65}
print(overall_score(scores))  # → 73.5 → "Good Fit"

```

The command then maps the float to the verdict bands using threshold comparisons identical to the pseudocode above.

## Architectural Design Principles

### Single Source of Truth

All gate definitions, dimension descriptions, weighting schemes, and verdict thresholds live in [`04-job-evaluation.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/04-job-evaluation.md). The `/rank` command loads this file once at execution start and passes the rubric to all scoring agents, preventing drift or inconsistency across evaluations.

### Agent-Driven Triage

Scoring agents receive only the posting text (fetched via WebFetch) and the compact rubric. They perform no external research—salary benchmarking and company-wide culture checks are explicitly reserved for the subsequent `/apply` workflow to keep the triage step lightweight.

### Persistent Audit Trail

After evaluation, the framework writes gate verdicts (`location_verdict`, `language_gate`), numeric scores, and identified gaps back into [`job_scraper/seen_jobs.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/job_scraper/seen_jobs.json). This persistence enables downstream commands (`/apply`, `/outcome`) to reference the original scoring rationale without recomputing.

### Threshold-Driven Presentation

The `/rank` command partitions results into **Shortlisted** (passing all gates) and **Excluded** (failing gates or expired) sections. Shortlisted jobs display their weighted score, verdict band, and any `FLAG` markers (e.g., heavy travel warnings) so users can quickly prioritize applications.

## Summary

- The framework evaluates job fit through **three distinct stages**: eligibility gates, weighted dimension scoring, and verdict aggregation.
- **Technical Skills Match** (30%) and **Career Alignment** (30%) carry the highest weights, while **Behavioral Fit** contributes 15%.
- **Location & Logistics** operates as a hard veto (FAIL) or warning (FLAG) rather than entering the weighted calculation.
- The `/rank` command in [`.claude/commands/rank.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/rank.md) implements the aggregation logic and persists all outputs to [`job_scraper/seen_jobs.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/job_scraper/seen_jobs.json).
- Verdict bands translate numeric scores into actionable decisions: **Strong Fit** (≥75) triggers immediate application, while **Poor Fit** (<30) results in automatic exclusion.

## Frequently Asked Questions

### What happens if a job fails the Language Gate?

The posting is immediately excluded from the ranked shortlist. The framework persists the specific failure reason—such as "Posting requires fluent Polish – candidate lists Polish at Basic"—to the `language_note` field in [`job_scraper/seen_jobs.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/job_scraper/seen_jobs.json). This allows users to review exclusion criteria and identify skill gaps or language learning priorities.

### How does the framework handle remote versus on-site requirements?

The **Location & Logistics** dimension evaluates relocation support and travel intensity. Fully remote positions receive a **PASS** and proceed to scoring. Mandatory relocation without sponsorship receives a **FAIL**, triggering immediate removal from consideration. Heavy travel requirements receive a **FLAG**, meaning the job remains in results but displays a visible warning to the user.

### Can the scoring weights be customized?

According to the source code 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), the weights are hardcoded as Technical (30%), Experience (25%), Behavioral (15%), and Career (30%). To modify these percentages, users must edit both the framework definition file and the aggregation logic in [`.claude/commands/rank.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/rank.md) where the calculation is explicitly implemented.

### Where does the scoring data persist after evaluation?

The `/rank` command writes all evaluation outputs—including numeric dimension scores, gate verdicts (`location_verdict`, `language_gate`), identified strengths/gaps, and final verdict classifications—to [`job_scraper/seen_jobs.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/job_scraper/seen_jobs.json). This JSON file serves as the persistent datastore that subsequent commands like `/apply` and `/outcome` reference when generating tailored applications or tracking interview outcomes.