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.
@@ -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.61",
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 { country?: string; fetch?: Fetch; indexUrl?: string; registryPath?: string; provider?: Ats; indexRecordLimit?: number; sampleTokenLimit?: number; excludeTokens?: string[] }
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
- if (!response.ok) throw new Error(`Common Crawl index returned HTTP ${response.status} for ${pattern}`);
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
- crawlPatterns: ["*.fa.oraclecloud.com/hcmUI/CandidateExperience*"],
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.61";
1
+ export const VERSION = "0.1.63";