openings 0.1.51 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/catalog.ts +17 -1
- package/src/crawler.ts +8 -2
- package/src/local-jobs.ts +2 -0
- package/src/runtime.ts +11 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/catalog.ts
CHANGED
|
@@ -35,6 +35,9 @@ interface LeverJob {
|
|
|
35
35
|
hostedUrl: string;
|
|
36
36
|
categories?: { location?: string };
|
|
37
37
|
descriptionPlain?: string;
|
|
38
|
+
/** The requirements and responsibilities, which Lever keeps apart from the opening paragraph. */
|
|
39
|
+
lists?: Array<{ text?: string; content?: string }>;
|
|
40
|
+
additionalPlain?: string;
|
|
38
41
|
workplaceType?: string;
|
|
39
42
|
createdAt?: number;
|
|
40
43
|
}
|
|
@@ -458,6 +461,19 @@ function normalizeAshby(company: Company, job: AshbyJob): Job {
|
|
|
458
461
|
});
|
|
459
462
|
}
|
|
460
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Lever splits an advert across three fields and a board may fill any of them. Reading only the opening
|
|
466
|
+
* paragraph left more than half of Lever's roles with no description at all: one aggregator alone publishes
|
|
467
|
+
* 4,076 postings whose `descriptionPlain` is empty and whose requirements sit entirely in `lists`. Those
|
|
468
|
+
* requirements are also where the skills a candidate searches for are written, so this is the part that counts.
|
|
469
|
+
*/
|
|
470
|
+
function leverDescription(job: LeverJob): string {
|
|
471
|
+
const sections = [job.descriptionPlain ?? ""];
|
|
472
|
+
for (const list of job.lists ?? []) sections.push([list.text ?? "", stripHtml(list.content ?? "")].filter(Boolean).join("\n"));
|
|
473
|
+
sections.push(job.additionalPlain ?? "");
|
|
474
|
+
return sections.map((section) => section.trim()).filter(Boolean).join("\n\n");
|
|
475
|
+
}
|
|
476
|
+
|
|
461
477
|
function normalizeLever(company: Company, job: LeverJob): Job {
|
|
462
478
|
const location = job.categories?.location ?? "Unspecified";
|
|
463
479
|
return classifyJob({
|
|
@@ -473,7 +489,7 @@ function normalizeLever(company: Company, job: LeverJob): Job {
|
|
|
473
489
|
eligibilityConfidence: "unknown",
|
|
474
490
|
url: job.hostedUrl,
|
|
475
491
|
updatedAt: job.createdAt ? new Date(job.createdAt).toISOString() : undefined,
|
|
476
|
-
description: job
|
|
492
|
+
description: leverDescription(job),
|
|
477
493
|
});
|
|
478
494
|
}
|
|
479
495
|
|
package/src/crawler.ts
CHANGED
|
@@ -22,6 +22,11 @@ interface CrawlerOptions {
|
|
|
22
22
|
describe?(source: Company, job: Job): Promise<string>;
|
|
23
23
|
/** Only roles open to these countries, posted in the last 30 days, get a description fetch. Empty: none do. */
|
|
24
24
|
describeCountries?: string[];
|
|
25
|
+
/** How many descriptions one source may be asked for in a run, and how long that may take. The nightly crawl
|
|
26
|
+
* keeps both small so no employer holds up the pass; the description catch-up raises them to reach an employer
|
|
27
|
+
* whose backlog is in the thousands, which 150 a night would never finish. */
|
|
28
|
+
describeLimit?: number;
|
|
29
|
+
describeBudgetMs?: number;
|
|
25
30
|
pacingNow?: () => number;
|
|
26
31
|
pacingSleep?: (delayMs: number) => Promise<void>;
|
|
27
32
|
now?: () => Date;
|
|
@@ -76,8 +81,8 @@ export function createCrawler(options: CrawlerOptions): Crawler {
|
|
|
76
81
|
// listed its jobs. Without this, an employer with 4 India roles among 11,581 American ones never reads the four.
|
|
77
82
|
// Ties keep the board's own order, which is newest first on every provider that dates its postings.
|
|
78
83
|
pending.sort((left, right) => describeRank(jobs[left]!) - describeRank(jobs[right]!) || left - right);
|
|
79
|
-
const deadline = Date.now() + DESCRIBE_BUDGET_MS;
|
|
80
|
-
let budget = DESCRIBE_LIMIT;
|
|
84
|
+
const deadline = Date.now() + (options.describeBudgetMs ?? DESCRIBE_BUDGET_MS);
|
|
85
|
+
let budget = options.describeLimit ?? DESCRIBE_LIMIT;
|
|
81
86
|
for (const index of pending) {
|
|
82
87
|
if (budget <= 0 || Date.now() >= deadline) break;
|
|
83
88
|
budget -= 1;
|
|
@@ -191,6 +196,7 @@ export function createCrawler(options: CrawlerOptions): Crawler {
|
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
// ponytail: per-source caps keep one crawl bounded; a big backlog (first run) drains over a few nights.
|
|
199
|
+
/** Defaults for the nightly crawl, where the point is to keep every source moving rather than finish any one. */
|
|
194
200
|
const DESCRIBE_LIMIT = 150;
|
|
195
201
|
const DESCRIBE_BUDGET_MS = 180_000;
|
|
196
202
|
|
package/src/local-jobs.ts
CHANGED
|
@@ -17,6 +17,8 @@ interface LocalJobsOptions {
|
|
|
17
17
|
workdayCountries?: string[];
|
|
18
18
|
describe?(source: Company, job: Job): Promise<string>;
|
|
19
19
|
describeCountries?: string[];
|
|
20
|
+
describeLimit?: number;
|
|
21
|
+
describeBudgetMs?: number;
|
|
20
22
|
now?: () => Date;
|
|
21
23
|
onCrawled?(source: Company, partition: JobPartition): void | Promise<void>;
|
|
22
24
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -37,6 +37,9 @@ export function createRuntime(options: { dataDir?: string; concurrency?: number;
|
|
|
37
37
|
// Server crawls set OPENINGS_DESCRIBE_COUNTRIES (IN) to read required experience from Workday detail pages; installs skip the extra requests.
|
|
38
38
|
describe: (source, job) => fetchJobDescription(source, job, globalThis.fetch),
|
|
39
39
|
describeCountries: (process.env.OPENINGS_DESCRIBE_COUNTRIES ?? "").split(",").map((code) => code.trim().toUpperCase()).filter((code) => /^[A-Z]{2}$/.test(code)),
|
|
40
|
+
// The description catch-up pass raises these; unset, the nightly crawl keeps the crawler's own small defaults.
|
|
41
|
+
describeLimit: positiveEnv("OPENINGS_DESCRIBE_LIMIT"),
|
|
42
|
+
describeBudgetMs: positiveEnv("OPENINGS_DESCRIBE_BUDGET_MS"),
|
|
40
43
|
onCrawled,
|
|
41
44
|
});
|
|
42
45
|
const recommender = createJobRecommender({ sources: companies, store, crawl: local.crawl });
|
|
@@ -96,3 +99,11 @@ export function createHostedRuntime(options: { sources: Company[]; store: Snapsh
|
|
|
96
99
|
prepareJobSearch: preparation.prepare, getJobCoverage: coverage.getCoverage, recommend: recommender.recommend, analyzeJobFit: analyzer.analyze, optimizeResume: optimizer.optimize,
|
|
97
100
|
};
|
|
98
101
|
}
|
|
102
|
+
|
|
103
|
+
/** A positive whole number from the environment, or undefined when unset or nonsense. */
|
|
104
|
+
function positiveEnv(name: string): number | undefined {
|
|
105
|
+
const raw = process.env[name];
|
|
106
|
+
if (raw === undefined) return undefined;
|
|
107
|
+
const value = Number(raw);
|
|
108
|
+
return Number.isInteger(value) && value > 0 ? value : undefined;
|
|
109
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.53";
|