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
package/src/mcp.ts ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bun
2
+ import { createRuntime } from "./runtime.ts";
3
+ import { createToolHandler } from "./tools.ts";
4
+
5
+ interface RpcRequest {
6
+ jsonrpc: "2.0";
7
+ id?: string | number | null;
8
+ method: string;
9
+ params?: unknown;
10
+ }
11
+
12
+ interface ToolHandler {
13
+ list(): Array<{ name: string; description: string; inputSchema: Record<string, unknown> }>;
14
+ call(name: string, input: Record<string, unknown>): Promise<unknown>;
15
+ }
16
+
17
+ export function createMcpHandler(tools: ToolHandler) {
18
+ return async (value: unknown) => {
19
+ if (!isRpcRequest(value)) return { jsonrpc: "2.0" as const, id: invalidRequestId(value), error: { code: -32600, message: "Invalid Request" } };
20
+ const request = value;
21
+ if (!("id" in request)) return null;
22
+ const base = { jsonrpc: "2.0" as const, id: request.id ?? null };
23
+ try {
24
+ if (request.method === "initialize") {
25
+ return { ...base, result: { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "openings", version: "0.1.0" } } };
26
+ }
27
+ if (request.method === "ping") return { ...base, result: {} };
28
+ if (request.method === "tools/list") return { ...base, result: { tools: tools.list() } };
29
+ if (request.method === "tools/call") {
30
+ if (!isRecord(request.params)) throw new Error("tools/call params must be an object");
31
+ const name = request.params.name;
32
+ const args = request.params.arguments;
33
+ if (typeof name !== "string") throw new Error("tools/call requires a tool name");
34
+ if (args !== undefined && !isRecord(args)) throw new Error("tools/call arguments must be an object");
35
+ try {
36
+ const result = await tools.call(name, args ?? {});
37
+ return { ...base, result: { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false } };
38
+ } catch (error) {
39
+ const details = errorDetails(error);
40
+ const payload = { error: { message: error instanceof Error ? error.message : String(error), ...(details ?? {}) } };
41
+ return { ...base, result: { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], isError: true } };
42
+ }
43
+ }
44
+ if (request.method.startsWith("notifications/")) return null;
45
+ return { ...base, error: { code: -32601, message: `Method not found: ${request.method}` } };
46
+ } catch (error) {
47
+ return { ...base, error: { code: -32602, message: error instanceof Error ? error.message : String(error) } };
48
+ }
49
+ };
50
+ }
51
+
52
+ function isRpcRequest(value: unknown): value is RpcRequest {
53
+ if (!isRecord(value) || value.jsonrpc !== "2.0" || typeof value.method !== "string") return false;
54
+ return !("id" in value) || value.id === null || typeof value.id === "string" || typeof value.id === "number";
55
+ }
56
+
57
+ function invalidRequestId(value: unknown): string | number | null {
58
+ if (!isRecord(value)) return null;
59
+ return typeof value.id === "string" || typeof value.id === "number" || value.id === null ? value.id : null;
60
+ }
61
+
62
+ function errorDetails(error: unknown): Record<string, unknown> | undefined {
63
+ if (!isRecord(error)) return undefined;
64
+ const allowed = new Set(["code", "field", "format", "supportedFormats"]);
65
+ const entries = Object.entries(error).filter(([key, value]) => allowed.has(key) && value !== undefined);
66
+ return entries.length ? Object.fromEntries(entries) : undefined;
67
+ }
68
+
69
+ function isRecord(value: unknown): value is Record<string, unknown> {
70
+ return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ }
72
+
73
+ export async function serve() {
74
+ const runtime = createRuntime();
75
+ const catalog = {
76
+ search: async (query: import("./types.ts").SearchQuery) => (await runtime.search(query, { offline: false, staleDays: 14 })).jobs,
77
+ get: async (id: string) => (await runtime.get(id, { offline: false, staleDays: 14 })).job,
78
+ };
79
+ const handle = createMcpHandler(createToolHandler(catalog, runtime));
80
+ const decoder = new TextDecoder();
81
+ let buffer = "";
82
+ for await (const chunk of Bun.stdin.stream()) {
83
+ buffer += decoder.decode(chunk, { stream: true });
84
+ const lines = buffer.split("\n");
85
+ buffer = lines.pop() ?? "";
86
+ for (const line of lines) {
87
+ if (!line.trim()) continue;
88
+ try {
89
+ const response = await handle(JSON.parse(line));
90
+ if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
91
+ } catch {
92
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } })}\n`);
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ if (import.meta.main) await serve();
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bun
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ process.env.OPENINGS_DATA_DIR ??= join(homedir(), ".openings");
6
+
7
+ const { serve } = await import("./mcp.ts");
8
+ await serve();
@@ -0,0 +1,114 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join, resolve, sep } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { atomicJson } from "./atomic-file.ts";
5
+ import { assertArtifactFile, openingsRootFor } from "./artifact-path.ts";
6
+ import { deriveLeadState, mergeEnrichmentLeads, readEnrichmentRegistry, type EnrichmentLead } from "./enrichment-registry.ts";
7
+ import { withFileLock } from "./file-lock.ts";
8
+ import { stampReport } from "./report-meta.ts";
9
+ import type { SourceCandidate } from "./types.ts";
10
+
11
+ export interface JoinedRecruiteeIdentity {
12
+ token: string;
13
+ companyName: string;
14
+ companyDomain: string;
15
+ reference: string;
16
+ method: "normalized_token" | "normalized_domain";
17
+ provenanceKind: "authoritative_dataset" | "company_owned_page" | "business_registry";
18
+ }
19
+
20
+ interface ArtifactOptions { artifactRoot: string; existingCompanyDomains?: string[]; now?: Date }
21
+ interface GenerationManifest { version: 1; generation: string; registryPath: string; candidatesPath: string; reportPath: string; identitiesPath: string }
22
+
23
+ const approvedDatasetReferences = new Set([".openings/company-domains.json", ".openings/lever-ashby-seeds.json", "data/companies-career-page.md"]);
24
+ const approvedBusinessRegistryDomains = ["mca.gov.in", "data.gov.in"];
25
+
26
+ export async function buildRecruiteeRoundArtifacts(identities: JoinedRecruiteeIdentity[], options: ArtifactOptions) {
27
+ const root = resolve(options.artifactRoot);
28
+ if (!root.split(sep).includes(".openings")) throw new Error("Round 5 artifact root must be under .openings");
29
+ const securityRoot = openingsRootFor(root);
30
+ const manifestPath = join(root, "current.json");
31
+ await assertArtifactFile(manifestPath, securityRoot);
32
+ const now = options.now ?? new Date();
33
+ return withFileLock(manifestPath, async () => {
34
+ const priorManifest = await readManifest(manifestPath, root);
35
+ if (priorManifest) await assertArtifactFile(priorManifest.identitiesPath, securityRoot);
36
+ const priorIdentities = priorManifest ? await readIdentities(priorManifest.identitiesPath) : [];
37
+ const allIdentities = uniqueIdentities([...priorIdentities, ...identities].map(validateIdentity));
38
+ const existing = new Set((options.existingCompanyDomains ?? []).map(normalizeDomain));
39
+ const grouped = new Map<string, JoinedRecruiteeIdentity[]>();
40
+ for (const identity of allIdentities) { const values = grouped.get(identity.token) ?? []; values.push(identity); grouped.set(identity.token, values); }
41
+
42
+ const leads: EnrichmentLead[] = [];
43
+ const candidates: SourceCandidate[] = [];
44
+ const quarantines: Array<{ token: string; domains: string[] }> = [];
45
+ const excluded: Array<{ token: string; companyDomain: string }> = [];
46
+ for (const [token, values] of [...grouped].sort(([left], [right]) => left.localeCompare(right))) {
47
+ const domains = [...new Set(values.map((value) => value.companyDomain))].sort();
48
+ if (domains.length !== 1) { quarantines.push({ token, domains }); continue; }
49
+ const companyDomain = domains[0]!;
50
+ if (existing.has(companyDomain)) { excluded.push({ token, companyDomain }); continue; }
51
+ const primary = [...values].sort((left, right) => left.reference.localeCompare(right.reference) || left.companyName.localeCompare(right.companyName))[0]!;
52
+ const sourceUrl = `https://${token}.recruitee.com`;
53
+ const discoveredFrom = { channel: "dataset" as const, reference: primary.reference };
54
+ const lead: EnrichmentLead = { sourceKey: `recruitee:${token}`, sourceUrl, ats: "recruitee", token, discoveredFrom: [discoveredFrom], companyMatches: values.map((value) => ({ companyName: value.companyName, companyDomain, method: value.method, reference: value.reference })), identityEvidence: [], attempts: [] };
55
+ if (deriveLeadState(lead) !== "matched") throw new Error(`Round 5 lead ${token} did not derive matched state`);
56
+ leads.push(lead);
57
+ candidates.push({ companyName: primary.companyName, companyDomain, sourceUrl, discoveredFrom });
58
+ }
59
+
60
+ const generation = createHash("sha256").update(JSON.stringify({ now: now.toISOString(), identities: allIdentities, existing: [...existing].sort() })).digest("hex").slice(0, 20);
61
+ const generationRoot = join(root, "generations", generation);
62
+ const registryPath = join(generationRoot, "registry.json");
63
+ const candidatesPath = join(generationRoot, "candidates.json");
64
+ const reportPath = join(generationRoot, "join-report.json");
65
+ const identitiesPath = join(generationRoot, "identities.json");
66
+ for (const path of [registryPath, candidatesPath, reportPath, identitiesPath]) await assertArtifactFile(path, securityRoot);
67
+ const report = stampReport("round5-recruitee-join:1", 1, { identities: allIdentities.length, matched: leads.length, quarantined: quarantines.length, excludedExisting: excluded.length, registryPath, candidatesPath, quarantines, excluded }, now);
68
+ await atomicJson(registryPath, { version: 1, updatedAt: now.toISOString(), leads });
69
+ await atomicJson(candidatesPath, candidates);
70
+ await atomicJson(reportPath, report);
71
+ await atomicJson(identitiesPath, allIdentities);
72
+ const manifest: GenerationManifest = { version: 1, generation, registryPath, candidatesPath, reportPath, identitiesPath };
73
+ await atomicJson(manifestPath, manifest);
74
+ return { ...report, manifestPath, generation };
75
+ }, { operation: "publish paired Round 5 Recruitee artifacts" });
76
+ }
77
+
78
+ export async function prepareRecruiteeRoundArtifacts(identityPath: string, catalogPath: string, options: ArtifactOptions) {
79
+ const identities = await readIdentities(identityPath);
80
+ const catalog: unknown = JSON.parse(await readFile(catalogPath, "utf8"));
81
+ if (!record(catalog)) throw new Error("Round 5 catalog must be a JSON object");
82
+ const existingCompanyDomains = Object.values(catalog).flatMap((value) => record(value) && typeof value.companyDomain === "string" ? [value.companyDomain] : []);
83
+ return buildRecruiteeRoundArtifacts(identities, { ...options, existingCompanyDomains });
84
+ }
85
+
86
+ export async function mergeAttemptedRoundLeads(isolatedRegistryPath: string, sharedRegistryPath: string, now = new Date()) {
87
+ if (resolve(isolatedRegistryPath) === resolve(sharedRegistryPath)) throw new Error("Isolated and shared enrichment registries must be different files");
88
+ const root = roundRootFromRegistry(isolatedRegistryPath);
89
+ await assertArtifactFile(isolatedRegistryPath, openingsRootFor(root));
90
+ const manifestPath = join(root, "current.json");
91
+ await assertArtifactFile(manifestPath, openingsRootFor(root));
92
+ // Publication and merging share this outer lock; the registry lock is always acquired second.
93
+ return withFileLock(manifestPath, async () => {
94
+ const manifest = await readManifest(manifestPath, root);
95
+ if (!manifest || resolve(manifest.registryPath) !== resolve(isolatedRegistryPath)) throw new Error("Isolated registry must be the current Round 5 generation registry");
96
+ return withFileLock(isolatedRegistryPath, async () => {
97
+ const isolated = await readEnrichmentRegistry(isolatedRegistryPath);
98
+ const attempted = isolated.leads.filter((lead) => lead.attempts.length > 0);
99
+ const merge = await mergeEnrichmentLeads(sharedRegistryPath, attempted, now);
100
+ return { selected: attempted.length, ...merge };
101
+ }, { operation: "merge attempted Round 5 Recruitee leads" });
102
+ }, { operation: "select current Round 5 Recruitee generation" });
103
+ }
104
+
105
+ async function readManifest(path: string, root: string): Promise<GenerationManifest | undefined> { try { const value: unknown = JSON.parse(await readFile(path, "utf8")); if (!record(value) || value.version !== 1 || typeof value.generation !== "string" || !/^[a-f0-9]{20}$/.test(value.generation) || typeof value.registryPath !== "string" || typeof value.candidatesPath !== "string" || typeof value.reportPath !== "string" || typeof value.identitiesPath !== "string") throw new Error("Invalid Round 5 artifact manifest"); const generationRoot = join(root, "generations", value.generation); const expected: GenerationManifest = { version: 1, generation: value.generation, registryPath: join(generationRoot, "registry.json"), candidatesPath: join(generationRoot, "candidates.json"), reportPath: join(generationRoot, "join-report.json"), identitiesPath: join(generationRoot, "identities.json") }; const supplied = value as unknown as GenerationManifest; for (const key of ["registryPath", "candidatesPath", "reportPath", "identitiesPath"] as const) if (resolve(supplied[key]) !== resolve(expected[key])) throw new Error("Round 5 manifest contains an unexpected artifact path"); return expected; } catch (error) { if (error instanceof Error && "code" in error && error.code === "ENOENT") return undefined; throw error; } }
106
+ async function readIdentities(path: string): Promise<JoinedRecruiteeIdentity[]> { const value: unknown = JSON.parse(await readFile(path, "utf8")); if (!Array.isArray(value)) throw new Error("Round 5 joined identities must be a JSON array"); return value.map(parseIdentity); }
107
+ function uniqueIdentities(values: JoinedRecruiteeIdentity[]) { return [...new Map(values.map((value) => [JSON.stringify(value), value])).values()].sort((a, b) => a.token.localeCompare(b.token) || a.companyDomain.localeCompare(b.companyDomain) || a.reference.localeCompare(b.reference)); }
108
+ function validateIdentity(value: JoinedRecruiteeIdentity): JoinedRecruiteeIdentity { const token = value.token.trim().toLowerCase(); const companyName = value.companyName.trim(); const companyDomain = normalizeDomain(value.companyDomain); const reference = value.reference.trim(); if (!/^[a-z0-9-]+$/.test(token)) throw new Error(`Invalid Recruitee token: ${value.token}`); if (!companyName) throw new Error("Joined companyName must be non-empty"); if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(companyDomain)) throw new Error(`Invalid joined company domain: ${value.companyDomain}`); if (!["normalized_token", "normalized_domain"].includes(value.method)) throw new Error("Round 5 identity method must be an explicit exact non-search match"); const normalizedToken = normalizeMatchValue(token); const observedMatch = value.method === "normalized_token" ? normalizeMatchValue(companyName) : normalizeMatchValue(companyDomain.split(".")[0]!); if (observedMatch !== normalizedToken) throw new Error(`Round 5 ${value.method} does not exactly match the Recruitee token`); if (!["authoritative_dataset", "company_owned_page", "business_registry"].includes(value.provenanceKind)) throw new Error("Round 5 identity requires authoritative provenance"); if (value.provenanceKind === "authoritative_dataset" && !approvedDatasetReferences.has(reference)) throw new Error("Round 5 authoritative_dataset reference is not an approved dataset"); if (value.provenanceKind !== "authoritative_dataset") { let url: URL; try { url = new URL(reference); } catch { throw new Error("Manual Round 5 identity references must be HTTPS URLs"); } if (url.protocol !== "https:") throw new Error("Manual Round 5 identity references must be HTTPS URLs"); if (value.provenanceKind === "company_owned_page" && !domainContains(url.hostname, companyDomain)) throw new Error("Company-owned identity reference must match the joined company domain"); if (value.provenanceKind === "business_registry" && !approvedBusinessRegistryDomains.some((domain) => domainContains(url.hostname, domain))) throw new Error("Round 5 business_registry reference is not an approved business registry"); } return { ...value, token, companyName, companyDomain, reference }; }
109
+ function parseIdentity(value: unknown): JoinedRecruiteeIdentity { if (!record(value) || typeof value.token !== "string" || typeof value.companyName !== "string" || typeof value.companyDomain !== "string" || typeof value.reference !== "string" || typeof value.method !== "string" || typeof value.provenanceKind !== "string") throw new Error("Each Round 5 identity requires token, companyName, companyDomain, reference, method, and provenanceKind strings"); return validateIdentity(value as unknown as JoinedRecruiteeIdentity); }
110
+ function record(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
111
+ function normalizeDomain(value: string): string { return value.trim().toLowerCase().replace(/^www\./, ""); }
112
+ function normalizeMatchValue(value: string): string { return value.toLowerCase().replace(/[^a-z0-9]/g, ""); }
113
+ function domainContains(host: string, domain: string) { const value = normalizeDomain(host); return value === domain || value.endsWith(`.${domain}`); }
114
+ function roundRootFromRegistry(path: string): string { const absolute = resolve(path); const marker = `${sep}generations${sep}`; const index = absolute.lastIndexOf(marker); if (index < 0) throw new Error("Isolated registry must belong to a Round 5 generation"); return absolute.slice(0, index); }
@@ -0,0 +1,17 @@
1
+ export interface ReportMeta {
2
+ schemaVersion: number;
3
+ pipelineVersion: string;
4
+ generatedAt: string;
5
+ }
6
+
7
+ export function stampReport<T extends object>(pipelineVersion: string, schemaVersion: number, payload: T, now = new Date()): T & ReportMeta {
8
+ return { ...payload, schemaVersion, pipelineVersion, generatedAt: now.toISOString() };
9
+ }
10
+
11
+ export function assertCompatibleReport(value: unknown, pipelineVersion: string, schemaVersion: number): asserts value is ReportMeta & Record<string, unknown> {
12
+ if (!value || typeof value !== "object") throw new Error("Report must be a JSON object");
13
+ const report = value as Record<string, unknown>;
14
+ if (report.pipelineVersion !== pipelineVersion || report.schemaVersion !== schemaVersion || typeof report.generatedAt !== "string" || !Number.isFinite(Date.parse(report.generatedAt))) {
15
+ throw new Error(`Incompatible report: expected ${pipelineVersion} schema ${schemaVersion}; regenerate it with the current CLI`);
16
+ }
17
+ }
@@ -0,0 +1,111 @@
1
+ export type TransferabilityKind = "backend_programming" | "frontend_programming" | "data_engineering" | "cloud_infrastructure";
2
+
3
+ interface RequirementDefinition {
4
+ canonical: string;
5
+ display: string;
6
+ aliases?: string[];
7
+ evidence: "exact_skill" | "fact_phrase";
8
+ matching?: { caseSensitive?: boolean; excludedPhrases?: string[]; excludedPatterns?: string[] };
9
+ }
10
+
11
+ interface TransferabilityEdge {
12
+ target: string;
13
+ sources: string[];
14
+ via: TransferabilityKind;
15
+ rationale: string;
16
+ }
17
+
18
+ const requirementDefinitions: RequirementDefinition[] = [
19
+ ...exactSkills([["java", "Java"]]),
20
+ exactSkill("go", "Go", undefined, { caseSensitive: true, excludedPhrases: ["go-to-market", "go-live", "on-the-go"] }),
21
+ ...exactSkills([
22
+ ["golang", "Golang"], ["python", "Python"], ["ruby", "Ruby"], ["node.js", "Node.js"],
23
+ ["postgresql", "PostgreSQL"], ["sql", "SQL"], ["javascript", "JavaScript"], ["typescript", "TypeScript"],
24
+ ["vue", "Vue"], ["angular", "Angular"], ["spark", "Spark"], ["hadoop", "Hadoop"], ["dbt", "dbt"], ["airflow", "Airflow"],
25
+ ["snowflake", "Snowflake"], ["aws", "AWS"], ["azure", "Azure"], ["gcp", "GCP"], ["kubernetes", "Kubernetes"],
26
+ ["docker", "Docker"], ["terraform", "Terraform"], ["kafka", "Kafka"], ["databricks", "Databricks"], ["redis", "Redis"],
27
+ ["mongodb", "MongoDB"], ["linux", "Linux"], ["nosql", "NoSQL"], ["c++", "C++"], ["machine learning", "Machine learning"],
28
+ ["jenkins", "Jenkins"], ["spring boot", "Spring Boot"],
29
+ ]),
30
+ exactSkill("react", "React", ["react.js", "reactjs"]),
31
+ exactSkill("spring", "Spring", undefined, {
32
+ excludedPatterns: [String.raw`\bspring(?:\s*[/,&-]\s*(?:fall|summer|winter))*\s+(?:semester\s+)?(?:19|20)\d{2}\b`],
33
+ }),
34
+ exactSkill("ci/cd", "CI/CD", ["continuous integration and continuous delivery", "continuous integration/continuous delivery"]),
35
+ exactSkill("llm", "LLM", ["large language model", "large language models"]),
36
+ phrase("financial products", "Financial products"),
37
+ phrase("data warehouses", "Data warehouses", ["data warehouse"]),
38
+ phrase("etl pipelines", "ETL pipelines", ["etl pipeline"]),
39
+ phrase("high-volume messaging", "High-volume messaging"),
40
+ phrase("streaming platforms", "Streaming platforms", ["streaming platform"]),
41
+ phrase("transaction processing", "Transaction processing", ["transaction-processing"]),
42
+ phrase("restful services", "RESTful services"),
43
+ phrase("microservices", "Microservices"),
44
+ ];
45
+
46
+ // Empty by default. Every future edge requires a durable rationale and public-behavior tests.
47
+ const transferabilityEdges: TransferabilityEdge[] = [];
48
+
49
+ const definitionsByCanonical = new Map(requirementDefinitions.map((definition) => [definition.canonical, definition]));
50
+
51
+ export function detectRequirementTerms(value: string): string[] {
52
+ const occurrences = requirementDefinitions.flatMap((definition) => matchingTerms(definition).flatMap((term) => findOccurrences(value, term, definition)));
53
+ const accepted: typeof occurrences = [];
54
+ for (const occurrence of occurrences.sort((left, right) => right.length - left.length || left.start - right.start)) {
55
+ if (!accepted.some((candidate) => occurrence.start < candidate.end && occurrence.end > candidate.start)) accepted.push(occurrence);
56
+ }
57
+ const matched = new Set(accepted.map((occurrence) => occurrence.definition.canonical));
58
+ return requirementDefinitions.filter((definition) => matched.has(definition.canonical)).map((definition) => definition.display);
59
+ }
60
+
61
+ export function matchesExactSkillEvidence(requirement: string, skill: string): boolean {
62
+ const definition = definitionsByCanonical.get(requirement.toLocaleLowerCase());
63
+ if (definition?.evidence !== "exact_skill") return false;
64
+ const normalized = skill.trim().toLocaleLowerCase();
65
+ return [definition.canonical, definition.display, ...(definition.aliases ?? [])].some((value) => value.toLocaleLowerCase() === normalized);
66
+ }
67
+
68
+ export function requiresExactSkillEvidence(requirement: string): boolean {
69
+ return definitionsByCanonical.get(requirement.toLocaleLowerCase())?.evidence === "exact_skill";
70
+ }
71
+
72
+ export function findTransferability(requirement: string, skills: Array<{ id: string; value: string }>): { via: TransferabilityKind; factIds: string[] } | undefined {
73
+ const edge = transferabilityEdges.find((candidate) => candidate.target === requirement.toLocaleLowerCase());
74
+ if (!edge) return undefined;
75
+ const facts = skills.filter((skill) => edge.sources.includes(skill.value.toLocaleLowerCase()));
76
+ return facts.length ? { via: edge.via, factIds: facts.map((fact) => fact.id) } : undefined;
77
+ }
78
+
79
+ function exactSkills(values: Array<[canonical: string, display: string]>): RequirementDefinition[] {
80
+ return values.map(([canonical, display]) => ({ canonical, display, evidence: "exact_skill" }));
81
+ }
82
+
83
+ function exactSkill(canonical: string, display: string, aliases?: string[], matching?: RequirementDefinition["matching"]): RequirementDefinition {
84
+ return { canonical, display, aliases, evidence: "exact_skill", matching };
85
+ }
86
+
87
+ function phrase(canonical: string, display: string, aliases?: string[]): RequirementDefinition {
88
+ return { canonical, display, aliases, evidence: "fact_phrase" };
89
+ }
90
+
91
+ function matchingTerms(definition: RequirementDefinition): string[] {
92
+ const base = definition.matching?.caseSensitive ? [definition.display, ...(definition.aliases ?? [])] : [definition.canonical, ...(definition.aliases ?? [])];
93
+ return [...new Set(base.flatMap((term) => [term, term.replace(/-/gu, " "), term.replace(/ /gu, "-")]))];
94
+ }
95
+
96
+ function findOccurrences(value: string, term: string, definition: RequirementDefinition): Array<{ definition: RequirementDefinition; start: number; end: number; length: number }> {
97
+ let searchable = value;
98
+ for (const excluded of definition.matching?.excludedPhrases ?? []) searchable = maskMatches(searchable, new RegExp(escapeRegex(excluded), "giu"));
99
+ for (const pattern of definition.matching?.excludedPatterns ?? []) searchable = maskMatches(searchable, new RegExp(pattern, "giu"));
100
+ const escaped = escapeRegex(term);
101
+ const flags = definition.matching?.caseSensitive ? "gu" : "giu";
102
+ const matches = searchable.matchAll(new RegExp(`(^|[^a-z0-9+#])(${escaped})(?=$|[^a-z0-9+#])`, flags));
103
+ return [...matches].map((match) => {
104
+ const start = match.index + match[1]!.length;
105
+ const end = start + match[2]!.length;
106
+ return { definition, start, end, length: end - start };
107
+ });
108
+ }
109
+
110
+ function escapeRegex(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); }
111
+ function maskMatches(value: string, pattern: RegExp): string { return value.replace(pattern, (match) => " ".repeat(match.length)); }
@@ -0,0 +1,118 @@
1
+ import type { CandidateProfile, NormalizedResume, ResumeInput } from "./candidate-profile.ts";
2
+ import type { AnalyzeJobFitResult, JobFitReason } from "./job-fit-analysis.ts";
3
+ import { assertKnownKeys, isRecord } from "./intent-validation.ts";
4
+ import type { Job } from "./types.ts";
5
+
6
+ export type ResumeOptimizationOutput = "suggestions" | "unified_diff" | "revised_markdown";
7
+ export interface OptimizeResumeInput { jobId: string; resume: ResumeInput; output: ResumeOptimizationOutput }
8
+ export interface ResumeSuggestion {
9
+ action: "elevate";
10
+ proposedText: string;
11
+ factIds: string[];
12
+ jobEvidence: JobFitReason["jobEvidence"];
13
+ rationale: string;
14
+ }
15
+ export interface OptimizeResumeResult {
16
+ job: Job;
17
+ profile: CandidateProfile;
18
+ output: ResumeOptimizationOutput;
19
+ suggestions: ResumeSuggestion[];
20
+ gaps: string[];
21
+ content?: string;
22
+ diffBase?: NormalizedResume;
23
+ originalOverwritten: false;
24
+ }
25
+ export interface ResumeOptimizerOptions { analyzeJobFit(input: unknown): Promise<AnalyzeJobFitResult> }
26
+
27
+ export class ResumeOptimizationError extends Error {
28
+ constructor(readonly code: "invalid_resume_optimization_input", message: string, readonly field?: string) { super(message); }
29
+ }
30
+
31
+ export function createResumeOptimizer(options: ResumeOptimizerOptions) {
32
+ return {
33
+ async optimize(value: unknown): Promise<OptimizeResumeResult> {
34
+ const input = validateInput(value);
35
+ const analysis = await options.analyzeJobFit({ jobId: input.jobId, resume: input.resume });
36
+ const facts = new Map(analysis.profile.facts.map((fact) => [fact.id, fact]));
37
+ const suggestions: ResumeSuggestion[] = [];
38
+ const used = new Set<string>();
39
+ for (const support of analysis.supported) {
40
+ const jobReason = analysis.assessment.reasons.find((candidate) => candidate.candidateFactIds?.some((id) => support.factIds.includes(id)));
41
+ for (const fact of analysis.profile.facts.filter((candidate) => achievementFactKinds.has(candidate.kind) && includesPhrase(candidate.value, support.requirement))) {
42
+ if (used.has(fact.id)) continue;
43
+ used.add(fact.id);
44
+ suggestions.push({
45
+ action: "elevate",
46
+ proposedText: fact.value,
47
+ factIds: [fact.id],
48
+ jobEvidence: jobReason?.jobEvidence ?? { field: "description", quote: analysis.job.description },
49
+ rationale: `Elevate this complete existing achievement because it demonstrates ${support.requirement}`,
50
+ });
51
+ }
52
+ for (const factId of support.factIds) {
53
+ const fact = facts.get(factId);
54
+ if (!fact || !editableFactKinds.has(fact.kind) || used.has(fact.id)) continue;
55
+ used.add(fact.id);
56
+ suggestions.push({
57
+ action: "elevate",
58
+ proposedText: fact.value,
59
+ factIds: [fact.id],
60
+ jobEvidence: jobReason?.jobEvidence ?? { field: "description", quote: analysis.job.description },
61
+ rationale: `Elevate this existing resume fact because it directly supports ${support.requirement}`,
62
+ });
63
+ }
64
+ }
65
+ return {
66
+ job: analysis.job,
67
+ profile: analysis.profile,
68
+ output: input.output,
69
+ suggestions,
70
+ gaps: analysis.unsupported,
71
+ ...(input.output === "revised_markdown" ? { content: revisedMarkdown(analysis.profile.normalizedResume.text, suggestions) } : {}),
72
+ ...(input.output === "unified_diff" ? { content: unifiedDiff(analysis.profile.normalizedResume.text, suggestions) } : {}),
73
+ ...(input.output === "unified_diff" ? { diffBase: analysis.profile.normalizedResume } : {}),
74
+ originalOverwritten: false,
75
+ };
76
+ },
77
+ };
78
+ }
79
+
80
+ const editableFactKinds = new Set(["skill", "outcome", "experience_statement", "project", "project_statement", "education", "certification"]);
81
+ const achievementFactKinds = new Set(["outcome", "experience_statement", "project_statement"]);
82
+
83
+ function includesPhrase(value: string, phrase: string): boolean {
84
+ const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
85
+ return new RegExp(`(^|[^A-Za-z0-9+.#])${escaped}(?=$|[^A-Za-z0-9+.#])`, "i").test(value);
86
+ }
87
+
88
+ function revisedMarkdown(original: string, suggestions: ResumeSuggestion[]): string {
89
+ if (!suggestions.length) return original;
90
+ return `# Targeted Highlights\n\n${suggestions.map((suggestion) => `- ${suggestion.proposedText}`).join("\n")}\n\n${original}`;
91
+ }
92
+
93
+ function unifiedDiff(original: string, suggestions: ResumeSuggestion[]): string {
94
+ if (!suggestions.length) return "";
95
+ const originalLines = original.split("\n");
96
+ const inserted = ["# Targeted Highlights", "", ...suggestions.map((suggestion) => `- ${suggestion.proposedText}`), ""];
97
+ return [
98
+ "--- resume",
99
+ "+++ resume.optimized.md",
100
+ `@@ -1,${originalLines.length} +1,${originalLines.length + inserted.length} @@`,
101
+ ...inserted.map((line) => `+${line}`),
102
+ ...originalLines.map((line) => ` ${line}`),
103
+ ].join("\n");
104
+ }
105
+
106
+ function validateInput(value: unknown): OptimizeResumeInput {
107
+ if (!isRecord(value)) throw invalid("input", "Resume optimization input must be an object");
108
+ assertKnownKeys(value, ["jobId", "resume", "output"], "input", invalid);
109
+ if (typeof value.jobId !== "string" || !value.jobId.trim()) throw invalid("jobId", "jobId must be a non-empty string");
110
+ if (!isRecord(value.resume)) throw invalid("resume", "Resume input must be an object");
111
+ assertKnownKeys(value.resume, ["content", "format"], "resume", invalid);
112
+ if (!(["suggestions", "unified_diff", "revised_markdown"] as unknown[]).includes(value.output)) throw invalid("output", "output must be suggestions, unified_diff, or revised_markdown");
113
+ return { jobId: value.jobId, resume: value.resume as unknown as ResumeInput, output: value.output as ResumeOptimizationOutput };
114
+ }
115
+
116
+ function invalid(field: string, message: string): ResumeOptimizationError {
117
+ return new ResumeOptimizationError("invalid_resume_optimization_input", message, field);
118
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { join } from "node:path";
2
+ import { fetchSourceJobs } from "./catalog.ts";
3
+ import { createCrawlReporter, fetchSeedSnapshot, resolveAggregatorUrl } from "./crawl-reporting.ts";
4
+ import { catalog as liveCatalog, companies } from "./index.ts";
5
+ import { createLocalJobs } from "./local-jobs.ts";
6
+ import { createJobRecommender } from "./job-recommendations.ts";
7
+ import { createJobFitAnalyzer } from "./job-fit-analysis.ts";
8
+ import { createResumeOptimizer } from "./resume-optimization.ts";
9
+ import { createSelectedJobLookup } from "./selected-job-lookup.ts";
10
+ import { createFileSnapshotStore } from "./snapshot-store.ts";
11
+ import { createJobCoverageReader } from "./job-coverage.ts";
12
+ import { createJobSearchPreparer } from "./job-search-preparation.ts";
13
+
14
+ export function createRuntime(options: { dataDir?: string; concurrency?: number; crawlDelayMs?: number; workdayPageDelayMs?: number; sourceCacheHours?: number; sourceLimit?: number } = {}) {
15
+ const dataDir = options.dataDir ?? process.env.OPENINGS_DATA_DIR ?? join(process.cwd(), ".openings");
16
+ const store = createFileSnapshotStore(join(dataDir, "snapshot.json"));
17
+ const aggregatorUrl = resolveAggregatorUrl(process.env.OPENINGS_AGGREGATOR_URL);
18
+ const onCrawled = aggregatorUrl ? createCrawlReporter({ url: aggregatorUrl }) : undefined;
19
+ const local = createLocalJobs({
20
+ sources: companies,
21
+ store,
22
+ fetchJobs: (source, signal, observer) => fetchSourceJobs(source, globalThis.fetch, signal, observer),
23
+ concurrency: options.concurrency,
24
+ sourceStartDelayMs: options.crawlDelayMs,
25
+ sourceFreshnessMs: (options.sourceCacheHours ?? 0) * 60 * 60 * 1000,
26
+ sourceLimit: options.sourceLimit,
27
+ workdayPageDelayMs: options.workdayPageDelayMs,
28
+ onCrawled,
29
+ });
30
+ const recommender = createJobRecommender({ sources: companies, store, crawl: local.crawl });
31
+ const coverage = createJobCoverageReader({ sources: companies, store });
32
+ const preparationLocal = createLocalJobs({
33
+ sources: companies,
34
+ store,
35
+ fetchJobs: (source, signal, observer) => fetchSourceJobs(source, globalThis.fetch, signal, observer),
36
+ concurrency: options.concurrency,
37
+ timeoutMs: 90_000,
38
+ maxAttempts: 1,
39
+ sourceStartDelayMs: options.crawlDelayMs,
40
+ workdayPageDelayMs: options.workdayPageDelayMs,
41
+ onCrawled,
42
+ });
43
+ const preparation = createJobSearchPreparer({ sources: companies, store, crawl: preparationLocal.crawl, seed: aggregatorUrl ? () => fetchSeedSnapshot(aggregatorUrl) : undefined });
44
+ const getSelectedJob = createSelectedJobLookup({
45
+ getSnapshotJob: async (id) => (await local.get(id, { offline: true, staleDays: 14 })).job,
46
+ getDetailedJob: (id) => liveCatalog.get(id),
47
+ });
48
+ const analyzer = createJobFitAnalyzer({ getJob: getSelectedJob });
49
+ const optimizer = createResumeOptimizer({ analyzeJobFit: analyzer.analyze });
50
+ return { ...local, prepareJobSearch: preparation.prepare, getJobCoverage: coverage.getCoverage, recommend: recommender.recommend, analyzeJobFit: analyzer.analyze, optimizeResume: optimizer.optimize };
51
+ }
@@ -0,0 +1,88 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { request } from "node:https";
3
+ import { isIP } from "node:net";
4
+ import { connect as connectTls } from "node:tls";
5
+
6
+ export interface SafeGetResult { status: number; finalUrl: string; contentType: string; body: string; requestCount: number }
7
+ export type ResolveGetHost = (hostname: string) => Promise<string[]>;
8
+ export class SafeGetError extends Error { constructor(message: string, readonly requestCount: number, readonly failedRequestCount: number, readonly safety: boolean) { super(message); } }
9
+
10
+ export async function fetchSafeGet(value: string, options: { timeoutMs?: number; maxBytes?: number; maxRequests?: number; allowedDomain?: string; allowedOrigin?: string; resolveHost?: ResolveGetHost } = {}): Promise<SafeGetResult> {
11
+ const timeoutMs = options.timeoutMs ?? 15_000;
12
+ const maxBytes = options.maxBytes ?? 2 * 1024 * 1024;
13
+ const controller = new AbortController();
14
+ const timer = setTimeout(() => controller.abort(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
15
+ let requests = 0;
16
+ let requestInFlight = false;
17
+ try {
18
+ let current = checkedUrl(value);
19
+ const allowedDomain = normalizeHost(options.allowedDomain ?? current.hostname);
20
+ const allowedOrigin = normalizeOriginHost(options.allowedOrigin ?? current.hostname);
21
+ for (let redirects = 0; redirects <= 3; redirects += 1) {
22
+ if (redirects >= (options.maxRequests ?? 4)) throw new Error("GET request budget exhausted during redirects");
23
+ if (!withinDomain(current.hostname, allowedDomain) || normalizeOriginHost(current.hostname) !== allowedOrigin) throw new Error("GET URL left the approved origin");
24
+ const addresses = isIP(current.hostname) ? [current.hostname] : await resolveWithAbort((options.resolveHost ?? resolveAddresses)(current.hostname), controller.signal);
25
+ if (!addresses.length || addresses.some((address) => !isPublicAddress(address))) throw new Error(`URL resolves to a non-public address: ${current.hostname}`);
26
+ requests += 1;
27
+ requestInFlight = true;
28
+ const response = await pinnedGet(current, addresses[0]!, controller.signal, maxBytes);
29
+ requestInFlight = false;
30
+ if (![301, 302, 303, 307, 308].includes(response.status)) return { ...response, finalUrl: current.href, requestCount: requests };
31
+ const location = response.location;
32
+ if (!location) return { ...response, finalUrl: current.href, requestCount: requests };
33
+ const next = checkedUrl(new URL(location, current).href);
34
+ if (!withinDomain(next.hostname, allowedDomain) || normalizeOriginHost(next.hostname) !== allowedOrigin) throw new Error("GET redirect left the approved origin");
35
+ current = next;
36
+ }
37
+ throw new Error("GET redirect limit exceeded");
38
+ } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new SafeGetError(message, requests, requestInFlight ? 1 : 0, /non-public|approved origin|company domain|credential-free HTTPS|request budget|redirect limit|content encoding|exceeds \d+ bytes/i.test(message)); }
39
+ finally { clearTimeout(timer); }
40
+ }
41
+
42
+ function checkedUrl(value: string): URL {
43
+ const url = new URL(value);
44
+ if (url.protocol !== "https:" || url.port && url.port !== "443" || url.username || url.password) throw new Error("Probe URL must use credential-free HTTPS on port 443");
45
+ return url;
46
+ }
47
+
48
+ function pinnedGet(url: URL, address: string, signal: AbortSignal, maxBytes: number): Promise<{ status: number; contentType: string; body: string; location?: string }> {
49
+ return new Promise((resolve, reject) => {
50
+ const req = request(url, {
51
+ method: "GET", signal, servername: url.hostname, agent: false,
52
+ headers: { accept: "text/html,application/xml,text/xml,text/plain", "accept-encoding": "identity", "user-agent": "Openings/0.1 (+https://github.com/)" },
53
+ createConnection: () => connectTls({ host: address, port: 443, servername: url.hostname }),
54
+ }, (response) => {
55
+ const encoding = String(response.headers["content-encoding"] ?? "identity").toLowerCase();
56
+ if (encoding !== "identity") { response.destroy(); reject(new Error(`Unsupported content encoding: ${encoding}`)); return; }
57
+ const chunks: Buffer[] = [];
58
+ let bytes = 0;
59
+ response.on("data", (chunk: Buffer) => {
60
+ bytes += chunk.length;
61
+ if (bytes > maxBytes) { response.destroy(new Error(`Response exceeds ${maxBytes} bytes`)); return; }
62
+ chunks.push(chunk);
63
+ });
64
+ response.once("error", reject);
65
+ response.once("end", () => resolve({ status: response.statusCode ?? 500, contentType: String(response.headers["content-type"] ?? ""), body: Buffer.concat(chunks).toString("utf8"), location: typeof response.headers.location === "string" ? response.headers.location : undefined }));
66
+ });
67
+ req.once("error", reject);
68
+ req.end();
69
+ });
70
+ }
71
+
72
+ async function resolveAddresses(hostname: string): Promise<string[]> { return (await lookup(hostname, { all: true, verbatim: true })).map(({ address }) => address); }
73
+ function normalizeHost(value: string) { return value.toLowerCase().replace(/^www\./, ""); }
74
+ function normalizeOriginHost(value: string) { return value.toLowerCase(); }
75
+ function withinDomain(host: string, domain: string): boolean { const value = normalizeHost(host); return value === domain || value.endsWith(`.${domain}`); }
76
+ function resolveWithAbort<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
77
+ if (signal.aborted) return Promise.reject(signal.reason);
78
+ return new Promise<T>((resolve, reject) => {
79
+ const abort = () => reject(signal.reason);
80
+ signal.addEventListener("abort", abort, { once: true });
81
+ operation.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
82
+ });
83
+ }
84
+ function isPublicAddress(address: string): boolean {
85
+ if (isIP(address) === 4) { const [a, b] = address.split(".").map(Number); return !(a === 0 || a === 10 || a === 127 || a === 100 && b! >= 64 && b! <= 127 || a === 169 && b === 254 || a === 172 && b! >= 16 && b! <= 31 || a === 192 && (b === 0 || b === 168) || a === 198 && (b === 18 || b === 19 || b === 51) || a === 203 && b === 0 || a! >= 224); }
86
+ if (isIP(address) === 6) { const value = address.toLowerCase(); return !(value === "::" || value === "::1" || value.startsWith("::ffff:") || value.startsWith("fc") || value.startsWith("fd") || /^fe[89ab]/.test(value) || value.startsWith("ff") || value.startsWith("2001:db8")); }
87
+ return false;
88
+ }