mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 08:36:25 +00:00
* feat(linkedin-search): add active status verification for job postings * fix(linkedin-search): scope closed-posting detection to the top card, pin with tests (#280) The first version matched five markers against the whole document, so recruiter boilerplate quoting 'no longer accepting applications' in a description flagged a live job CLOSED. Detection now stops where the description markup begins and matches only the two markers real closed pages carry (closed-job__flavor and the banner text, verified against live guest pages); the three speculative phrases are dropped. Four new fixture tests pin both directions plus the two description false-positive cases - the false-positive pair fails on the unscoped version. * feat(scrape): mark closed-at-source LinkedIn postings expired, never drop (#280) /scrape Step 2 now consumes linkedin-search detail's isActive: a job whose posting page renders the closed banner is written to seen_jobs.json with status expired rather than silently dropped, per the /rank marking pattern - the fix for the ghost-jobs class in #331. isActive: true is documented as absence of the banner, not proof the posting is open. --------- Co-authored-by: Navakanth Reddy Dumpa <navkanthr@gmail.com>
This commit is contained in:
co-authored by
Navakanth Reddy Dumpa
parent
730dcfb079
commit
3d296448bd
@@ -39,6 +39,7 @@ export async function runDetail(opts: DetailOpts): Promise<number> {
|
||||
job.employmentType ? `Employment: ${job.employmentType}` : "",
|
||||
job.jobFunction ? `Function: ${job.jobFunction}` : "",
|
||||
job.industries ? `Industries: ${job.industries}` : "",
|
||||
`Status: ${job.isActive ? "ACTIVE" : "CLOSED / EXPIRED"}`,
|
||||
"",
|
||||
job.description || "(no description)",
|
||||
"",
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface JobDetail extends JobCard {
|
||||
employmentType: string | null
|
||||
jobFunction: string | null
|
||||
industries: string | null
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,6 +228,21 @@ export function parseJobDetail(html: string, id: string): JobDetail {
|
||||
criteria[clean(cm[1]).toLowerCase()] = clean(cm[2])
|
||||
}
|
||||
|
||||
// Closed-state detection, scoped to the top card. A closed posting renders
|
||||
// <figure class="closed-job closed-job__flavor topcard__flavor-row">
|
||||
// <figcaption ...>No longer accepting applications</figcaption>
|
||||
// </figure>
|
||||
// there; that class and its visible text are the only markers real closed
|
||||
// pages carry (verified against live guest pages, 2026-08-09). The search
|
||||
// stops where the description markup begins: recruiter boilerplate quotes
|
||||
// these phrases, and a false CLOSED talks a user out of a live job.
|
||||
// Absence of the banner is absence of evidence, not proof the posting is
|
||||
// open - markup drift or a consent-walled response also renders no banner -
|
||||
// so isActive: true means only "no closed banner found".
|
||||
const descStart = html.search(/class="(?:show-more-less-html__markup|description__text)/i)
|
||||
const topcard = descStart === -1 ? html : html.slice(0, descStart)
|
||||
const isActive = !/closed-job__flavor|no longer accepting applications/i.test(topcard)
|
||||
|
||||
return {
|
||||
id,
|
||||
title: title ? clean(title) : "(untitled)",
|
||||
@@ -240,6 +256,7 @@ export function parseJobDetail(html: string, id: string): JobDetail {
|
||||
employmentType: criteria["employment type"] ?? null,
|
||||
jobFunction: criteria["job function"] ?? null,
|
||||
industries: criteria["industries"] ?? null,
|
||||
isActive,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user