From 7e1bce924e667c1a6136de5f7772da1cf190586d Mon Sep 17 00:00:00 2001 From: Matthew Jackson Date: Sat, 7 Mar 2026 09:26:47 -0800 Subject: [PATCH] =?UTF-8?q?Remove=20separate=20job=20profile=20files=20?= =?UTF-8?q?=E2=80=94=20filter=20uses=20search=20config=20+=20profile.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search config already defines what each track is looking for (keywords, exclude_keywords, salary_min, remote). Profile.json defines who the candidate is. No need for a third file duplicating both. Co-Authored-By: Claude Opus 4.6 --- job_filter.mjs | 27 ++++++++++++--------------- lib/filter.mjs | 38 +++++++++++++++++++------------------- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/job_filter.mjs b/job_filter.mjs index eca36c5..f608215 100644 --- a/job_filter.mjs +++ b/job_filter.mjs @@ -31,7 +31,7 @@ process.stdout.write = (chunk, ...args) => { logStream.write(chunk); return orig process.stderr.write = (chunk, ...args) => { logStream.write(chunk); return origStderrWrite(chunk, ...args); }; import { getJobsByStatus, updateJobStatus, loadConfig, loadQueue, saveQueue, dedupeAfterFilter, initQueue } from './lib/queue.mjs'; -import { loadProfile, submitBatches, checkBatch, downloadResults } from './lib/filter.mjs'; +import { submitBatches, checkBatch, downloadResults } from './lib/filter.mjs'; import { sendTelegram, formatFilterSummary } from './lib/notify.mjs'; import { DEFAULT_FILTER_MODEL, DEFAULT_FILTER_MIN_SCORE } from './lib/constants.mjs'; @@ -234,31 +234,28 @@ async function submit(settings, searchConfig, candidateProfile) { return; } - // Build job profiles map by track - const profilePaths = settings.filter?.job_profiles || {}; - const jobProfilesByTrack = {}; - for (const [track, path] of Object.entries(profilePaths)) { - const profile = await loadProfile(path); - if (profile) jobProfilesByTrack[track] = profile; - else console.warn(`⚠️ Could not load job profile for track "${track}" at ${path}`); + // Build search tracks map from search config + const searchesByTrack = {}; + for (const search of searchConfig.searches) { + searchesByTrack[search.track] = search; } - // Filter out jobs with no profile (will pass through unscored) - const filterable = jobs.filter(j => jobProfilesByTrack[j.track || 'ae']); - const noProfile = jobs.length - filterable.length; + // Filter out jobs with no matching search track + const filterable = jobs.filter(j => searchesByTrack[j.track || 'ae']); + const noTrack = jobs.length - filterable.length; - if (noProfile > 0) console.warn(`⚠️ ${noProfile} jobs skipped — no profile for their track`); + if (noTrack > 0) console.warn(`⚠️ ${noTrack} jobs skipped — no search track configured`); if (filterable.length === 0) { - console.log('Nothing filterable — no job profiles configured for any track.'); + console.log('Nothing filterable — no matching search tracks.'); return; } const model = settings.filter?.model || DEFAULT_FILTER_MODEL; const submittedAt = new Date().toISOString(); - console.log(`🚀 Submitting batches — ${filterable.length} jobs across ${Object.keys(jobProfilesByTrack).length} tracks, model: ${model}`); + console.log(`🚀 Submitting batches — ${filterable.length} jobs across ${Object.keys(searchesByTrack).length} tracks, model: ${model}`); - const submitted = await submitBatches(filterable, jobProfilesByTrack, candidateProfile, model, apiKey); + const submitted = await submitBatches(filterable, searchesByTrack, candidateProfile, model, apiKey); writeState({ batches: submitted, diff --git a/lib/filter.mjs b/lib/filter.mjs index 05fee9a..84578d5 100644 --- a/lib/filter.mjs +++ b/lib/filter.mjs @@ -7,34 +7,34 @@ import { ANTHROPIC_BATCH_API_URL, FILTER_DESC_MAX_CHARS, FILTER_BATCH_MAX_TOKENS } from './constants.mjs'; -import { loadJSON } from './storage.mjs'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -export async function loadProfile(profilePath) { - if (!profilePath) return null; - try { return await loadJSON(profilePath, null); } catch { return null; } -} +function buildSystemPrompt(searchTrack, candidateProfile) { + const trackInfo = { + keywords: searchTrack.keywords, + exclude_keywords: searchTrack.exclude_keywords, + salary_min: searchTrack.salary_min, + remote: searchTrack.filters?.remote, + }; -function buildSystemPrompt(jobProfile, candidateProfile) { - return `You are a job relevance scorer. Score each job listing 0-10 based on how well it matches the candidate profile below. + return `You are a job relevance scorer. Score each job listing 0-10 based on how well it matches the candidate and search criteria below. -## Candidate Profile +## Candidate ${JSON.stringify(candidateProfile, null, 2)} -## Target Job Profile -${JSON.stringify(jobProfile, null, 2)} +## Search Criteria (${searchTrack.name}) +${JSON.stringify(trackInfo)} ## Instructions -- Use the candidate profile and target job profile above as your only criteria -- Score based on title fit, industry fit, experience match, salary range, location/remote requirements, and any exclude_keywords +- Score based on title fit, industry fit, experience match, salary range, location/remote, and exclude_keywords - 10 = perfect match, 0 = completely irrelevant - If salary is unknown, do not penalize - If a posting is from a staffing agency but the role itself matches, score the role — not the agency -Return ONLY a JSON object: {"score": <0-10>, "reason": ""}`; +Return ONLY: {"score": <0-10>, "reason": ""}`; } function sanitize(str) { @@ -66,27 +66,27 @@ function apiHeaders(apiKey) { } /** - * Submit one batch per track (one per job profile/search description). + * Submit one batch per track. * Each batch uses the system prompt for that track only — maximizes prompt caching. * Returns array of { track, batchId, idMap, jobCount } */ -export async function submitBatches(jobs, jobProfilesByTrack, candidateProfile, model, apiKey) { +export async function submitBatches(jobs, searchesByTrack, candidateProfile, model, apiKey) { // Group jobs by track const byTrack = {}; for (const job of jobs) { const track = job.track || 'ae'; - if (!jobProfilesByTrack[track]) continue; // no profile → skip + if (!searchesByTrack[track]) continue; if (!byTrack[track]) byTrack[track] = []; byTrack[track].push(job); } - if (Object.keys(byTrack).length === 0) throw new Error('No jobs to submit — check job profiles are configured'); + if (Object.keys(byTrack).length === 0) throw new Error('No jobs to submit — check search config has tracks matching queued jobs'); const submitted = []; for (const [track, trackJobs] of Object.entries(byTrack)) { - const jobProfile = jobProfilesByTrack[track]; - const systemPrompt = buildSystemPrompt(jobProfile, candidateProfile); + const searchTrack = searchesByTrack[track]; + const systemPrompt = buildSystemPrompt(searchTrack, candidateProfile); const idMap = {}; const requests = [];