The exact TypeScript that powers SearchSteward's free, no-signup tools:
- Résumé ↔ Job-Description Matcher —
scoreMatch() - Ghost Job Checker —
checkGhostJob()
Zero dependencies, fully deterministic, runs entirely in the browser — your résumé and the job posting never leave the page. This repo is the same code, published so you can verify that claim (and reuse it).
Keyword-coverage scoring, the way ATS filters and skimming recruiters actually read a résumé:
- Drop the employer's self-description. A posting opens by describing the company ("Acme provides settlement rails to marketplaces") and closes by describing the job; the opening is unmatchable by construction, and scoring it spends the term budget on terms no candidate can earn.
- Tokenize both texts; drop stopwords, generic JD filler ("experience", "strong", "responsibilities"…), posting scaffolding ("seeking", "candidate", "comfortable"), and bare numbers.
- Aggressively stem so meaning-preserving variants unify
(
managed≈management≈manager,led≈lead,drove≈drive). - Preserve acronyms (SQL, ETL, CPA) and punctuation-bearing tech names
(
C++,C#,.NET,Node.js,CI/CD) as first-class tokens — stripping the punctuation would makeC++andC#the same word. - Take the top 40 JD terms by frequency — bigram phrases get a 1.2× boost — and compute weighted coverage against the résumé. A phrase counts only where the résumé actually contains it: both halves adjacent, in one clause.
import { scoreMatch } from "./src/resumeMatch";
const r = scoreMatch(resumeText, jdText);
// { score: 82, band: "strong", matched: [...], missing: [...], ok: true, thinJd: false }Bands: strong ≥ 75, partial ≥ 45, else weak. The calibration suite in
tests/ pins real-world fixtures so the score can't silently inflate.
matched and missing are weight-ordered and capped at 12 — the missing list
is advice, and advice past a dozen items is a wall rather than a list.
thinJd: true means the job description carried fewer than 35 meaningful
tokens — a pasted teaser, not a full posting. The score is still computed,
but treat it as low-confidence and ask for the full text. (Coverage of a
stub reads as false confidence; flagging it is the honest move.)
A weighted checklist of documented scam and ghost-job signals: upfront-payment asks, requests for sensitive personal/financial info, chat-app interviews, personal-email contacts, MLM language, missing compensation, vague stubs. Every flag returned carries a human-readable explanation of why it matters.
import { checkGhostJob } from "./src/ghostJob";
const r = checkGhostJob(postingText);
// { score: 40, band: "caution", flags: [{ label, detail, weight }...], positives: [...], ok: true }It's a heuristic starting point, not a verdict — a careful scam can avoid these patterns and a real posting can trip a flag or two.
npm install
npm testMIT.