openings 0.1.52 → 0.1.54
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 +11 -2
- package/src/local-jobs.ts +1 -0
- package/src/runtime.ts +9 -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
|
@@ -27,6 +27,10 @@ interface CrawlerOptions {
|
|
|
27
27
|
* whose backlog is in the thousands, which 150 a night would never finish. */
|
|
28
28
|
describeLimit?: number;
|
|
29
29
|
describeBudgetMs?: number;
|
|
30
|
+
/** How old a posting may be and still be worth reading, in days. 0 reads any live role, whatever its date,
|
|
31
|
+
* including one whose board states no date at all. The nightly crawl keeps the 30-day default so it spends
|
|
32
|
+
* its small budget on what is new; the catch-up pass lifts it to reach roles still listed but long posted. */
|
|
33
|
+
describeMaxAgeDays?: number;
|
|
30
34
|
pacingNow?: () => number;
|
|
31
35
|
pacingSleep?: (delayMs: number) => Promise<void>;
|
|
32
36
|
now?: () => Date;
|
|
@@ -66,14 +70,18 @@ export function createCrawler(options: CrawlerOptions): Crawler {
|
|
|
66
70
|
/** Required experience for every job: from its description, the last crawl's reading, or a paced detail fetch for recent roles in describeCountries. */
|
|
67
71
|
async function withExperience(source: Company, jobs: Job[], previous: Job[] | undefined): Promise<Job[]> {
|
|
68
72
|
const known = new Map((previous ?? []).map(normalizeJobExperience).filter((job) => job.experienceVersion === EXPERIENCE_VERSION && job.experience !== undefined).map((job) => [job.id, job]));
|
|
69
|
-
const
|
|
73
|
+
const maxAgeDays = options.describeMaxAgeDays ?? DESCRIBE_MAX_AGE_DAYS;
|
|
74
|
+
const since = maxAgeDays > 0 ? now().getTime() - maxAgeDays * 86_400_000 : 0;
|
|
75
|
+
// An undated posting is still a live posting. Inside a window it cannot be placed, so it waits; with the
|
|
76
|
+
// window lifted there is no reason to leave it unread.
|
|
77
|
+
const recentEnough = (job: Job) => (maxAgeDays <= 0 ? true : Date.parse(job.updatedAt ?? "") >= since);
|
|
70
78
|
const out: Job[] = [];
|
|
71
79
|
const pending: number[] = [];
|
|
72
80
|
for (const job of jobs) {
|
|
73
81
|
if (job.description.trim() || job.experienceVersion === EXPERIENCE_VERSION) out.push(normalizeJobExperience(job));
|
|
74
82
|
else if (known.has(job.id)) out.push({ ...job, description: known.get(job.id)!.description, experience: known.get(job.id)!.experience, experienceVersion: EXPERIENCE_VERSION });
|
|
75
83
|
else {
|
|
76
|
-
if (options.describe && describeRank(job) < describeOrder.length &&
|
|
84
|
+
if (options.describe && describeRank(job) < describeOrder.length && recentEnough(job)) pending.push(out.length);
|
|
77
85
|
out.push(normalizeJobExperience(job));
|
|
78
86
|
}
|
|
79
87
|
}
|
|
@@ -198,6 +206,7 @@ export function createCrawler(options: CrawlerOptions): Crawler {
|
|
|
198
206
|
// ponytail: per-source caps keep one crawl bounded; a big backlog (first run) drains over a few nights.
|
|
199
207
|
/** Defaults for the nightly crawl, where the point is to keep every source moving rather than finish any one. */
|
|
200
208
|
const DESCRIBE_LIMIT = 150;
|
|
209
|
+
const DESCRIBE_MAX_AGE_DAYS = 30;
|
|
201
210
|
const DESCRIBE_BUDGET_MS = 180_000;
|
|
202
211
|
|
|
203
212
|
function partitionTime(value: string | undefined): number {
|
package/src/local-jobs.ts
CHANGED
package/src/runtime.ts
CHANGED
|
@@ -40,6 +40,7 @@ export function createRuntime(options: { dataDir?: string; concurrency?: number;
|
|
|
40
40
|
// The description catch-up pass raises these; unset, the nightly crawl keeps the crawler's own small defaults.
|
|
41
41
|
describeLimit: positiveEnv("OPENINGS_DESCRIBE_LIMIT"),
|
|
42
42
|
describeBudgetMs: positiveEnv("OPENINGS_DESCRIBE_BUDGET_MS"),
|
|
43
|
+
describeMaxAgeDays: wholeEnv("OPENINGS_DESCRIBE_MAX_AGE_DAYS"),
|
|
43
44
|
onCrawled,
|
|
44
45
|
});
|
|
45
46
|
const recommender = createJobRecommender({ sources: companies, store, crawl: local.crawl });
|
|
@@ -107,3 +108,11 @@ function positiveEnv(name: string): number | undefined {
|
|
|
107
108
|
const value = Number(raw);
|
|
108
109
|
return Number.isInteger(value) && value > 0 ? value : undefined;
|
|
109
110
|
}
|
|
111
|
+
|
|
112
|
+
/** A whole number from the environment, zero included, or undefined when unset or nonsense. */
|
|
113
|
+
function wholeEnv(name: string): number | undefined {
|
|
114
|
+
const raw = process.env[name];
|
|
115
|
+
if (raw === undefined) return undefined;
|
|
116
|
+
const value = Number(raw);
|
|
117
|
+
return Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
118
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.54";
|