# Procyon — self-audit before send. Every item must PASS before an application # is eligible for the user's approval/send gate. import re import db import tailor def run_audit(app_id, job, email_subject, email_body, tailored_resume): """Run the checklist, log results, return list of {item, result, detail}.""" checks = [] def add(item, result, detail): checks.append({'item': item, 'result': result, 'detail': detail}) db.add_audit(app_id, job['id'], item, result, detail) company = (job.get('company') or '').strip() title = (job.get('title') or '').strip() # 1. Employer name present in email (personalization signal, NOT a safety gate — the LLM # often abbreviates ("PSI" for "Physical Superintelligence") or says "your team", and the # subject line already carries the company, so a missing exact match is only a WARN). if company: if company.lower() in (email_body or '').lower(): add('Employer name in email', 'PASS', f'"{company}" appears in body') else: add('Employer name in email', 'WARN', f'"{company}" not verbatim in body (may be abbreviated)') else: add('Employer name in email', 'WARN', 'No company recorded — verify before send') # 2. No leftover placeholder / other-company names placeholders = ['[company]', '[name]', '{{', '}}', 'lorem', 'TODO', 'xxx'] found = [p for p in placeholders if p.lower() in (email_body or '').lower()] add('No placeholder tokens', 'FAIL' if found else 'PASS', f'placeholders: {found}' if found else 'clean') # 3. Contact info present phone = db.get_setting('resume_phone', '') email = db.get_setting('resume_email', '') missing = [] if phone and phone not in (email_body or ''): missing.append('phone') if email and email not in (email_body or ''): missing.append('email') add('Contact info in email', 'FAIL' if missing else 'PASS', f'missing: {missing}' if missing else 'phone + email present') # 4. Subject present and sane length if not email_subject: add('Email subject', 'FAIL', 'missing') elif len(email_subject) > 120: add('Email subject', 'FAIL', f'too long ({len(email_subject)} chars)') else: add('Email subject', 'PASS', email_subject) # 5. Tailored resume non-empty and no fabrication markers if not tailored_resume or len(tailored_resume) < 100: add('Tailored resume', 'FAIL', 'too short / empty') else: add('Tailored resume', 'PASS', f'{len(tailored_resume)} chars') # 6. Resume is a real deterministic resume (was "targeting header matches job" — removed: # the resume is now deterministic and no longer carries a job-name header, so that check # always false-warned). if tailored_resume and len(tailored_resume) > 100: add('Resume present', 'PASS', f'{len(tailored_resume)} chars, deterministic facts') else: add('Resume present', 'FAIL', 'resume missing') # 7. ATS keyword coverage — quality signal, never blocks sending (informational only). try: kws = tailor.extract_job_keywords(job.get('description') or '', top_n=12) cov, _kws, missing = tailor.ats_coverage(tailored_resume, kws) if cov >= 0.5: add('ATS keyword coverage', 'PASS', f'{int(cov*100)}% of top keywords present') else: add('ATS keyword coverage', 'WARN', f'{int(cov*100)}% — resume may not strongly echo this role ({", ".join(missing[:5])})') except Exception: add('ATS keyword coverage', 'WARN', 'could not compute') return checks def all_pass(checks): return all(c['result'] != 'FAIL' for c in checks)