pulse-cv / server /analysis /engine.ts
qyam23's picture
Deploy PulseCV Docker Space
95abc1b verified
Raw
History Blame Contribute Delete
46.4 kB
import crypto from "crypto";
import { manufacturingPack } from "../domain/manufacturingPack";
import {
includesAlias,
lineSpans,
normalizeText,
overlapRatio,
sentenceSpans,
slugify,
tokenize,
unique,
} from "./normalization";
import { lintJobDescription } from "./jdLinter";
import { detectTextLanguage, type TextLanguage } from "./language";
import type {
AnalysisMeta,
CandidateRecommendations,
DomainDetection,
Evidence,
EvidenceMapRow,
FitAnalysis,
JDQualityWarning,
MatchState,
RecruiterRecommendations,
Requirement,
RequirementMatch,
RequirementType,
ScoringBreakdown,
SupportLevel,
} from "./types";
const GENERIC_TOKENS = new Set([
"required", "minimum", "least", "looking", "seeking", "company", "position", "must", "plus", "high", "strong", "individual_contributor", "role_scope",
"לפחות", "נדרשת", "מחפשת", "מובילה", "יצרנית", "מרכזיים", "נרחב", "חובה", "ברמה", "כמהנדס", "הבנה"
]);
const MUST_HAVE_PATTERNS = [
/\bmust\b/i,
/\brequired\b/i,
/\bmandatory\b/i,
/\bminimum\b/i,
/חובה/u,
/נדרש/u,
/נדרשת/u,
/ניסיון של/u
];
const NICE_TO_HAVE_PATTERNS = [/\bpreferred\b/i, /\badvantage\b/i, /\bnice to have\b/i, /יתרון/u];
const LEADERSHIP_SCOPE_PATTERNS = /lead|head|manager|director|management|מנהל|הובלה|סמנכ/iu;
const YEARS_PATTERNS = /\d+\s*(years|year|שנים|שנה)/i;
const REQUIRED_CONTEXT_PATTERNS = /must|required|mandatory|minimum|experience with|knowledge of|hands[- ]on|חובה|נדרש|נדרשת|ידע ב|ידע וניסיון|ניסיון ב|ניסיון עם/u;
const ROLE_HIRING_MARKERS = /looking|seeking|hiring|required|role|position|job|דרוש|דרושה|דרוש\.ה|מחפש|מחפשת|משרה|תפקיד/iu;
const ROLE_LEADERSHIP_MARKERS = /lead|head|manager|director|owner|מנהל|מנהלת|מוביל|מובילה|הובלת|ראש|אחריות/iu;
const ROLE_TECHNICAL_MARKERS = /technical|engineering|engineer|system|project|program|manufacturing|process|mechanical|electrical|software|hardware|טכני|הנדסי|מהנדס|מערכת|פרויקט|תהליך|ייצור|מכאנ|אלקטרו|תוכנה|חומרה/iu;
function stableHash(...parts: string[]): string {
return crypto.createHash("sha256").update(parts.join("|")).digest("hex");
}
function requirementTypeLabel(type: RequirementType): string {
switch (type) {
case "role_title":
return "Role";
case "seniority":
return "Seniority";
case "education_background":
case "degree":
return "Education";
case "hard_skill":
case "methodology":
return "Hard Skills";
case "tool_system":
return "Tools / Systems";
case "leadership_responsibility":
return "Leadership";
case "manufacturing_domain":
case "industry_context":
return "Manufacturing / Domain";
case "must_have_requirement":
return "Must-Haves";
case "language_requirement":
return "Language";
case "years_experience":
return "Experience";
default:
return "Other";
}
}
function localizedRequirementLabel(label: string, language: TextLanguage): string {
if (language !== "he" && language !== "mixed") return label;
const normalized = normalizeText(label);
const dictionary: Record<string, string> = {
"technical manager": "מנהל טכני",
"factory engineering lead": "מוביל הנדסת מפעל",
"leadership": "הובלה ניהולית",
"manufacturing": "ייצור תעשייתי",
"english": "אנגלית",
"electronics engineering": "הנדסת אלקטרוניקה",
"aeronautical engineering": "הנדסת אווירונאוטיקה",
"software engineering": "הנדסת תוכנה",
"systems architecture": "ארכיטקטורת מערכות",
"requirements documentation": "כתיבת מסמכי דרישות"
};
return dictionary[normalized] || label;
}
function localizedDomainLabel(value: string, language: TextLanguage): string {
if (language !== "he" && language !== "mixed") return value.replace(/_/g, " ");
const normalized = normalizeText(value);
const dictionary: Record<string, string> = {
manufacturing: "ייצור תעשייתי",
industrial: "תעשייה",
engineering_leadership: "הובלה הנדסית",
manufacturing_engineering: "הנדסת ייצור",
operations: "תפעול"
};
return dictionary[normalized] || value.replace(/_/g, " ");
}
function he(value: string): string {
return value.replace(/\\u([0-9a-fA-F]{4})/g, (_, code) => String.fromCharCode(parseInt(code, 16)));
}
export function detectDomain(jobDescription: string): DomainDetection {
const text = normalizeText(jobDescription);
const titleHits = manufacturingPack.titlesCatalog.filter((entry) => entry.aliases.some((alias) => includesAlias(text, alias)));
const toolHits = manufacturingPack.toolsCatalog.filter((entry) => entry.aliases.some((alias) => includesAlias(text, alias)));
const domainHits = manufacturingPack.domainConcepts.filter((entry) => entry.aliases.some((alias) => includesAlias(text, alias)));
const leadershipHits = manufacturingPack.leadershipSignals.filter((entry) => entry.aliases.some((alias) => includesAlias(text, alias)));
const primaryDomain = domainHits.length ? "manufacturing" : "industrial";
const secondaryDomain = leadershipHits.length ? "operations" : domainHits.find((entry) => entry.canonical !== "Manufacturing")?.canonical || null;
const roleFamily = titleHits[0]?.roleFamily || (leadershipHits.length ? "engineering_leadership" : "manufacturing_engineering");
const seniority =
titleHits.find((entry) => entry.seniorityHint)?.seniorityHint ||
(titleHits.some((entry) => /manager|head|director|leadership/i.test(entry.roleFamily || "")) ? "leadership" : "individual_contributor");
const signalCount = titleHits.length + toolHits.length + domainHits.length + leadershipHits.length;
return {
primaryDomain,
secondaryDomain,
roleFamily,
seniority,
confidence: Math.max(0.58, Math.min(0.96, 0.58 + signalCount * 0.05)),
signals: unique([
...titleHits.map((entry) => entry.canonical),
...toolHits.map((entry) => entry.canonical),
...domainHits.map((entry) => entry.canonical),
...leadershipHits.map((entry) => entry.canonical)
]).slice(0, 12)
};
}
function lineImportance(line: string, mustHave: boolean): number {
const base = mustHave ? 0.88 : 0.62;
if (/lead|head|manager|director|מנהל|הובלה|מחלקת/u.test(line)) return Math.min(1, base + 0.08);
if (/safety|quality|בטיחות|איכות/i.test(line)) return Math.min(1, base + 0.05);
return base;
}
function strengthForLine(line: string, mustHave: boolean): number {
let strength = mustHave ? 0.86 : 0.58;
if (/must|required|mandatory|minimum|חובה|נדרש|נדרשת/u.test(line)) strength += 0.08;
if (YEARS_PATTERNS.test(line)) strength += 0.05;
return Math.min(1, strength);
}
function shouldEmitSeniorityRequirement(domain: DomainDetection, titleText: string, jobDescription: string): boolean {
if (domain.seniority === "leadership") return true;
return /senior|principal|staff|lead engineer|בכיר|בכירה/u.test(`${titleText} ${jobDescription}`);
}
function resolveRequirementMustHave(
type: RequirementType,
line: string,
mustHaveHint: boolean,
niceToHaveHint: boolean,
domain: DomainDetection
): boolean {
if (type === "role_title" || type === "years_experience") return true;
if (niceToHaveHint && !mustHaveHint) return false;
const leadershipScope = LEADERSHIP_SCOPE_PATTERNS.test(line);
const hasRequiredContext = REQUIRED_CONTEXT_PATTERNS.test(line);
switch (type) {
case "seniority":
return domain.seniority === "leadership" && leadershipScope;
case "education_background":
case "degree":
case "tool_system":
case "language_requirement":
return mustHaveHint || hasRequiredContext;
case "leadership_responsibility":
return (mustHaveHint || hasRequiredContext) && leadershipScope;
case "manufacturing_domain":
case "methodology":
case "hard_skill":
case "industry_context":
return mustHaveHint && hasRequiredContext;
default:
return mustHaveHint;
}
}
function resolveRequirementConfidence(line: string, mustHave: boolean, niceToHave: boolean): number {
if (mustHave) return 0.9;
if (niceToHave) return 0.66;
if (YEARS_PATTERNS.test(line)) return 0.84;
if (REQUIRED_CONTEXT_PATTERNS.test(line)) return 0.8;
return 0.74;
}
function addRequirement(
requirements: Requirement[],
{
label,
normalizedValue,
type,
subtype,
sourceText,
sourceSpanStart,
sourceSpanEnd,
mustHave,
confidence,
senioritySignal,
domain
}: Omit<Requirement, "id" | "importance" | "requirementStrength">
) {
const key = `${type}:${normalizeText(normalizedValue)}`;
if (requirements.some((item) => `${item.type}:${normalizeText(item.normalizedValue)}` === key)) {
return;
}
requirements.push({
id: slugify(`${type}-${normalizedValue}-${sourceSpanStart}`),
label,
normalizedValue,
type,
subtype,
sourceText,
sourceSpanStart,
sourceSpanEnd,
domain,
importance: lineImportance(sourceText, mustHave),
requirementStrength: strengthForLine(sourceText, mustHave),
senioritySignal,
mustHave,
confidence
});
}
function pickBestAlias(text: string, aliases: string[]): string | null {
const matches = aliases.filter((alias) => includesAlias(text, alias));
if (!matches.length) return null;
return matches.sort((left, right) => right.length - left.length)[0];
}
function scoreRoleTitleCandidate(text: string, alias: string | null): number {
const tokenCount = tokenize(text).length;
const aliasTokenCount = alias ? tokenize(alias).length : 0;
let score = alias ? aliasTokenCount * 6 : 0;
if (ROLE_HIRING_MARKERS.test(text)) score += 3;
if (ROLE_LEADERSHIP_MARKERS.test(text)) score += 2;
if (ROLE_TECHNICAL_MARKERS.test(text)) score += 2;
if (tokenCount <= 10) score += 4;
else if (tokenCount <= 18) score += 1;
else score -= 4;
if (text.length <= 96) score += 2;
if (/[,:;]\s/.test(text) && tokenCount > 12) score -= 2;
return score;
}
function extractRoleTitleRequirement(jobDescription: string, domain: DomainDetection) {
const rawCandidates = [
...lineSpans(jobDescription).slice(0, 10),
...sentenceSpans(jobDescription).slice(0, 6),
];
const candidates = rawCandidates.filter(
(span, index, array) => array.findIndex((item) => item.start === span.start && item.end === span.end) === index,
);
let best:
| {
span: { text: string; start: number; end: number };
entry: (typeof manufacturingPack.titlesCatalog)[number];
alias: string;
score: number;
}
| null = null;
for (const span of candidates) {
for (const entry of manufacturingPack.titlesCatalog) {
const alias = pickBestAlias(span.text, entry.aliases);
if (!alias) continue;
const score = scoreRoleTitleCandidate(span.text, alias);
if (!best || score > best.score) {
best = { span, entry, alias, score };
}
}
}
if (best) {
return {
label: best.entry.canonical,
normalizedValue: best.entry.canonical,
type: "role_title" as const,
subtype: best.entry.roleFamily || domain.roleFamily,
sourceText: best.alias,
sourceSpanStart: best.span.start,
sourceSpanEnd: best.span.end,
mustHave: true,
confidence: best.score >= 10 ? 0.93 : 0.82,
senioritySignal: best.entry.seniorityHint || domain.seniority,
domain: domain.primaryDomain,
};
}
const fallback = lineSpans(jobDescription)[0];
if (!fallback) return null;
return {
label: domain.roleFamily.replace(/_/g, " "),
normalizedValue: domain.roleFamily.replace(/_/g, " "),
type: "role_title" as const,
subtype: domain.roleFamily,
sourceText: fallback.text.slice(0, 96),
sourceSpanStart: fallback.start,
sourceSpanEnd: fallback.end,
mustHave: true,
confidence: 0.62,
senioritySignal: domain.seniority,
domain: domain.primaryDomain,
};
}
export function extractRequirements(jobDescription: string, domain: DomainDetection): Requirement[] {
const requirements: Requirement[] = [];
const spans = lineSpans(jobDescription);
const titleRequirement = extractRoleTitleRequirement(jobDescription, domain);
const titleLine = titleRequirement
? { text: titleRequirement.sourceText, start: titleRequirement.sourceSpanStart, end: titleRequirement.sourceSpanEnd }
: spans[0];
if (titleRequirement) {
addRequirement(requirements, titleRequirement);
}
if (shouldEmitSeniorityRequirement(domain, titleLine?.text || "", jobDescription)) {
addRequirement(requirements, {
label: domain.seniority,
normalizedValue: domain.seniority,
type: "seniority",
subtype: "role_scope",
sourceText: titleLine?.text || jobDescription.slice(0, 80),
sourceSpanStart: titleLine?.start || 0,
sourceSpanEnd: titleLine?.end || Math.min(80, jobDescription.length),
mustHave: true,
confidence: domain.confidence,
senioritySignal: domain.seniority,
domain: domain.primaryDomain
});
}
for (const span of spans) {
const mustHaveHint = MUST_HAVE_PATTERNS.some((pattern) => pattern.test(span.text));
const niceToHaveHint = NICE_TO_HAVE_PATTERNS.some((pattern) => pattern.test(span.text));
for (const entry of manufacturingPack.educationCatalog) {
if (entry.aliases.some((alias) => includesAlias(span.text, alias))) {
const mustHave = resolveRequirementMustHave("education_background", span.text, mustHaveHint, niceToHaveHint, domain);
addRequirement(requirements, {
label: entry.canonical,
normalizedValue: entry.canonical,
type: "education_background",
subtype: entry.subtype || "degree",
sourceText: span.text,
sourceSpanStart: span.start,
sourceSpanEnd: span.end,
mustHave,
confidence: resolveRequirementConfidence(span.text, mustHave, niceToHaveHint),
senioritySignal: null,
domain: domain.primaryDomain
});
}
}
for (const entry of manufacturingPack.toolsCatalog) {
if (entry.aliases.some((alias) => includesAlias(span.text, alias))) {
const mustHave = resolveRequirementMustHave("tool_system", span.text, mustHaveHint, niceToHaveHint, domain);
addRequirement(requirements, {
label: entry.canonical,
normalizedValue: entry.canonical,
type: "tool_system",
subtype: entry.subtype || "tool",
sourceText: span.text,
sourceSpanStart: span.start,
sourceSpanEnd: span.end,
mustHave,
confidence: resolveRequirementConfidence(span.text, mustHave, niceToHaveHint),
senioritySignal: null,
domain: domain.primaryDomain
});
}
}
for (const entry of manufacturingPack.methodsCatalog) {
if (entry.aliases.some((alias) => includesAlias(span.text, alias))) {
const mustHave = resolveRequirementMustHave("methodology", span.text, mustHaveHint, niceToHaveHint, domain);
addRequirement(requirements, {
label: entry.canonical,
normalizedValue: entry.canonical,
type: "methodology",
subtype: entry.subtype || "method",
sourceText: span.text,
sourceSpanStart: span.start,
sourceSpanEnd: span.end,
mustHave,
confidence: resolveRequirementConfidence(span.text, mustHave, niceToHaveHint),
senioritySignal: null,
domain: domain.primaryDomain
});
}
}
for (const entry of manufacturingPack.domainConcepts) {
if (entry.aliases.some((alias) => includesAlias(span.text, alias))) {
const mustHave = resolveRequirementMustHave("manufacturing_domain", span.text, mustHaveHint, niceToHaveHint, domain);
addRequirement(requirements, {
label: entry.canonical,
normalizedValue: entry.canonical,
type: "manufacturing_domain",
subtype: entry.subtype || "domain",
sourceText: span.text,
sourceSpanStart: span.start,
sourceSpanEnd: span.end,
mustHave,
confidence: resolveRequirementConfidence(span.text, mustHave, niceToHaveHint),
senioritySignal: null,
domain: domain.primaryDomain
});
}
}
for (const entry of manufacturingPack.leadershipSignals) {
if (entry.aliases.some((alias) => includesAlias(span.text, alias))) {
const mustHave = resolveRequirementMustHave("leadership_responsibility", span.text, mustHaveHint, niceToHaveHint, domain);
addRequirement(requirements, {
label: entry.canonical,
normalizedValue: entry.canonical,
type: "leadership_responsibility",
subtype: entry.subtype || "leadership",
sourceText: span.text,
sourceSpanStart: span.start,
sourceSpanEnd: span.end,
mustHave,
confidence: resolveRequirementConfidence(span.text, mustHave, niceToHaveHint),
senioritySignal: domain.seniority,
domain: domain.primaryDomain
});
}
}
for (const entry of manufacturingPack.languageCatalog) {
if (entry.aliases.some((alias) => includesAlias(span.text, alias))) {
const mustHave = resolveRequirementMustHave("language_requirement", span.text, mustHaveHint, niceToHaveHint, domain);
addRequirement(requirements, {
label: entry.canonical,
normalizedValue: entry.canonical,
type: "language_requirement",
subtype: "language",
sourceText: span.text,
sourceSpanStart: span.start,
sourceSpanEnd: span.end,
mustHave,
confidence: resolveRequirementConfidence(span.text, mustHave, niceToHaveHint),
senioritySignal: null,
domain: domain.primaryDomain
});
}
}
const yearsMatch = span.text.match(/(\d+)\s*(years|year|שנים|שנה)/i);
if (yearsMatch) {
addRequirement(requirements, {
label: `${yearsMatch[1]} years experience`,
normalizedValue: yearsMatch[1],
type: "years_experience",
subtype: "experience",
sourceText: span.text,
sourceSpanStart: span.start,
sourceSpanEnd: span.end,
mustHave: true,
confidence: 0.94,
senioritySignal: null,
domain: domain.primaryDomain
});
}
}
return requirements;
}
function aliasUniverse(requirement: Requirement): string[] {
const collections = [
...manufacturingPack.titlesCatalog,
...manufacturingPack.toolsCatalog,
...manufacturingPack.methodsCatalog,
...manufacturingPack.domainConcepts,
...manufacturingPack.leadershipSignals,
...manufacturingPack.educationCatalog,
...manufacturingPack.languageCatalog
];
const matched = collections.find((entry) => entry.canonical === requirement.normalizedValue || entry.canonical === requirement.label);
return unique([requirement.label, requirement.normalizedValue, ...(matched?.aliases || [])]).filter(Boolean);
}
function scoreSupportLevel(level: SupportLevel): number {
switch (level) {
case "explicit":
return 1;
case "strong_partial":
return 0.75;
case "weak_partial":
return 0.45;
case "implied":
return 0.3;
case "unclear":
return 0.18;
default:
return 0;
}
}
function findEvidenceForRequirement(resumeText: string, requirement: Requirement): Evidence[] {
const spans = sentenceSpans(resumeText);
const aliases = aliasUniverse(requirement);
const evidences: Evidence[] = [];
for (const span of spans) {
const normalizedSpan = normalizeText(span.text);
const exactAlias = aliases.find((alias) => includesAlias(normalizedSpan, alias));
const overlap = Math.max(...aliases.map((alias) => overlapRatio(span.text, alias)), 0);
const quantifiedImpactPresent = /\b\d+[%x]?\b/.test(span.text) || /(reduced|improved|increase|improvement|הפחתה|שיפור|חסכון|ייעול)/i.test(span.text);
const yearsMatch = span.text.match(/(\d+)\s*(years|year|שנים|שנה)/i);
let supportLevel: SupportLevel = "missing";
let evidenceType: Evidence["evidenceType"] = "semantic_equivalent";
let rationale = "";
if (exactAlias) {
supportLevel = "explicit";
evidenceType =
requirement.type === "tool_system"
? "tool_usage"
: requirement.type === "education_background"
? "education_signal"
: requirement.type === "role_title"
? "title_signal"
: "exact_term";
rationale = `The CV explicitly mentions ${exactAlias}.`;
} else if (overlap >= 0.7) {
supportLevel = "strong_partial";
evidenceType = requirement.type === "leadership_responsibility" ? "responsibility_evidence" : "normalized_alias";
rationale = "The CV uses closely related wording that strongly supports this requirement.";
} else if (overlap >= 0.45) {
supportLevel = "weak_partial";
evidenceType = requirement.type === "manufacturing_domain" ? "domain_context" : "semantic_equivalent";
rationale = "The CV suggests adjacent evidence, but not a clean direct proof.";
} else if (
requirement.type === "leadership_responsibility" &&
/(ניהול|lead|manager|supervis|operators|technicians|engineers)/i.test(span.text)
) {
supportLevel = "implied";
evidenceType = "responsibility_evidence";
rationale = "The CV implies leadership scope, but without a precise manufacturing leadership proof.";
} else if (
requirement.type === "manufacturing_domain" &&
/(manufacturing|production|factory|plant|ייצור|מפעל|תהליכי ייצור|תעשייתי)/i.test(span.text)
) {
supportLevel = "implied";
evidenceType = "domain_context";
rationale = "The CV shows relevant manufacturing context, but not the exact JD wording.";
} else if (
supportLevel === "missing" &&
requirement.type === "seniority" &&
ROLE_LEADERSHIP_MARKERS.test(span.text)
) {
supportLevel = ROLE_TECHNICAL_MARKERS.test(span.text) ? "strong_partial" : "implied";
evidenceType = "title_signal";
rationale = "The CV shows leadership scope in a prominent title or responsibility line.";
}
if (
supportLevel === "missing" &&
requirement.type === "role_title" &&
ROLE_LEADERSHIP_MARKERS.test(span.text) &&
ROLE_TECHNICAL_MARKERS.test(span.text)
) {
supportLevel = requirement.senioritySignal === "leadership" ? "strong_partial" : "weak_partial";
evidenceType = "title_signal";
rationale = "The CV shows leadership plus technical or engineering scope that is adjacent to the requested title.";
}
if (supportLevel !== "missing") {
evidences.push({
requirementId: requirement.id,
supportLevel,
confidence: Math.min(0.98, 0.44 + scoreSupportLevel(supportLevel) * 0.5 + (quantifiedImpactPresent ? 0.06 : 0)),
cvSourceText: span.text,
cvSpanStart: span.start,
cvSpanEnd: span.end,
evidenceType,
rationale,
matchedAlias: exactAlias || null,
recencySignal: span.start < resumeText.length * 0.45 ? "recent_or_prominent" : null,
strengthSignal: quantifiedImpactPresent ? "quantified_impact" : null,
yearsInferred: yearsMatch ? Number(yearsMatch[1]) : null,
quantifiedImpactPresent,
ambiguityFlag: supportLevel === "weak_partial" || supportLevel === "implied"
});
}
}
if (!evidences.length) {
return [{
requirementId: requirement.id,
supportLevel: "missing",
confidence: 0.82,
cvSourceText: "",
cvSpanStart: -1,
cvSpanEnd: -1,
evidenceType: "semantic_equivalent",
rationale: "No direct CV evidence was found for this requirement.",
matchedAlias: null,
recencySignal: null,
strengthSignal: null,
yearsInferred: null,
quantifiedImpactPresent: false,
ambiguityFlag: false
}];
}
return evidences.sort((left, right) => scoreSupportLevel(right.supportLevel) - scoreSupportLevel(left.supportLevel) || right.confidence - left.confidence);
}
function stateFromEvidence(requirement: Requirement, evidence: Evidence[]): MatchState {
const top = evidence[0];
if (!top || top.supportLevel === "missing") return "missing";
if (top.supportLevel === "explicit") return "matched";
if (top.supportLevel === "strong_partial") return "partially_matched";
if (top.supportLevel === "weak_partial") return "weakly_supported";
if (requirement.mustHave) return "uncertain";
return "weakly_supported";
}
function rationaleForMatch(match: RequirementMatch): string {
if (!match.topEvidence || match.topEvidence.supportLevel === "missing") {
return `No CV evidence was found for ${match.requirement.label}.`;
}
return match.topEvidence.rationale;
}
export function buildMatches(resumeText: string, requirements: Requirement[]): RequirementMatch[] {
return requirements.map((requirement) => {
const evidence = findEvidenceForRequirement(resumeText, requirement);
const topEvidence = evidence[0] || null;
const state = stateFromEvidence(requirement, evidence);
return {
requirement,
evidence,
state,
topEvidence,
confidence: topEvidence?.confidence || 0.25,
rationale: rationaleForMatch({
requirement,
evidence,
topEvidence,
state,
confidence: topEvidence?.confidence || 0.25,
rationale: ""
})
};
});
}
function categoryScore(matches: RequirementMatch[], types: RequirementType[]): number {
const filtered = matches.filter((match) => types.includes(match.requirement.type));
if (!filtered.length) return 70;
const totalWeight = filtered.reduce((sum, match) => sum + match.requirement.importance, 0);
const earned = filtered.reduce((sum, match) => sum + match.requirement.importance * scoreSupportLevel(match.topEvidence?.supportLevel || "missing"), 0);
return Math.round((earned / Math.max(totalWeight, 0.001)) * 100);
}
function computeScoring(matches: RequirementMatch[]): ScoringBreakdown {
const roleFitScore = categoryScore(matches, ["role_title", "seniority"]);
const domainFitScore = categoryScore(matches, ["manufacturing_domain", "industry_context"]);
const hardSkillsFitScore = categoryScore(matches, ["hard_skill", "methodology", "education_background", "degree"]);
const toolsFitScore = categoryScore(matches, ["tool_system"]);
const leadershipFitScore = categoryScore(matches, ["leadership_responsibility"]);
const mustHave = matches.filter((match) => match.requirement.mustHave);
const mustHaveCoverage = mustHave.length
? Math.round(
mustHave.reduce((sum, match) => sum + scoreSupportLevel(match.topEvidence?.supportLevel || "missing"), 0) /
mustHave.length *
100
)
: 100;
const evidenceStrengthScore = matches.length
? Math.round(matches.reduce((sum, match) => sum + (match.topEvidence?.quantifiedImpactPresent ? 1 : 0), 0) / matches.length * 100)
: 50;
const confidenceScore = matches.length
? Math.round(matches.reduce((sum, match) => sum + match.confidence, 0) / matches.length * 100)
: 50;
const uncertaintyPenalty = Math.round(
matches.filter((match) => match.state === "uncertain" || match.state === "weakly_supported").length / Math.max(matches.length, 1) * 28
);
const weights = manufacturingPack.requirementsWeighting;
const finalScore = Math.round(
roleFitScore * weights.role +
domainFitScore * weights.domain +
hardSkillsFitScore * weights.hardSkills +
toolsFitScore * weights.tools +
leadershipFitScore * weights.leadership +
mustHaveCoverage * weights.mustHave +
evidenceStrengthScore * weights.evidenceStrength -
uncertaintyPenalty * weights.uncertaintyPenalty
);
return {
roleFitScore,
domainFitScore,
hardSkillsFitScore,
toolsFitScore,
leadershipFitScore,
mustHaveCoverage,
evidenceStrengthScore,
confidenceScore,
uncertaintyPenalty,
finalScore: Math.max(18, Math.min(96, finalScore))
};
}
function groupRequirementsByType(requirements: Requirement[]): Record<string, Requirement[]> {
return requirements.reduce<Record<string, Requirement[]>>((groups, requirement) => {
const key = requirementTypeLabel(requirement.type);
groups[key] = groups[key] || [];
groups[key].push(requirement);
return groups;
}, {});
}
function groupMatches(matches: RequirementMatch[], filter: (match: RequirementMatch) => boolean): Record<string, RequirementMatch[]> {
return matches.filter(filter).reduce<Record<string, RequirementMatch[]>>((groups, match) => {
const key = requirementTypeLabel(match.requirement.type);
groups[key] = groups[key] || [];
groups[key].push(match);
return groups;
}, {});
}
function buildEvidenceMap(matches: RequirementMatch[]): EvidenceMapRow[] {
return matches.map((match) => ({
requirementId: match.requirement.id,
requirement: match.requirement.label,
category: requirementTypeLabel(match.requirement.type),
importance: match.requirement.importance,
supportLevel: match.topEvidence?.supportLevel || "missing",
matchState: match.state,
confidence: match.confidence,
jdEvidence: match.requirement.sourceText,
cvEvidence: match.topEvidence?.cvSourceText || "No direct CV evidence found.",
rationale: match.rationale,
mustHave: match.requirement.mustHave
}));
}
function uncertaintyFlags(matches: RequirementMatch[], warnings: JDQualityWarning[]): string[] {
const flags: string[] = [];
if (matches.some((match) => match.state === "uncertain")) {
flags.push("Some must-have requirements remain uncertain and need manual review.");
}
if (matches.some((match) => match.state === "weakly_supported")) {
flags.push("Several requirements have only weak contextual support rather than explicit proof.");
}
if (warnings.some((warning) => warning.severity === "high_risk")) {
flags.push("The JD itself appears broad or over-constrained, which may inflate mismatch risk.");
}
return flags;
}
function buildCandidateRecommendations(matches: RequirementMatch[], domain: DomainDetection, language: TextLanguage): CandidateRecommendations {
const missing = matches.filter((match) => match.state === "missing").slice(0, 4);
const weak = matches.filter((match) => match.state === "weakly_supported" || match.state === "uncertain").slice(0, 3);
if (language === "he" || language === "mixed") {
return {
wordingFixes: weak.map((match) => `${he("\u05d7\u05d6\u05e7 \u05d0\u05ea \u05d4\u05d4\u05d5\u05db\u05d7\u05d4 \u05e1\u05d1\u05d9\u05d1")} ${localizedRequirementLabel(match.requirement.label, language)} ${he("\u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05d9\u05e7\u05e3 \u05d0\u05d7\u05e8\u05d9\u05d5\u05ea, \u05db\u05dc\u05d9 \u05e2\u05d1\u05d5\u05d3\u05d4 \u05d0\u05d5 \u05ea\u05d5\u05e6\u05d0\u05d4 \u05de\u05d3\u05d9\u05d3\u05d4 \u05e9\u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05de\u05d9\u05dd \u05d1\u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05e9\u05dc\u05da.")}`),
proofGaps: missing.map((match) => `${he("\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d4 \u05e8\u05d0\u05d9\u05d4 \u05d1\u05e8\u05d5\u05e8\u05d4 \u05dc-")}${localizedRequirementLabel(match.requirement.label, language)}. ${he("\u05dc\u05dc\u05d0 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05de\u05d5\u05db\u05d7, \u05d6\u05d4 \u05e0\u05e9\u05d0\u05e8 \u05e4\u05e2\u05e8 \u05d0\u05de\u05d9\u05ea\u05d9 \u05d5\u05dc\u05d0 \u05e8\u05e7 \u05e4\u05e2\u05e8 \u05e0\u05d9\u05e1\u05d5\u05d7.")}`),
likelyInterviewQuestions: manufacturingPack.interviewQuestionTemplates
.slice(0, 2)
.concat(weak.map((match) => `${he("\u05d4\u05db\u05df \u05d3\u05d5\u05d2\u05de\u05d4 \u05d1\u05e8\u05d5\u05e8\u05d4 \u05e9\u05de\u05e8\u05d0\u05d4 \u05d0\u05d9\u05e4\u05d4 \u05e7\u05d5\u05e8\u05d5\u05ea \u05d4\u05d7\u05d9\u05d9\u05dd \u05e9\u05dc\u05da \u05de\u05d5\u05db\u05d9\u05d7\u05d9\u05dd \u05d0\u05ea")} ${localizedRequirementLabel(match.requirement.label, language)}.`))
.slice(0, 4),
titleAlignmentSuggestions: [
`${he("\u05db\u05d5\u05d5\u05df \u05d0\u05ea \u05d4\u05db\u05d5\u05ea\u05e8\u05ea \u05d4\u05de\u05e7\u05e6\u05d5\u05e2\u05d9\u05ea \u05e9\u05dc\u05da \u05dc\u05de\u05e9\u05e4\u05d7\u05ea \u05d4\u05ea\u05e4\u05e7\u05d9\u05d3:")} ${localizedDomainLabel(domain.roleFamily, language)}.`,
he("\u05d0\u05dd \u05d4\u05ea\u05e4\u05e7\u05d9\u05d3 \u05d3\u05d5\u05e8\u05e9 \u05d4\u05d5\u05d1\u05dc\u05d4 \u05d4\u05e0\u05d3\u05e1\u05d9\u05ea \u05d0\u05d5 \u05d9\u05d9\u05e6\u05d5\u05e8\u05d9\u05ea, \u05d5\u05d3\u05d0 \u05e9\u05d4\u05d9\u05e7\u05e3 \u05d4\u05d4\u05d5\u05d1\u05dc\u05d4 \u05d4\u05d0\u05d7\u05e8\u05d5\u05df \u05e9\u05dc\u05da \u05de\u05d5\u05e4\u05d9\u05e2 \u05d1\u05d7\u05dc\u05e7 \u05d4\u05e2\u05dc\u05d9\u05d5\u05df \u05e9\u05dc \u05e7\u05d5\u05e8\u05d5\u05ea \u05d4\u05d7\u05d9\u05d9\u05dd.")
]
};
}
return {
wordingFixes: weak.map((match) => `Strengthen the proof around ${match.requirement.label} with concrete scope, methods, or measurable outcomes already backed by your experience.`),
proofGaps: missing.map((match) => `There is no clear evidence for ${match.requirement.label}. This is not fixable by wording unless you can cite real experience.`),
likelyInterviewQuestions: manufacturingPack.interviewQuestionTemplates
.slice(0, 2)
.concat(weak.map((match) => `Be ready to explain where your CV proves ${match.requirement.label}.`))
.slice(0, 4),
titleAlignmentSuggestions: [
`Align your headline with the JD's role family: ${domain.roleFamily.replace(/_/g, " ")}.`,
"If the role expects plant or manufacturing leadership, make recent plant scope visible near the top of the CV.",
]
};
}
function buildRecruiterRecommendations(matches: RequirementMatch[]): RecruiterRecommendations {
const weak = matches.filter((match) => match.state === "weakly_supported" || match.state === "uncertain").slice(0, 4);
const missingMustHaves = matches.filter((match) => match.requirement.mustHave && match.state === "missing").slice(0, 4);
return {
verifyManually: missingMustHaves.map((match) => `Verify whether the candidate truly has ${match.requirement.label}; the CV does not currently prove it.`),
weakEvidenceZones: weak.map((match) => `${match.requirement.label}: ${match.rationale}`),
interviewProbes: weak.map((match) => `Ask for a concrete example that proves ${match.requirement.label}.`),
possibleFalseNegatives: matches
.filter((match) => match.state === "missing" && /(leadership|management|engineering)/i.test(match.requirement.label))
.slice(0, 2)
.map((match) => `This may be a wording-driven miss if the candidate has adjacent experience for ${match.requirement.label}.`)
};
}
function buildSummary(matches: RequirementMatch[], scoring: ScoringBreakdown, domain: DomainDetection, language: TextLanguage): { profileSummary: string; tailoredBio: string; strengths: string[]; weaknesses: string[]; recommendations: string[] } {
const strengths = matches
.filter((match) => match.state === "matched" || match.state === "partially_matched")
.sort((left, right) => right.requirement.importance - left.requirement.importance)
.slice(0, 5)
.map((match) => localizedRequirementLabel(match.requirement.label, language));
const weaknesses = matches
.filter((match) => match.state === "missing" || match.state === "uncertain")
.sort((left, right) => Number(right.requirement.mustHave) - Number(left.requirement.mustHave) || right.requirement.importance - left.requirement.importance)
.slice(0, 5)
.map((match) => localizedRequirementLabel(match.requirement.label, language));
const recommendations = language === "he" || language === "mixed"
? weaknesses.map((item) => `${he("\u05d4\u05d1\u05d4\u05e8 \u05d0\u05d5 \u05d4\u05d5\u05db\u05d7 \u05d0\u05ea")} ${item} ${he("\u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e8\u05d0\u05d9\u05d4 \u05d9\u05e9\u05d9\u05e8\u05d4, \u05e2\u05d3\u05db\u05e0\u05d9\u05ea \u05d5\u05de\u05d2\u05d5\u05d1\u05d4.")}`)
: weaknesses.map((item) => `Clarify or prove ${item} with direct, recent, evidence-backed content.`);
const profileSummary = language === "he" || language === "mixed"
? `${he("\u05d4\u05ea\u05e4\u05e7\u05d9\u05d3 \u05d9\u05d5\u05e9\u05d1 \u05d1\u05d3\u05d5\u05de\u05d9\u05d9\u05df")} ${localizedDomainLabel(domain.primaryDomain, language)} / ${localizedDomainLabel(domain.roleFamily, language)}. ${he("\u05e0\u05de\u05e6\u05d0\u05d5")} ${strengths.length} ${he("\u05d4\u05ea\u05d0\u05de\u05d5\u05ea \u05d7\u05d6\u05e7\u05d5\u05ea \u05de\u05d2\u05d5\u05d1\u05d5\u05ea \u05e8\u05d0\u05d9\u05d4 \u05d5-")} ${weaknesses.length} ${he("\u05e4\u05e2\u05e8\u05d9\u05dd \u05d0\u05d5 \u05d0\u05d6\u05d5\u05e8\u05d9 \u05d0\u05d9-\u05d5\u05d3\u05d0\u05d5\u05ea. \u05d7\u05e9\u05d5\u05d1 \u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea \u05d4\u05d4\u05ea\u05d0\u05de\u05d4 \u05d9\u05d7\u05d3 \u05e2\u05dd \u05d7\u05d5\u05d6\u05e7 \u05d4\u05e8\u05d0\u05d9\u05d5\u05ea \u05d5\u05db\u05d9\u05e1\u05d5\u05d9 \u05d3\u05e8\u05d9\u05e9\u05d5\u05ea \u05d4\u05d7\u05d5\u05d1\u05d4, \u05d5\u05dc\u05d0 \u05dc\u05e4\u05d9 \u05d4\u05e6\u05d9\u05d5\u05df \u05d1\u05dc\u05d1\u05d3.")}`
: `The role sits in ${localizedDomainLabel(domain.primaryDomain, language)} / ${localizedDomainLabel(domain.roleFamily, language)}. The analysis found ${strengths.length} strong evidence-backed matches and ${weaknesses.length} real gaps or uncertain zones. Final fit should be read together with evidence strength and must-have coverage, not score alone.`;
const tailoredBio = language === "he" || language === "mixed"
? he("\u05de\u05e0\u05d4\u05dc \u05d4\u05e0\u05d3\u05e1\u05d9 \u05d1\u05db\u05d9\u05e8 \u05e2\u05dd \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05de\u05d5\u05db\u05d7 \u05d1\u05e1\u05d1\u05d9\u05d1\u05d5\u05ea \u05d9\u05d9\u05e6\u05d5\u05e8 \u05d5\u05ea\u05e2\u05e9\u05d9\u05d9\u05d4, \u05d4\u05de\u05e9\u05dc\u05d1 \u05d4\u05d5\u05d1\u05dc\u05ea \u05e4\u05e8\u05d5\u05d9\u05e7\u05d8\u05d9\u05dd, \u05d0\u05d7\u05e8\u05d9\u05d5\u05ea \u05ea\u05d4\u05dc\u05d9\u05db\u05d9\u05ea, \u05db\u05dc\u05d9\u05dd \u05d4\u05e0\u05d3\u05e1\u05d9\u05d9\u05dd, \u05e9\u05d9\u05e4\u05d5\u05e8 \u05d1\u05d9\u05e6\u05d5\u05e2\u05d9\u05dd \u05d5\u05d4\u05d5\u05d1\u05dc\u05ea \u05de\u05de\u05e9\u05e7\u05d9\u05dd \u05db\u05e4\u05d9 \u05e9\u05e2\u05d5\u05dc\u05d4 \u05de\u05e7\u05d5\u05e8\u05d5\u05ea \u05d4\u05d7\u05d9\u05d9\u05dd.")
: `Manufacturing and industrial engineering professional with proven scope in ${localizedDomainLabel(domain.roleFamily, language)} environments, combining process ownership, engineering tools, operational improvement, and leadership signals that are supported by the CV.`;
return { profileSummary, tailoredBio, strengths, weaknesses, recommendations };
}
function jobFitDecision(finalScore: number): "High" | "Medium" | "Low" {
if (finalScore >= 78) return "High";
if (finalScore >= 55) return "Medium";
return "Low";
}
function buildBulletOptimization(matches: RequirementMatch[], language: TextLanguage): FitAnalysis["bulletPointOptimization"] {
return matches
.filter((match) => match.state === "weakly_supported" || match.state === "uncertain")
.slice(0, 3)
.map((match) => ({
original: match.topEvidence?.cvSourceText || (language === "he" || language === "mixed" ? `${he("\u05d0\u05d9\u05df \u05e8\u05d0\u05d9\u05d4 \u05d9\u05e9\u05d9\u05e8\u05d4 \u05dc-")}${localizedRequirementLabel(match.requirement.label, language)}.` : `No direct proof for ${match.requirement.label}.`),
optimized: language === "he" || language === "mixed"
? `${localizedRequirementLabel(match.requirement.label, language)} ${he("\u05d1\u05d0\u05d9\u05dd \u05dc\u05d9\u05d3\u05d9 \u05d1\u05d9\u05d8\u05d5\u05d9 \u05d3\u05e8\u05da \u05d4\u05d9\u05e7\u05e3 \u05d0\u05d7\u05e8\u05d9\u05d5\u05ea \u05d1\u05e8\u05d5\u05e8, \u05db\u05dc\u05d9 \u05e2\u05d1\u05d5\u05d3\u05d4 \u05e8\u05dc\u05d5\u05d5\u05e0\u05d8\u05d9\u05d9\u05dd \u05d5\u05ea\u05d5\u05e6\u05d0\u05d4 \u05de\u05d3\u05d9\u05d3\u05d4.")}`
: `${match.requirement.label} is demonstrated through clear ownership scope, relevant tools or methods, and measurable outcomes.`,
rationale: language === "he" || language === "mixed"
? he("\u05d3\u05d5\u05d2\u05de\u05ea \u05e0\u05d9\u05e1\u05d5\u05d7 \u05de\u05d2\u05d5\u05d9\u05e1\u05ea \u05d4\u05de\u05d1\u05d5\u05e1\u05e1\u05ea \u05e2\u05dc \u05d4\u05e8\u05d0\u05d9\u05d5\u05ea \u05e9\u05db\u05d1\u05e8 \u05d6\u05d5\u05d4\u05d5, \u05dc\u05dc\u05d0 \u05d4\u05d5\u05e1\u05e4\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05d7\u05d3\u05e9.")
: match.rationale
}));
}
export function buildEvidenceBasedAnalysis(resumeText: string, jobDescription: string): FitAnalysis {
const resumeLanguage = detectTextLanguage(resumeText);
const domain = detectDomain(jobDescription);
const requirements = extractRequirements(jobDescription, domain);
const matches = buildMatches(resumeText, requirements);
const scoring = computeScoring(matches);
const jdWarnings = lintJobDescription(jobDescription, requirements);
const uncertainty = uncertaintyFlags(matches, jdWarnings);
const summary = buildSummary(matches, scoring, domain, resumeLanguage.language);
const evidenceMap = buildEvidenceMap(matches);
const inputHash = stableHash(resumeText, jobDescription);
const matchedKeywords = matches
.filter((match) => match.state === "matched" || match.state === "partially_matched")
.map((match) => localizedRequirementLabel(match.requirement.label, resumeLanguage.language))
.filter((value) => !GENERIC_TOKENS.has(normalizeText(value)))
.slice(0, 10);
const missingKeywords = matches
.filter((match) => match.state === "missing" || match.state === "uncertain")
.map((match) => localizedRequirementLabel(match.requirement.label, resumeLanguage.language))
.filter((value) => !GENERIC_TOKENS.has(normalizeText(value)))
.slice(0, 10);
return {
domainDetection: domain,
jdRequirementsByType: groupRequirementsByType(requirements),
matchedEvidenceByType: groupMatches(matches, (match) => match.state === "matched" || match.state === "partially_matched"),
missingRequirementsByType: groupMatches(matches, (match) => match.state === "missing" || match.state === "uncertain" || match.state === "weakly_supported"),
uncertaintyFlags: uncertainty,
scoringBreakdown: scoring,
finalScore: scoring.finalScore,
confidenceScore: scoring.confidenceScore,
jdQualityWarnings: jdWarnings,
candidateRecommendations: buildCandidateRecommendations(matches, domain, resumeLanguage.language),
recruiterRecommendations: buildRecruiterRecommendations(matches),
evidenceMap,
analysisMeta: {
version: "evidence-beta-v1",
analysisMode: "evidence_based_hiring_intelligence",
vertical: "manufacturing",
generatedAt: new Date().toISOString(),
inputHash,
resumeLanguage: resumeLanguage.language,
resumeLanguageConfidence: resumeLanguage.confidence
} satisfies AnalysisMeta,
profileSummary: summary.profileSummary,
tailoredBio: summary.tailoredBio,
bulletPointOptimization: buildBulletOptimization(matches, resumeLanguage.language),
matchedKeywords,
missingKeywords,
matchScore: scoring.finalScore,
atsVisibilityScore: Math.round((scoring.roleFitScore + scoring.domainFitScore + scoring.mustHaveCoverage) / 3),
jobFitDecision: jobFitDecision(scoring.finalScore),
strengths: summary.strengths,
weaknesses: summary.weaknesses,
recommendations: summary.recommendations,
scoringMode: "evidence-based-deterministic",
scoreLocked: true,
candidateViewMode: "free"
};
}
export function previewJobDescription(jobDescription: string) {
const domainDetection = detectDomain(jobDescription);
const requirements = extractRequirements(jobDescription, domainDetection);
const jdQualityWarnings = lintJobDescription(jobDescription, requirements);
return {
domainDetection,
jdRequirementsByType: groupRequirementsByType(requirements),
jdQualityWarnings,
analysisMeta: {
version: "evidence-beta-v1",
analysisMode: "jd_parse_preview",
vertical: "manufacturing",
generatedAt: new Date().toISOString(),
inputHash: stableHash(jobDescription)
} satisfies AnalysisMeta
};
}
export function previewResumeText(resumeText: string) {
const spans = lineSpans(resumeText);
const signals = unique(
[
...manufacturingPack.toolsCatalog.filter((entry) => entry.aliases.some((alias) => includesAlias(resumeText, alias))).map((entry) => entry.canonical),
...manufacturingPack.methodsCatalog.filter((entry) => entry.aliases.some((alias) => includesAlias(resumeText, alias))).map((entry) => entry.canonical),
...manufacturingPack.domainConcepts.filter((entry) => entry.aliases.some((alias) => includesAlias(resumeText, alias))).map((entry) => entry.canonical),
...manufacturingPack.leadershipSignals.filter((entry) => entry.aliases.some((alias) => includesAlias(resumeText, alias))).map((entry) => entry.canonical),
...manufacturingPack.educationCatalog.filter((entry) => entry.aliases.some((alias) => includesAlias(resumeText, alias))).map((entry) => entry.canonical)
]
);
return {
parsedSections: {
topLines: spans.slice(0, 8).map((span) => span.text),
detectedSignals: signals
},
analysisMeta: {
version: "evidence-beta-v1",
analysisMode: "cv_parse_preview",
vertical: "manufacturing",
generatedAt: new Date().toISOString(),
inputHash: stableHash(resumeText)
} satisfies AnalysisMeta
};
}