openings 0.1.0

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.
Files changed (52) hide show
  1. package/.codex-plugin/plugin.json +23 -0
  2. package/.mcp.json +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +110 -0
  5. package/data/companies.json +2550 -0
  6. package/docs/job-seeker-quickstart.md +109 -0
  7. package/package.json +42 -0
  8. package/skills/openings/SKILL.md +28 -0
  9. package/src/artifact-path.ts +48 -0
  10. package/src/atomic-file.ts +13 -0
  11. package/src/candidate-profile.ts +273 -0
  12. package/src/career-tracing.ts +181 -0
  13. package/src/catalog.ts +382 -0
  14. package/src/cli.ts +601 -0
  15. package/src/common-crawl-discovery.ts +137 -0
  16. package/src/company-seeds.ts +43 -0
  17. package/src/country-coverage.ts +203 -0
  18. package/src/crawl-reporting.ts +56 -0
  19. package/src/crawler.ts +146 -0
  20. package/src/enrichment-registry.ts +137 -0
  21. package/src/file-lock.ts +85 -0
  22. package/src/index.ts +37 -0
  23. package/src/intent-validation.ts +28 -0
  24. package/src/job-coverage.ts +73 -0
  25. package/src/job-fit-analysis.ts +247 -0
  26. package/src/job-matching.ts +445 -0
  27. package/src/job-recommendations.ts +193 -0
  28. package/src/job-search-preparation.ts +116 -0
  29. package/src/jobposting-probe.ts +167 -0
  30. package/src/local-jobs.ts +113 -0
  31. package/src/locations.ts +180 -0
  32. package/src/mcp.ts +98 -0
  33. package/src/package-mcp.ts +8 -0
  34. package/src/recruitee-round.ts +114 -0
  35. package/src/report-meta.ts +17 -0
  36. package/src/requirement-vocabulary.ts +111 -0
  37. package/src/resume-optimization.ts +118 -0
  38. package/src/runtime.ts +51 -0
  39. package/src/safe-get.ts +88 -0
  40. package/src/safe-head.ts +79 -0
  41. package/src/screening-requirements.ts +99 -0
  42. package/src/selected-job-lookup.ts +14 -0
  43. package/src/snapshot-catalog.ts +21 -0
  44. package/src/snapshot-export.ts +66 -0
  45. package/src/snapshot-store.ts +31 -0
  46. package/src/source-discovery-pipeline.ts +26 -0
  47. package/src/source-discovery.ts +231 -0
  48. package/src/source-enrichment.ts +92 -0
  49. package/src/source-pipeline.ts +245 -0
  50. package/src/source-verification.ts +297 -0
  51. package/src/tools.ts +194 -0
  52. package/src/types.ts +136 -0
