# Resume & Interview Portfolio Doc — AI-Powered Resume Ranking System

## User-confirmed context — 2026-10-18

Professional context: this work was performed in my current Consultant Data Analyst role at ALTEN, September 2025–present.

> Source: direct user answers in the portfolio review. These are user-confirmed statements, not independently rerun repository or production measurements.

- **Ownership:** independently built the system.
- **Evaluation:** tested on 50 resumes against a real job description; the manager assessed the results positively and approved the tool.
- **Adoption status:** currently in the adoption phase, not confirmed routine production usage.
- **Unmeasured:** ranking accuracy or shortlist-agreement percentage, throughput, users, time savings and operational completion rates. Positive feedback is a qualitative assessment, not a numerical accuracy score.

> Existing draft variants and unanswered placeholders are retained. This confirmed context governs personal ownership, adoption and outcomes; unresolved draft claims are not newly verified.


> Every claim below is derived from code in this repo. Items marked **[verify]** are things I could not confirm statically (real-world numbers, deployment). Note: `TECHNOLOGIES.md` and `pyproject.toml` mention a Django frontend (port 8001, `frontend` dependency group), but **no frontend source is in this repository** — don't claim frontend work from this repo unless you have that code elsewhere.

---

## 1. High-Level Project Overview

**One-line pitch:** A FastAPI + PostgreSQL backend that lets hiring managers upload a job description and a batch of CVs (PDF/DOCX/TXT), uses an LLM (NVIDIA-hosted Llama 3.1) to extract structured data, and ranks candidates with an explainable, deterministic weighted scoring engine, with live progress over WebSockets and PDF/XLSX/CSV exports.

**Problem & solution**
- *Problem:* Screening dozens/hundreds of unstructured CVs per role is slow and inconsistent; pure-LLM scoring is non-deterministic and hard to justify.
- *Solution:* A split pipeline. The LLM is used only for **extraction** (text → Pydantic-validated JSON schemas, `src/schemas/`). **Scoring is deterministic and explainable** (`src/pipelines/matching.py`, weights: skills 40 / experience 20 / projects 15 / technologies 10 / languages 10 / certifications 5). Long-running work is pushed off the request path into a **Postgres-backed job queue + worker** ([queue.py](backend/app/services/queue.py), [worker.py](backend/app/worker.py)), so uploads return `202 Accepted` and progress streams to the UI.

---

## 2. Comprehensive Tech Stack & Tooling

**Languages & Runtimes**
- Python ≥ 3.11 (`.python-version`, `pyproject.toml`); Bash + PowerShell setup scripts (`setup.sh`, `setup.ps1`, `run.sh`, `test.sh`).

**Frontend / UI**
- Not present in this repo. Docs reference a Django-template frontend (port 8001) consuming this API; backend CORS is configured for it (`CORS_ORIGINS` in [main.py](backend/app/main.py)).

**Backend / API**
- FastAPI 0.115 + Uvicorn (standard), Pydantic 2 (`pydantic[email]`) for request/LLM-output schemas, `python-multipart` for uploads.
- REST under `/api/v1` + one WebSocket (`/ws/jobs/{job_id}`).
- Auth: PyJWT (HS256), PBKDF2-HMAC-SHA256 password hashing (stdlib).
- LLM: NVIDIA Chat Completions API via `requests` (default model `meta/llama-3.1-8b-instruct`), pluggable provider layer (`services/ai/`).
- Document parsing: PyMuPDF, pdfplumber, python-docx. Exports: openpyxl (XLSX), PyMuPDF (PDF), CSV.
- Optional semantic matching: `sentence-transformers` (extra `embeddings`) with scikit-learn cosine similarity; gracefully degrades if absent.
- `python-dateutil` for fuzzy date parsing (including French month names).

**Database & Storage**
- PostgreSQL (`psycopg2-binary`), SQLAlchemy 2.0 ORM, Alembic migrations (4 revisions).
- JSON columns for structured JD/resume payloads.
- Optional Redis cache with automatic in-memory TTL fallback ([cache.py](backend/app/services/cache.py)).
- Temporary file storage service for uploaded CVs ([storage.py](backend/app/services/storage.py)).

