Skip to content

feat: add ATS score breakdown to resume improvement response#813

Merged
srbhr merged 3 commits into
srbhr:mainfrom
gingeekrishna:feature/ats-score-breakdown
Jul 6, 2026
Merged

feat: add ATS score breakdown to resume improvement response#813
srbhr merged 3 commits into
srbhr:mainfrom
gingeekrishna:feature/ats-score-breakdown

Conversation

@gingeekrishna

@gingeekrishna gingeekrishna commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #810

  • Introduces compute_ats_score() (apps/backend/app/services/ats.py) that computes three ATS-style sub-scores from already-processed data — no extra LLM calls needed:
    • Keyword Match (55%) — fraction of extracted job keywords found in the resume
    • Skills Coverage (25%) — fraction of job qualifications matched against resume skills
    • Section Completeness (20%) — detects presence of Summary, Experience, Education, Skills sections
  • Weighted overall_score (0–100) is added to both /api/v1/resumes/improve (confirm) and /api/v1/resumes/improve/preview responses as ats_score
  • missing_keywords (up to 10), injectable_keywords, and recommendations are generated from the sub-scores
  • New ATSScoreCard component on the tailor page renders per-category progress bars, missing-keywords chips, injectable-keywords chips, and actionable tips once a preview result is available
  • ATSScore / ATSSubScores TypeScript interfaces added to the shared context

Test plan

  • Upload a resume (PDF/DOCX) and paste a job description
  • Click Improve — confirm the ATS Score Breakdown card appears below the action buttons with three progress bars and colour-coded scores
  • Verify the overall score reflects the weighted combination of the three sub-scores (55/25/20)
  • Check missing-keywords chips (red) and injectable-keywords chips (blue) render correctly when present
  • Confirm existing improve/preview functionality is unaffected (original_score, new_score, resume_preview still present in response)

Summary by cubic

Adds an ATS score breakdown to resume improvement responses and the tailor page. Computes overall and sub-scores, missing/injectable keywords, and tips from existing data with no extra LLM calls, fulfilling Linear #810.

  • New Features

    • Backend: compute_ats_score() calculates Keyword Match (55%), Skills Coverage (25%), Section Completeness (20%), and a weighted overall score; whole‑word regex matching with pre‑lowered text for speed.
    • API/Schemas: Preview and confirm responses include ats_score (ATSScore/ATSSubScores), using refinement_result.keyword_analysis when available or calculate_keyword_match as fallback; includes missing/injectable keywords and recommendations.
    • Frontend: Adds types and an ATSScoreCard on the tailor page when a preview is available.
  • Bug Fixes

    • Frontend: Clamped NaN/invalid bar widths; stable keys for keyword chips and recommendations.
    • Backend: Error logging now uses exc_info=True; clearer names and docstrings in ats.py.

Written for commit 6df6eee. Summary will update on new commits.

Review in cubic

Copilot AI review requested due to automatic review settings May 28, 2026 20:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an ATS-style scoring breakdown to the resume analysis UI by computing sub-scores on the backend and displaying them (with bars, missing keywords, and recommendations) on the dashboard.

Changes:

  • Introduces ATSScoreService to compute ATS sub-scores, missing keywords, and recommendations.
  • Wires ATS score data through the backend response and frontend data types to the dashboard.
  • Updates the Resume Analysis component to render ATS breakdown UI when atsScore is present.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/frontend/components/dashboard/resume-analysis.tsx Renders ATS overall/sub-scores, missing keywords, and recommendations in the analysis card + modal.
apps/frontend/components/common/resume_previewer_context.tsx Adds TypeScript types for ATS score payload (ATSScore, ATSSubScores) and extends Data.
apps/frontend/app/(default)/dashboard/page.tsx Passes ats_score from API response into the ResumeAnalysis component.
apps/backend/app/services/score_improvement_service.py Computes ATS score during score improvement execution and returns it in the payload.
apps/backend/app/services/ats_score_service.py New service that calculates ATS sub-scores and aggregated overall score.
apps/backend/app/services/init.py Exposes ATSScoreService in the services package exports.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/backend/app/services/score_improvement_service.py Outdated
Comment thread apps/backend/app/services/ats_score_service.py Outdated
Comment thread apps/backend/app/services/ats_score_service.py Outdated
Comment thread apps/backend/app/services/ats_score_service.py Outdated
Comment thread apps/frontend/components/dashboard/resume-analysis.tsx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread apps/backend/app/services/ats_score_service.py Outdated
Comment thread apps/backend/app/services/ats_score_service.py Outdated
Comment thread apps/backend/app/services/score_improvement_service.py Outdated
Comment thread apps/frontend/components/dashboard/resume-analysis.tsx Outdated
Comment thread apps/frontend/components/dashboard/resume-analysis.tsx Outdated
@gingeekrishna gingeekrishna force-pushed the feature/ats-score-breakdown branch 4 times, most recently from 25f8218 to 221e276 Compare May 28, 2026 20:54
@gingeekrishna gingeekrishna requested a review from Copilot May 28, 2026 20:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Comment thread apps/frontend/components/tailor/ats-score-card.tsx Outdated
Comment thread apps/frontend/components/tailor/ats-score-card.tsx Outdated
Comment thread apps/frontend/components/tailor/ats-score-card.tsx Outdated
Comment thread apps/backend/app/services/ats.py
Comment thread apps/backend/app/services/ats.py Outdated
Comment thread apps/backend/app/routers/resumes.py Outdated
Computes an ATS-style sub-score breakdown on the backend and surfaces
it in the tailor UI with progress bars, missing keywords, injectable
keywords, and actionable recommendations.

