openings 0.1.60 → 0.1.62
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/data/companies.json +15637 -0
- package/package.json +1 -1
- package/src/board-verification.ts +20 -2
- package/src/common-crawl-discovery.ts +28 -4
- package/src/locations.ts +7 -1
- package/src/providers.ts +9 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import excludedData from "../data/excluded.json" with { type: "json" };
|
|
1
2
|
import { atomicJson } from "./atomic-file.ts";
|
|
2
3
|
import { readEnrichmentRegistry, mergeEnrichmentLeads, type EnrichmentLead, type LeadAttempt } from "./enrichment-registry.ts";
|
|
3
4
|
import { withFileLock } from "./file-lock.ts";
|
|
@@ -7,6 +8,22 @@ import type { Ats, Company, SourceVerification } from "./types.ts";
|
|
|
7
8
|
|
|
8
9
|
type Fetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Sources that must never be admitted, by provider and token.
|
|
13
|
+
*
|
|
14
|
+
* Deleting a row from the catalog does not hold: the lead stays in the registry and the next verification run
|
|
15
|
+
* puts it straight back, which is how two employers excluded on terms grounds could have quietly returned. The
|
|
16
|
+
* reason travels with the entry so a later reader does not have to guess why.
|
|
17
|
+
*/
|
|
18
|
+
const EXCLUDED: ReadonlySet<string> = new Set(
|
|
19
|
+
excludedData.entries.map((entry) => `${entry.ats}:${entry.token.toLowerCase()}`),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/** Why a given source is excluded, for reporting. */
|
|
23
|
+
export function exclusionReason(ats: string, token: string): string | undefined {
|
|
24
|
+
return excludedData.entries.find((entry) => entry.ats === ats && entry.token.toLowerCase() === token.toLowerCase())?.reason;
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
/** Providers whose public board is accepted as identity on its own. Workday boards are identified by tenant; their crawls are heavier but they carry the large employers. */
|
|
11
28
|
export const BOARD_TIER_PROVIDERS: ReadonlySet<Ats> = new Set(["greenhouse", "lever", "ashby", "recruitee", "smartrecruiters", "workable", "breezy", "workday", "freshteam", "keka", "zohorecruit"]);
|
|
12
29
|
|
|
@@ -24,7 +41,7 @@ export interface BoardVerificationReport {
|
|
|
24
41
|
generatedAt: string;
|
|
25
42
|
considered: number;
|
|
26
43
|
selected: number;
|
|
27
|
-
skipped: { inCatalog: number; coolingDown: number; unsupported: number; deferred: number };
|
|
44
|
+
skipped: { inCatalog: number; coolingDown: number; unsupported: number; deferred: number; excluded: number };
|
|
28
45
|
verified: number;
|
|
29
46
|
added: string[];
|
|
30
47
|
rejected: Array<{ sourceKey: string; reason: "unreachable" | "invalid_payload" | "empty_board" | "duplicate_slug"; detail: string }>;
|
|
@@ -50,10 +67,11 @@ export async function verifyBoards(registryPath: string, catalogPath: string, op
|
|
|
50
67
|
const catalog = await readCatalog(catalogPath);
|
|
51
68
|
const registry = await readEnrichmentRegistry(registryPath);
|
|
52
69
|
const knownSources = new Set(Object.values(catalog).map((entry) => `${entry.ats}:${entry.token.toLowerCase()}`));
|
|
53
|
-
const skipped = { inCatalog: 0, coolingDown: 0, unsupported: 0, deferred: 0 };
|
|
70
|
+
const skipped = { inCatalog: 0, coolingDown: 0, unsupported: 0, deferred: 0, excluded: 0 };
|
|
54
71
|
const eligible: EnrichmentLead[] = [];
|
|
55
72
|
for (const lead of registry.leads) {
|
|
56
73
|
if (!BOARD_TIER_PROVIDERS.has(lead.ats) || !resolveSource(lead.sourceUrl)) { skipped.unsupported += 1; continue; }
|
|
74
|
+
if (EXCLUDED.has(`${lead.ats}:${lead.token.toLowerCase()}`)) { skipped.excluded += 1; continue; }
|
|
57
75
|
if (knownSources.has(`${lead.ats}:${lead.token.toLowerCase()}`)) { skipped.inCatalog += 1; continue; }
|
|
58
76
|
// Keka tokens contain tenant/UUID; the provider-issued routing UUID is not a company name.
|
|
59
77
|
const boardName = lead.ats === "keka" ? lead.token.split("/")[0]! : lead.token;
|
|
@@ -8,7 +8,9 @@ import type { Ats } from "./types.ts";
|
|
|
8
8
|
import { assertArtifactFile } from "./artifact-path.ts";
|
|
9
9
|
|
|
10
10
|
type Fetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
11
|
-
interface Options {
|
|
11
|
+
interface Options {
|
|
12
|
+
/** Backoff between index retries. Only set in tests, where real seconds are dead time. */
|
|
13
|
+
retryDelayMs?: number; country?: string; fetch?: Fetch; indexUrl?: string; registryPath?: string; provider?: Ats; indexRecordLimit?: number; sampleTokenLimit?: number; excludeTokens?: string[] }
|
|
12
14
|
interface Lead { sourceUrl: string; ats: string; token: string; discoveredFrom: { channel: "dataset"; reference: string } }
|
|
13
15
|
interface Collection { id?: unknown; "cdx-api"?: unknown }
|
|
14
16
|
|
|
@@ -26,6 +28,10 @@ export interface CommonCrawlDiscoveryReport extends ReportMeta {
|
|
|
26
28
|
rejected: number;
|
|
27
29
|
truncated: boolean;
|
|
28
30
|
truncatedPatterns: string[];
|
|
31
|
+
/** Patterns the index had no captures for, so a later run can tell an empty region from a missing one. */
|
|
32
|
+
emptyPatterns: string[];
|
|
33
|
+
/** Patterns the index refused, with the status it gave. Reported rather than thrown, so one bad pattern cannot discard the sweep. */
|
|
34
|
+
failedPatterns: Array<{ pattern: string; status: number }>;
|
|
29
35
|
candidatesPath: string;
|
|
30
36
|
reportPath: string;
|
|
31
37
|
registryPath?: string;
|
|
@@ -62,6 +68,8 @@ export async function discoverCommonCrawlSources(candidatesPath: string, reportP
|
|
|
62
68
|
const seenUrls = new Set<string>();
|
|
63
69
|
const rejections: Array<{ value: string; reason: string }> = [];
|
|
64
70
|
const truncatedPatterns: string[] = [];
|
|
71
|
+
const emptyPatterns: string[] = [];
|
|
72
|
+
const failedPatterns: Array<{ pattern: string; status: number }> = [];
|
|
65
73
|
let indexRecordsExamined = 0;
|
|
66
74
|
|
|
67
75
|
for (const pattern of options.provider ? providerPatterns[options.provider] : patterns) {
|
|
@@ -75,10 +83,20 @@ export async function discoverCommonCrawlSources(candidatesPath: string, reportP
|
|
|
75
83
|
let response = await fetcher(query);
|
|
76
84
|
for (let attempt = 1; !response.ok && response.status >= 500 && attempt <= 3; attempt += 1) { // the index server sheds load with 502s; back off and retry
|
|
77
85
|
await response.body?.cancel().catch(() => undefined);
|
|
78
|
-
await new Promise((resolve) => setTimeout(resolve, attempt * 5_000));
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * (options.retryDelayMs ?? 5_000)));
|
|
79
87
|
response = await fetcher(query);
|
|
80
88
|
}
|
|
81
|
-
|
|
89
|
+
// The index answers 404 when a pattern matched nothing at all. That is an empty region, not a broken run.
|
|
90
|
+
if (response.status === 404) { await response.body?.cancel().catch(() => undefined); emptyPatterns.push(pattern); continue; }
|
|
91
|
+
// Anything else that survived the retries is this pattern's problem, not the run's. A sparse pattern asked
|
|
92
|
+
// for too many records times out (collapse and filter make the index do real work), and throwing there
|
|
93
|
+
// discarded every pattern that had already answered. A partial sweep is worth keeping; only a sweep where
|
|
94
|
+
// nothing at all succeeded is a failure.
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
await response.body?.cancel().catch(() => undefined);
|
|
97
|
+
failedPatterns.push({ pattern, status: response.status });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
82
100
|
const lines = (await response.text()).split(/\r?\n/).filter(Boolean).slice(0, indexRecordLimit);
|
|
83
101
|
indexRecordsExamined += lines.length;
|
|
84
102
|
if (lines.length >= indexRecordLimit) truncatedPatterns.push(pattern);
|
|
@@ -93,6 +111,12 @@ export async function discoverCommonCrawlSources(candidatesPath: string, reportP
|
|
|
93
111
|
}
|
|
94
112
|
}
|
|
95
113
|
|
|
114
|
+
// Every pattern refused is a broken sweep, not an empty one, and must not be reported as a quiet zero.
|
|
115
|
+
const attempted = options.provider ? providerPatterns[options.provider] : patterns;
|
|
116
|
+
if (failedPatterns.length === attempted.length && attempted.length > 0) {
|
|
117
|
+
throw new Error(`Common Crawl index refused every pattern: ${failedPatterns.map((f) => `${f.pattern} HTTP ${f.status}`).join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
96
120
|
const availableLeads: Lead[] = [];
|
|
97
121
|
const excludedTokens = new Set((options.excludeTokens ?? []).map((token) => token.toLowerCase()));
|
|
98
122
|
let alreadyKnown = 0;
|
|
@@ -113,7 +137,7 @@ export async function discoverCommonCrawlSources(candidatesPath: string, reportP
|
|
|
113
137
|
const registry = options.registryPath ? await mergeEnrichmentLeads(options.registryPath, registryLeads) : { added: 0, bytes: undefined, lockHeldMs: undefined };
|
|
114
138
|
const report: CommonCrawlDiscoveryReport = stampReport("common-crawl-discovery:1", 1, {
|
|
115
139
|
country, provider: options.provider, index, indexRecordsExamined, sampleTokenLimit, sampleShortfall: sampleTokenLimit === undefined ? 0 : Math.max(0, sampleTokenLimit - leads.length), urlsSeen: seenUrls.size, sourcesFound: found.size, alreadyKnown, unresolved: leads.length, rejected: rejections.length,
|
|
116
|
-
truncated: truncatedPatterns.length > 0, truncatedPatterns,
|
|
140
|
+
truncated: truncatedPatterns.length > 0, truncatedPatterns, emptyPatterns, failedPatterns,
|
|
117
141
|
candidatesPath, reportPath, registryPath: options.registryPath, registryAdded: registry.added, registryBytes: registry.bytes, registryLockHeldMs: registry.lockHeldMs, availableLeads, leads, rejections,
|
|
118
142
|
});
|
|
119
143
|
await atomicJson(reportPath, report);
|
package/src/locations.ts
CHANGED
|
@@ -106,6 +106,12 @@ const US_STATE_CODES = new Set(["AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE",
|
|
|
106
106
|
const US_CITY_STATE = /,\s*([A-Z]{2})(?=\s*(?:$|,|\d{5}|\(|\/|-))/g;
|
|
107
107
|
/** Workday's "ST-CITY, street" and "ST - City" shapes, e.g. "IN-INDIANAPOLIS, 220 VIRGINIA AVE" or "IN - Indianapolis". */
|
|
108
108
|
const US_STATE_PREFIX = /^([A-Z]{2})\s*-\s*(?=[A-Za-z])/;
|
|
109
|
+
/**
|
|
110
|
+
* Workday also writes the country in front of the state: "US-IN-FT WAYNE-150A ~ 1010 Production Rd".
|
|
111
|
+
* US_STATE_PREFIX reads the leading "US" as the state, finds it is not one, and gives up, so Fort Wayne
|
|
112
|
+
* was filed under India on the strength of a bare "IN" further along the string.
|
|
113
|
+
*/
|
|
114
|
+
const US_COUNTRY_STATE_PREFIX = /^US\s*-\s*([A-Z]{2})\s*-\s*(?=[A-Za-z0-9])/;
|
|
109
115
|
|
|
110
116
|
function detectCountries(location: string): string[] {
|
|
111
117
|
const byName: string[] = [];
|
|
@@ -117,7 +123,7 @@ function detectCountries(location: string): string[] {
|
|
|
117
123
|
// "Indianapolis, IN" is Indiana, not India: a bare code in the US "City, ST" position is a state unless a country name
|
|
118
124
|
// or a known Indian place says otherwise.
|
|
119
125
|
// Case matters: "Berlin, de" is a lowercase country code, "Gary, IN" is a state.
|
|
120
|
-
const prefix = US_STATE_PREFIX.exec(location)?.[1];
|
|
126
|
+
const prefix = US_COUNTRY_STATE_PREFIX.exec(location)?.[1] ?? US_STATE_PREFIX.exec(location)?.[1];
|
|
121
127
|
const stateCodes = [...[...location.matchAll(US_CITY_STATE)].map((match) => match[1]!), ...(prefix ? [prefix] : [])].filter((code) => US_STATE_CODES.has(code));
|
|
122
128
|
if (stateCodes.length && byName.length === 0 && !INDIA_PATTERN.test(location)) {
|
|
123
129
|
return [...new Set(["US", ...byCode.filter((code) => !US_STATE_CODES.has(code))])];
|
package/src/providers.ts
CHANGED
|
@@ -210,7 +210,15 @@ const oraclecloud: ProviderSpec = {
|
|
|
210
210
|
ats: "oraclecloud",
|
|
211
211
|
label: "Oracle Cloud Recruiting",
|
|
212
212
|
hosts: ["oraclecloud.com"],
|
|
213
|
-
|
|
213
|
+
// One pattern per Fusion region. A wildcard in the middle of the host plus a deep path suffix is more than
|
|
214
|
+
// the Common Crawl index will serve: "*.fa.oraclecloud.com/hcmUI/CandidateExperience*" answered 504 every
|
|
215
|
+
// time, which is why Oracle had no discovered tenants at all despite being supported. Per-region host
|
|
216
|
+
// patterns answer 200, and the CandidateExperience path is filtered from the returned URLs instead.
|
|
217
|
+
crawlPatterns: ["*.fa.us2.oraclecloud.com/*", "*.fa.us6.oraclecloud.com/*", "*.fa.us9.oraclecloud.com/*",
|
|
218
|
+
"*.fa.em2.oraclecloud.com/*", "*.fa.em3.oraclecloud.com/*", "*.fa.em4.oraclecloud.com/*",
|
|
219
|
+
"*.fa.ap1.oraclecloud.com/*", "*.fa.ap2.oraclecloud.com/*", "*.fa.ap3.oraclecloud.com/*",
|
|
220
|
+
"*.fa.ca2.oraclecloud.com/*", "*.fa.ca3.oraclecloud.com/*", "*.fa.uk1.oraclecloud.com/*",
|
|
221
|
+
"*.fa.sa1.oraclecloud.com/*"],
|
|
214
222
|
resolve(url) {
|
|
215
223
|
if (!ORACLE_HOST.test(url.hostname)) return null;
|
|
216
224
|
const site = /\/sites\/([A-Za-z0-9_]{2,32})/.exec(url.pathname)?.[1] ?? url.searchParams.get("siteNumber");
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.62";
|