openings 0.1.61 → 0.1.63
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/excluded.json +63 -0
- package/package.json +2 -1
- 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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"comment": "Sources that must never enter the catalog, and why. Verification consults this before admitting a board, so a decision taken once is not quietly undone by the next discovery run. Removing a row from data/companies.json does not survive: the lead stays in the registry and the next verification re-admits it.",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"ats": "oraclecloud",
|
|
7
|
+
"token": "ejwl.fa.us2.oraclecloud.com/CX",
|
|
8
|
+
"name": "Marriott International",
|
|
9
|
+
"reason": "terms",
|
|
10
|
+
"detail": "The terms page refuses our requests, and being unable to read a contract is not permission to act under it.",
|
|
11
|
+
"excludedAt": "2026-09-17"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"ats": "oraclecloud",
|
|
15
|
+
"token": "fa-ewjt-saasfaprod1.fa.ocs.oraclecloud.com/CX_2",
|
|
16
|
+
"name": "EXL",
|
|
17
|
+
"reason": "terms",
|
|
18
|
+
"detail": "Terms prohibit automated data gathering.",
|
|
19
|
+
"excludedAt": "2026-09-17"
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
"ats": "workable",
|
|
24
|
+
"token": "eramtalent-1",
|
|
25
|
+
"name": "Eram Talent",
|
|
26
|
+
"reason": "staffing",
|
|
27
|
+
"detail": "A recruiting firm's board lists other companies' vacancies, which is the opposite of the employer's own posting we promise.",
|
|
28
|
+
"excludedAt": "2026-10-07"
|
|
29
|
+
},
|
|
30
|
+
{ "ats": "workable", "token": "flextal-staffing-llc", "name": "FlexTal Staffing LLC", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
31
|
+
{ "ats": "workable", "token": "marathon-talent", "name": "Marathon Talent", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
32
|
+
{ "ats": "workable", "token": "next-job-abroad", "name": "Next Job Abroad", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
33
|
+
{ "ats": "workable", "token": "nobox-hr-outsourcing-solutions", "name": "Nobox Aviation Recruitment", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
34
|
+
{ "ats": "workable", "token": "opendatajobs", "name": "OpenDataJobs", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
35
|
+
{ "ats": "workable", "token": "osa", "name": "Outsourcing Advantage", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
36
|
+
{ "ats": "workable", "token": "pairedrecruiting", "name": "Paired", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
37
|
+
{ "ats": "workable", "token": "pearltalent", "name": "Pearl Talent", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
38
|
+
{ "ats": "workable", "token": "remote-recruitment", "name": "Remote Recruitment", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
39
|
+
{ "ats": "workable", "token": "remote-talent-cloud", "name": "Remote Talent Cloud", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
40
|
+
{ "ats": "workable", "token": "remote-talent-latam", "name": "Remote Talent LATAM", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
41
|
+
{ "ats": "workable", "token": "staffing-for-doctors", "name": "Staffing for Doctors", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
42
|
+
{ "ats": "workable", "token": "talent-engine", "name": "Talent Engine", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
43
|
+
{ "ats": "workable", "token": "uptalent-dot-i-o", "name": "Uptalent.io", "reason": "staffing", "excludedAt": "2026-10-07" },
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
"ats": "workable",
|
|
47
|
+
"token": "entaingroup",
|
|
48
|
+
"name": "Entain",
|
|
49
|
+
"reason": "duplicate",
|
|
50
|
+
"detail": "Already crawled on SmartRecruiters as `entain`. The same vacancy reached from two boards is two rows in search, which is the one thing the count of unique posts must not include.",
|
|
51
|
+
"excludedAt": "2026-10-07"
|
|
52
|
+
},
|
|
53
|
+
{ "ats": "workable", "token": "rentokil-initial", "name": "Rentokil Initial", "reason": "duplicate", "detail": "Already crawled on SmartRecruiters as `rentokilinitial1`.", "excludedAt": "2026-10-07" },
|
|
54
|
+
{
|
|
55
|
+
"ats": "workday",
|
|
56
|
+
"token": "accenture.wd103.myworkdayjobs.com/accenture/AccentureCareers",
|
|
57
|
+
"name": "Accenture",
|
|
58
|
+
"reason": "duplicate",
|
|
59
|
+
"detail": "Accenture's India roles already arrive through the `accenture-india` portal, which is the single largest source in the catalog at 24,400 live roles. The global Workday board overlaps it.",
|
|
60
|
+
"excludedAt": "2026-10-07"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openings",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
4
4
|
"description": "A free, candidate-safe job-search substrate for AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": { "type": "git", "url": "git+https://github.com/abhay-avagama/hiring-agent.git" },
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"src/",
|
|
15
15
|
"data/companies.json",
|
|
16
|
+
"data/excluded.json",
|
|
16
17
|
"skills/openings/",
|
|
17
18
|
".codex-plugin/plugin.json",
|
|
18
19
|
".mcp.json",
|
|
@@ -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.63";
|