Backend:
- Add apps/backend/app/services/ats.py with compute_ats_score()
  - Weighted composite of keyword_match (55%), skills_coverage (25%),
    section_completeness (20%)
  - Whole-word regex matching in _keyword_in_text to prevent false
    positives ('go' no longer matches 'going')
  - _keyword_in_text receives pre-lowercased text (text_lower param)
    so the caller's .lower() is not repeated per skill in the hot loop
- Wire ats_score into ImproveResumeData in resumes.py via _build_ats_score
  - Logs with exc_info=True to preserve stack traces on failure
- Add ATSScore / ATSSubScores schemas to schemas/__init__.py and models.py

Frontend:
- Add ATSScoreCard component (components/tailor/ats-score-card.tsx)
  - clampWidth clamps to [0, 100] to guard against NaN or negative values
  - Composite stable keys for missing/injectable keyword chips and
    recommendation list items
- Add ATSScore / ATSSubScores TypeScript types to resume_previewer_context.tsx
- Render ATSScoreCard in tailor/page.tsx when ats_score is present
@gingeekrishna gingeekrishna force-pushed the feature/ats-score-breakdown branch from cc74f80 to 9d178d7 Compare June 19, 2026 15:50
@gingeekrishna

gingeekrishna commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Here is the summary what was addressed overall:

Copilot / cubic-dev-ai review comments:

  • Return None instead of {} for missing DB records — The original ats_score_service.py was removed. The new ats.py (compute_ats_score()) always returns a fully-shaped dict or propagates an exception; the caller (_build_ats_score in resumes.py) returns None on any exception, which serialises as JSON null so the frontend optional check works correctly.

  • Overly broad substring matching (P1) — Replaced with whole-word regex matching via _keyword_in_text() (uses (?<!\w)…(?!\w) boundaries), so 'go' no longer matches 'going'.

  • json.loads dict/list payload handling — Resolved by redesign: the new service receives already-parsed dicts from the router, not raw JSON strings.

  • Unused Job import / unused imports — Removed (the old ats_score_service.py is gone; ats.py has no unused imports).

  • Score normalization (updated_score / 100) — The old score_improvement_service.py was removed. The router passes final_match_percentage (already 0–100) directly into compute_ats_score() which clamps it with min(100, max(0, …)).

  • ats_score missing from run_and_stream() — The streaming path was not carried over; both the /improve/preview and /improve endpoints in resumes.py now call _build_ats_score consistently.

  • Docstring/code mismatch in _compute_section_completeness — Fixed: the docstring now correctly describes the fallback text-scan path rather than claiming it accepts a raw markdown string.

  • exc_info=True in _build_ats_scorelogger.warning("ATS score computation failed", exc_info=True) preserves the full traceback.

  • Duplicated ATS sub-score rendering in modal (resume-analysis.tsx) — Resolved by architecture: the new ATSScoreCard component is the single rendering location; there is no inline modal duplication.

  • React key collisions on keyword chipsats-score-card.tsx uses composite keys (missing-${i}-${kw}, injectable-${i}-${kw}) rather than keyword text alone.

  • Unclamped CSS percentage widthsclampWidth() in ats-score-card.tsx applies Math.min(Math.max(value, 0), 100) with an isFinite guard for NaN.

  • Tip list React keys — Composite key rec-${i}-${tip.slice(0, 30)} used instead of bare index.

jd_skills.extend(job_keywords.get("preferred_skills", []))

if not jd_skills:
return 0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: When the job description has no extractable required_skills or preferred_skills, skills coverage returns 0.0, imposing a guaranteed 25-point penalty on the overall score (since skills_coverage weight is 0.25).

This means a resume that perfectly matches the JD keywords and sections will still only reach a max overall of ~75 points — misleading for users.

