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,137 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { atomicJson } from "./atomic-file.ts";
3
+ import { withFileLock } from "./file-lock.ts";
4
+ import type { Ats, DiscoveryProvenance, DomainEvidence } from "./types.ts";
5
+ import { resolveSource } from "./source-verification.ts";
6
+
7
+ export type EnrichmentState = "unresolved" | "matched" | "evidence_ready" | "verified" | "rejected";
8
+ export type MatchMethod = "normalized_token" | "normalized_name" | "normalized_domain" | "search_result";
9
+
10
+ export interface CompanyMatch { companyName: string; companyDomain: string; method: MatchMethod; reference: string }
11
+ export interface IdentityEvidence { companyName: string; companyDomain: string; kind: DomainEvidence["kind"] | "provider_structured_domain"; reference: string; observedAt: string }
12
+ export interface LeadAttempt { attemptedAt: string; outcome: "success" | "transient_failure" | "permanent_failure"; category?: string; detail?: string; nextEligibleAt?: string; evidenceRank?: number }
13
+ export interface EnrichmentLead {
14
+ sourceKey: string; sourceUrl: string; ats: Ats; token: string;
15
+ discoveredFrom: DiscoveryProvenance[];
16
+ companyMatches: CompanyMatch[];
17
+ identityEvidence: IdentityEvidence[];
18
+ attempts: LeadAttempt[];
19
+ promotedAt?: string;
20
+ }
21
+ export interface EnrichmentRegistry { version: 1; updatedAt: string; leads: EnrichmentLead[] }
22
+
23
+ const evidenceRank: Record<IdentityEvidence["kind"], number> = {
24
+ provider_structured_domain: 4,
25
+ company_redirect: 3,
26
+ company_registry: 2,
27
+ authoritative_dataset: 1,
28
+ };
29
+
30
+ export function deriveLeadState(lead: EnrichmentLead): EnrichmentState {
31
+ const latestAttempt = lead.attempts.at(-1);
32
+ if (latestAttempt?.outcome === "permanent_failure" && !supersededFailure(lead, latestAttempt)) return "rejected";
33
+ if (!lead.companyMatches.length) return lead.promotedAt && latestAttempt?.outcome === "success" ? "verified" : "unresolved";
34
+ const qualifying = lead.identityEvidence.filter((evidence) => !["lever", "ashby"].includes(lead.ats) || ["provider_structured_domain", "company_redirect"].includes(evidence.kind));
35
+ if (!qualifying.length) return lead.promotedAt && latestAttempt?.outcome === "success" ? "verified" : "matched";
36
+ const winningRank = Math.max(...qualifying.map((evidence) => evidenceRank[evidence.kind]));
37
+ const winningDomains = new Set(qualifying.filter((evidence) => evidenceRank[evidence.kind] === winningRank).map((evidence) => normalizeDomain(evidence.companyDomain)));
38
+ if (winningDomains.size !== 1) return "rejected";
39
+ const domain = [...winningDomains][0]!;
40
+ const state = lead.companyMatches.some((match) => normalizeDomain(match.companyDomain) === domain) ? "evidence_ready" : "rejected";
41
+ return state === "evidence_ready" && lead.promotedAt && latestAttempt?.outcome === "success" ? "verified" : state;
42
+ }
43
+
44
+ export function retryDisposition(lead: EnrichmentLead, now = new Date()): "fresh" | "retryable" | "cooling_down" | "repeatedly_failing" {
45
+ if (!lead.attempts.length) return "fresh";
46
+ const latest = lead.attempts.at(-1)!;
47
+ if (latest.outcome !== "transient_failure") return "retryable";
48
+ if (latest.nextEligibleAt && Date.parse(latest.nextEligibleAt) > now.getTime()) return "cooling_down";
49
+ const consecutive = consecutiveTransientFailures(lead.attempts);
50
+ return consecutive >= 3 ? "repeatedly_failing" : "retryable";
51
+ }
52
+
53
+ export function strongestEvidenceRank(lead: EnrichmentLead): number { return Math.max(0, ...lead.identityEvidence.map((evidence) => evidenceRank[evidence.kind])); }
54
+
55
+ export async function readEnrichmentRegistry(path: string): Promise<EnrichmentRegistry> {
56
+ try {
57
+ const value: unknown = JSON.parse(await readFile(path, "utf8"));
58
+ if (!isRegistry(value)) throw new Error(`Invalid enrichment registry: ${path}`);
59
+ return value;
60
+ } catch (error) {
61
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return { version: 1, updatedAt: new Date(0).toISOString(), leads: [] };
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ export async function mergeEnrichmentLeads(path: string, additions: EnrichmentLead[], now = new Date()): Promise<{ added: number; updated: number; total: number; bytes: number; lockHeldMs: number; durationMs: number }> {
67
+ const startedAt = Date.now();
68
+ const result = await withFileLock(path, async () => {
69
+ const lockStartedAt = Date.now();
70
+ const registry = await readEnrichmentRegistry(path);
71
+ const byKey = new Map(registry.leads.map((lead) => [lead.sourceKey, lead]));
72
+ let added = 0;
73
+ let updated = 0;
74
+ for (const addition of additions) {
75
+ const current = byKey.get(addition.sourceKey);
76
+ if (!current) { byKey.set(addition.sourceKey, canonicalLead(addition)); added += 1; continue; }
77
+ byKey.set(addition.sourceKey, mergeLead(current, addition)); updated += 1;
78
+ }
79
+ const leads = [...byKey.values()].sort((left, right) => left.sourceKey.localeCompare(right.sourceKey));
80
+ const value = { version: 1, updatedAt: now.toISOString(), leads } satisfies EnrichmentRegistry;
81
+ await atomicJson(path, value);
82
+ return { added, updated, total: leads.length, bytes: Buffer.byteLength(JSON.stringify(value)), lockHeldMs: Date.now() - lockStartedAt };
83
+ }, { operation: "merge enrichment leads" });
84
+ return { ...result, durationMs: Date.now() - startedAt };
85
+ }
86
+
87
+ export function transientAttempt(attemptedAt: Date, consecutiveFailures: number, category: string, detail: string): LeadAttempt {
88
+ const delayMs = Math.min(7 * 86_400_000, 60_000 * 2 ** Math.max(0, consecutiveFailures - 1));
89
+ return { attemptedAt: attemptedAt.toISOString(), outcome: "transient_failure", category, detail, nextEligibleAt: new Date(attemptedAt.getTime() + delayMs).toISOString() };
90
+ }
91
+
92
+ function mergeLead(left: EnrichmentLead, right: EnrichmentLead): EnrichmentLead {
93
+ return canonicalLead({
94
+ ...left,
95
+ sourceUrl: right.sourceUrl,
96
+ discoveredFrom: unique([...left.discoveredFrom, ...right.discoveredFrom]),
97
+ companyMatches: unique([...left.companyMatches, ...right.companyMatches]),
98
+ identityEvidence: unique([...left.identityEvidence, ...right.identityEvidence]),
99
+ attempts: unique([...left.attempts, ...right.attempts]).sort((a, b) => a.attemptedAt.localeCompare(b.attemptedAt)),
100
+ promotedAt: right.promotedAt ?? left.promotedAt,
101
+ });
102
+ }
103
+ function canonicalLead(lead: EnrichmentLead): EnrichmentLead {
104
+ const sorted = <T>(values: T[]) => unique(values).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
105
+ return { ...lead, discoveredFrom: sorted(lead.discoveredFrom), companyMatches: sorted(lead.companyMatches), identityEvidence: sorted(lead.identityEvidence), attempts: unique(lead.attempts).sort((a, b) => a.attemptedAt.localeCompare(b.attemptedAt) || JSON.stringify(a).localeCompare(JSON.stringify(b))) };
106
+ }
107
+ function unique<T>(values: T[]): T[] { return [...new Map(values.map((value) => [JSON.stringify(value), value])).values()]; }
108
+ function normalizeDomain(value: string): string { return value.toLowerCase().replace(/^www\./, ""); }
109
+ function consecutiveTransientFailures(attempts: LeadAttempt[]): number { let count = 0; for (const attempt of [...attempts].reverse()) { if (attempt.outcome !== "transient_failure") break; count += 1; } return count; }
110
+ function isRegistry(value: unknown): value is EnrichmentRegistry {
111
+ if (!isRecord(value) || value.version !== 1 || typeof value.updatedAt !== "string" || !Array.isArray(value.leads)) return false;
112
+ return value.leads.every((lead) => isRecord(lead)
113
+ && typeof lead.sourceKey === "string" && /^(greenhouse|lever|ashby|workday|recruitee):.+/.test(lead.sourceKey)
114
+ && typeof lead.sourceUrl === "string" && typeof lead.token === "string"
115
+ && ["greenhouse", "lever", "ashby", "workday", "recruitee"].includes(String(lead.ats))
116
+ && Array.isArray(lead.discoveredFrom) && lead.discoveredFrom.every(validProvenance)
117
+ && Array.isArray(lead.companyMatches) && lead.companyMatches.every(validMatch)
118
+ && Array.isArray(lead.identityEvidence) && lead.identityEvidence.every(validEvidence)
119
+ && Array.isArray(lead.attempts) && lead.attempts.every(validAttempt)
120
+ && sourceIdentityMatches(lead)
121
+ && (lead.promotedAt === undefined || typeof lead.promotedAt === "string" && Number.isFinite(Date.parse(lead.promotedAt))));
122
+ }
123
+ function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
124
+ function validProvenance(value: unknown): boolean { return isRecord(value) && ["search", "career_page", "provider_directory", "community", "dataset", "legacy"].includes(String(value.channel)) && typeof value.reference === "string"; }
125
+ function validMatch(value: unknown): boolean { return isRecord(value) && typeof value.companyName === "string" && typeof value.companyDomain === "string" && /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value.companyDomain) && ["normalized_token", "normalized_name", "normalized_domain", "search_result"].includes(String(value.method)) && typeof value.reference === "string"; }
126
+ function validEvidence(value: unknown): boolean { return isRecord(value) && typeof value.companyName === "string" && typeof value.companyDomain === "string" && /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value.companyDomain) && ["provider_structured_domain", "company_redirect", "company_registry", "authoritative_dataset"].includes(String(value.kind)) && typeof value.reference === "string" && typeof value.observedAt === "string" && Number.isFinite(Date.parse(value.observedAt)); }
127
+ function validAttempt(value: unknown): boolean { return isRecord(value) && typeof value.attemptedAt === "string" && Number.isFinite(Date.parse(value.attemptedAt)) && ["success", "transient_failure", "permanent_failure"].includes(String(value.outcome)) && (value.nextEligibleAt === undefined || typeof value.nextEligibleAt === "string" && Number.isFinite(Date.parse(value.nextEligibleAt))) && (value.category === undefined || typeof value.category === "string") && (value.detail === undefined || typeof value.detail === "string") && (value.evidenceRank === undefined || typeof value.evidenceRank === "number" && Number.isFinite(value.evidenceRank)); }
128
+ function sourceIdentityMatches(value: Record<string, unknown>): boolean {
129
+ const source = resolveSource(String(value.sourceUrl));
130
+ if (source) return source.ats === value.ats && source.token === value.token && `${source.ats}:${source.token.toLowerCase()}` === value.sourceKey;
131
+ if (value.ats !== "workday" || typeof value.token !== "string" || typeof value.sourceKey !== "string") return false;
132
+ try {
133
+ const host = new URL(String(value.sourceUrl)).hostname.toLowerCase();
134
+ return /\.myworkdayjobs\.com$/u.test(host) && value.token.startsWith(`${host}/`) && value.sourceKey === `workday:${value.token.toLowerCase()}`;
135
+ } catch { return false; }
136
+ }
137
+ function supersededFailure(lead: EnrichmentLead, attempt: LeadAttempt): boolean { return attempt.category === "identity_mismatch" && lead.identityEvidence.some((evidence) => Date.parse(evidence.observedAt) > Date.parse(attempt.attemptedAt) && evidenceRank[evidence.kind] > (attempt.evidenceRank ?? 0)); }
@@ -0,0 +1,85 @@
1
+ import { mkdir, open, readFile, stat, unlink } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+
4
+ interface FileLockOptions { acquireTimeoutMs?: number; operation?: string }
5
+ export interface FileLockStatus { locked: boolean; lockPath: string; pid?: number; operation?: string; createdAt?: string }
6
+
7
+ export async function withFileLock<T>(path: string, operation: () => Promise<T>, options: FileLockOptions = {}): Promise<T> {
8
+ await mkdir(dirname(path), { recursive: true });
9
+ const lockPath = `${path}.lock`;
10
+ const handle = await acquire(lockPath, Math.max(1, options.acquireTimeoutMs ?? configuredTimeout()));
11
+ try {
12
+ await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), operation: options.operation ?? "file update" }));
13
+ return await operation();
14
+ } finally {
15
+ await handle.close().catch(() => undefined);
16
+ await unlink(lockPath).catch(() => undefined);
17
+ }
18
+ }
19
+
20
+ export async function inspectFileLock(path: string): Promise<FileLockStatus> {
21
+ const lockPath = `${path}.lock`;
22
+ try {
23
+ const value: unknown = JSON.parse(await readFile(lockPath, "utf8"));
24
+ if (!value || typeof value !== "object") return { locked: true, lockPath };
25
+ return { locked: true, lockPath,
26
+ ...( "pid" in value && typeof value.pid === "number" ? { pid: value.pid } : {}),
27
+ ...( "operation" in value && typeof value.operation === "string" ? { operation: value.operation } : {}),
28
+ ...( "createdAt" in value && typeof value.createdAt === "string" ? { createdAt: value.createdAt } : {}),
29
+ };
30
+ } catch (error) {
31
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return { locked: false, lockPath };
32
+ return { locked: true, lockPath };
33
+ }
34
+ }
35
+
36
+ export async function forceReleaseFileLock(path: string): Promise<FileLockStatus> {
37
+ const status = await inspectFileLock(path);
38
+ if (status.locked) await unlink(status.lockPath);
39
+ return status;
40
+ }
41
+
42
+ function configuredTimeout(): number {
43
+ const value = Number(process.env.OPENINGS_LOCK_TIMEOUT_MS ?? 60_000);
44
+ return Number.isFinite(value) && value > 0 ? value : 60_000;
45
+ }
46
+
47
+ async function acquire(lockPath: string, timeoutMs: number) {
48
+ const startedAt = Date.now();
49
+ while (true) {
50
+ try {
51
+ return await open(lockPath, "wx");
52
+ } catch (error) {
53
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
54
+ if (await lockIsStale(lockPath)) { await unlink(lockPath).catch(() => undefined); continue; }
55
+ if (Date.now() - startedAt >= timeoutMs) throw new Error(`Timed out waiting ${timeoutMs}ms for ${lockPath} (${await describeHolder(lockPath)})`);
56
+ await new Promise((resolve) => setTimeout(resolve, Math.min(25, timeoutMs)));
57
+ }
58
+ }
59
+ }
60
+
61
+ async function describeHolder(path: string): Promise<string> {
62
+ try {
63
+ const value: unknown = JSON.parse(await readFile(path, "utf8"));
64
+ if (typeof value !== "object" || value === null) return "holder unknown";
65
+ const pid = "pid" in value && typeof value.pid === "number" ? `pid ${value.pid}` : "pid unknown";
66
+ const operation = "operation" in value && typeof value.operation === "string" ? value.operation : "operation unknown";
67
+ const createdAt = "createdAt" in value && typeof value.createdAt === "string" ? value.createdAt : "time unknown";
68
+ return `${operation}, ${pid}, since ${createdAt}`;
69
+ } catch { return "holder unknown"; }
70
+ }
71
+
72
+ async function lockIsStale(path: string): Promise<boolean> {
73
+ try {
74
+ const value: unknown = JSON.parse(await readFile(path, "utf8"));
75
+ if (typeof value === "object" && value !== null && "pid" in value && typeof value.pid === "number") {
76
+ // PID reuse can defer reclamation; it preserves mutual exclusion and is preferable to reclaiming a live writer.
77
+ try { process.kill(value.pid, 0); return false; }
78
+ catch (error) { if (error instanceof Error && "code" in error && error.code === "ESRCH") return true; }
79
+ }
80
+ return Date.now() - (await stat(path)).mtimeMs > 60_000;
81
+ } catch {
82
+ try { return Date.now() - (await stat(path)).mtimeMs > 60_000; }
83
+ catch (error) { return error instanceof Error && "code" in error && error.code === "ENOENT"; }
84
+ }
85
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ import companyData from "../data/companies.json";
2
+ import { createCatalog } from "./catalog.ts";
3
+ import type { Ats, Company } from "./types.ts";
4
+
5
+ export const companies: Company[] = Object.entries(companyData).map(([slug, value]) => ({
6
+ slug,
7
+ name: value.name,
8
+ ats: value.ats as Ats,
9
+ token: value.token,
10
+ cohorts: "cohorts" in value ? value.cohorts as string[] : undefined,
11
+ companyDomain: "companyDomain" in value ? value.companyDomain as string : undefined,
12
+ sourceUrl: "sourceUrl" in value ? value.sourceUrl as string : undefined,
13
+ discoveredFrom: "discoveredFrom" in value ? value.discoveredFrom as Company["discoveredFrom"] : undefined,
14
+ verification: "verification" in value ? value.verification as Company["verification"] : undefined,
15
+ domainEvidence: "domainEvidence" in value ? value.domainEvidence as Company["domainEvidence"] : undefined,
16
+ }));
17
+
18
+ export const catalog = createCatalog({ companies });
19
+ export { createCatalog } from "./catalog.ts";
20
+ export type { Catalog } from "./catalog.ts";
21
+ export type { Ats, Company, CrawlReport, Job, JobPartition, JobSnapshot, JobSummary, SearchQuery } from "./types.ts";
22
+ export { createCrawlReporter, fetchSeedSnapshot, resolveAggregatorUrl } from "./crawl-reporting.ts";
23
+ export type { CrawlReportPayload } from "./crawl-reporting.ts";
24
+ export { parseCandidateProfile, ResumeInputError, validateCandidateProfileEvidence } from "./candidate-profile.ts";
25
+ export type { CandidateFact, CandidateFactKind, CandidateInference, CandidateProfile, EvidenceSpan, EvidenceValidationResult, NormalizedResume, ResumeFormat, ResumeInput } from "./candidate-profile.ts";
26
+ export { matchJobs } from "./job-matching.ts";
27
+ export type { CandidateIntent, FilteredJob, JobMatch, JobMatchingResult, RoleFamily, RoleFamilyExpansion, SupportedRequirement, TitleExpansion, TransferableRequirement } from "./job-matching.ts";
28
+ export { createJobRecommender, RecommendationError } from "./job-recommendations.ts";
29
+ export type { JobRecommenderOptions, RecommendJobsInput, RecommendJobsResult, RecommendationRefreshInput, RecommendationRefreshResult, RefreshPolicy } from "./job-recommendations.ts";
30
+ export { createJobFitAnalyzer, JobFitAnalysisError } from "./job-fit-analysis.ts";
31
+ export type { AnalyzeJobFitInput, AnalyzeJobFitResult, JobFitAnalyzerOptions, JobFitAssessment, JobFitReason, PartiallySupportedRequirement } from "./job-fit-analysis.ts";
32
+ export { createResumeOptimizer, ResumeOptimizationError } from "./resume-optimization.ts";
33
+ export type { OptimizeResumeInput, OptimizeResumeResult, ResumeOptimizationOutput, ResumeOptimizerOptions, ResumeSuggestion } from "./resume-optimization.ts";
34
+ export { createSelectedJobLookup } from "./selected-job-lookup.ts";
35
+ export type { SelectedJobLookupOptions } from "./selected-job-lookup.ts";
36
+ export { createJobCoverageReader, projectJobCoverage } from "./job-coverage.ts";
37
+ export type { CountryJobCoverage, JobCoverageSummary } from "./job-coverage.ts";
@@ -0,0 +1,28 @@
1
+ import type { CandidateIntent } from "./job-matching.ts";
2
+
3
+ type InvalidInput = (field: string, message: string) => Error;
4
+ const arrayFields = ["roles", "countries", "locations", "seniority", "requiredSkills", "excludedTerms", "excludedCountries", "excludedLocations", "excludedRoles"] as const;
5
+
6
+ export function validateCandidateIntent(value: unknown, invalid: InvalidInput, options: { optional?: boolean } = {}): CandidateIntent {
7
+ if (value === undefined && options.optional) return {};
8
+ if (!isRecord(value)) throw invalid("intent", "Candidate intent must be an object");
9
+ assertKnownKeys(value, [...arrayFields, "remote"], "intent", invalid);
10
+ for (const field of arrayFields) {
11
+ const candidate = value[field];
12
+ if (candidate !== undefined && (!Array.isArray(candidate) || !candidate.every((item) => typeof item === "string" && item.trim().length > 0))) throw invalid(`intent.${field}`, `${field} must be an array of non-empty strings`);
13
+ if (Array.isArray(candidate) && new Set(candidate).size !== candidate.length) throw invalid(`intent.${field}`, `${field} must not contain duplicate values`);
14
+ }
15
+ for (const field of ["countries", "excludedCountries"] as const) {
16
+ const countries = value[field];
17
+ if (Array.isArray(countries) && !countries.every((country) => typeof country === "string" && /^[A-Za-z]{2}$/.test(country))) throw invalid(`intent.${field}`, `${field} must contain two-letter country codes`);
18
+ }
19
+ if (value.remote !== undefined && typeof value.remote !== "boolean") throw invalid("intent.remote", "remote must be a boolean");
20
+ return value as unknown as CandidateIntent;
21
+ }
22
+
23
+ export function assertKnownKeys(value: Record<string, unknown>, allowed: readonly string[], field: string, invalid: InvalidInput): void {
24
+ const unknown = Object.keys(value).find((key) => !allowed.includes(key));
25
+ if (unknown) throw invalid(`${field}.${unknown}`, `Unknown field: ${field}.${unknown}`);
26
+ }
27
+
28
+ export function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
@@ -0,0 +1,73 @@
1
+ import { isEligibleForCountry } from "./locations.ts";
2
+ import type { SnapshotStore } from "./crawler.ts";
3
+ import type { Company, JobSnapshot } from "./types.ts";
4
+
5
+ export interface CountryJobCoverage {
6
+ country: string;
7
+ indexedSourcesWithEligibleJobs: number;
8
+ eligibleJobs: number;
9
+ distinctEligibleEmployers: number;
10
+ }
11
+
12
+ export interface JobCoverageSummary {
13
+ snapshotUpdatedAt: string;
14
+ countries: CountryJobCoverage[];
15
+ }
16
+
17
+ export function createJobCoverageReader(options: { sources: Company[]; store: SnapshotStore }) {
18
+ return {
19
+ async getCoverage(value: unknown): Promise<JobCoverageSummary> {
20
+ const countries = validateCoverageInput(value);
21
+ const snapshot = await options.store.read();
22
+ if (!snapshot) throw new Error("No local job snapshot is available");
23
+ return projectJobCoverage(options.sources, snapshot, countries);
24
+ },
25
+ };
26
+ }
27
+
28
+ export function projectJobCoverage(sources: Company[], snapshot: JobSnapshot, countries: string[]): JobCoverageSummary {
29
+ const normalizedCountries = [...new Set(countries.map(normalizeCountry))];
30
+ const indexedSources = sources.flatMap((source) => {
31
+ const partition = snapshot.partitions[source.slug];
32
+ return partition ? [{ source, jobs: partition.jobs }] : [];
33
+ });
34
+
35
+ return {
36
+ snapshotUpdatedAt: snapshot.updatedAt,
37
+ countries: normalizedCountries.map((country) => {
38
+ const eligibleSources = indexedSources.filter(({ jobs }) => jobs.some((job) => isEligibleForCountry(job, country)));
39
+ return {
40
+ country,
41
+ indexedSourcesWithEligibleJobs: eligibleSources.length,
42
+ eligibleJobs: eligibleSources.reduce((total, { jobs }) => total + jobs.filter((job) => isEligibleForCountry(job, country)).length, 0),
43
+ distinctEligibleEmployers: new Set(eligibleSources.flatMap(({ source }) => source.companyDomain ? [normalizeDomain(source.companyDomain)] : [])).size,
44
+ };
45
+ }),
46
+ };
47
+ }
48
+
49
+ function normalizeCountry(country: string): string {
50
+ const normalized = country.toUpperCase();
51
+ if (!/^[A-Z]{2}$/u.test(normalized)) throw new Error("Coverage requires two-letter country codes");
52
+ return normalized;
53
+ }
54
+
55
+ function normalizeDomain(domain: string): string {
56
+ return domain.trim().toLowerCase().replace(/^www\./u, "");
57
+ }
58
+
59
+ function validateCoverageInput(value: unknown): string[] {
60
+ if (!isRecord(value)) throw new Error("Coverage input must be an object");
61
+ const unknown = Object.keys(value).find((key) => key !== "countries");
62
+ if (unknown) throw new Error(`get_job_coverage does not accept field: ${unknown}`);
63
+ if (!Array.isArray(value.countries) || value.countries.length === 0) throw new Error("get_job_coverage requires at least one country");
64
+ if (value.countries.length > 20) throw new Error("get_job_coverage accepts at most 20 countries");
65
+ if (!value.countries.every((country) => typeof country === "string" && /^[a-z]{2}$/iu.test(country))) {
66
+ throw new Error("countries must contain only two-letter country codes");
67
+ }
68
+ return value.countries;
69
+ }
70
+
71
+ function isRecord(value: unknown): value is Record<string, unknown> {
72
+ return typeof value === "object" && value !== null && !Array.isArray(value);
73
+ }
@@ -0,0 +1,247 @@
1
+ import { parseCandidateProfile, type CandidateProfile, type ResumeInput } from "./candidate-profile.ts";
2
+ import { matchJobs, type CandidateIntent, type JobMatch, type SupportedRequirement, type TransferableRequirement } from "./job-matching.ts";
3
+ import { assertKnownKeys, isRecord, validateCandidateIntent } from "./intent-validation.ts";
4
+ import type { Job } from "./types.ts";
5
+ import { evaluateScreeningRequirements, type ScreeningRequirement } from "./screening-requirements.ts";
6
+
7
+ export interface AnalyzeJobFitInput { jobId: string; resume: ResumeInput; intent?: CandidateIntent }
8
+ export interface JobFitReason {
9
+ claim: string;
10
+ candidateFactIds?: string[];
11
+ jobEvidence: { field: "title" | "description" | "eligibility" | "location" | "workMode" | "company"; quote: string; start?: number; end?: number };
12
+ }
13
+ export interface JobFitAssessment { fit: "strong" | "good" | "stretch" | "poor"; reasons: JobFitReason[] }
14
+ export interface PartiallySupportedRequirement extends SupportedRequirement {
15
+ via: TransferableRequirement["via"] | "experience_below_requirement";
16
+ }
17
+ export interface AnalyzeJobFitResult {
18
+ job: Job;
19
+ profile: CandidateProfile;
20
+ scores: JobMatch["scores"];
21
+ supported: SupportedRequirement[];
22
+ partiallySupported: PartiallySupportedRequirement[];
23
+ unsupported: string[];
24
+ screeningRisks: string[];
25
+ interviewPreparationGaps: string[];
26
+ assessment: JobFitAssessment;
27
+ }
28
+
29
+ export interface JobFitAnalyzerOptions { getJob(id: string): Promise<Job | null> }
30
+
31
+ export class JobFitAnalysisError extends Error {
32
+ constructor(readonly code: "invalid_job_fit_input" | "job_not_found", message: string, readonly field?: string) { super(message); }
33
+ }
34
+
35
+ export function createJobFitAnalyzer(options: JobFitAnalyzerOptions) {
36
+ return {
37
+ async analyze(value: unknown): Promise<AnalyzeJobFitResult> {
38
+ const input = validateJobFitInput(value);
39
+ const profile = parseCandidateProfile(input.resume);
40
+ const job = await options.getJob(input.jobId);
41
+ if (!job) throw new JobFitAnalysisError("job_not_found", `Unknown job: ${input.jobId}`, "jobId");
42
+ const intent = input.intent ?? {};
43
+ const filtered = matchJobs(profile, intent, [job], 1);
44
+ const relevance = filtered.matches[0] ?? matchJobs(profile, softIntent(intent), [job], 1).matches[0]!;
45
+ const screening = evaluateScreeningRequirements(profile, job);
46
+ const structured = structuredRequirements(job, profile, screening);
47
+ const supported = mergeSupported(relevance.supported, structured.supported);
48
+ const partiallySupported = mergePartial(relevance.transferable, structured.partial);
49
+ const resolved = new Set([...supported, ...partiallySupported].map((item) => item.requirement.toLocaleLowerCase()));
50
+ const certificationRequirements = [...structured.supported.map((item) => item.requirement), ...structured.unsupported]
51
+ .filter((item) => item.toLocaleLowerCase().endsWith(" certification"));
52
+ const matchingGaps = relevance.gaps.filter((gap) => !certificationRequirements.some((item) => item.toLocaleLowerCase() === `${gap.toLocaleLowerCase()} certification`));
53
+ const unsupported = compactRequirements(unique([...matchingGaps, ...structured.unsupported])
54
+ .filter((requirement) => !resolved.has(requirement.toLocaleLowerCase()))
55
+ .filter((requirement) => !supported.some((item) => item.requirement.toLocaleLowerCase() === `${requirement.toLocaleLowerCase()} certification`)));
56
+ const hardRisks = filtered.filteredOut.flatMap((candidate) => candidate.reasons.map(describeFilterRisk));
57
+ const requirementRisks = unsupported.map((requirement) =>
58
+ requirement.startsWith("Work authorization in ")
59
+ ? `Work authorization is required but cannot be inferred from resume silence or geographic intent: ${requirement.slice("Work authorization in ".length)}`
60
+ : screening.some((item) => item.requirement === requirement)
61
+ ? `Resume does not provide evidence satisfying mandatory ${screening.find((item) => item.requirement === requirement)!.kind} requirement: ${requirement}`
62
+ : `No explicit or transferable resume evidence supports required skill: ${requirement}`,
63
+ );
64
+ const partialScreeningRisks = screening.filter((item) => item.status === "partial").map((item) => `Resume evidence does not meet minimum requirement: ${item.requirement}`);
65
+ const seniorityRisks = relevance.reasons.filter((reason) => reason.startsWith("seniority differs"));
66
+ const screeningRisks = [...hardRisks, ...seniorityRisks, ...partialScreeningRisks, ...requirementRisks];
67
+ const fit = hardRisks.length || screening.some((item) => item.status !== "supported") || (!supported.length && !partiallySupported.length && unsupported.length)
68
+ ? "poor"
69
+ : relevance.fit === "strong" && unsupported.length ? "good" : relevance.fit;
70
+ return {
71
+ job,
72
+ profile,
73
+ scores: relevance.scores,
74
+ supported,
75
+ partiallySupported,
76
+ unsupported,
77
+ screeningRisks,
78
+ interviewPreparationGaps: [
79
+ ...partiallySupported.map((item) => `Prepare examples connecting ${item.via} evidence to the ${item.requirement} requirement without claiming direct experience`),
80
+ ...unsupported.map((requirement) => `Prepare a truthful response about the unsupported ${requirement} requirement; do not add it to the resume as experience`),
81
+ ],
82
+ assessment: {
83
+ fit,
84
+ reasons: assessmentReasons(job, profile, relevance.reasons, screeningRisks, supported, partiallySupported),
85
+ },
86
+ };
87
+ },
88
+ };
89
+ }
90
+
91
+ function structuredRequirements(job: Job, profile: CandidateProfile, screening: ScreeningRequirement[]): {
92
+ supported: SupportedRequirement[];
93
+ partial: PartiallySupportedRequirement[];
94
+ unsupported: string[];
95
+ } {
96
+ const supported: SupportedRequirement[] = [];
97
+ const partial: PartiallySupportedRequirement[] = [];
98
+ const unsupported: string[] = [];
99
+ const text = job.description;
100
+
101
+ for (const item of screening) {
102
+ if (item.status === "supported") supported.push({ requirement: item.requirement, factIds: item.factIds });
103
+ else if (item.status === "partial") partial.push({ requirement: item.requirement, via: "experience_below_requirement", factIds: item.factIds });
104
+ else unsupported.push(item.requirement);
105
+ }
106
+
107
+ for (const match of text.matchAll(/\b(?:authorized|authorization)\s+to\s+work\s+in\s+([A-Za-z][A-Za-z ]{1,30}?)(?=[.,;\n]|\s+(?:is|required|must)\b)/gi)) {
108
+ const country = match[1]!.trim();
109
+ unsupported.push(`Work authorization in ${country}`);
110
+ }
111
+
112
+ for (const match of text.matchAll(/\b([A-Za-z][A-Za-z0-9+.#-]{1,30})\s+certification\s+(?:is\s+)?(?:required|mandatory|must)\b/gi)) {
113
+ const name = match[1]!;
114
+ const label = `${name} certification`;
115
+ const fact = profile.facts.find((item) => item.kind === "certification" && includesTerm(item.value, name));
116
+ if (fact) supported.push({ requirement: label, factIds: [fact.id] }); else unsupported.push(label);
117
+ }
118
+ unsupported.push(...explicitRequirementLabels(text));
119
+ return { supported, partial, unsupported };
120
+ }
121
+
122
+ function explicitRequirementLabels(text: string): string[] {
123
+ const labels: string[] = [];
124
+ for (const sentence of text.split(/[.!?\n;]+/).map((value) => value.trim()).filter(Boolean)) {
125
+ const suffix = /^(.+?)\s+(?:is|are)\s+(?:strictly\s+)?required$/i.exec(sentence)?.[1];
126
+ const must = /^(?:candidates?\s+)?must\s+(?:have|possess|demonstrate)?\s*(.+)$/i.exec(sentence)?.[1];
127
+ const value = suffix ?? must;
128
+ if (!value || /\b(?:authorized|authorization)\s+to\s+work\b/i.test(value)) continue;
129
+ for (const atom of value.split(/,|\band\b/i)) {
130
+ const label = atom.trim().replace(/^(?:a|an|the|minimum of)\s+/i, "").replace(/^(?:experience|proficiency)\s+(?:with|in)\s+/i, "").trim();
131
+ if (label.length >= 2) labels.push(label[0]!.toUpperCase() + label.slice(1));
132
+ }
133
+ }
134
+ return unique(labels);
135
+ }
136
+
137
+ function assessmentReasons(job: Job, profile: CandidateProfile, matchingReasons: string[], risks: string[], supported: SupportedRequirement[], partial: PartiallySupportedRequirement[]): JobFitReason[] {
138
+ const result: JobFitReason[] = [];
139
+ for (const item of [...supported, ...partial]) {
140
+ result.push({ claim: `${item.requirement} is supported to the stated degree by resume evidence`, candidateFactIds: item.factIds, jobEvidence: jobDescriptionEvidence(job, item.requirement) });
141
+ }
142
+ for (const reason of matchingReasons.filter((value) => value.includes("title") || value.startsWith("seniority"))) {
143
+ const resumeGroundedIds = reason.startsWith("seniority")
144
+ ? profile.inferences.filter((item) => item.kind === "seniority").flatMap((item) => item.derivedFromFactIds)
145
+ : reason === "title aligns with resume role evidence"
146
+ ? [...profile.facts.filter((item) => item.kind === "role").map((item) => item.id), ...profile.inferences.filter((item) => item.kind === "role_family").flatMap((item) => item.derivedFromFactIds)]
147
+ : [];
148
+ result.push({ claim: reason, ...(resumeGroundedIds.length ? { candidateFactIds: unique(resumeGroundedIds) } : {}), jobEvidence: { field: "title", quote: job.title, start: 0, end: job.title.length } });
149
+ }
150
+ for (const risk of risks) {
151
+ const riskFactIds = risk.startsWith("seniority differs")
152
+ ? profile.inferences.filter((item) => item.kind === "seniority").flatMap((item) => item.derivedFromFactIds)
153
+ : partial.filter((item) => risk.includes(item.requirement)).flatMap((item) => item.factIds);
154
+ result.push({ claim: risk, ...(riskFactIds.length ? { candidateFactIds: unique(riskFactIds) } : {}), jobEvidence: evidenceForRisk(job, risk) });
155
+ }
156
+ const merged = new Map<string, JobFitReason>();
157
+ for (const reason of result) {
158
+ const key = JSON.stringify([reason.claim, reason.jobEvidence]);
159
+ const existing = merged.get(key);
160
+ if (!existing) merged.set(key, reason);
161
+ else merged.set(key, { ...existing, candidateFactIds: unique([...(existing.candidateFactIds ?? []), ...(reason.candidateFactIds ?? [])]) });
162
+ }
163
+ return [...merged.values()];
164
+ }
165
+
166
+ function evidenceForRisk(job: Job, risk: string): JobFitReason["jobEvidence"] {
167
+ if (/country/i.test(risk)) return { field: "eligibility", quote: JSON.stringify({ eligibleCountries: job.eligibleCountries, excludedCountries: job.excludedCountries }) };
168
+ if (/location/i.test(risk)) return { field: "location", quote: job.location };
169
+ if (/excluded role/i.test(risk) || risk.startsWith("seniority differs")) return { field: "title", quote: job.title, start: 0, end: job.title.length };
170
+ if (/remote|work-mode/i.test(risk)) return { field: "workMode", quote: job.workMode };
171
+ if (/excluded term/i.test(risk)) {
172
+ const term = risk.split(":", 2)[1]?.trim() ?? "";
173
+ for (const [field, value] of [["title", job.title], ["company", job.company], ["location", job.location], ["description", job.description]] as const) {
174
+ const start = value.toLocaleLowerCase().indexOf(term.toLocaleLowerCase());
175
+ if (start >= 0) return { field, quote: value.slice(start, start + term.length), start, end: start + term.length };
176
+ }
177
+ }
178
+ return jobDescriptionEvidence(job, riskRequirement(risk));
179
+ }
180
+
181
+ function riskRequirement(risk: string): string {
182
+ return risk.match(/required skill: (.+)$/)?.[1] ?? risk.match(/intent: (.+)$/)?.[1] ?? "";
183
+ }
184
+
185
+ function jobDescriptionEvidence(job: Job, requirement: string): JobFitReason["jobEvidence"] {
186
+ const start = requirement ? job.description.toLocaleLowerCase().indexOf(requirement.toLocaleLowerCase()) : -1;
187
+ if (start >= 0) return { field: "description", quote: job.description.slice(start, start + requirement.length), start, end: start + requirement.length };
188
+ return { field: "description", quote: job.description };
189
+ }
190
+
191
+ function includesTerm(value: string, term: string): boolean {
192
+ return new RegExp(`(^|[^A-Za-z0-9+.#])${escapeRegex(term)}(?=$|[^A-Za-z0-9+.#])`, "i").test(value);
193
+ }
194
+
195
+ function escapeRegex(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
196
+ function unique(values: string[]): string[] { return [...new Set(values.map((value) => value.trim()).filter(Boolean))]; }
197
+ function compactRequirements(values: string[]): string[] {
198
+ return values.filter((value, index) => !values.some((other, otherIndex) => otherIndex !== index && other.length < value.length && includesTerm(value, other)));
199
+ }
200
+ function mergeSupported(...groups: SupportedRequirement[][]): SupportedRequirement[] {
201
+ const merged = new Map<string, SupportedRequirement>();
202
+ for (const item of groups.flat()) {
203
+ const key = item.requirement.toLocaleLowerCase();
204
+ const existing = merged.get(key);
205
+ if (existing) existing.factIds = unique([...existing.factIds, ...item.factIds]); else merged.set(key, { ...item, factIds: [...item.factIds] });
206
+ }
207
+ return [...merged.values()];
208
+ }
209
+ function mergePartial(...groups: PartiallySupportedRequirement[][]): PartiallySupportedRequirement[] {
210
+ const merged = new Map<string, PartiallySupportedRequirement>();
211
+ for (const item of groups.flat()) if (!merged.has(item.requirement.toLocaleLowerCase())) merged.set(item.requirement.toLocaleLowerCase(), item);
212
+ return [...merged.values()];
213
+ }
214
+
215
+ function validateJobFitInput(value: unknown): AnalyzeJobFitInput {
216
+ if (!isRecord(value)) throw invalidFitInput("input", "Job fit input must be an object");
217
+ assertKnownKeys(value, ["jobId", "resume", "intent"], "input", invalidFitInput);
218
+ if (typeof value.jobId !== "string" || !value.jobId.trim()) throw invalidFitInput("jobId", "jobId must be a non-empty string");
219
+ if (!isRecord(value.resume)) throw invalidFitInput("resume", "Resume input must be an object");
220
+ assertKnownKeys(value.resume, ["content", "format"], "resume", invalidFitInput);
221
+ const intent = validateCandidateIntent(value.intent, invalidFitInput, { optional: true });
222
+ return { jobId: value.jobId, resume: value.resume as unknown as ResumeInput, ...(value.intent === undefined ? {} : { intent }) };
223
+ }
224
+
225
+ function invalidFitInput(field: string, message: string): JobFitAnalysisError {
226
+ return new JobFitAnalysisError("invalid_job_fit_input", message, field);
227
+ }
228
+
229
+ function softIntent(intent: CandidateIntent): CandidateIntent {
230
+ const { countries: _countries, locations: _locations, remote: _remote, excludedCountries: _excludedCountries, excludedLocations: _excludedLocations, excludedRoles: _excludedRoles, excludedTerms: _excludedTerms, ...soft } = intent;
231
+ return soft;
232
+ }
233
+
234
+ function describeFilterRisk(reason: string): string {
235
+ const [kind, detail] = reason.split(":", 2);
236
+ const descriptions: Record<string, string> = {
237
+ country_not_eligible: `Job is not eligible for requested country: ${detail}`,
238
+ country_excluded: `Job is eligible in an excluded country: ${detail}`,
239
+ location_mismatch: "Job location does not match explicit location intent",
240
+ location_excluded: `Job is in an excluded location: ${detail}`,
241
+ role_excluded: `Job matches an excluded role: ${detail}`,
242
+ remote_required: "Job is not explicitly remote",
243
+ non_remote_required: "Job does not satisfy explicit non-remote intent",
244
+ excluded_term: `Job contains an excluded term: ${detail}`,
245
+ };
246
+ return descriptions[kind ?? ""] ?? reason;
247
+ }