**DevOps, Testing & Tooling**
- `uv` + `uv.lock` for reproducible environments; `pyproject.toml` with setuptools.
- pytest 8 + httpx (FastAPI test client); `conftest.py`, `tests/db_utils.py`.
- `benchmark_performance.py`; runtime readiness check ([readiness.py](src/api/readiness.py)); startup schema check ([schema_check.py](backend/app/config/schema_check.py)).
- No Dockerfile or CI workflow in the repo (don't claim them).

---

## 3. Architecture & Technical Highlights

### 3.1 Database-backed job queue with safe multi-worker claiming
[queue.py](backend/app/services/queue.py), [models.py](backend/app/models/models.py) (`JobQueueItem`)
- `pop()` uses `SELECT … FOR UPDATE SKIP LOCKED` ordered by `created_at`, so multiple workers can poll without double-claiming — no Redis/Celery needed.
- `push()` is **idempotent**: dedupes on `(job_id, task, active status)`, and recovers from unique-constraint races (partial unique index on active items).
- Retry policy: `MAX_RETRIES = 5`, failures requeue with `last_error`; `cleanup_stale()` requeues items stuck in `picked` >30 min, and discards ones whose job already failed/finished. Workers send queue heartbeats (`heartbeat_queue_item`).

### 3.2 Job state machine, distributed lock, and crash recovery
[enums.py](backend/app/models/enums.py), [job_lock.py](backend/app/services/job_lock.py), [job_state_manager.py](backend/app/services/job_state_manager.py), [main.py](backend/app/main.py)
- Explicit `JobStep` state machine (9 states) with a `VALID_TRANSITIONS` table; illegal moves raise (`validate_transition`).
- Per-job lock implemented as an atomic conditional `UPDATE … WHERE processing_lock = false` (compare-and-set), with **stale-lock takeover after 30 min** and exponential back-off (10 attempts, 0.1s base) on DB lock conflicts.
- `JobState` checkpoints + `JobEventLog` audit trail; on app startup (`lifespan`) `recover_interrupted_jobs(stale_after_minutes=10)` resumes jobs orphaned by a crash, and Alembic migrations are applied automatically.

### 3.3 Resilient LLM extraction pipeline with schema validation
[outlines_parser.py](src/core/outlines_parser.py), [nvidia_provider.py](services/ai/providers/nvidia_provider.py), [worker.py](backend/app/worker.py)
- LLM output is validated against Pydantic schemas (`SCHEMA_MAP`: JD / resume / ranking) — invalid JSON or schema violations trigger re-prompting (3 attempts, linear back-off).
- Provider layer adds its own HTTP retries (`NVIDIA_MAX_RETRIES=3`, 120 s timeout, temperature 0.1 for stable extraction).
- Worker adds a third layer: per-resume retries (`MAX_RESUME_RETRIES=3`), per-resume timeout 120 s, job timeout 3600 s. Unreadable/scanned PDFs are marked failed with a user-facing reason **without failing the batch** (partial completion is a first-class state, e.g. `PARTIAL_COMPLETION` in the WebSocket payload).
- Batches are parsed concurrently with `ThreadPoolExecutor(max_workers=12)` (`NVIDIA_CONCURRENCY`), each resume in an isolated DB session.

### 3.4 Explainable deterministic scoring engine
[matching.py](src/pipelines/matching.py) (~1,500 lines)
- Weighted dimensions (`WEIGHTS`), but **only dimensions the job actually specifies are active and are re-normalised to 100%** — absent requirements don't give free points. Penalty of −5 per missing mandatory language; score bands (Excellent ≥90 … Incompatible <40) with role-alternative recommendations only for <40.
- Skill matching: synonym normalisation (`_SKILL_SYNONYMS`, e.g. `reactjs→react`), token Jaccard similarity with `SKILL_MATCH_THRESHOLD = 0.86`, optional embedding similarity (cached in `_EMBEDDING_CACHE`) that falls back cleanly when sentence-transformers/sklearn are missing.
- Experience computed from date ranges (French/English month and "present" markers handled), employment-type filtering (`_COUNTED_EMPLOYMENT_TYPES`), seniority-progression and context scores.
- Parallelised ranking via `ThreadPoolExecutor` with a `progress_callback` feeding live progress.
- Stored output includes method/version (`deterministic_weighted_v2_simple`) for traceability.

### 3.5 Security & upload hardening
[auth.py](backend/app/api/auth.py), [jobs.py](backend/app/api/jobs.py), [config.py](backend/app/config/config.py), [docs/SECURITY.md](docs/SECURITY.md)
- JWT (24 h expiry, `iat`/`exp`), PBKDF2-SHA256 with **600,000 iterations** (env-tunable, lowered only for tests), random 16-byte salt, constant-time comparison (`secrets.compare_digest`).
- Password policy + E.164 phone validation via Pydantic validators.
- Uploads: streaming size cap (`file.read(max+1)` → 413), 10 MB per file / 50 MB total (env-configurable), extension allow-list, **magic-byte checks** (`%PDF`, `PK\x03\x04`, NUL/encoding checks for txt), duplicate detection by file hash and filename, cleanup of accepted files on failed batches.
- Tenant isolation: job/result/export/interview queries filter by `user_id` (6 explicit ownership filters in `jobs.py`), plus an `AuditLog` table and `/audit-logs` endpoint.

### 3.6 Real-time progress + caching
[websocket.py](backend/app/services/websocket.py), [stats.py](backend/app/api/stats.py), [cache.py](backend/app/services/cache.py)
- `ConnectionManager` keeps per-job socket lists, prunes dead connections on broadcast, and exposes a unified progress payload (parsing %, matching %, failed counts, stage enum). Worker threads publish into the FastAPI event loop via `set_main_event_loop` (thread→async bridge). HTTP `/jobs/{id}/progress` offers a polling fallback.
- `/stats` dashboard endpoint cached for **60 s**; cache service defaults to 300 s TTL, Redis if `REDIS_URL` is set, otherwise in-process dict — failures never break requests.

---

## 4. Quantifiable Metrics & Scope (from the codebase)

| Metric | Value |
|---|---|
| HTTP/WS endpoints | **22 total**: 19 business REST routes under `/api/v1` (auth 2, jobs 13, interviews 2, audit 1, stats 1) + `/` + `/health` + 1 WebSocket |
| DB models/tables | **9**: `User`, `AnalysisJob`, `Resume`, `RankedResult`, `JobQueueItem`, `JobState`, `JobEventLog`, `AuditLog`, `InterviewDecision` |
| Relationships | `User 1─N AnalysisJob`; `AnalysisJob 1─N Resume / RankedResult / JobQueueItem / JobEventLog / InterviewDecision` (all `ON DELETE CASCADE`); `AnalysisJob 1─1 JobState`; `AuditLog.job_id ON DELETE SET NULL` |
| Indexes / constraints | 15 explicit composite/single `Index()` definitions + unique constraints `uq_ranked_results_job_resume`, `uq_interview_job_resume` |
| Migrations | 4 Alembic revisions (incl. schema-repair migration + startup schema check) |
| Tests | **96 test functions in 12 files** (API endpoints 28, NVIDIA provider 14, JD pipeline 12, worker pipeline 11, AI service 6, outlines parser 5, concurrency 4, batch 4, readiness 4, e2e 3, scoring 3, schema repair 2) |
| Code size | ~3,000 lines backend app (jobs.py 1,293; worker.py 877), ~1,500-line matching engine |
| Frontend | N/A in repo |
| Tunables | 12 concurrent LLM workers; 0.2–0.3 s poll interval; 5 queue retries; 3 resume retries; 3 LLM parse attempts; 120 s resume / 3600 s job timeout; 10-attempt lock back-off; 30 min stale lock/queue; 10 min job recovery; 10 MB/50 MB upload caps; 60 s stats cache; 600k PBKDF2 iterations; 24 h JWT |
| Export formats | 3 (PDF, XLSX, CSV) |
| Supported inputs | 3 (PDF, DOCX, TXT) |

---

## 5. Ready-to-Use Resume Bullets (Google XYZ)

### A. Full-Stack / General Software Engineering
1. **Independently built** an AI-assisted resume-ranking platform (FastAPI, PostgreSQL, LLM extraction and explainable scoring), evaluated on **50 resumes against a real job description**. It received positive manager feedback and approval and is **currently in the adoption phase**. Operational time savings and routine hiring-manager usage are not yet measured.
2. **Engineered** a deterministic, explainable scoring engine with 6 weighted dimensions and dynamic re-weighting. The system was tested on **50 resumes** against a real job description with a positive manager assessment; recruiter-shortlist agreement remains **[X%]** (not measured).
3. **Built** a multi-format export pipeline (PDF, XLSX, CSV) and a 3-format document ingestion layer (PDF/DOCX/TXT) enabling recruiters to share ranked shortlists in [X seconds] instead of manual spreadsheets.

### B. Backend / Systems Architecture
4. **Designed** a PostgreSQL-backed task queue using `FOR UPDATE SKIP LOCKED`, idempotent enqueue, 5-attempt retries and stale-item recovery, achieving [X%] job completion with zero double-processing across [N] concurrent workers — without introducing Redis/Celery infrastructure.
5. **Implemented** a 9-state job state machine with compare-and-set row locks, stale-lock takeover (30 min), and startup crash recovery, reducing stuck jobs from [X] to [Y] per week.
6. **Optimized** batch CV processing by parsing resumes in a 12-thread pool with isolated DB sessions and layered retries (LLM re-prompt ×3, provider HTTP ×3, per-resume ×3), improving throughput to [X CVs/min] and sustaining [Y%] success on malformed inputs.
7. **Hardened** the API with PBKDF2-SHA256 (600k iterations), JWT auth, per-user ownership filtering, magic-byte upload validation, and 10 MB/50 MB size caps, blocking [N] invalid/oversized uploads in testing; backed by **96 automated tests** across 12 modules.

### C. Frontend / Product Engineering (API-side contribution; backend-driven UX)
8. **Delivered** real-time processing feedback via a per-job WebSocket manager plus a polling-compatible progress API (parsing %, matching %, partial-completion states), lowering user-perceived wait/abandonment by [X%].
9. **Shipped** recruiter-facing workflow features — manual score overrides, interview-decision tracking, audit logs, and a cached (60 s) dashboard stats endpoint — supporting [N] managers and [M] jobs with sub-[Y ms] dashboard responses.
10. **Designed** graceful-degradation UX contracts (user-readable failure reasons for scanned PDFs, `PARTIAL_COMPLETION` state, optional-embedding fallback) so one bad CV never blocks a [N]-CV batch.

---

## 6. Interview Prep: Deep-Dive Talking Points

### Problem 1 — Reliable background processing without a message broker
*Files:* [queue.py](backend/app/services/queue.py), [job_lock.py](backend/app/services/job_lock.py), [job_state_manager.py](backend/app/services/job_state_manager.py), [enums.py](backend/app/models/enums.py), [main.py](backend/app/main.py)
- **Situation:** CV parsing/matching takes minutes (LLM calls) and must survive crashes, double clicks, and multiple workers.
- **Task:** Guarantee each job runs once at a time, retries on failure, and recovers after restart — using the existing Postgres.
- **Action:** Built a table-backed queue with `SKIP LOCKED` claiming, idempotent `push`, attempt counters and stale cleanup; added a CAS-based per-job lock with stale takeover; modelled the lifecycle as a validated state machine with checkpoints and event logs; added startup recovery.
- **Result:** Duplicate enqueues are no-ops, crashed jobs auto-resume, illegal state changes fail fast, and no extra infra is required. *Trade-off to discuss:* DB polling (0.2–0.3 s) vs. a broker (latency/throughput vs. operational simplicity); lock-timeout (30 min) vs. risk of double work on very long jobs.

### Problem 2 — Making non-deterministic LLM output production-safe
*Files:* [outlines_parser.py](src/core/outlines_parser.py), [nvidia_provider.py](services/ai/providers/nvidia_provider.py), [schemas/](src/schemas), [worker.py](backend/app/worker.py) (`process_resume_single`)
- **Situation:** Llama-3.1-8B sometimes returns malformed JSON, times out, or hits API limits; CVs may be scanned or corrupt.
- **Task:** Turn arbitrary CV text into reliable structured data at batch scale without one failure sinking a job.
- **Action:** Pydantic-schema validation of every response with re-prompt retries; low temperature (0.1); provider-level HTTP retry; per-resume retry/timeout with explicit error classification (`ExtractionError` vs `PipelineError`); partial-completion semantics; 12-way thread pool with per-thread DB sessions.
- **Result:** Failures are isolated and reported per CV with human-readable reasons; successful CVs proceed to ranking. *Trade-off:* three nested retry layers raise success rate but can amplify latency/cost under outage — worth mentioning the 120 s/3600 s timeouts as the cap.

### Problem 3 — Explainable ranking that doesn't reward missing requirements
*Files:* [matching.py](src/pipelines/matching.py) (`WEIGHTS`, `_score_*`, `_compute_match_category`, `rank_resumes`), [tests/test_matching_scoring.py](tests/test_matching_scoring.py)
- **Situation:** Recruiters need to justify rankings; LLM-assigned scores are inconsistent, and naive weighted sums inflate scores when a JD omits a category.
- **Task:** Produce reproducible 0–100 scores that handle synonyms, multilingual (EN/FR) dates, and partial data.
- **Action:** Deterministic per-dimension scorers with active-dimension re-normalisation, synonym maps + Jaccard threshold (0.86), optional cached embedding similarity with graceful fallback, date parsing with French month names/"present" markers, mandatory-language penalties, banded classification, and a versioned `score_method` tag.
- **Result:** Same input → same score, every score decomposable by dimension, and ranking runs in parallel with progress callbacks. *Trade-off:* hand-tuned weights/synonyms are transparent but need maintenance and calibration against recruiter judgement **[verify with real data]**; embeddings are optional to keep deployments light.
