Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,4 @@ cache.bin
# local agent logs
.devin/logs/
.agent-logs/
.codevetter/
55 changes: 55 additions & 0 deletions src/__tests__/ats-score-performance.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
21 changes: 13 additions & 8 deletions src/lib/ats-score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -253,6 +258,6 @@ export function calculateATSScore(resumeText: string, jdText: string): ATSResult
score: Math.round(score),
matchedKeywords,
missingKeywords,
totalKeywords: allKeywords.size,
totalKeywords,
};
}
Loading