Consider returning 100.0 (assume no penalty if nothing was expected) or falling back to keyword-based skill detection when jd_skills is empty.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

<h3 className="text-base font-semibold text-white">ATS Score Breakdown</h3>
<div className="flex items-end gap-1">
<span className={`text-3xl font-bold tabular-nums ${scoreColor(overall_score)}`}>
{overall_score.toFixed(1)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: overall_score.toFixed(1) has no Number.isFinite guard, unlike SubScoreRow (line 37) which checks Number.isFinite(value). If the API returns NaN or undefined, this will render the string "NaN" or throw a TypeError.

Consider adding the same guard pattern used in SubScoreRow:

{Number.isFinite(overall_score) ? overall_score.toFixed(1) : '—'}

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if found == 0:
text = _extract_all_text(resume).lower()
for patterns in _SECTION_PATTERNS.values():
if any(p in text for p in patterns):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: p in text performs substring matching on the fully flattened text. Short patterns like "about" (summary section), "technical" (skills section), or "degree" (education section) can match inside body content like description text, not just actual section headings — leading to false positives in section detection.

Since this fallback only triggers when found == 0, the practical impact is limited, but consider using a more targeted heuristic (e.g., only scanning the first N characters, or checking for heading-like patterns) to reduce false matches.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/backend/app/services/ats.py 78 _compute_skills_coverage returns 0.0 when JD has no extractable skills, imposing a permanent 25-point penalty on the overall score — max attainable becomes ~75 even with perfect keyword/section match
apps/frontend/components/tailor/ats-score-card.tsx 61 overall_score.toFixed(1) has no Number.isFinite guard (unlike SubScoreRow which checks it). Malformed API data could render "NaN" or throw TypeError

SUGGESTION

File Line Issue
apps/backend/app/services/ats.py 122 Section detection fallback uses substring matching (p in text) on fully flattened resume text. Short patterns like "about", "technical", "degree" can match inside body content, causing false positives for section presence
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
apps/backend/app/routers/resumes.py 938 If RefinementStats construction raises (e.g., keyword_analysis is None when accessing .injectable_keywords), refinement_result is set but refinement_successful stays False. _build_ats_score then falls to the else branch, losing keyword_analysis data even though it exists on the refinement result. Edge case, but worth noting.
Files Reviewed (7 files)
  • apps/backend/app/services/ats.py - 2 issues
  • apps/frontend/components/tailor/ats-score-card.tsx - 1 issue
  • apps/backend/app/routers/resumes.py - 0 new issues (existing comments cover concerns)
  • apps/backend/app/schemas/models.py - 0 issues
  • apps/backend/app/schemas/__init__.py - 0 issues
  • apps/frontend/components/common/resume_previewer_context.tsx - 0 issues
  • apps/frontend/app/(default)/tailor/page.tsx - 0 issues

Fix these issues in Kilo Cloud

@kilo-code-bot

kilo-code-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/backend/app/services/ats.py 78 _compute_skills_coverage returns 0.0 when JD has no extractable skills, imposing a permanent 25-point penalty on the overall score — max attainable becomes ~75 even with perfect keyword/section match
apps/frontend/components/tailor/ats-score-card.tsx 61 overall_score.toFixed(1) has no Number.isFinite guard (unlike SubScoreRow which checks it). Malformed API data could render "NaN" or throw TypeError

SUGGESTION

File Line Issue
apps/backend/app/services/ats.py 122 Section detection fallback uses substring matching (p in text) on fully flattened resume text. Short patterns like "about", "technical", "degree" can match inside body content, causing false positives for section presence
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
apps/backend/app/routers/resumes.py 938 If RefinementStats construction raises (e.g., keyword_analysis is None when accessing .injectable_keywords), refinement_result is set but refinement_successful stays False. _build_ats_score then falls to the else branch, losing keyword_analysis data even though it exists on the refinement result. Edge case, but worth noting.
Files Reviewed (7 files)
  • apps/backend/app/services/ats.py - 2 issues
  • apps/frontend/components/tailor/ats-score-card.tsx - 1 issue
  • apps/backend/app/routers/resumes.py - 0 new issues (existing comments cover concerns)
  • apps/backend/app/schemas/models.py - 0 issues
  • apps/backend/app/schemas/__init__.py - 0 issues
  • apps/frontend/components/common/resume_previewer_context.tsx - 0 issues
  • apps/frontend/app/(default)/tailor/page.tsx - 0 issues

Fix these issues in Kilo Cloud


Reviewed by qwen3.7-plus-20260602 · Input: 4.9K · Output: 10.2K · Cached: 407K

@srbhr srbhr merged commit eb5527a into srbhr:main Jul 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add Resume Scoring + Job-Specific Tailoring (ATS Simulation)

3 participants