@hasna/microservices 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +63 -0
- package/bin/mcp.js +63 -0
- package/dist/index.js +63 -0
- package/microservices/microservice-ads/package.json +27 -0
- package/microservices/microservice-ads/src/cli/index.ts +605 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +797 -0
- package/microservices/microservice-ads/src/db/database.ts +93 -0
- package/microservices/microservice-ads/src/db/migrations.ts +60 -0
- package/microservices/microservice-ads/src/index.ts +39 -0
- package/microservices/microservice-ads/src/mcp/index.ts +480 -0
- package/microservices/microservice-contracts/package.json +27 -0
- package/microservices/microservice-contracts/src/cli/index.ts +770 -0
- package/microservices/microservice-contracts/src/db/contracts.ts +925 -0
- package/microservices/microservice-contracts/src/db/database.ts +93 -0
- package/microservices/microservice-contracts/src/db/migrations.ts +141 -0
- package/microservices/microservice-contracts/src/index.ts +43 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +617 -0
- package/microservices/microservice-domains/package.json +27 -0
- package/microservices/microservice-domains/src/cli/index.ts +691 -0
- package/microservices/microservice-domains/src/db/database.ts +93 -0
- package/microservices/microservice-domains/src/db/domains.ts +1164 -0
- package/microservices/microservice-domains/src/db/migrations.ts +60 -0
- package/microservices/microservice-domains/src/index.ts +65 -0
- package/microservices/microservice-domains/src/mcp/index.ts +536 -0
- package/microservices/microservice-hiring/package.json +27 -0
- package/microservices/microservice-hiring/src/cli/index.ts +741 -0
- package/microservices/microservice-hiring/src/db/database.ts +93 -0
- package/microservices/microservice-hiring/src/db/hiring.ts +1085 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +89 -0
- package/microservices/microservice-hiring/src/index.ts +80 -0
- package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +709 -0
- package/microservices/microservice-payments/package.json +27 -0
- package/microservices/microservice-payments/src/cli/index.ts +609 -0
- package/microservices/microservice-payments/src/db/database.ts +93 -0
- package/microservices/microservice-payments/src/db/migrations.ts +81 -0
- package/microservices/microservice-payments/src/db/payments.ts +1204 -0
- package/microservices/microservice-payments/src/index.ts +51 -0
- package/microservices/microservice-payments/src/mcp/index.ts +683 -0
- package/microservices/microservice-payroll/package.json +27 -0
- package/microservices/microservice-payroll/src/cli/index.ts +643 -0
- package/microservices/microservice-payroll/src/db/database.ts +93 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +95 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +1377 -0
- package/microservices/microservice-payroll/src/index.ts +48 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +666 -0
- package/microservices/microservice-shipping/package.json +27 -0
- package/microservices/microservice-shipping/src/cli/index.ts +606 -0
- package/microservices/microservice-shipping/src/db/database.ts +93 -0
- package/microservices/microservice-shipping/src/db/migrations.ts +69 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +1093 -0
- package/microservices/microservice-shipping/src/index.ts +53 -0
- package/microservices/microservice-shipping/src/mcp/index.ts +533 -0
- package/microservices/microservice-social/package.json +27 -0
- package/microservices/microservice-social/src/cli/index.ts +689 -0
- package/microservices/microservice-social/src/db/database.ts +93 -0
- package/microservices/microservice-social/src/db/migrations.ts +88 -0
- package/microservices/microservice-social/src/db/social.ts +1046 -0
- package/microservices/microservice-social/src/index.ts +46 -0
- package/microservices/microservice-social/src/mcp/index.ts +655 -0
- package/microservices/microservice-subscriptions/package.json +27 -0
- package/microservices/microservice-subscriptions/src/cli/index.ts +715 -0
- package/microservices/microservice-subscriptions/src/db/database.ts +93 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +125 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +1256 -0
- package/microservices/microservice-subscriptions/src/index.ts +41 -0
- package/microservices/microservice-subscriptions/src/mcp/index.ts +631 -0
- package/package.json +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface MigrationEntry {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
sql: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const MIGRATIONS: MigrationEntry[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
name: "initial_schema",
|
|
11
|
+
sql: `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
title TEXT NOT NULL,
|
|
15
|
+
department TEXT,
|
|
16
|
+
location TEXT,
|
|
17
|
+
type TEXT NOT NULL DEFAULT 'full-time' CHECK (type IN ('full-time', 'part-time', 'contract')),
|
|
18
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'closed', 'paused')),
|
|
19
|
+
description TEXT,
|
|
20
|
+
requirements TEXT NOT NULL DEFAULT '[]',
|
|
21
|
+
salary_range TEXT,
|
|
22
|
+
posted_at TEXT,
|
|
23
|
+
closed_at TEXT,
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS applicants (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
31
|
+
name TEXT NOT NULL,
|
|
32
|
+
email TEXT,
|
|
33
|
+
phone TEXT,
|
|
34
|
+
resume_url TEXT,
|
|
35
|
+
status TEXT NOT NULL DEFAULT 'applied' CHECK (status IN ('applied', 'screening', 'interviewing', 'offered', 'hired', 'rejected')),
|
|
36
|
+
stage TEXT,
|
|
37
|
+
rating INTEGER,
|
|
38
|
+
notes TEXT,
|
|
39
|
+
source TEXT,
|
|
40
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
41
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
42
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
43
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS interviews (
|
|
47
|
+
id TEXT PRIMARY KEY,
|
|
48
|
+
applicant_id TEXT NOT NULL REFERENCES applicants(id) ON DELETE CASCADE,
|
|
49
|
+
interviewer TEXT,
|
|
50
|
+
scheduled_at TEXT,
|
|
51
|
+
duration_min INTEGER,
|
|
52
|
+
type TEXT NOT NULL DEFAULT 'phone' CHECK (type IN ('phone', 'video', 'onsite')),
|
|
53
|
+
status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'completed', 'canceled')),
|
|
54
|
+
feedback TEXT,
|
|
55
|
+
rating INTEGER,
|
|
56
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_department ON jobs(department);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_applicants_job_id ON applicants(job_id);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_applicants_status ON applicants(status);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_applicants_email ON applicants(email);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_interviews_applicant_id ON interviews(applicant_id);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_interviews_status ON interviews(status);
|
|
66
|
+
`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 2,
|
|
70
|
+
name: "add_job_templates",
|
|
71
|
+
sql: `
|
|
72
|
+
CREATE TABLE IF NOT EXISTS job_templates (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
name TEXT NOT NULL UNIQUE,
|
|
75
|
+
title TEXT NOT NULL,
|
|
76
|
+
department TEXT,
|
|
77
|
+
location TEXT,
|
|
78
|
+
type TEXT NOT NULL DEFAULT 'full-time' CHECK (type IN ('full-time', 'part-time', 'contract')),
|
|
79
|
+
description TEXT,
|
|
80
|
+
requirements TEXT NOT NULL DEFAULT '[]',
|
|
81
|
+
salary_range TEXT,
|
|
82
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
83
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_job_templates_name ON job_templates(name);
|
|
87
|
+
`,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* microservice-hiring — Applicant tracking and recruitment microservice
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createJob,
|
|
7
|
+
getJob,
|
|
8
|
+
listJobs,
|
|
9
|
+
updateJob,
|
|
10
|
+
closeJob,
|
|
11
|
+
deleteJob,
|
|
12
|
+
type Job,
|
|
13
|
+
type CreateJobInput,
|
|
14
|
+
type UpdateJobInput,
|
|
15
|
+
type ListJobsOptions,
|
|
16
|
+
} from "./db/hiring.js";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
createApplicant,
|
|
20
|
+
getApplicant,
|
|
21
|
+
listApplicants,
|
|
22
|
+
updateApplicant,
|
|
23
|
+
advanceApplicant,
|
|
24
|
+
rejectApplicant,
|
|
25
|
+
deleteApplicant,
|
|
26
|
+
searchApplicants,
|
|
27
|
+
listByStage,
|
|
28
|
+
getPipeline,
|
|
29
|
+
getHiringStats,
|
|
30
|
+
type Applicant,
|
|
31
|
+
type CreateApplicantInput,
|
|
32
|
+
type UpdateApplicantInput,
|
|
33
|
+
type ListApplicantsOptions,
|
|
34
|
+
type PipelineEntry,
|
|
35
|
+
type HiringStats,
|
|
36
|
+
} from "./db/hiring.js";
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
createInterview,
|
|
40
|
+
getInterview,
|
|
41
|
+
listInterviews,
|
|
42
|
+
updateInterview,
|
|
43
|
+
addInterviewFeedback,
|
|
44
|
+
deleteInterview,
|
|
45
|
+
type Interview,
|
|
46
|
+
type CreateInterviewInput,
|
|
47
|
+
type UpdateInterviewInput,
|
|
48
|
+
type ListInterviewsOptions,
|
|
49
|
+
} from "./db/hiring.js";
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
bulkImportApplicants,
|
|
53
|
+
generateOffer,
|
|
54
|
+
getHiringForecast,
|
|
55
|
+
submitStructuredFeedback,
|
|
56
|
+
bulkReject,
|
|
57
|
+
getReferralStats,
|
|
58
|
+
saveJobAsTemplate,
|
|
59
|
+
getJobTemplate,
|
|
60
|
+
getJobTemplateByName,
|
|
61
|
+
listJobTemplates,
|
|
62
|
+
createJobFromTemplate,
|
|
63
|
+
deleteJobTemplate,
|
|
64
|
+
type BulkImportResult,
|
|
65
|
+
type OfferDetails,
|
|
66
|
+
type HiringForecast,
|
|
67
|
+
type StructuredFeedback,
|
|
68
|
+
type BulkRejectResult,
|
|
69
|
+
type ReferralStats,
|
|
70
|
+
type JobTemplate,
|
|
71
|
+
} from "./db/hiring.js";
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
scoreApplicant,
|
|
75
|
+
rankApplicants,
|
|
76
|
+
type ScoreResult,
|
|
77
|
+
type RankEntry,
|
|
78
|
+
} from "./lib/scoring.js";
|
|
79
|
+
|
|
80
|
+
export { getDatabase, closeDatabase } from "./db/database.js";
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI resume scoring — uses OpenAI or Anthropic to evaluate applicant fit
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApplicant, getJob, updateApplicant } from "../db/hiring.js";
|
|
6
|
+
import type { Applicant, Job } from "../db/hiring.js";
|
|
7
|
+
|
|
8
|
+
export interface ScoreResult {
|
|
9
|
+
match_pct: number;
|
|
10
|
+
strengths: string[];
|
|
11
|
+
gaps: string[];
|
|
12
|
+
recommendation: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RankEntry {
|
|
16
|
+
applicant: Applicant;
|
|
17
|
+
score: ScoreResult;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---- Provider abstraction ----
|
|
21
|
+
|
|
22
|
+
async function callAI(prompt: string): Promise<string> {
|
|
23
|
+
// Try Anthropic first, then OpenAI
|
|
24
|
+
const anthropicKey = process.env["ANTHROPIC_API_KEY"];
|
|
25
|
+
const openaiKey = process.env["OPENAI_API_KEY"];
|
|
26
|
+
|
|
27
|
+
if (anthropicKey) {
|
|
28
|
+
return callAnthropic(anthropicKey, prompt);
|
|
29
|
+
}
|
|
30
|
+
if (openaiKey) {
|
|
31
|
+
return callOpenAI(openaiKey, prompt);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw new Error(
|
|
35
|
+
"No AI API key found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function callAnthropic(apiKey: string, prompt: string): Promise<string> {
|
|
40
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"x-api-key": apiKey,
|
|
45
|
+
"anthropic-version": "2023-06-01",
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
model: "claude-sonnet-4-20250514",
|
|
49
|
+
max_tokens: 1024,
|
|
50
|
+
messages: [{ role: "user", content: prompt }],
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
throw new Error(`Anthropic API error ${response.status}: ${text}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = (await response.json()) as {
|
|
60
|
+
content: Array<{ type: string; text: string }>;
|
|
61
|
+
};
|
|
62
|
+
return data.content[0]?.text || "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function callOpenAI(apiKey: string, prompt: string): Promise<string> {
|
|
66
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
Authorization: `Bearer ${apiKey}`,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
model: "gpt-4o-mini",
|
|
74
|
+
messages: [{ role: "user", content: prompt }],
|
|
75
|
+
max_tokens: 1024,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const text = await response.text();
|
|
81
|
+
throw new Error(`OpenAI API error ${response.status}: ${text}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = (await response.json()) as {
|
|
85
|
+
choices: Array<{ message: { content: string } }>;
|
|
86
|
+
};
|
|
87
|
+
return data.choices[0]?.message?.content || "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- Scoring ----
|
|
91
|
+
|
|
92
|
+
function buildScoringPrompt(job: Job, applicant: Applicant): string {
|
|
93
|
+
const requirements = job.requirements.length
|
|
94
|
+
? job.requirements.join(", ")
|
|
95
|
+
: "No specific requirements listed";
|
|
96
|
+
|
|
97
|
+
const applicantInfo = [
|
|
98
|
+
`Name: ${applicant.name}`,
|
|
99
|
+
applicant.resume_url ? `Resume: ${applicant.resume_url}` : null,
|
|
100
|
+
applicant.notes ? `Notes/Summary: ${applicant.notes}` : null,
|
|
101
|
+
applicant.source ? `Source: ${applicant.source}` : null,
|
|
102
|
+
applicant.stage ? `Current stage: ${applicant.stage}` : null,
|
|
103
|
+
Object.keys(applicant.metadata).length > 0
|
|
104
|
+
? `Additional info: ${JSON.stringify(applicant.metadata)}`
|
|
105
|
+
: null,
|
|
106
|
+
]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join("\n");
|
|
109
|
+
|
|
110
|
+
return `You are an expert hiring evaluator. Analyze this applicant against the job requirements and return a JSON assessment.
|
|
111
|
+
|
|
112
|
+
JOB:
|
|
113
|
+
Title: ${job.title}
|
|
114
|
+
Department: ${job.department || "N/A"}
|
|
115
|
+
Description: ${job.description || "N/A"}
|
|
116
|
+
Requirements: ${requirements}
|
|
117
|
+
Salary Range: ${job.salary_range || "N/A"}
|
|
118
|
+
|
|
119
|
+
APPLICANT:
|
|
120
|
+
${applicantInfo}
|
|
121
|
+
|
|
122
|
+
Return ONLY a valid JSON object (no markdown, no code fences) with this exact structure:
|
|
123
|
+
{
|
|
124
|
+
"match_pct": <number 0-100>,
|
|
125
|
+
"strengths": ["strength1", "strength2"],
|
|
126
|
+
"gaps": ["gap1", "gap2"],
|
|
127
|
+
"recommendation": "<hire/strong_hire/no_hire/maybe> — brief explanation"
|
|
128
|
+
}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseScoreResponse(text: string): ScoreResult {
|
|
132
|
+
// Strip markdown code fences if present
|
|
133
|
+
let cleaned = text.trim();
|
|
134
|
+
if (cleaned.startsWith("```")) {
|
|
135
|
+
cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(cleaned);
|
|
140
|
+
return {
|
|
141
|
+
match_pct: Math.max(0, Math.min(100, Number(parsed.match_pct) || 0)),
|
|
142
|
+
strengths: Array.isArray(parsed.strengths) ? parsed.strengths : [],
|
|
143
|
+
gaps: Array.isArray(parsed.gaps) ? parsed.gaps : [],
|
|
144
|
+
recommendation: String(parsed.recommendation || "Unable to determine"),
|
|
145
|
+
};
|
|
146
|
+
} catch {
|
|
147
|
+
return {
|
|
148
|
+
match_pct: 0,
|
|
149
|
+
strengths: [],
|
|
150
|
+
gaps: [],
|
|
151
|
+
recommendation: `AI response could not be parsed: ${text.slice(0, 200)}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function scoreApplicant(applicantId: string): Promise<ScoreResult> {
|
|
157
|
+
const applicant = getApplicant(applicantId);
|
|
158
|
+
if (!applicant) throw new Error(`Applicant '${applicantId}' not found`);
|
|
159
|
+
|
|
160
|
+
const job = getJob(applicant.job_id);
|
|
161
|
+
if (!job) throw new Error(`Job '${applicant.job_id}' not found`);
|
|
162
|
+
|
|
163
|
+
const prompt = buildScoringPrompt(job, applicant);
|
|
164
|
+
const response = await callAI(prompt);
|
|
165
|
+
const score = parseScoreResponse(response);
|
|
166
|
+
|
|
167
|
+
// Store in applicant metadata
|
|
168
|
+
const metadata = { ...applicant.metadata, ai_score: score, scored_at: new Date().toISOString() };
|
|
169
|
+
updateApplicant(applicantId, { metadata });
|
|
170
|
+
|
|
171
|
+
return score;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function rankApplicants(jobId: string): Promise<RankEntry[]> {
|
|
175
|
+
const job = getJob(jobId);
|
|
176
|
+
if (!job) throw new Error(`Job '${jobId}' not found`);
|
|
177
|
+
|
|
178
|
+
const applicants = (await import("../db/hiring.js")).listApplicants({ job_id: jobId });
|
|
179
|
+
|
|
180
|
+
const results: RankEntry[] = [];
|
|
181
|
+
|
|
182
|
+
for (const applicant of applicants) {
|
|
183
|
+
// Use cached score if available and recent (less than 24h old)
|
|
184
|
+
const cached = applicant.metadata?.ai_score as ScoreResult | undefined;
|
|
185
|
+
const cachedAt = applicant.metadata?.scored_at as string | undefined;
|
|
186
|
+
const isFresh =
|
|
187
|
+
cachedAt && Date.now() - new Date(cachedAt).getTime() < 24 * 60 * 60 * 1000;
|
|
188
|
+
|
|
189
|
+
if (cached && isFresh) {
|
|
190
|
+
results.push({ applicant, score: cached });
|
|
191
|
+
} else {
|
|
192
|
+
const score = await scoreApplicant(applicant.id);
|
|
193
|
+
// Re-fetch to get updated metadata
|
|
194
|
+
const updated = getApplicant(applicant.id)!;
|
|
195
|
+
results.push({ applicant: updated, score });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Sort by match_pct descending
|
|
200
|
+
results.sort((a, b) => b.score.match_pct - a.score.match_pct);
|
|
201
|
+
|
|
202
|
+
return results;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Exported for testing
|
|
206
|
+
export { buildScoringPrompt, parseScoreResponse, callAI };
|