DEV Community

Alex
Alex

Posted on

I built a deterministic, quote-verified ATS resume scanner & job aggregator so AI stops hallucinating fake skills

Most "AI Resume Scanners" on the market have two fatal flaws:

  1. Random Score Generation: Because LLMs are probabilistic (temperature > 0), submitting the exact same resume twice often yields 62% first and 84% a minute later.
  2. Skill Hallucinations: When asked to tailor a resume or cover letter, standard LLM wrappers cheerfully invent experience with technologies the candidate has never touched—leading to instant rejection during the first 5 minutes of a technical screen.

While building LaunchPad AI PRO (a job aggregator powered by the Adzuna API with a built-in ATS scanner and cover letter generator), I replaced "AI score guessing" with a deterministic verification pipeline.

The Architecture: Quote-Verified Scoring

Instead of asking an LLM to guess a number from 0 to 100, our engine calculates the score across 4 weighted vectors:

  • 55% — Quote-Verified Hard Skills (resumeQuoteEvidence): A skill from the job description is only counted as matched if the parser extracts the exact sentence from the resume as proof.
  • 15% — Measurable Impact: Regex & NLP detection of quantifiable metrics (%, $, x growth, latency ms).
  • 15% — Soft Skills & Role Markers.
  • 15% — Structural Completeness: Contact info, GitHub/LinkedIn links, standard section headers, and optimal word count.

Core Implementation (TypeScript)

Here is the core deterministic scoring and quote-extraction logic from our engine:

export interface SkillMatchEvidence {
  skill: string;
  foundInResume: boolean;
  resumeQuoteEvidence: string | null;
  category: 'hard' | 'soft';
}

function escapeRegExp(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// 1. Verify Hard Skills only if an exact sentence quote exists in the resume
export function verifyHardSkillsWithQuotes(
  requiredSkills: string[],
  resumeSentences: string[]
): { matched: SkillMatchEvidence[]; missing: string[] } {
  const matched: SkillMatchEvidence[] = [];
  const missing: string[] = [];

  for (const skill of requiredSkills) {
    const regex = new RegExp(
      `(^|[^a-zA-Z0-9])${escapeRegExp(skill.toLowerCase())}([^a-zA-Z0-9]|$)`,
      'i'
    );
    const evidenceSentence = resumeSentences.find((sentence) =>
      regex.test(sentence.toLowerCase())
    );

    if (evidenceSentence) {
      matched.push({
        skill,
        foundInResume: true,
        resumeQuoteEvidence: evidenceSentence.slice(0, 180),
        category: 'hard',
      });
    } else {
      missing.push(skill);
    }
  }

  return { matched, missing };
}

// 2. Extract quantifiable engineering/business impact (%, $, x, ms, users)
export function extractMeasurableMetrics(resumeText: string): string[] {
  const metricRegex =
    /\b(\d+(?:[.,]\d+)?\s*(?:%|x|k|m|\$|€|users|clients|ms|sec))\b/gi;
  return Array.from(
    new Set((resumeText.match(metricRegex) || []).map((m) => m.trim()))
  ).slice(0, 10);
}

// 3. Deterministic weighted ATS score formula
export function calculateATSScore(params: {
  matchedHardCount: number;
  totalHardCount: number;
  matchedSoftCount: number;
  totalSoftCount: number;
  metricsCount: number;
  structureChecksPassed: number;
  totalStructureChecks: number;
}) {
  const hardSkillsScore =
    params.totalHardCount > 0
      ? Math.round((params.matchedHardCount / params.totalHardCount) * 100)
      : 75;

  const softSkillsScore =
    params.totalSoftCount > 0
      ? Math.round((params.matchedSoftCount / params.totalSoftCount) * 100)
      : 80;

  const impactScore = Math.min(100, Math.round((params.metricsCount / 4) * 100));
  const structureScore = Math.round(
    (params.structureChecksPassed / params.totalStructureChecks) * 100
  );

  return Math.round(
    hardSkillsScore * 0.55 +
    softSkillsScore * 0.15 +
    impactScore * 0.15 +
    structureScore * 0.15
  );
}
Enter fullscreen mode Exit fullscreen mode

Zero-Hallucination Cover Letters & XYZ Rewrites

Once the deterministic pass finishes, we have two verified arrays: matchedHardSkills and missingHardSkills.
Only then do we invoke the LLM (temperature: 0.15, strict JSON schema) with an explicit constraint: never claim experience with any skill in missingHardSkills.

  1. Actionable XYZ Rewrites: It flags weak sentences without metrics and suggests concrete improvements.
  2. One-Click Workflow: You can search live jobs (via Adzuna), click "⚡ ATS Scan & Cover Letter" on any job card, inspect your quote-verified score, and save the application to a built-in Kanban board (Saved -> Applied -> Interview -> Offer).

You can test it live with your own resume here (no signup wall for core scanning):
👉 LaunchPad AI PRO

I'd love feedback from fellow devs: what other deterministic heuristics would you add to a resume parser?



Top comments (0)