@@ -0,0 +1,116 @@
1
+ import type { SnapshotStore } from "./crawler.ts";
2
+ import { projectJobCoverage, type JobCoverageSummary } from "./job-coverage.ts";
3
+ import type { CrawlScope } from "./local-jobs.ts";
4
+ import type { Company, CrawlReport, JobSnapshot } from "./types.ts";
5
+
6
+ export interface PrepareJobSearchResult {
7
+ status: "ready" | "partial";
8
+ nextAction: "ready" | "call_again" | "retry_later";
9
+ continuation?: string;
10
+ networkAttempted: boolean;
11
+ sources: { catalog: number; indexed: number; fresh: number; stale: number; missing: number; pending: number };
12
+ coverage: JobCoverageSummary;
13
+ crawl?: Pick<CrawlReport, "selected" | "succeeded" | "failed">;
14
+ }
15
+
16
+ export function createJobSearchPreparer(options: {
17
+ sources: Company[];
18
+ store: SnapshotStore;
19
+ crawl(scope: CrawlScope): Promise<CrawlReport>;
20
+ /** Optional published snapshot used instead of crawling when no local snapshot exists yet. */
21
+ seed?(): Promise<JobSnapshot | null>;
22
+ now?: () => Date;
23
+ freshnessDays?: number;
24
+ batchSize?: number;
25
+ }) {
26
+ const now = options.now ?? (() => new Date());
27
+ const freshnessMs = (options.freshnessDays ?? 14) * 86_400_000;
28
+ const batchSize = Math.max(1, Math.min(10, Math.trunc(options.batchSize ?? 10)));
29
+ return {
30
+ async prepare(value: unknown): Promise<PrepareJobSearchResult> {
31
+ const input = validateInput(value, options.sources);
32
+ const { countries } = input;
33
+ let snapshot = await options.store.read();
34
+ if (!snapshot && options.seed) {
35
+ const seeded = await options.seed().catch(() => null);
36
+ if (seeded) { await options.store.write(seeded); snapshot = seeded; }
37
+ }
38
+ const pendingBefore = pendingSources(options.sources, snapshot, now(), freshnessMs);
39
+ const selected = pendingBefore.filter((source) => !input.attempted.has(source.slug)).slice(0, batchSize).map((source) => source.slug);
40
+ const crawl = selected.length ? await options.crawl({ slugs: selected }) : undefined;
41
+ if (crawl) snapshot = await options.store.read();
42
+ if (!snapshot) throw new Error("Job search preparation did not produce a local snapshot");
43
+
44
+ const state = sourceState(options.sources, snapshot, now(), freshnessMs);
45
+ const attempted = new Set([...input.attempted, ...selected]);
46
+ const hasUnattemptedPending = pendingSources(options.sources, snapshot, now(), freshnessMs).some((source) => !attempted.has(source.slug));
47
+ const nextAction = state.pending === 0 ? "ready" : hasUnattemptedPending ? "call_again" : "retry_later";
48
+ return {
49
+ status: state.pending === 0 ? "ready" : "partial",
50
+ nextAction,
51
+ ...(nextAction === "call_again" ? { continuation: encodeContinuation(countries, attempted) } : {}),
52
+ networkAttempted: Boolean(crawl),
53
+ sources: { catalog: options.sources.length, ...state },
54
+ coverage: projectJobCoverage(options.sources, snapshot, countries),
55
+ ...(crawl ? { crawl: { selected: crawl.selected, succeeded: crawl.succeeded, failed: crawl.failed } } : {}),
56
+ };
57
+ },
58
+ };
59
+ }
60
+
61
+ function pendingSources(sources: Company[], snapshot: Awaited<ReturnType<SnapshotStore["read"]>>, now: Date, freshnessMs: number): Company[] {
62
+ const cutoff = now.getTime() - freshnessMs;
63
+ return sources.filter((source) => {
64
+ const fetchedAt = Date.parse(snapshot?.partitions[source.slug]?.fetchedAt ?? "");
65
+ return !Number.isFinite(fetchedAt) || fetchedAt < cutoff;
66
+ });
67
+ }
68
+
69
+ function sourceState(sources: Company[], snapshot: NonNullable<Awaited<ReturnType<SnapshotStore["read"]>>>, now: Date, freshnessMs: number) {
70
+ const cutoff = now.getTime() - freshnessMs;
71
+ let missing = 0;
72
+ let stale = 0;
73
+ for (const source of sources) {
74
+ const partition = snapshot.partitions[source.slug];
75
+ if (!partition) missing += 1;
76
+ else {
77
+ const fetchedAt = Date.parse(partition.fetchedAt);
78
+ if (!Number.isFinite(fetchedAt) || fetchedAt < cutoff) stale += 1;
79
+ }
80
+ }
81
+ const indexed = sources.length - missing;
82
+ return { indexed, fresh: indexed - stale, stale, missing, pending: stale + missing };
83
+ }
84
+
85
+ function validateInput(value: unknown, sources: Company[]): { countries: string[]; attempted: Set<string> } {
86
+ if (!isRecord(value)) throw new Error("prepare_job_search input must be an object");
87
+ const unknown = Object.keys(value).find((key) => key !== "countries" && key !== "continuation");
88
+ if (unknown) throw new Error(`prepare_job_search does not accept field: ${unknown}`);
89
+ if (!Array.isArray(value.countries) || value.countries.length === 0 || value.countries.length > 20) {
90
+ throw new Error("prepare_job_search requires between 1 and 20 countries");
91
+ }
92
+ if (!value.countries.every((country) => typeof country === "string" && /^[a-z]{2}$/iu.test(country))) {
93
+ throw new Error("countries must contain only two-letter country codes");
94
+ }
95
+ const countries = [...new Set(value.countries.map((country) => String(country).toUpperCase()))];
96
+ if (value.continuation === undefined) return { countries, attempted: new Set() };
97
+ if (typeof value.continuation !== "string" || value.continuation.length > 16_384) throw new Error("continuation must be a valid preparation token");
98
+ try {
99
+ const parsed = JSON.parse(Buffer.from(value.continuation, "base64url").toString("utf8")) as unknown;
100
+ if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.countries) || !Array.isArray(parsed.attempted)) throw new Error("invalid");
101
+ if (JSON.stringify(parsed.countries) !== JSON.stringify(countries)) throw new Error("country mismatch");
102
+ const known = new Set(sources.map((source) => source.slug));
103
+ if (!parsed.attempted.every((slug) => typeof slug === "string" && known.has(slug))) throw new Error("unknown source");
104
+ return { countries, attempted: new Set(parsed.attempted as string[]) };
105
+ } catch {
106
+ throw new Error("continuation must be a valid preparation token for the requested countries");
107
+ }
108
+ }
109
+
110
+ function encodeContinuation(countries: string[], attempted: Set<string>): string {
111
+ return Buffer.from(JSON.stringify({ version: 1, countries, attempted: [...attempted].sort() }), "utf8").toString("base64url");
112
+ }
113
+
114
+ function isRecord(value: unknown): value is Record<string, unknown> {
115
+ return typeof value === "object" && value !== null && !Array.isArray(value);
116
+ }
@@ -0,0 +1,167 @@
1
+ import { lstat, mkdir, readFile, realpath } from "node:fs/promises";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { atomicJson } from "./atomic-file.ts";
4
+ import { fetchSafeGet, SafeGetError } from "./safe-get.ts";
5
+ import { stampReport, type ReportMeta } from "./report-meta.ts";
6
+
7
+ export interface ProbePage { status: number; finalUrl: string; contentType: string; body: string; requestCount?: number }
8
+ interface ProbeOptions { fetchPage?: (url: string, maxRequests: number, companyDomain: string) => Promise<ProbePage>; companyLimit?: number; now?: Date; delayMs?: number; sleep?: (ms: number) => Promise<void>; reportRoot?: string }
9
+ interface CompanyResult { companyName: string; companyDomain: string; originHost: string; inputReference: string; status: "qualified" | "unresolved" | "failed"; acceptedJobs: number; attemptedDetailPages: number; discoveredPostings: number; admissionFailures: number; requestAttempts: number; failedRequests: number; jobs: AcceptedJob[]; issues: string[] }
10
+ interface AcceptedJob { key: string; signature: string; identifier: boolean; explicitCountry: boolean }
11
+ export interface JobPostingProbeReport extends ReportMeta { sample: Array<{ companyName: string; companyDomain: string; inputReference: string }>; companiesChecked: number; acceptedJobs: number; jobsWithIdentifier: number; identifierPresencePercent: number; jobsWithExplicitCountry: number; explicitCountryPercent: number; requests: number; stopped: boolean; stopReason?: string; viability: "pending_second_phase" | "passed" | "failed"; companies: Array<Omit<CompanyResult, "jobs" | "originHost">> }
12
+
13
+ export async function probeJobPostingJsonLd(inputPath: string, catalogPath: string, reportPath: string, options: ProbeOptions = {}): Promise<JobPostingProbeReport> {
14
+ await assertReportPath(reportPath, options.reportRoot ?? ".openings");
15
+ const fetchPage = options.fetchPage ?? ((url, maxRequests, domain) => fetchSafeGet(url, { maxRequests, allowedDomain: domain, allowedOrigin: new URL(url).hostname }));
16
+ const [markdown, catalogText] = await Promise.all([readFile(inputPath, "utf8"), readFile(catalogPath, "utf8")]);
17
+ const catalog = JSON.parse(catalogText) as Record<string, { companyDomain?: string }>;
18
+ const known = [...new Set(Object.values(catalog).map((company) => normalizeDomain(company.companyDomain ?? "")).filter(Boolean))];
19
+ const seeds = parseSeeds(markdown).filter((seed) => !known.some((domain) => seed.companyDomain === domain || seed.companyDomain.endsWith(`.${domain}`))).slice(0, Math.min(20, options.companyLimit ?? 20));
20
+ let requests = 0;
21
+ const delayMs = options.delayMs ?? 500;
22
+ const sleep = options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
23
+ let requestGate = Promise.resolve();
24
+ let fatalReason: string | undefined;
25
+ const get = async (url: string, domain: string) => {
26
+ let release!: () => void; const previous = requestGate; requestGate = new Promise<void>((resolve) => { release = resolve; }); await previous;
27
+ try {
28
+ if (fatalReason) throw new SafetyViolation(fatalReason);
29
+ const remaining = 320 - requests; if (remaining <= 0) { fatalReason = "global_request_cap"; throw new SafetyViolation(fatalReason); }
30
+ if (requests && delayMs) await sleep(delayMs);
31
+ try { const page = await fetchPage(url, remaining, domain); requests += page.requestCount ?? 1; if (requests > 320) { fatalReason = "global_request_cap"; throw new SafetyViolation(fatalReason); } return page; }
32
+ catch (error) { if (error instanceof SafeGetError) { requests += error.requestCount; if (error.safety) fatalReason = error.message; } else if (!(error instanceof SafetyViolation)) requests += 1; throw error; }
33
+ } finally { release(); }
34
+ };
35
+ const processCompany = async (seed: ReturnType<typeof parseSeeds>[number]): Promise<CompanyResult> => {
36
+ const issues: string[] = [];
37
+ let attemptedDetailPages = 0;
38
+ let discoveredPostings = 0;
39
+ let admissionFailures = 0;
40
+ let requestAttempts = 0;
41
+ let failedRequests = 0;
42
+ const jobs = new Map<string, AcceptedJob>();
43
+ const conflicts = new Set<string>();
44
+ try {
45
+ const companyGet = async (url: string) => { try { const page = await get(url, seed.companyDomain); requestAttempts += page.requestCount ?? 1; if (page.status === 429 || page.status >= 500) { failedRequests += 1; throw new Error(`transport_http_${page.status}`); } return page; } catch (error) { if (error instanceof SafeGetError) { requestAttempts += error.requestCount; failedRequests += error.failedRequestCount; if (error.safety) throw new SafetyViolation(error.message); } throw error; } };
46
+ const policies = new Map<string, ReturnType<typeof parseRobots>>();
47
+ const policyFor = async (host: string) => { const normalized = normalizeDomain(host); const existing = policies.get(normalized); if (existing) return existing; const robots = await companyGet(`https://${normalized}/robots.txt`); const policy = robotsPolicy(robots); policies.set(normalized, policy); return policy; };
48
+ const rules = await policyFor(seed.originHost);
49
+ const sitemapUrls = rules.sitemaps.length ? rules.sitemaps : [`https://${seed.originHost}/sitemap.xml`];
50
+ const detailUrls: string[] = [];
51
+ const sitemapQueue = sitemapUrls.map((url) => ({ url, depth: 0 }));
52
+ const seenSitemaps = new Set<string>();
53
+ while (sitemapQueue.length && seenSitemaps.size < 5) {
54
+ const item = sitemapQueue.shift()!;
55
+ const sitemapUrl = item.url;
56
+ if (seenSitemaps.has(sitemapUrl)) continue;
57
+ if (!allowedUrl(sitemapUrl, seed.companyDomain)) continue;
58
+ const sitemapParsed = new URL(sitemapUrl); const sitemapRules = await policyFor(sitemapParsed.hostname);
59
+ if (!robotsAllows(sitemapParsed, sitemapRules)) continue;
60
+ seenSitemaps.add(sitemapUrl);
61
+ const sitemap = await companyGet(sitemapUrl);
62
+ if (sitemap.status !== 200 || !xmlMediaTypes.has(mediaType(sitemap.contentType))) continue;
63
+ if (/<!DOCTYPE|<!ENTITY/i.test(sitemap.body)) { fatalReason = "unsafe_xml"; throw new SafetyViolation(fatalReason); }
64
+ for (const url of sitemapLocations(sitemap.body)) {
65
+ if (!allowedUrl(url, seed.companyDomain)) continue;
66
+ const parsed = new URL(url); const urlRules = await policyFor(parsed.hostname); if (!robotsAllows(parsed, urlRules)) continue;
67
+ if (item.depth < 1 && /\.xml(?:$|\?)/i.test(url)) sitemapQueue.push({ url, depth: item.depth + 1 });
68
+ else detailUrls.push(url);
69
+ }
70
+ }
71
+ for (const url of [...new Set(detailUrls)].slice(0, 10)) {
72
+ attemptedDetailPages += 1; const page = await companyGet(url);
73
+ if (page.status !== 200 || mediaType(page.contentType) !== "text/html") continue;
74
+ const postings = extractJobPostings(page.body);
75
+ discoveredPostings += postings.length;
76
+ if (postings.length > 1) { fatalReason = "multiple_jobpostings"; throw new SafetyViolation(fatalReason); }
77
+ for (const job of postings) {
78
+ const result = validateJob(job, seed.companyDomain, url, options.now ?? new Date());
79
+ if (!result.accepted) { issues.push(result.reason); admissionFailures += 1; continue; }
80
+ const previous = jobs.get(result.job.key);
81
+ if (previous && previous.signature !== result.job.signature) { conflicts.add(result.job.key); jobs.delete(result.job.key); issues.push("identity_conflict"); }
82
+ else if (!conflicts.has(result.job.key)) jobs.set(result.job.key, result.job);
83
+ }
84
+ }
85
+ return { ...seed, status: jobs.size ? "qualified" : "unresolved", acceptedJobs: jobs.size, attemptedDetailPages, discoveredPostings, admissionFailures, requestAttempts, failedRequests, jobs: [...jobs.values()], issues: [...new Set(issues)] };
86
+ } catch (error) {
87
+ if (error instanceof SafetyViolation) throw error;
88
+ if (!failedRequests) failedRequests = 1;
89
+ return { ...seed, status: "failed", acceptedJobs: 0, attemptedDetailPages, discoveredPostings, admissionFailures, requestAttempts, failedRequests, jobs: [], issues: [error instanceof Error ? error.message : String(error)] };
90
+ }
91
+ };
92
+ const companies: CompanyResult[] = [];
93
+ let stopReason: string | undefined;
94
+ try {
95
+ companies.push(...await runPhase(seeds.slice(0, 10), processCompany));
96
+ const firstGate = healthStop(companies);
97
+ if (firstGate) stopReason = firstGate;
98
+ else if (seeds.length > 10) companies.push(...await runPhase(seeds.slice(10, 20), processCompany));
99
+ } catch (error) { stopReason = error instanceof Error ? error.message : String(error); fatalReason = stopReason; }
100
+ const jobs = companies.flatMap((company) => company.jobs);
101
+ const acceptedJobs = jobs.length;
102
+ const jobsWithIdentifier = jobs.filter((job) => job.identifier).length;
103
+ const jobsWithExplicitCountry = jobs.filter((job) => job.explicitCountry).length;
104
+ const viability: JobPostingProbeReport["viability"] = stopReason ? "failed" : seeds.length < 20 || companies.length < 20 ? "pending_second_phase" : companies.filter((company) => company.status === "qualified").length >= 5 && acceptedJobs >= 20 && percent(jobsWithIdentifier, acceptedJobs) >= 80 && percent(jobsWithExplicitCountry, acceptedJobs) >= 95 ? "passed" : "failed";
105
+ const publicCompanies = companies.map(({ jobs: _jobs, originHost: _originHost, ...company }) => company);
106
+ const sample = seeds.map(({ companyName, companyDomain, inputReference }) => ({ companyName, companyDomain, inputReference }));
107
+ const report = stampReport("jobposting-jsonld-probe:1", 1, { sample, companiesChecked: companies.length, acceptedJobs, jobsWithIdentifier, identifierPresencePercent: percent(jobsWithIdentifier, acceptedJobs), jobsWithExplicitCountry, explicitCountryPercent: percent(jobsWithExplicitCountry, acceptedJobs), requests, stopped: Boolean(stopReason), ...(stopReason ? { stopReason } : {}), viability, companies: publicCompanies }, options.now);
108
+ await atomicJson(reportPath, report);
109
+ return report;
110
+ }
111
+
112
+ function parseSeeds(markdown: string) {
113
+ const rows: Array<{ companyName: string; companyDomain: string; originHost: string; inputReference: string }> = [];
114
+ const pattern = /<li>\s*<a\s+href="(https:\/\/[^"#]+)[^"]*"[^>]*>([^<]+)<\/a>/gi;
115
+ for (const match of markdown.matchAll(pattern)) { const url = new URL(match[1]!); const domain = normalizeDomain(url.hostname); if (!thirdParty(domain)) rows.push({ companyName: decode(match[2]!).trim(), companyDomain: domain, originHost: url.hostname.toLowerCase(), inputReference: match[1]! }); }
116
+ return [...new Map(rows.map((row) => [row.companyDomain, row])).values()];
117
+ }
118
+ interface RobotsRule { allow: boolean; pattern: string }
119
+ function robotsPolicy(page: ProbePage) { if (page.status === 401 || page.status === 403) return { sitemaps: [], rules: [{ allow: false, pattern: "/" }] }; if (page.status === 404 || page.status === 410) return { sitemaps: [], rules: [] as RobotsRule[] }; if (page.status < 200 || page.status >= 300) throw new Error(`robots_unavailable_${page.status}`); return parseRobots(page.body); }
120
+ function parseRobots(body: string) {
121
+ const sitemaps: string[] = []; const groups: Array<{ agents: string[]; rules: RobotsRule[] }> = []; let group: { agents: string[]; rules: RobotsRule[] } | undefined; let sawRule = false;
122
+ for (const raw of body.split(/\r?\n/)) { const line = raw.replace(/#.*/, "").trim(); if (!line) continue; const split = line.indexOf(":"); if (split < 0) continue; const key = line.slice(0, split).trim().toLowerCase(); const value = line.slice(split + 1).trim();
123
+ if (key === "sitemap" && value) { sitemaps.push(value); continue; }
124
+ if (key === "user-agent") { if (!group || sawRule) { group = { agents: [], rules: [] }; groups.push(group); sawRule = false; } group.agents.push(value.toLowerCase()); continue; }
125
+ if ((key === "allow" || key === "disallow") && group) { sawRule = true; if (value) group.rules.push({ allow: key === "allow", pattern: value }); }
126
+ }
127
+ const exact = groups.filter((item) => item.agents.some((agent) => "openings".startsWith(agent) && agent !== "*")); const selected = exact.length ? exact : groups.filter((item) => item.agents.includes("*"));
128
+ return { sitemaps, rules: selected.flatMap((item) => item.rules) };
129
+ }
130
+ function robotsAllows(url: URL, policy: ReturnType<typeof parseRobots>) { const target = `${url.pathname}${url.search}`; const matches = policy.rules.flatMap((rule) => { const source = `^${rule.pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\\\$$/, "$")}`; return new RegExp(source).test(target) ? [{ ...rule, length: rule.pattern.replace(/[*$]/g, "").length }] : []; }); matches.sort((a, b) => b.length - a.length || Number(b.allow) - Number(a.allow)); return matches[0]?.allow ?? true; }
131
+ function sitemapLocations(xml: string) { return [...xml.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/gi)].map((match) => decode(match[1]!)); }
132
+ function allowedUrl(value: string, domain: string) { try { const url = new URL(value); const host = normalizeDomain(url.hostname); return url.protocol === "https:" && (!url.port || url.port === "443") && (host === domain || host.endsWith(`.${domain}`)); } catch { return false; } }
133
+ function extractJobPostings(html: string): Record<string, unknown>[] { const results: Record<string, unknown>[] = []; for (const match of html.matchAll(/<script\b[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)) { try { const value: unknown = JSON.parse(match[1]!.trim()); visit(value, results); } catch { /* malformed JSON-LD is inert */ } } return results; }
134
+ function visit(value: unknown, output: Record<string, unknown>[]) { if (Array.isArray(value)) { value.forEach((item) => visit(item, output)); return; } if (!record(value)) return; const type = value["@type"]; if (type === "JobPosting" || Array.isArray(type) && type.includes("JobPosting")) output.push(value); const graph = value["@graph"]; if (graph) visit(graph, output); }
135
+ function validateJob(job: Record<string, unknown>, domain: string, pageUrl: string, now: Date): { accepted: false; reason: string } | { accepted: true; reason: ""; job: AcceptedJob } { const org = record(job.hiringOrganization) ? job.hiringOrganization : undefined; if (org?.sameAs !== undefined && !organizationDomainsMatch(org.sameAs, domain)) return fail("organization_domain_conflict"); if (!text(job.title) || !text(job.description) || !validDate(job.datePosted) || !text(org?.name)) return fail("missing_required_field"); if (job.validThrough !== undefined && (!validDate(job.validThrough) || Date.parse(String(job.validThrough)) < now.getTime())) return fail("expired_or_invalid"); const physicalCountry = hasPhysicalCountry(job.jobLocation); const remote = job.jobLocationType === "TELECOMMUTE"; const explicitCountry = physicalCountry || remote && hasApplicantCountry(job.applicantLocationRequirements); if (!explicitCountry) return fail("missing_country"); const identifierValue = record(job.identifier) && text(job.identifier.value) ? job.identifier.value.trim() : undefined; const url = typeof job.url === "string" ? job.url : pageUrl; if (!identifierValue && !allowedUrl(url, domain)) return fail("missing_identity"); const key = identifierValue ? `${domain}:id:${identifierValue}` : `${domain}:url:${normalizeJobUrl(url)}`; const signature = stableStringify(job); return { accepted: true, reason: "", job: { key, signature, identifier: Boolean(identifierValue), explicitCountry } }; }
136
+ function organizationDomainsMatch(value: unknown, domain: string): boolean { const values = Array.isArray(value) ? value : [value]; return values.length > 0 && values.every((item) => typeof item === "string" && allowedUrl(item, domain)); }
137
+ function hasPhysicalCountry(value: unknown): boolean { if (Array.isArray(value)) return value.some(hasPhysicalCountry); return record(value) && record(value.address) && validCountry(value.address.addressCountry); }
138
+ function hasApplicantCountry(value: unknown): boolean { if (Array.isArray(value)) return value.some(hasApplicantCountry); if (!record(value)) return false; const country = record(value.address) ? value.address.addressCountry : value.name; return validCountry(country); }
139
+ function fail(reason: string): { accepted: false; reason: string } { return { accepted: false, reason }; }
140
+ function thirdParty(domain: string) { return ["linkedin.com", "indeed.com", "github.com", "twitter.com", "angel.co", "wellfound.com", "lever.co", "ashbyhq.com", "greenhouse.io", "myworkdayjobs.com", "recruitee.com"].some((value) => domain === value || domain.endsWith(`.${value}`)); }
141
+ function normalizeDomain(value: string) { return value.trim().toLowerCase().replace(/^www\./, ""); }
142
+ function normalizeJobUrl(value: string) { const url = new URL(value); url.hash = ""; if (url.port === "443") url.port = ""; return url.href; }
143
+ function stableStringify(value: unknown): string { if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`; if (record(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`; return JSON.stringify(value); }
144
+ function decode(value: string) { return value.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'"); }
145
+ function validDate(value: unknown) { return typeof value === "string" && Number.isFinite(Date.parse(value)); }
146
+ function text(value: unknown): value is string { return typeof value === "string" && Boolean(value.trim()); }
147
+ function record(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
148
+ function percent(value: number, total: number) { return total ? Math.round(value / total * 10_000) / 100 : 0; }
149
+ async function runPhase<T, R>(values: T[], operation: (value: T) => Promise<R>): Promise<R[]> { const output = new Array<R>(values.length); let cursor = 0; let failure: unknown; async function worker() { while (cursor < values.length && !failure) { const index = cursor++; try { output[index] = await operation(values[index]!); } catch (error) { failure = error; } } } await Promise.all(Array.from({ length: Math.min(3, values.length) }, worker)); if (failure) throw failure; return output; }
150
+ function healthStop(companies: CompanyResult[]): string | undefined { if (companies.filter((company) => company.status === "qualified").length < 2) return "phase_one_low_yield"; const attempts = companies.reduce((sum, company) => sum + company.requestAttempts, 0); const failed = companies.reduce((sum, company) => sum + company.failedRequests, 0); if (attempts && failed / attempts > 0.1) return "phase_one_transport_failures"; const discovered = companies.reduce((sum, company) => sum + company.discoveredPostings, 0); const admissionFailures = companies.reduce((sum, company) => sum + company.admissionFailures, 0); if (discovered && admissionFailures / discovered > 0.5) return "phase_one_schema_failures"; return undefined; }
151
+ class SafetyViolation extends Error {}
152
+ const countryNames = new Set(["india", "united states", "usa", "united kingdom", "canada", "germany", "france", "australia", "singapore", "japan", "netherlands", "ireland", "spain", "italy", "brazil", "mexico"]);
153
+ function validCountry(value: unknown): boolean { return typeof value === "string" && (/^[A-Z]{2}$/i.test(value.trim()) || countryNames.has(value.trim().toLowerCase())); }
154
+ function mediaType(value: string) { return value.split(";", 1)[0]!.trim().toLowerCase(); }
155
+ const xmlMediaTypes = new Set(["application/xml", "text/xml", "application/sitemap+xml"]);
156
+ async function assertReportPath(path: string, root: string) {
157
+ const expectedRoot = resolve(root);
158
+ const target = resolve(path);
159
+ const lexicalRelation = relative(expectedRoot, target);
160
+ if (!lexicalRelation || lexicalRelation.startsWith("..")) throw new Error("JobPosting probe report must be a file under .openings");
161
+ await mkdir(expectedRoot, { recursive: true });
162
+ if ((await lstat(expectedRoot)).isSymbolicLink()) throw new Error("JobPosting probe report must be a file under .openings");
163
+ await mkdir(dirname(target), { recursive: true });
164
+ const [actualRoot, actualParent] = await Promise.all([realpath(expectedRoot), realpath(dirname(target))]);
165
+ const relation = relative(actualRoot, actualParent);
166
+ if (relation.startsWith("..") || resolve(actualRoot, relation) !== actualParent) throw new Error("JobPosting probe report must be a file under .openings");
167
+ }
@@ -0,0 +1,113 @@
1
+ import type { FetchJobsObserver } from "./catalog.ts";
2
+ import { createCrawler, type SnapshotStore } from "./crawler.ts";
3
+ import { createSnapshotCatalog } from "./snapshot-catalog.ts";
4
+ import type { Company, CrawlReport, Job, JobPartition, JobSnapshot, JobSummary, SearchQuery } from "./types.ts";
5
+
6
+ interface LocalJobsOptions {
7
+ sources: Company[];
8
+ store: SnapshotStore;
9
+ fetchJobs(source: Company, signal?: AbortSignal, observer?: FetchJobsObserver): Promise<Job[]>;
10
+ concurrency?: number;
11
+ timeoutMs?: number;
12
+ maxAttempts?: number;
13
+ sourceStartDelayMs?: number;
14
+ sourceFreshnessMs?: number;
15
+ sourceLimit?: number;
16
+ workdayPageDelayMs?: number;
17
+ now?: () => Date;
18
+ onCrawled?(source: Company, partition: JobPartition): void | Promise<void>;
19
+ }
20
+
21
+ export interface CrawlScope {
22
+ country?: string;
23
+ countries?: string[];
24
+ slugs?: string[];
25
+ }
26
+
27
+ export interface SnapshotStatus {
28
+ updatedAt: string;
29
+ ageDays: number;
30
+ stale: boolean;
31
+ refreshed: boolean;
32
+ sources: number;
33
+ jobs: number;
34
+ failures: number;
35
+ }
36
+
37
+ export function createLocalJobs(options: LocalJobsOptions) {
38
+ const now = options.now ?? (() => new Date());
39
+ const crawler = createCrawler({ ...options, now });
40
+ const catalog = createSnapshotCatalog(options.store);
41
+
42
+ async function crawl(scope: CrawlScope = {}): Promise<CrawlReport> {
43
+ const selected = selectSources(options.sources, scope);
44
+ return crawler.crawl(selected, { prune: !scope.country && !scope.countries && !scope.slugs });
45
+ }
46
+
47
+ async function ensureFresh(offline: boolean, staleDays: number): Promise<{ snapshot: JobSnapshot; refreshed: boolean }> {
48
+ let snapshot = await options.store.read();
49
+ const stale = !snapshot || snapshotIsStale(snapshot, staleDays, now());
50
+ if (stale && !offline) {
51
+ await crawl();
52
+ snapshot = await options.store.read();
53
+ if (!snapshot) throw new Error("Crawl completed without producing a snapshot");
54
+ return { snapshot, refreshed: true };
55
+ }
56
+ if (!snapshot) throw new Error("No local snapshot. Run `openings crawl` or search without --offline.");
57
+ return { snapshot, refreshed: false };
58
+ }
59
+
60
+ return {
61
+ crawl,
62
+ async search(query: SearchQuery, settings: { offline: boolean; staleDays: number }): Promise<{ jobs: JobSummary[]; snapshot: SnapshotStatus }> {
63
+ const ready = await ensureFresh(settings.offline, settings.staleDays);
64
+ return { jobs: await catalog.search(query), snapshot: snapshotStatus(ready.snapshot, settings.staleDays, now(), ready.refreshed) };
65
+ },
66
+ async get(id: string, settings: { offline: boolean; staleDays: number }): Promise<{ job: Job | null; snapshot: SnapshotStatus }> {
67
+ const ready = await ensureFresh(settings.offline, settings.staleDays);
68
+ return { job: await catalog.get(id), snapshot: snapshotStatus(ready.snapshot, settings.staleDays, now(), ready.refreshed) };
69
+ },
70
+ ensureFresh,
71
+ catalog,
72
+ };
73
+ }
74
+
75
+ function selectSources(sources: Company[], scope: CrawlScope): Company[] {
76
+ if (scope.slugs) {
77
+ const wanted = new Set(scope.slugs);
78
+ const selected = sources.filter((source) => wanted.has(source.slug));
79
+ const missing = [...wanted].filter((slug) => !selected.some((source) => source.slug === slug));
80
+ if (missing.length) throw new Error(`Unknown company source(s): ${missing.join(", ")}`);
81
+ return selected;
82
+ }
83
+ if (scope.country) {
84
+ const country = scope.country.toUpperCase();
85
+ return sources.filter((source) => source.cohorts?.includes(country));
86
+ }
87
+ if (scope.countries) {
88
+ const countries = new Set(scope.countries.map((country) => country.toUpperCase()));
89
+ return sources.filter((source) => source.cohorts?.some((country) => countries.has(country)));
90
+ }
91
+ return sources;
92
+ }
93
+
94
+ export function snapshotIsStale(snapshot: JobSnapshot, staleDays: number, now: Date): boolean {
95
+ const timestamps = Object.values(snapshot.partitions).map((partition) => Date.parse(partition.fetchedAt));
96
+ if (timestamps.length === 0) return true;
97
+ const oldest = Math.min(...timestamps);
98
+ return now.getTime() - oldest > staleDays * 86_400_000;
99
+ }
100
+
101
+ export function snapshotStatus(snapshot: JobSnapshot, staleDays: number, now: Date, refreshed: boolean): SnapshotStatus {
102
+ const timestamps = Object.values(snapshot.partitions).map((partition) => Date.parse(partition.fetchedAt));
103
+ const oldest = timestamps.length ? Math.min(...timestamps) : Date.parse(snapshot.updatedAt);
104
+ return {
105
+ updatedAt: snapshot.updatedAt,
106
+ ageDays: Math.max(0, Math.floor((now.getTime() - oldest) / 86_400_000)),
107
+ stale: snapshotIsStale(snapshot, staleDays, now),
108
+ refreshed,
109
+ sources: Object.keys(snapshot.partitions).length,
110
+ jobs: Object.values(snapshot.partitions).reduce((sum, partition) => sum + partition.jobs.length, 0),
111
+ failures: snapshot.lastCrawl.failed.length,
112
+ };
113
+ }
@@ -0,0 +1,180 @@
1
+ import type { Job } from "./types.ts";
2
+
3
+ const INDIA_PLACES = [
4
+ "india", "bharat",
5
+ "andhra pradesh", "arunachal pradesh", "assam", "bihar", "chhattisgarh", "goa", "gujarat",
6
+ "haryana", "himachal pradesh", "jharkhand", "karnataka", "kerala", "madhya pradesh", "maharashtra",
7
+ "manipur", "meghalaya", "mizoram", "nagaland", "odisha", "orissa", "punjab", "rajasthan", "sikkim",
8
+ "tamil nadu", "telangana", "tripura", "uttar pradesh", "uttarakhand", "west bengal",
9
+ "andaman", "chandigarh", "dadra", "daman", "delhi", "jammu", "kashmir", "ladakh", "lakshadweep", "puducherry",
10
+ "bengaluru", "bangalore", "hyderabad", "pune", "chennai", "mumbai", "gurugram", "gurgaon", "noida",
11
+ "kolkata", "ahmedabad", "kochi", "cochin", "jaipur", "coimbatore", "indore", "thiruvananthapuram",
12
+ "mysuru", "mysore", "bhubaneswar", "lucknow", "nagpur", "visakhapatnam", "vizag", "vadodara", "surat",
13
+ ];
14
+
15
+ const INDIA_PATTERN = new RegExp(`\\b(${INDIA_PLACES.map(escapeRegExp).join("|")})\\b`, "i");
16
+ const INDIA_INCLUSIVE_REGION_PATTERN = /\b(apac|asia|asia[ -]pacific|worldwide|anywhere|global)\b/i;
17
+ const INDIA_EXCLUSION_PATTERN = /\b(not available|unavailable|excluding|except|cannot hire|can't hire|unable to hire|do not hire|does not hire)\b.{0,80}\b(india|apac|asia)\b|\b(india|apac|asia)\b.{0,40}\b(excluded|not eligible|not supported)\b/i;
18
+ const INDIA_ELIGIBILITY_PATTERN = /\b(remote (?:in|from)|available (?:in|to)|open to|hiring (?:in|from)|candidates? (?:in|from)|applicants? (?:in|from)|work (?:in|from)|based in)\b.{0,80}\b(india|apac|asia)\b|\b(india|apac|asia)\b.{0,40}\b(remote|candidates?|applicants?|eligible|hiring)\b/i;
19
+
20
+ export function normalizeLocation(value: string): string {
21
+ const aliases: Record<string, string> = {
22
+ bangalore: "bengaluru",
23
+ gurgaon: "gurugram",
24
+ bombay: "mumbai",
25
+ calcutta: "kolkata",
26
+ madras: "chennai",
27
+ mysore: "mysuru",
28
+ orissa: "odisha",
29
+ };
30
+ return value.trim().toLocaleLowerCase().replace(/\b(bangalore|gurgaon|bombay|calcutta|madras|mysore|orissa)\b/g, (name) => aliases[name] ?? name);
31
+ }
32
+
33
+ export function isExplicitlyIndiaEligible(job: Job): boolean {
34
+ if (job.remote && INDIA_EXCLUSION_PATTERN.test(`${job.location}\n${job.description}`)) return false;
35
+ if (INDIA_PATTERN.test(job.location)) return true;
36
+ if (!job.remote) return false;
37
+ return INDIA_INCLUSIVE_REGION_PATTERN.test(job.location) || INDIA_ELIGIBILITY_PATTERN.test(job.description);
38
+ }
39
+
40
+ export function classifyJob(job: Job): Job {
41
+ const workMode = inferWorkMode(job.location, job.workMode);
42
+ const evidence = `${job.location}\n${job.description}`;
43
+ const excludedCountries = detectExcludedCountries(evidence);
44
+ const eligibleCountries = detectCountries(job.location);
45
+ if (workMode === "remote") eligibleCountries.push(...detectEligibleCountries(job.description));
46
+ const uniqueEligibleCountries = [...new Set(eligibleCountries)].filter((code) => !excludedCountries.includes(code));
47
+ const eligibleRegions = workMode === "remote" ? detectRegions(job.location, job.description) : [];
48
+ const withMode = { ...job, remote: workMode === "remote", workMode };
49
+ const indiaLocation = INDIA_PATTERN.test(job.location);
50
+ const indiaDescription = workMode === "remote" && INDIA_ELIGIBILITY_PATTERN.test(job.description);
51
+ if ((indiaLocation || indiaDescription) && !excludedCountries.includes("IN") && !uniqueEligibleCountries.includes("IN")) uniqueEligibleCountries.push("IN");
52
+ return {
53
+ ...withMode,
54
+ eligibleCountries: uniqueEligibleCountries.sort(),
55
+ excludedCountries,
56
+ eligibleRegions,
57
+ eligibilityConfidence: uniqueEligibleCountries.length > 0 ? "explicit" : eligibleRegions.length > 0 ? "inferred" : "unknown",
58
+ };
59
+ }
60
+
61
+ export function isEligibleForCountry(job: Job, country: string): boolean {
62
+ const code = country.toUpperCase();
63
+ if (job.excludedCountries.includes(code)) return false;
64
+ if (job.eligibleCountries.includes(code)) return true;
65
+ if (job.eligibleRegions.includes("worldwide")) return true;
66
+ return job.eligibleRegions.some((region) => regionIncludes(region, code));
67
+ }
68
+
69
+ function detectExcludedCountries(value: string): string[] {
70
+ const excluded: string[] = [];
71
+ for (const rule of countryRules()) {
72
+ if (rule.exclusion.test(value)) excluded.push(rule.code);
73
+ }
74
+ return excluded.sort();
75
+ }
76
+
77
+ function inferWorkMode(location: string, supplied: Job["workMode"]): Job["workMode"] {
78
+ if (supplied !== "unknown") return supplied;
79
+ if (/\bremote\b/i.test(location)) return "remote";
80
+ if (/\bhybrid\b/i.test(location)) return "hybrid";
81
+ if (/\b(on[ -]?site|office[ -]?based)\b/i.test(location)) return "onsite";
82
+ return "unknown";
83
+ }
84
+
85
+ function detectRegions(location: string, description: string): string[] {
86
+ const regions: Array<[string, RegExp]> = [
87
+ ["worldwide", /\b(worldwide|anywhere|global)\b/i],
88
+ ["APAC", /\b(APAC|Asia[ -]Pacific)\b/i],
89
+ ["Asia", /\bAsia\b/i],
90
+ ["EMEA", /\bEMEA\b/i],
91
+ ["LATAM", /\b(LATAM|Latin America)\b/i],
92
+ ];
93
+ const fromLocation = regions.filter(([, pattern]) => pattern.test(location)).map(([name]) => name);
94
+ const eligibilityPhrase = /\b(open to|hiring (?:in|from|across)|(?:candidates?|applicants?) (?:in|from|across)|eligible (?:in|for|across)|work from|available (?:in|to))\b.{0,60}\b(worldwide|anywhere|global|APAC|Asia[ -]Pacific|Asia|EMEA|LATAM|Latin America)\b/gi;
95
+ const fromDescription: string[] = [];
96
+ for (const match of description.matchAll(eligibilityPhrase)) {
97
+ const value = match[2] ?? "";
98
+ const region = regions.find(([, pattern]) => pattern.test(value))?.[0];
99
+ if (region) fromDescription.push(region);
100
+ }
101
+ return [...new Set([...fromLocation, ...fromDescription])];
102
+ }
103
+
104
+ function detectCountries(location: string): string[] {
105
+ const matches: string[] = [];
106
+ for (const rule of countryRules()) {
107
+ if (rule.location.test(location) || rule.codeLocation.test(location)) matches.push(rule.code);
108
+ }
109
+ return matches;
110
+ }
111
+
112
+ function detectEligibleCountries(description: string): string[] {
113
+ const matches: string[] = [];
114
+ for (const rule of countryRules()) {
115
+ if (rule.eligibility.test(description)) matches.push(rule.code);
116
+ }
117
+ if (/\b(open to|hiring|candidates?|applicants?|eligible|remote (?:in|from)|work (?:in|from)|based in|available (?:in|to))\b.{0,80}\bGeorgia\b/i.test(description)) matches.push("GE");
118
+ return matches;
119
+ }
120
+
121
+ let cachedCountryNames: Array<[string, string]> | undefined;
122
+ function countryNames(): Array<[string, string]> {
123
+ if (cachedCountryNames) return cachedCountryNames;
124
+ const display = new Intl.DisplayNames(["en"], { type: "region" });
125
+ const names: Array<[string, string]> = [];
126
+ for (let first = 65; first <= 90; first += 1) {
127
+ for (let second = 65; second <= 90; second += 1) {
128
+ const code = String.fromCharCode(first, second);
129
+ const name = display.of(code);
130
+ if (name && name !== code && !name.startsWith("Unknown Region")) names.push([code, name]);
131
+ }
132
+ }
133
+ cachedCountryNames = names;
134
+ return names;
135
+ }
136
+
137
+ let cachedCountryMatchers: Array<[string, string]> | undefined;
138
+ function countryMatchers(): Array<[string, string]> {
139
+ if (cachedCountryMatchers) return cachedCountryMatchers;
140
+ const aliases: Record<string, string[]> = { US: ["United States", "USA", "U\\.S\\.A\\.?", "U\\.S\\.?"], GB: ["United Kingdom", "UK", "U\\.K\\.?"], AE: ["United Arab Emirates", "UAE"] };
141
+ cachedCountryMatchers = countryNames().map(([code, name]) => {
142
+ if (code === "GE") return [code, "(?:\\b(?:Tbilisi|Batumi|Kutaisi|Rustavi|Gori|Zugdidi),?\\s+Georgia\\b|^Georgia$|\\bGeorgia,?\\s+(?:Country|Europe)\\b)"];
143
+ const names = [name, ...(aliases[code] ?? [])];
144
+ return [code, names.length ? `\\b(?:${names.map(escapeRegExpUnlessPattern).join("|")})\\b` : "(?!)"];
145
+ });
146
+ return cachedCountryMatchers;
147
+ }
148
+
149
+ interface CountryRule { code: string; location: RegExp; codeLocation: RegExp; exclusion: RegExp; eligibility: RegExp }
150
+ let cachedCountryRules: CountryRule[] | undefined;
151
+ function countryRules(): CountryRule[] {
152
+ if (cachedCountryRules) return cachedCountryRules;
153
+ cachedCountryRules = countryMatchers().map(([code, country]) => ({
154
+ code,
155
+ location: new RegExp(country, "i"),
156
+ codeLocation: new RegExp(`(?:^|[,(/-]\\s*)${code}(?=\\s*(?:$|[,)/-]))`, "i"),
157
+ exclusion: new RegExp(`\\b(not available|unavailable|excluding|except|cannot hire|can't hire|unable to hire|do not hire|does not hire)\\b.{0,80}(?:${country})|(?:${country}).{0,40}\\b(excluded|not eligible|not supported)\\b`, "i"),
158
+ eligibility: new RegExp(`\\b(open to|hiring|candidates?|applicants?|eligible|remote (?:in|from)|work (?:in|from)|based in|available (?:in|to))\\b.{0,80}(?:${country})`, "i"),
159
+ }));
160
+ return cachedCountryRules;
161
+ }
162
+
163
+ function regionIncludes(region: string, code: string): boolean {
164
+ if (region === "worldwide") return true;
165
+ const groups: Record<string, string> = {
166
+ APAC: "AU BN CN FJ HK ID IN JP KH KR LA MM MN MO MY NP NZ PH PK SG TH TW VN",
167
+ Asia: "AE AF AM AZ BD BH BN BT CN CY GE HK ID IL IN IQ IR JO JP KG KH KP KR KW KZ LA LB LK MM MN MO MV MY NP OM PH PK PS QA SA SG SY TH TJ TL TM TR TW UZ VN YE",
168
+ EMEA: "AD AE AF AL AM AO AT AZ BA BE BF BG BH BI BJ BW BY CD CF CG CH CI CM CV CY CZ DE DJ DK DZ EE EG ER ES ET FI FR GA GB GE GH GM GN GQ GR GW HR HU IE IL IQ IR IS IT JO KE KG KM KW KZ LB LI LR LS LT LU LV LY MA MC MD ME MG MK ML MR MT MU MW MZ NA NE NG NL NO OM PL PS PT QA RO RS RU RW SA SC SD SE SI SK SL SM SN SO SS ST SY SZ TD TG TJ TM TN TR TZ UA UG UZ VA YE ZA ZM ZW",
169
+ LATAM: "AR BO BR BZ CL CO CR CU DO EC GT GY HN HT MX NI PA PE PR PY SR SV UY VE",
170
+ };
171
+ return (` ${groups[region] ?? ""} `).includes(` ${code} `);
172
+ }
173
+
174
+ function escapeRegExpUnlessPattern(value: string): string {
175
+ return value.includes("\\") ? value : escapeRegExp(value);
176
+ }
177
+
178
+ function escapeRegExp(value: string): string {
179
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
180
+ }