/* CareerMatch AI Static: explainable browser-only lexical CV/job alignment. Original Gradio/Python project retained separately. No embeddings here. */ (function(root) { 'use strict'; const SKILLS = { 'Project Management':['project management','gestión de proyectos','gerenciamento de projetos'], 'Program Management':['program management','gestión de programas','gerenciamento de programas'], 'Operations':['operations','operaciones','operações','operacoes','operational management','gestión operativa'], 'Process Improvement':['process improvement','mejora de procesos','melhoria de processos','continuous improvement'], 'Stakeholder Management':['stakeholder management','gestión de stakeholders','gestão de stakeholders','stakeholders'], 'Change Management':['change management','gestión del cambio','gestão da mudança'], 'Strategic Planning':['strategic planning','planificación estratégica','planejamento estratégico'], 'Team Leadership':['team leadership','liderazgo de equipos','liderança de equipes','people management','gestión de equipos'], 'Customer Success':['customer success','éxito del cliente','sucesso do cliente'], 'Customer Service':['customer service','servicio al cliente','atención al cliente','atendimento ao cliente'], 'Logistics':['logistics','logística','logistica'], 'Supply Chain':['supply chain','cadena de suministro','cadeia de suprimentos'], 'Procurement':['procurement','compras','adquisiciones','suprimentos'], 'Business Analysis':['business analysis','análisis de negocio','análise de negócios'], 'Market Research':['market research','investigación de mercados','pesquisa de mercado'], 'Digital Marketing':['digital marketing','marketing digital'], 'SEO':['seo','search engine optimization','optimización para motores de búsqueda'], 'CRM':['crm','customer relationship management','gestión de relaciones con clientes'], 'Sales':['sales','ventas','vendas','business development','desarrollo de negocios'], 'Financial Analysis':['financial analysis','análisis financiero','análise financeira'], 'Budgeting':['budgeting','presupuesto','orçamento','orcamento'], 'Forecasting':['forecasting','pronóstico','proyecciones','previsão'], 'Risk Management':['risk management','gestión de riesgos','gestão de riscos'], 'Data Analysis':['data analysis','análisis de datos','análise de dados','analítica de datos'], 'Data Visualization':['data visualization','visualización de datos','visualização de dados'], 'Machine Learning':['machine learning','aprendizaje automático','aprendizado de máquina'], 'Generative AI':['generative ai','genai','ia generativa','inteligencia artificial generativa'], 'NLP':['natural language processing','nlp','procesamiento de lenguaje natural','processamento de linguagem natural'], 'RAG':['retrieval augmented generation','rag','generación aumentada por recuperación'], 'Prompt Engineering':['prompt engineering','ingeniería de prompts','engenharia de prompts'], 'Python':['python'], 'SQL':['sql','postgresql','mysql','sql server'], 'Excel':['excel','microsoft excel','spreadsheets','hojas de cálculo','planilhas'], 'Power BI':['power bi','powerbi'], 'Tableau':['tableau'], 'Pandas':['pandas'], 'scikit-learn':['scikit-learn','sklearn'], 'Transformers':['transformers','hugging face transformers'], 'Hugging Face':['hugging face','huggingface'], 'Git':['git','version control','control de versiones'], 'GitHub':['github'], 'GitLab':['gitlab'], 'Docker':['docker','containers','contenedores'], 'APIs':['api','apis','rest api','restful'], 'Cloud':['cloud','aws','azure','gcp','google cloud'], 'Research':['research','investigación','investigação','pesquisa acadêmica'], 'Academic Writing':['academic writing','escritura académica','redacción académica','escrita acadêmica'], 'Literature Review':['literature review','revisión de literatura','revisão de literatura','revisión bibliográfica'], 'Quantitative Research':['quantitative research','investigación cuantitativa','pesquisa quantitativa'], 'Qualitative Research':['qualitative research','investigación cualitativa','pesquisa qualitativa'], 'Teaching':['teaching','docencia','enseñanza','ensino','professor','lecturer'], 'Training':['training','capacitación','treinamento'], 'International Relations':['international relations','relaciones internacionales','relações internacionais'], 'International Trade':['international trade','comercio internacional','comércio internacional'], 'Public Policy':['public policy','políticas públicas','politicas publicas'], 'Economic Development':['economic development','desarrollo económico','desenvolvimento econômico'], 'Agile':['agile','scrum','kanban'], 'KPI Management':['kpi','kpis','key performance indicators','indicadores clave','indicadores de desempenho'], 'Process Documentation':['process documentation','documentación de procesos','documentação de processos','sop'], 'Automation':['automation','automatización','automação','workflow automation'] }; const LANGUAGES = { 'English':['english','inglés','ingles','inglês'], 'Spanish':['spanish','español','espanol','castellano'], 'Portuguese':['portuguese','portugués','portugues','português'], 'Italian':['italian','italiano','italiana'], 'French':['french','francés','frances','francês'], 'German':['german','alemán','aleman','alemão'] }; const STOPWORDS = new Set(('the and for with from that this your you our are will have has job role years year experience must be or of in as an to we a is on not by who can and plus strong ability required preferred position seeking skills skill excellent support demonstrated using and of ' + 'de la el los las y en con para por que se del un una su sus como al es lo las o este esta ser tener experiencia años año trabajo puesto buscamos deberá debe mínima mínimo ' + 'da do das dos e em com para por que um uma sua seu como os as na no profissional vaga').split(/\s+/)); const NECESSARY=['required','requirements','must','minimum','essential','mandatory','requerido','requisitos','debe','deberá','mínimo','obligatorio','necessário','obrigatório','precisa']; const PREFERRED=['preferred','nice to have','plus','desirable','advantage','deseable','preferido','valorable','desejável','preferencial','diferencial']; function norm(s) { return String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase().replace(/\s+/g,' ').trim(); } function terms(s) { return (norm(s).match(/[\p{L}\p{N}+#.-]{2,}/gu)||[]).map(t=>t.replace(/^[.-]+|[.-]+$/g,'')).filter(t=>t && !STOPWORDS.has(t) && !/^\d+$/.test(t)); } function hasAlias(text,alias) { const h=' '+norm(text).replace(/[^\p{L}\p{N}]+/gu,' ')+' '; const n=' '+norm(alias).replace(/[^\p{L}\p{N}]+/gu,' ').trim()+' '; return h.includes(n); } function extract(text,map){return Object.keys(map).filter(k=>map[k].some(a=>hasAlias(text,a)));} function splitSentences(text){ return String(text||'').replace(/\r/g,'\n').split(/(?:\n+|(?<=[.!?;])\s+|\s*[•▪◦]\s*)/u).map(s=>s.trim()).filter(s=>s.length>=15).slice(0,100); } function keywordStats(text){const c=new Map();for(const t of terms(text)){if(t.length<3)continue;c.set(t,(c.get(t)||0)+1);}return Array.from(c.entries()).sort((a,b)=>b[1]-a[1]);} function tfidfCosine(a,b) { const docs=[terms(a),terms(b)]; if (!docs[0].length||!docs[1].length) return 0; const maps=docs.map(arr=>{const features=arr.concat(arr.slice(1).map((x,i)=>arr[i]+' '+x));const count=new Map();for(const t of features)count.set(t,(count.get(t)||0)+1);return count;}); const features=new Set([...maps[0].keys(),...maps[1].keys()]);let dot=0,na=0,nb=0; for(const t of features){const df=Number(maps[0].has(t))+Number(maps[1].has(t));const idf=1+Math.log(3/(1+df));const aa=(maps[0].get(t)||0)*idf,bb=(maps[1].get(t)||0)*idf;dot+=aa*bb;na+=aa*aa;nb+=bb*bb;} return na&&nb?dot/Math.sqrt(na*nb):0; } const clamp=v=>Math.max(0,Math.min(1,v)); function requirements(job){ const ss=splitSentences(job); return ss.map(s=>{ const lower=norm(s);const required=NECESSARY.some(t=>hasAlias(lower,t));const preferred=PREFERRED.some(t=>hasAlias(lower,t)); return {text:s.slice(0,300),importance:required?'Required':preferred?'Preferred':'Context',weight:required?1.5:preferred?0.75:0.3}; }).sort((a,b)=>b.weight-a.weight).slice(0,12); } function compare(cv,job){ cv=String(cv||'').trim();job=String(job||'').trim(); if(cv.length<40||job.length<40)throw new Error('Enter at least 40 characters in BOTH the CV and job description.'); if(cv.length>60000||job.length>60000)throw new Error('Please limit each document to 60,000 characters.'); const cvSkills=extract(cv,SKILLS), jobSkills=extract(job,SKILLS),jobLang=extract(job,LANGUAGES),cvLang=extract(cv,LANGUAGES); const matchedSkills=jobSkills.filter(x=>cvSkills.includes(x)),missingSkills=jobSkills.filter(x=>!cvSkills.includes(x)); const matchedLang=jobLang.filter(x=>cvLang.includes(x)),missingLang=jobLang.filter(x=>!cvLang.includes(x)); const semantic=clamp(tfidfCosine(cv,job)); const skill=jobSkills.length?matchedSkills.length/jobSkills.length:0.5; const cvWords=new Set(terms(cv)); const jwords=[...new Set(keywordStats(job).slice(0,35).map(x=>x[0]))]; const kwMatched=jwords.filter(x=>cvWords.has(x)),kwMissing=jwords.filter(x=>!cvWords.has(x)); const keyword=jwords.length?kwMatched.length/jwords.length:0; const reqs=requirements(job).map(r=>{ const a=clamp(tfidfCosine(cv,r.text));const ts=terms(r.text);const m=ts.filter(w=>cvWords.has(w)); return {...r, evidence:Math.max(a,ts.length?m.length/ts.length:0),signals:m.slice(0,6)}; }); const weightSum=reqs.reduce((a,r)=>a+r.weight,0); const reqScore=weightSum?reqs.reduce((a,r)=>a+r.weight*r.evidence,0)/weightSum:0.5; const langRatio=jobLang.length?matchedLang.length/jobLang.length:1; const weighted=semantic*.35+skill*.30+reqScore*.20+keyword*.15; const score=Math.round(100*clamp(weighted*(jobLang.length?0.9+0.1*langRatio:1))); const recommendations=[]; if(missingSkills.length)recommendations.push('Check whether you have evidence for these unmentioned job skills: '+missingSkills.slice(0,7).join(', ')+'. Add only verifiable experience.'); if(missingLang.length)recommendations.push('The role mentions '+missingLang.join(', ')+'; verify your proficiency before adding language claims.'); if(reqs.filter(x=>x.importance==='Required'&&x.evidence<0.25).length)recommendations.push('Review the required statements with weak textual evidence. Consider adding a specific achievement or example if true.'); if(semantic<0.25)recommendations.push('Your CV and the job description use different vocabulary. Clarify relevant experience with truthful, role-specific terms.'); if(!recommendations.length) recommendations.push('Most detected textual signals are present. Review them manually and focus on clear, specific achievements.'); recommendations.push('This analysis does not assess actual competence, eligibility, years worked or hiring probability.'); return { score,band:score>=75?'Strong textual alignment':score>=50?'Partial textual alignment':'Limited textual alignment', components:[{name:'Lexical TF-IDF alignment',value:Math.round(semantic*100),weight:35},{name:'Skill taxonomy coverage',value:Math.round(skill*100),weight:30},{name:'Requirement evidence',value:Math.round(reqScore*100),weight:20},{name:'Keyword overlap',value:Math.round(keyword*100),weight:15}], matchedSkills,missingSkills,cvSkills,jobSkills,matchedLang,missingLang,jobLang, reqs,keywordsMatched:kwMatched.slice(0,15),keywordsMissing:kwMissing.slice(0,15),recommendations, method:'Browser-only TF-IDF (unigrams and bigrams), multilingual alias taxonomy, and heuristic requirement evidence. NO semantic embeddings or generative model.' }; } root.CareerMatchEngine=Object.freeze({compare,tfidfCosine,extractSkills:t=>extract(t,SKILLS),extractLanguages:t=>extract(t,LANGUAGES),requirements}); })(typeof window!=='undefined'?window:globalThis);