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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openings",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
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" },
@@ -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 { 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.60";
1
+ export const VERSION = "0.1.62";