feat: add ATS score breakdown to resume improvement response#813
Conversation
There was a problem hiding this comment.
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
ATSScoreServiceto 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
atsScoreis 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.
There was a problem hiding this comment.
5 issues found across 6 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
25f8218 to
221e276
Compare
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
cc74f80 to
9d178d7
Compare
|
Here is the summary what was addressed overall: Copilot / cubic-dev-ai review comments:
|
| jd_skills.extend(job_keywords.get("preferred_skills", [])) | ||
|
|
||
| if not jd_skills: | ||
| return 0.0 |
There was a problem hiding this comment.
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)} |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
Code Review SummaryStatus: 3 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Other Observations (not in diff)Issues found in unchanged code that cannot receive inline comments:
Files Reviewed (7 files)
|
Code Review SummaryStatus: 3 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Other Observations (not in diff)Issues found in unchanged code that cannot receive inline comments:
Files Reviewed (7 files)
Fix these issues in Kilo Cloud Reviewed by qwen3.7-plus-20260602 · Input: 4.9K · Output: 10.2K · Cached: 407K |
Summary
Closes #810
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:overall_score(0–100) is added to both/api/v1/resumes/improve(confirm) and/api/v1/resumes/improve/previewresponses asats_scoremissing_keywords(up to 10),injectable_keywords, andrecommendationsare generated from the sub-scoresATSScoreCardcomponent on the tailor page renders per-category progress bars, missing-keywords chips, injectable-keywords chips, and actionable tips once a preview result is availableATSScore/ATSSubScoresTypeScript interfaces added to the shared contextTest plan
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
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.ats_score(ATSScore/ATSSubScores), usingrefinement_result.keyword_analysiswhen available orcalculate_keyword_matchas fallback; includes missing/injectable keywords and recommendations.ATSScoreCardon the tailor page when a preview is available.Bug Fixes
exc_info=True; clearer names and docstrings inats.py.Written for commit 6df6eee. Summary will update on new commits.