Files
santifer--career-ops/classify-tier.mjs
T
wehub-resource-sync d083df1fdb
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 2s
Web CI / web typecheck + build (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
CodeQL Analysis / Analyze (go) (push) Failing after 16s
chore: import upstream snapshot with attribution
2026-07-13 12:02:43 +08:00

161 lines
5.4 KiB
JavaScript

#!/usr/bin/env node
/**
* classify-tier.mjs — Seniority-tier classifier for job titles
*
* Classifies a job title into one of the following tiers:
* - 'intern'
* - 'entry'
* - 'mid'
* - 'senior'
*
* It uses weighted keyword matching to handle conflicts (higher weight wins).
* Default tier is 'mid' if no keywords match.
*/
import path from 'path';
import { fileURLToPath } from 'url';
/**
* Classifies a job title into exactly one seniority tier.
*
* NOTE: Unrecognized or plain titles (e.g., "Software Engineer" with no explicit level
* indicators) fall back to 'mid' as the default/unknown bucket. Consequently, configuring
* `skip_tiers: [mid]` in portals.yml will exclude most unmatched/ordinary listings, not
* just explicit mid-level roles.
*
* @param {string} title - The job title to classify.
* @returns {'intern' | 'entry' | 'mid' | 'senior'}
*/
export function classifyTier(title) {
if (typeof title !== 'string') {
return 'mid';
}
// Preprocess title to avoid false positives with common acronyms
let cleanTitle = title
.replace(/\bA\.I\./ig, 'AI')
.replace(/\bA\.I\b/ig, 'AI')
.replace(/\bA\.\s+I\b/ig, 'AI')
.replace(/\bI\.T\./ig, 'IT')
.replace(/\bI\.T\b/ig, 'IT')
.replace(/\bI\.\s+T\b/ig, 'IT')
.replace(/\bi\/o\b/ig, 'IO');
// Define matchers with tier and weight (higher weight wins)
const matchers = [
// Senior Tier (weight 4)
{ pattern: /\bchief\b/i, tier: 'senior', weight: 4 },
{ pattern: /\bvp\b/i, tier: 'senior', weight: 4 },
{ pattern: /\bvice\s+president\b/i, tier: 'senior', weight: 4 },
{ pattern: /\bdirector\b/i, tier: 'senior', weight: 4 },
{ pattern: /\bprincipal\b/i, tier: 'senior', weight: 4 },
{ pattern: /\bstaff\b/i, tier: 'senior', weight: 4 },
{ pattern: /\blead\b/i, tier: 'senior', weight: 4 },
{ pattern: /\bsenior\b/i, tier: 'senior', weight: 4 },
{ pattern: /\bsr\b/i, tier: 'senior', weight: 4 },
{ pattern: /\bsr\./i, tier: 'senior', weight: 4 },
{ pattern: /\bhead\s+of\b/i, tier: 'senior', weight: 4 },
{ pattern: /\b[a-z]{2,}[\s-](iii|iv|v)\b/i, tier: 'senior', weight: 4 },
// Mid Tier (weight 3)
{ pattern: /\bmid-level\b/i, tier: 'mid', weight: 3 },
{ pattern: /\bmid\b/i, tier: 'mid', weight: 3 },
{ pattern: /\b[a-z]{2,}[\s-](ii)\b/i, tier: 'mid', weight: 3 },
{ pattern: /\b(l4|l5)\b/i, tier: 'mid', weight: 3 },
// Entry Tier (weight 2)
{ pattern: /\bentry-level\b/i, tier: 'entry', weight: 2 },
{ pattern: /\bentry\b/i, tier: 'entry', weight: 2 },
{ pattern: /\bassociate\b/i, tier: 'entry', weight: 2 },
{ pattern: /\bjunior\b/i, tier: 'entry', weight: 2 },
{ pattern: /\b[a-z]{2,}[\s-](i)\b/i, tier: 'entry', weight: 2 },
{ pattern: /\b(l1|l2)\b/i, tier: 'entry', weight: 2 },
// Intern Tier (weight 1)
{ pattern: /\binternship\b/i, tier: 'intern', weight: 1 },
{ pattern: /\bintern\b/i, tier: 'intern', weight: 1 },
{ pattern: /\btrainee\b/i, tier: 'intern', weight: 1 },
{ pattern: /\bco-op\b/i, tier: 'intern', weight: 1 },
{
pattern: {
test: (t) => /\bgraduate\b/i.test(t) && /\b(program|scheme)\b/i.test(t)
},
tier: 'intern',
weight: 1
}
];
let bestMatch = null;
for (const matcher of matchers) {
if (matcher.pattern.test(cleanTitle)) {
if (!bestMatch || matcher.weight > bestMatch.weight) {
bestMatch = matcher;
}
}
}
return bestMatch ? bestMatch.tier : 'mid';
}
export default classifyTier;
// CLI and inline test mode
const isDirect = process.argv[1] &&
(path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url)));
if (isDirect) {
const args = process.argv.slice(2);
if (args.includes('--test')) {
runTests();
} else if (args.length > 0) {
console.log(classifyTier(args[0]));
} else {
console.log('Usage:');
console.log(' node classify-tier.mjs "<job-title>"');
console.log(' node classify-tier.mjs --test');
}
}
function runTests() {
const testCases = [
{ title: "Software Engineer Intern", expected: "intern" },
{ title: "Junior Software Engineer", expected: "entry" },
{ title: "Software Engineer I", expected: "entry" },
{ title: "Software Engineer II", expected: "mid" },
{ title: "Senior Software Engineer", expected: "senior" },
{ title: "Staff Engineer", expected: "senior" },
{ title: "Principal Engineer", expected: "senior" },
{ title: "VP of Engineering", expected: "senior" },
{ title: "Engineering Intern Program", expected: "intern" },
{ title: "Software Engineer", expected: "mid" },
{ title: "Senior Intern Coordinator", expected: "senior" },
// additional checks to verify our regex logic
{ title: "Graduate Engineer", expected: "mid" },
{ title: "Graduate Engineer Program", expected: "intern" },
{ title: "A.I. Researcher", expected: "mid" },
{ title: "I.T. Specialist II", expected: "mid" }
];
let failed = 0;
console.log("Running classify-tier.mjs tests...");
for (const { title, expected } of testCases) {
const result = classifyTier(title);
if (result === expected) {
console.log(`✅ [PASS] "${title}" -> ${result}`);
} else {
console.error(`❌ [FAIL] "${title}": expected ${expected}, got ${result}`);
failed++;
}
}
if (failed > 0) {
console.error(`\nTest run failed: ${failed} failure(s)`);
process.exit(1);
} else {
console.log("\nAll tests passed successfully!");
process.exit(0);
}
}