From 48895c2a861a26a7c0172936488c8d8180ae2b0b Mon Sep 17 00:00:00 2001 From: Rachel Petry <287457313+rachel-petry@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:02:42 -0600 Subject: [PATCH] feat(classifier): add recruiting ATS platforms and enrich career interview scheduling shield (#658) --- src/gmail-ai-classifier/code.gs | 17 ++++++-- src/gmail-ai-classifier/src/index.js | 17 ++++++-- src/gmail-ai-classifier/tests/code.test.js | 45 ++++++++++++++++++++++ 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/src/gmail-ai-classifier/code.gs b/src/gmail-ai-classifier/code.gs index d152da2e..2793029a 100644 --- a/src/gmail-ai-classifier/code.gs +++ b/src/gmail-ai-classifier/code.gs @@ -1293,10 +1293,10 @@ const MANUAL_PAYMENT_SNIPPET_TERMS_ = 'autopay is disabled|autopay is off|autopay is not enabled|auto-pay is off|not enrolled in autopay|please pay|pay by|pay now|payment is due|amount due|balance due|payment due|payment request|money request|sent you an invoice|sent you a money request|to avoid interruption|to avoid a late fee|to avoid late fees|manual payment' const CAREER_SUBJECT_TERMS_ = - 'employment agreement|employment contract|offer letter|job offer|employment offer|terms of employment|independent contractor agreement|restrictive covenant|confidentiality agreement|interview confirmation|interview invitation|invitation to interview|interview with |schedule an interview|recruiter screen|phone screen' + 'employment agreement|employment contract|offer letter|job offer|employment offer|terms of employment|independent contractor agreement|restrictive covenant|confidentiality agreement|interview confirmation|interview invitation|invitation to interview|interview with |schedule an interview|recruiter screen|phone screen|interview schedule|schedule your interview|interview availability|next round of interview|next round of interviews|round 2 interview|interview debrief|next steps in the interview|update on your candidacy|update on your application|your application for|application status:' const CAREER_SNIPPET_TERMS_ = - 'employment agreement|offer of employment|terms of employment|pleased to offer you the position|pleased to offer you|pleased to confirm our offer|congratulations on your offer|employment document|restrictive covenants agreement' + 'employment agreement|offer of employment|terms of employment|pleased to offer you the position|pleased to offer you|pleased to confirm our offer|congratulations on your offer|employment document|restrictive covenants agreement|interview schedule|schedule your interview|schedule an interview|interview availability|next round of interview|next round of interviews|next steps in the interview|interview with our team|speak with our team|discuss the role|candidacy for the position|your application for the position|invitation to interview' const DOCUMENT_SIGNATURE_COMPLETED_TERMS_ = 'completed: docusign|docusign: completed|has completed signing|all parties have signed|document is completed|signed copy' @@ -1724,10 +1724,19 @@ function deterministicRuleClassify_(sender, subject, snippet, thread, config) { } // 06_Work_Career: Employment Agreements, Offer Letters, Employment Contracts, Recruiter Screens & Interviews - if ( + const isAtsPlatformSender = matchesSenderDomain_( + sender, + 'greenhouse.io|lever.co|myworkday.com|workday.com|avature.net|icims.com|smartrecruiters.com|ashbyhq.com|taleo.net' + ) + const isCareerContent = containsAny_(subLower, CAREER_SUBJECT_TERMS_) || containsAny_(snipLower, CAREER_SNIPPET_TERMS_) - ) { + const isJobAlertSpam = containsAny_( + subLower, + 'job alert|job recommendations|recommended jobs|new jobs matching|jobs you might like' + ) + + if (isCareerContent || (isAtsPlatformSender && !isJobAlertSpam)) { return applyUserSentShield_( makeDeterministicResult_( '06_Work_Career', diff --git a/src/gmail-ai-classifier/src/index.js b/src/gmail-ai-classifier/src/index.js index 198ca2a8..7672eaa6 100644 --- a/src/gmail-ai-classifier/src/index.js +++ b/src/gmail-ai-classifier/src/index.js @@ -1288,10 +1288,10 @@ const MANUAL_PAYMENT_SNIPPET_TERMS_ = 'autopay is disabled|autopay is off|autopay is not enabled|auto-pay is off|not enrolled in autopay|please pay|pay by|pay now|payment is due|amount due|balance due|payment due|payment request|money request|sent you an invoice|sent you a money request|to avoid interruption|to avoid a late fee|to avoid late fees|manual payment' const CAREER_SUBJECT_TERMS_ = - 'employment agreement|employment contract|offer letter|job offer|employment offer|terms of employment|independent contractor agreement|restrictive covenant|confidentiality agreement|interview confirmation|interview invitation|invitation to interview|interview with |schedule an interview|recruiter screen|phone screen' + 'employment agreement|employment contract|offer letter|job offer|employment offer|terms of employment|independent contractor agreement|restrictive covenant|confidentiality agreement|interview confirmation|interview invitation|invitation to interview|interview with |schedule an interview|recruiter screen|phone screen|interview schedule|schedule your interview|interview availability|next round of interview|next round of interviews|round 2 interview|interview debrief|next steps in the interview|update on your candidacy|update on your application|your application for|application status:' const CAREER_SNIPPET_TERMS_ = - 'employment agreement|offer of employment|terms of employment|pleased to offer you the position|pleased to offer you|pleased to confirm our offer|congratulations on your offer|employment document|restrictive covenants agreement' + 'employment agreement|offer of employment|terms of employment|pleased to offer you the position|pleased to offer you|pleased to confirm our offer|congratulations on your offer|employment document|restrictive covenants agreement|interview schedule|schedule your interview|schedule an interview|interview availability|next round of interview|next round of interviews|next steps in the interview|interview with our team|speak with our team|discuss the role|candidacy for the position|your application for the position|invitation to interview' const DOCUMENT_SIGNATURE_COMPLETED_TERMS_ = 'completed: docusign|docusign: completed|has completed signing|all parties have signed|document is completed|signed copy' @@ -1719,10 +1719,19 @@ function deterministicRuleClassify_(sender, subject, snippet, thread, config) { } // 06_Work_Career: Employment Agreements, Offer Letters, Employment Contracts, Recruiter Screens & Interviews - if ( + const isAtsPlatformSender = matchesSenderDomain_( + sender, + 'greenhouse.io|lever.co|myworkday.com|workday.com|avature.net|icims.com|smartrecruiters.com|ashbyhq.com|taleo.net' + ) + const isCareerContent = containsAny_(subLower, CAREER_SUBJECT_TERMS_) || containsAny_(snipLower, CAREER_SNIPPET_TERMS_) - ) { + const isJobAlertSpam = containsAny_( + subLower, + 'job alert|job recommendations|recommended jobs|new jobs matching|jobs you might like' + ) + + if (isCareerContent || (isAtsPlatformSender && !isJobAlertSpam)) { return applyUserSentShield_( makeDeterministicResult_( '06_Work_Career', diff --git a/src/gmail-ai-classifier/tests/code.test.js b/src/gmail-ai-classifier/tests/code.test.js index 406d1615..901da956 100644 --- a/src/gmail-ai-classifier/tests/code.test.js +++ b/src/gmail-ai-classifier/tests/code.test.js @@ -4375,6 +4375,51 @@ describe('reclassifyThreadsByQuery Historical Realignment', () => { expect(res.canonicalDomain).toBe('06_Work_Career') expect(res.category).toBe('Primary') }) + + test('classifies interview schedule, availability, and next round requests into 06_Work_Career Primary keep', () => { + const schedRes = mod.deterministicRuleClassify_( + 'recruiting@enterprise.example', + 'Interview Schedule: Technical Leadership Role', + 'Please find the interview schedule with the engineering directors.' + ) + expect(schedRes).not.toBeNull() + expect(schedRes.canonicalDomain).toBe('06_Work_Career') + expect(schedRes.subLabel).toBe('Work/Career') + expect(schedRes.category).toBe('Primary') + expect(schedRes.action).toBe('keep') + + const nextRoundRes = mod.deterministicRuleClassify_( + 'talent@consulting.example', + 'Next Steps: Round 2 Interview', + 'We enjoyed speaking with you and would like to invite you to the next round of interviews.' + ) + expect(nextRoundRes).not.toBeNull() + expect(nextRoundRes.canonicalDomain).toBe('06_Work_Career') + expect(nextRoundRes.subLabel).toBe('Work/Career') + expect(nextRoundRes.category).toBe('Primary') + expect(nextRoundRes.action).toBe('keep') + }) + + test('classifies recruiting ATS platform messages (Avature, Greenhouse, Lever) into 06_Work_Career while excluding job alerts', () => { + const atsRes = mod.deterministicRuleClassify_( + 'notifications@avature.net', + 'Update regarding your application', + 'Thank you for your application. Our talent team is reviewing your materials.' + ) + expect(atsRes).not.toBeNull() + expect(atsRes.canonicalDomain).toBe('06_Work_Career') + expect(atsRes.subLabel).toBe('Work/Career') + expect(atsRes.category).toBe('Primary') + expect(atsRes.action).toBe('keep') + + // Job alert marketing from ATS platform must not match career interview rule + const alertRes = mod.deterministicRuleClassify_( + 'alerts@greenhouse.io', + 'New Job Alert: Recommended Jobs You Might Like', + 'Here are 5 new jobs matching your search preferences.' + ) + expect(alertRes).toBeNull() + }) }) }) })