resuml 1.20.1 → 2.0.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.
- package/DOCS.md +314 -0
- package/README.md +7 -2
- package/dist/{chunk-KRJMZ2RQ.js → chunk-GRIYYG45.js} +242 -2
- package/dist/chunk-GRIYYG45.js.map +1 -0
- package/dist/index.d.ts +422 -3
- package/dist/index.js +119 -54
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +4 -8
- package/dist/mcp/server.js.map +1 -1
- package/package.json +26 -52
- package/dist/api.d.ts +0 -9
- package/dist/api.js +0 -20
- package/dist/api.js.map +0 -1
- package/dist/chunk-4ZOTZUAW.js +0 -6666
- package/dist/chunk-4ZOTZUAW.js.map +0 -1
- package/dist/chunk-JP7UCR3P.js +0 -182
- package/dist/chunk-JP7UCR3P.js.map +0 -1
- package/dist/chunk-KRJMZ2RQ.js.map +0 -1
- package/dist/chunk-ZLA7NFYP.js +0 -90
- package/dist/chunk-ZLA7NFYP.js.map +0 -1
- package/dist/index-yHdKpxms.d.ts +0 -422
- package/dist/themeLoader-ZGWEGYXG.js +0 -7
- package/dist/themeLoader-ZGWEGYXG.js.map +0 -1
- package/scripts/build-builder.js +0 -25
- package/scripts/build-skills-db.js +0 -314
- package/scripts/bundle-themes.js +0 -1104
- package/scripts/dev-server.js +0 -392
- package/scripts/enrich-themes-manifest.mjs +0 -156
- package/scripts/generate-types.cjs +0 -55
- package/scripts/mcp-call.mjs +0 -99
- package/scripts/quick-bundle.cjs +0 -129
- package/scripts/render-theme-thumbs.mjs +0 -117
- package/scripts/test-mcp.mjs +0 -583
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Build the skills taxonomy used by the ATS keyword matcher.
|
|
4
|
-
*
|
|
5
|
-
* Sources:
|
|
6
|
-
* 1. O*NET (US Department of Labor, public domain), "Technology Skills"
|
|
7
|
-
* and "Tools Used" for all IT occupations (SOC codes 15-*).
|
|
8
|
-
* 2. src/ats/skills/emerging.ts, hand-curated allowlist of modern
|
|
9
|
-
* frameworks/tools O*NET hasn't absorbed yet.
|
|
10
|
-
*
|
|
11
|
-
* Output: data/skills/skills.json, bundled by the app at build time.
|
|
12
|
-
*
|
|
13
|
-
* Run: node scripts/build-skills-db.js
|
|
14
|
-
* --onet-zip <path> Use a local O*NET zip instead of downloading
|
|
15
|
-
* --no-download Fail if zip isn't cached
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
-
import { fileURLToPath } from 'node:url';
|
|
20
|
-
import { dirname, join, resolve } from 'node:path';
|
|
21
|
-
import { tmpdir } from 'node:os';
|
|
22
|
-
import { execFileSync } from 'node:child_process';
|
|
23
|
-
import { get } from 'node:https';
|
|
24
|
-
|
|
25
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const ROOT = resolve(__dirname, '..');
|
|
27
|
-
const OUT_FILE = join(ROOT, 'data/skills/skills.json');
|
|
28
|
-
const CACHE_DIR = join(tmpdir(), 'resuml-skills-cache');
|
|
29
|
-
const ONET_URL = 'https://www.onetcenter.org/dl_files/database/db_29_0_text.zip';
|
|
30
|
-
|
|
31
|
-
function parseArgs() {
|
|
32
|
-
const args = process.argv.slice(2);
|
|
33
|
-
const out = { onetZip: null, download: true };
|
|
34
|
-
for (let i = 0; i < args.length; i++) {
|
|
35
|
-
if (args[i] === '--onet-zip') out.onetZip = args[++i];
|
|
36
|
-
if (args[i] === '--no-download') out.download = false;
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function download(url, dest) {
|
|
42
|
-
return new Promise((resolvePromise, reject) => {
|
|
43
|
-
const file = createWriteStream(dest);
|
|
44
|
-
const req = get(url, (res) => {
|
|
45
|
-
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
46
|
-
file.close();
|
|
47
|
-
return download(res.headers.location, dest).then(resolvePromise, reject);
|
|
48
|
-
}
|
|
49
|
-
if (res.statusCode !== 200) {
|
|
50
|
-
reject(new Error(`HTTP ${res.statusCode} from ${url}`));
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
res.pipe(file);
|
|
54
|
-
file.on('finish', () => { file.close(resolvePromise); });
|
|
55
|
-
});
|
|
56
|
-
req.on('error', reject);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function unzip(zipPath, destDir) {
|
|
61
|
-
execFileSync('unzip', ['-q', '-o', zipPath, '-d', destDir]);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Parse a tab-separated O*NET file with a header row.
|
|
66
|
-
* Returns an array of {header: value} objects.
|
|
67
|
-
*/
|
|
68
|
-
function parseTsv(path) {
|
|
69
|
-
const text = readFileSync(path, 'utf8');
|
|
70
|
-
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
71
|
-
const [headerLine, ...rows] = lines;
|
|
72
|
-
const headers = headerLine.split('\t');
|
|
73
|
-
return rows.map((row) => {
|
|
74
|
-
const cells = row.split('\t');
|
|
75
|
-
const obj = {};
|
|
76
|
-
for (let i = 0; i < headers.length; i++) obj[headers[i]] = cells[i] ?? '';
|
|
77
|
-
return obj;
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Turn a human skill name into a slug id.
|
|
83
|
-
*/
|
|
84
|
-
function slugify(name) {
|
|
85
|
-
return name
|
|
86
|
-
.toLowerCase()
|
|
87
|
-
.normalize('NFKD')
|
|
88
|
-
.replace(/[̀-ͯ]/g, '')
|
|
89
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
90
|
-
.replace(/^-+|-+$/g, '');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Heuristic cleaner: O*NET entries include parenthetical descriptors ("Microsoft
|
|
95
|
-
* Azure (Cloud)") and vendor prefixes ("Microsoft SQL Server"). Emit the main
|
|
96
|
-
* canonical name plus useful aliases.
|
|
97
|
-
*/
|
|
98
|
-
function nameToCanonicalAndAliases(raw) {
|
|
99
|
-
const aliases = new Set();
|
|
100
|
-
let canonical = raw.trim();
|
|
101
|
-
// Drop trailing generic suffixes like " software" / " tools" (noise for match)
|
|
102
|
-
const suffixRe = /\s+(software|tools?|systems?|applications?|programs?)$/i;
|
|
103
|
-
if (suffixRe.test(canonical)) {
|
|
104
|
-
const stripped = canonical.replace(suffixRe, '').trim();
|
|
105
|
-
if (stripped.length >= 2) {
|
|
106
|
-
aliases.add(canonical);
|
|
107
|
-
canonical = stripped;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// Parenthetical expansion: "Microsoft Excel (spreadsheet)" → canonical "Microsoft Excel", alias "Excel"
|
|
111
|
-
const paren = canonical.match(/^(.+?)\s*\(([^)]+)\)\s*$/);
|
|
112
|
-
if (paren) {
|
|
113
|
-
canonical = paren[1].trim();
|
|
114
|
-
const inside = paren[2].trim();
|
|
115
|
-
if (inside && inside.length <= 40) aliases.add(inside);
|
|
116
|
-
}
|
|
117
|
-
// Acronym alias: CapsInitials like "Amazon Web Services" → "AWS". Need at
|
|
118
|
-
// least 3 words to avoid generating 2-letter acronyms that collide with
|
|
119
|
-
// common English words ("IN", "IS", "NO", "IT", "OR", etc.).
|
|
120
|
-
const words = canonical.split(/\s+/);
|
|
121
|
-
if (words.length >= 3 && words.length <= 6 && words.every((w) => /^[A-Z]/.test(w))) {
|
|
122
|
-
const acronym = words.map((w) => w[0]).join('');
|
|
123
|
-
if (acronym.length >= 3 && acronym.length <= 6) aliases.add(acronym);
|
|
124
|
-
}
|
|
125
|
-
// Don't auto-generate vendor-prefix tail aliases, too many collide with
|
|
126
|
-
// common English words ("Microsoft Project" → "Project", "Oracle Forms" →
|
|
127
|
-
// "Forms"). Use data/skills/emerging.json for curated shortforms instead.
|
|
128
|
-
|
|
129
|
-
// Final cleanup: drop any alias shorter than 3 chars or identical to canonical
|
|
130
|
-
const clean = [...aliases]
|
|
131
|
-
.filter((a) => a.length >= 3)
|
|
132
|
-
.filter((a) => a.toLowerCase() !== canonical.toLowerCase());
|
|
133
|
-
return { canonical, aliases: clean };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Single-word skill names that collide with common English prose. O*NET
|
|
138
|
-
* contains obscure products literally named "Switch", "Access", "Contact",
|
|
139
|
-
* "Notes", "Software development" etc., matching those against prose
|
|
140
|
-
* produces nonsense. Drop them and rely on the emerging-tech allowlist
|
|
141
|
-
* for the genuine single-word brands (e.g. "React", "Docker", "Vite").
|
|
142
|
-
*/
|
|
143
|
-
const GENERIC_SINGLE_WORD_BAN = new Set([
|
|
144
|
-
'access', 'apps', 'cache', 'channel', 'channels', 'clear', 'clone',
|
|
145
|
-
'code', 'connect', 'contact', 'deploy', 'design', 'drive', 'ease',
|
|
146
|
-
'enable', 'express', 'fast', 'flow', 'focus', 'forms', 'frame',
|
|
147
|
-
'groove', 'hive', 'impact', 'link', 'mail', 'mind', 'monitor',
|
|
148
|
-
'motion', 'notes', 'notify', 'pages', 'plan', 'pop', 'present',
|
|
149
|
-
'publish', 'pulse', 'relay', 'reports', 'schedule', 'scope',
|
|
150
|
-
'sense', 'share', 'shift', 'show', 'simple', 'sketch', 'slate',
|
|
151
|
-
'slides', 'snap', 'source', 'space', 'sphere', 'spotlight', 'stack',
|
|
152
|
-
'stream', 'switch', 'tabs', 'taskmaster', 'teams', 'trigger',
|
|
153
|
-
'update', 'vision', 'voice', 'word',
|
|
154
|
-
]);
|
|
155
|
-
|
|
156
|
-
const GENERIC_PHRASE_BAN = new Set([
|
|
157
|
-
'software development', 'software engineering', 'project management',
|
|
158
|
-
'data management', 'content management', 'information management',
|
|
159
|
-
'knowledge management', 'document management', 'operating system',
|
|
160
|
-
'operating systems', 'computer systems', 'application software',
|
|
161
|
-
]);
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Filter: only keep entries relevant to software/IT work. The O*NET list is
|
|
165
|
-
* enormous and includes entries like "3M Post-it App" that add noise.
|
|
166
|
-
*/
|
|
167
|
-
function keepEntry(name, _commodityTitle) {
|
|
168
|
-
const lower = name.toLowerCase().trim();
|
|
169
|
-
// Reject obvious non-skills
|
|
170
|
-
if (/^\d+[a-z]{0,3}$/i.test(lower)) return false; // "3M", "24SevenOffice Project"
|
|
171
|
-
if (lower.length < 2) return false;
|
|
172
|
-
if (lower.length > 60) return false;
|
|
173
|
-
// Skip entries with fluffy qualifiers
|
|
174
|
-
if (/\bproject\b|\binitiative\b/i.test(name)) return false;
|
|
175
|
-
// Drop single-word entries that are common English words / generic verbs
|
|
176
|
-
const tokens = lower.split(/\s+/);
|
|
177
|
-
if (tokens.length === 1 && GENERIC_SINGLE_WORD_BAN.has(lower)) return false;
|
|
178
|
-
// Drop phrases that are too generic to be useful skill labels
|
|
179
|
-
if (GENERIC_PHRASE_BAN.has(lower)) return false;
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Map O*NET commodity titles to our coarse type labels.
|
|
185
|
-
*/
|
|
186
|
-
function commodityToType(commodityTitle) {
|
|
187
|
-
const t = (commodityTitle || '').toLowerCase();
|
|
188
|
-
if (t.includes('programming')) return 'language';
|
|
189
|
-
if (t.includes('platform') || t.includes('operating system')) return 'platform';
|
|
190
|
-
if (t.includes('database')) return 'database';
|
|
191
|
-
if (t.includes('version control') || t.includes('configuration management')) return 'tool';
|
|
192
|
-
if (t.includes('development environment') || t.includes('ide')) return 'tool';
|
|
193
|
-
if (t.includes('framework')) return 'framework';
|
|
194
|
-
if (t.includes('testing')) return 'tool';
|
|
195
|
-
if (t.includes('analytics')) return 'tool';
|
|
196
|
-
return 'tool';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function main() {
|
|
200
|
-
const args = parseArgs();
|
|
201
|
-
|
|
202
|
-
if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
|
|
203
|
-
const zipPath = args.onetZip || join(CACHE_DIR, 'onet.zip');
|
|
204
|
-
const extractDir = join(CACHE_DIR, 'onet');
|
|
205
|
-
|
|
206
|
-
if (!existsSync(zipPath)) {
|
|
207
|
-
if (!args.download) throw new Error(`O*NET zip missing at ${zipPath}. Re-run without --no-download.`);
|
|
208
|
-
console.log(`Downloading O*NET from ${ONET_URL}…`);
|
|
209
|
-
await download(ONET_URL, zipPath);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (!existsSync(join(extractDir, 'db_29_0_text'))) {
|
|
213
|
-
console.log(`Extracting to ${extractDir}…`);
|
|
214
|
-
mkdirSync(extractDir, { recursive: true });
|
|
215
|
-
unzip(zipPath, extractDir);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const base = join(extractDir, 'db_29_0_text');
|
|
219
|
-
const techRows = parseTsv(join(base, 'Technology Skills.txt'));
|
|
220
|
-
const toolRows = parseTsv(join(base, 'Tools Used.txt'));
|
|
221
|
-
|
|
222
|
-
// Keep only IT/software occupations (SOC 15-*), drops noise from unrelated jobs
|
|
223
|
-
const itOnly = (rows) => rows.filter((r) => r['O*NET-SOC Code']?.startsWith('15-'));
|
|
224
|
-
|
|
225
|
-
const skillsMap = new Map(); // canonical (lowercased) → {id, canonical, aliases, type, hot, sources}
|
|
226
|
-
|
|
227
|
-
function upsert(rawName, commodityTitle, hot, source) {
|
|
228
|
-
if (!rawName || !keepEntry(rawName, commodityTitle)) return;
|
|
229
|
-
const { canonical, aliases } = nameToCanonicalAndAliases(rawName);
|
|
230
|
-
if (!canonical || canonical.length < 2) return;
|
|
231
|
-
// Apply ban to the canonical form (post-suffix-strip), "Software
|
|
232
|
-
// development tools" → "Software development" should trip the phrase ban.
|
|
233
|
-
if (!keepEntry(canonical, commodityTitle)) return;
|
|
234
|
-
const key = canonical.toLowerCase();
|
|
235
|
-
const existing = skillsMap.get(key);
|
|
236
|
-
if (existing) {
|
|
237
|
-
for (const a of aliases) {
|
|
238
|
-
if (!existing.aliases.includes(a)) existing.aliases.push(a);
|
|
239
|
-
}
|
|
240
|
-
existing.hot = existing.hot || hot;
|
|
241
|
-
if (!existing.sources.includes(source)) existing.sources.push(source);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
skillsMap.set(key, {
|
|
245
|
-
id: slugify(canonical),
|
|
246
|
-
canonical,
|
|
247
|
-
aliases,
|
|
248
|
-
type: commodityToType(commodityTitle),
|
|
249
|
-
hot: !!hot,
|
|
250
|
-
sources: [source],
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
for (const row of itOnly(techRows)) {
|
|
255
|
-
upsert(row['Example'], row['Commodity Title'], row['Hot Technology'] === 'Y', 'onet-tech');
|
|
256
|
-
}
|
|
257
|
-
for (const row of itOnly(toolRows)) {
|
|
258
|
-
upsert(row['Example'], row['Commodity Title'], false, 'onet-tools');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Merge emerging-tech allowlist (hand-curated JSON)
|
|
262
|
-
const emergingPath = join(ROOT, 'data/skills/emerging.json');
|
|
263
|
-
if (existsSync(emergingPath)) {
|
|
264
|
-
const emerging = JSON.parse(readFileSync(emergingPath, 'utf8'));
|
|
265
|
-
for (const s of (emerging.skills || [])) {
|
|
266
|
-
const key = s.canonical.toLowerCase();
|
|
267
|
-
const existing = skillsMap.get(key);
|
|
268
|
-
if (existing) {
|
|
269
|
-
for (const a of (s.aliases || [])) {
|
|
270
|
-
if (!existing.aliases.includes(a)) existing.aliases.push(a);
|
|
271
|
-
}
|
|
272
|
-
if (!existing.sources.includes('emerging')) existing.sources.push('emerging');
|
|
273
|
-
if (s.type) existing.type = s.type; // emerging takes precedence (better typed)
|
|
274
|
-
if (s.hot) existing.hot = true;
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
skillsMap.set(key, {
|
|
278
|
-
id: slugify(s.canonical),
|
|
279
|
-
canonical: s.canonical,
|
|
280
|
-
aliases: s.aliases || [],
|
|
281
|
-
type: s.type || 'tool',
|
|
282
|
-
hot: !!s.hot,
|
|
283
|
-
sources: ['emerging'],
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Output, sorted by canonical name for stable diffs
|
|
289
|
-
const skills = [...skillsMap.values()].sort((a, b) =>
|
|
290
|
-
a.canonical.localeCompare(b.canonical)
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
if (!existsSync(dirname(OUT_FILE))) mkdirSync(dirname(OUT_FILE), { recursive: true });
|
|
294
|
-
writeFileSync(OUT_FILE, JSON.stringify({
|
|
295
|
-
version: 1,
|
|
296
|
-
generatedAt: new Date().toISOString(),
|
|
297
|
-
sources: ['onet-29.0', 'resuml-emerging'],
|
|
298
|
-
license: 'O*NET: public domain; emerging list: MIT (this repo)',
|
|
299
|
-
count: skills.length,
|
|
300
|
-
skills,
|
|
301
|
-
}, null, 0));
|
|
302
|
-
|
|
303
|
-
console.log(`✅ Wrote ${skills.length} skills → ${OUT_FILE}`);
|
|
304
|
-
const byType = skills.reduce((acc, s) => { acc[s.type] = (acc[s.type] || 0) + 1; return acc; }, {});
|
|
305
|
-
console.log(' Types:', byType);
|
|
306
|
-
console.log(' Hot:', skills.filter((s) => s.hot).length);
|
|
307
|
-
const size = (JSON.stringify(skills).length / 1024).toFixed(1);
|
|
308
|
-
console.log(` Size (uncompressed): ${size} KB`);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
main().catch((e) => {
|
|
312
|
-
console.error(e);
|
|
313
|
-
process.exit(1);
|
|
314
|
-
});
|