From 6e97de4e355618d7cefc125865ec330af062b6c6 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Mon, 10 Aug 2026 04:26:45 +0530 Subject: [PATCH] perf: streamline ATS keyword scoring --- .gitignore | 1 + src/__tests__/ats-score-performance.test.ts | 55 +++++++++++++++++++++ src/lib/ats-score.ts | 21 +++++--- 3 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 src/__tests__/ats-score-performance.test.ts diff --git a/.gitignore b/.gitignore index 3f4e267..66afb75 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ cache.bin # local agent logs .devin/logs/ .agent-logs/ +.codevetter/ diff --git a/src/__tests__/ats-score-performance.test.ts b/src/__tests__/ats-score-performance.test.ts new file mode 100644 index 0000000..97715eb --- /dev/null +++ b/src/__tests__/ats-score-performance.test.ts @@ -0,0 +1,55 @@ +import { createHash } from 'node:crypto'; +import { performance } from 'node:perf_hooks'; + +import { expect, test } from 'vitest'; + +import { calculateATSScore } from '@/lib/ats-score'; + +const SIZES = [2_000, 10_000, 20_000]; +const ITERATIONS = 25; +const VOCABULARY = Array.from({ length: 800 }, (_, index) => `skill${index}`); +const EXPECTED_HASHES = new Map([ + [2_000, '252044279edd44086b42af44df09f0e59b63f257b9579fcf3e101d2a6202b68b'], + [10_000, '0709ebe2ba2564eb32f453122c4ce111d574a20f25665a6257831e29191f0701'], + [20_000, '016ad6058b2347dcc3f00257d9e00709d60cab9250aef690b4e36a94e5a55cbb'], +]); + +test('ATS scoring scales through the supported resume size', { timeout: 30_000 }, () => { + const metrics: string[] = []; + + for (const size of SIZES) { + const resume = buildText(size, 2); + const jobDescription = buildText(size, 1); + const expected = JSON.stringify(calculateATSScore(resume, jobDescription)); + const expectedHash = createHash('sha256').update(expected).digest('hex'); + expect(expectedHash).toBe(EXPECTED_HASHES.get(size)); + let durationMs = 0; + + for (let iteration = 0; iteration < ITERATIONS; iteration += 1) { + const startedAt = performance.now(); + const result = calculateATSScore(resume, jobDescription); + durationMs += performance.now() - startedAt; + const serialized = JSON.stringify(result); + expect(serialized).toBe(expected); + expect(createHash('sha256').update(serialized).digest('hex')).toBe(expectedHash); + } + + metrics.push(`size${size}=${(durationMs / ITERATIONS).toFixed(3)}ms/op`); + } + + console.log(`[benchmark] ${metrics.join(' ')} (${ITERATIONS} iterations)`); + console.log(`[resource] maximum_supported_resume_chars=${SIZES.at(-1)}`); +}); + +function buildText(targetCharacters: number, stride: number): string { + const words: string[] = []; + let length = 0; + let index = 0; + while (length < targetCharacters) { + const word = VOCABULARY[(index * stride) % VOCABULARY.length]; + words.push(word); + length += word.length + 1; + index += 1; + } + return words.join(' ').slice(0, targetCharacters); +} diff --git a/src/lib/ats-score.ts b/src/lib/ats-score.ts index 50de8e4..af480d5 100644 --- a/src/lib/ats-score.ts +++ b/src/lib/ats-score.ts @@ -224,19 +224,24 @@ export function calculateATSScore(resumeText: string, jdText: string): ATSResult regular.clear(); } - const allKeywords = new Set([...important, ...regular]); - if (allKeywords.size === 0) { + const totalKeywords = important.size + regular.size; + if (totalKeywords === 0) { return { score: 0, matchedKeywords: [], missingKeywords: [], totalKeywords: 0 }; } - function matches(keyword: string): boolean { - return resumeLower.includes(keyword); + const matchedImportant: string[] = []; + const matchedRegular: string[] = []; + const missingKeywords: string[] = []; + for (const keyword of important) { + if (resumeLower.includes(keyword)) matchedImportant.push(keyword); + else missingKeywords.push(keyword); + } + for (const keyword of regular) { + if (resumeLower.includes(keyword)) matchedRegular.push(keyword); + else missingKeywords.push(keyword); } - const matchedImportant = [...important].filter(matches); - const matchedRegular = [...regular].filter(matches); const matchedKeywords = [...matchedImportant, ...matchedRegular]; - const missingKeywords = [...allKeywords].filter((k) => !matches(k)); // Score: important keywords worth 70%, regular worth 30% let score: number; @@ -253,6 +258,6 @@ export function calculateATSScore(resumeText: string, jdText: string): ATSResult score: Math.round(score), matchedKeywords, missingKeywords, - totalKeywords: allKeywords.size, + totalKeywords, }; }