Files
CapaKraken/apps/web/src/lib/skillMatrixParser.ts
T
Hartmut cd78f72f33 chore: full technical rename planarchy → capakraken
Complete rename of all technical identifiers across the codebase:

Package names (11 packages):
- @planarchy/* → @capakraken/* in all package.json, tsconfig, imports

Import statements: 277 files, 548 occurrences replaced

Database & Docker:
- PostgreSQL user/db: planarchy → capakraken
- Docker volumes: planarchy_pgdata → capakraken_pgdata
- Connection strings updated in docker-compose, .env, CI

CI/CD:
- GitHub Actions workflow: all filter commands updated
- Test database credentials updated

Infrastructure:
- Redis channel: planarchy:sse → capakraken:sse
- Logger service name: planarchy-api → capakraken-api
- Anonymization seed updated
- Start/stop/restart scripts updated

Test data:
- Seed emails: @planarchy.dev → @capakraken.dev
- E2E test credentials: all 11 spec files updated
- Email defaults: @planarchy.app → @capakraken.app
- localStorage keys: planarchy_* → capakraken_*

Documentation: 30+ .md files updated

Verification:
- pnpm install: workspace resolution works
- TypeScript: only pre-existing TS2589 (no new errors)
- Engine: 310/310 tests pass
- Staffing: 37/37 tests pass

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-27 13:18:09 +01:00

151 lines
4.8 KiB
TypeScript

import type { SkillEntry } from "@capakraken/shared";
let _xlsx: typeof import("xlsx") | null = null;
async function getXLSX() {
if (!_xlsx) {
_xlsx = await import("xlsx");
}
return _xlsx;
}
export interface ParsedEmployeeInfo {
displayName?: string;
areaOfExpertise?: string;
yearsOfExperience?: number;
portfolioUrl?: string;
}
export interface ParsedSkillMatrix {
employeeInfo: ParsedEmployeeInfo;
skills: SkillEntry[];
}
// Maps Excel proficiency (1-4) → plANARCHY proficiency (2-5)
function mapProficiency(raw: string): 1 | 2 | 3 | 4 | 5 | null {
const n = parseInt(raw, 10);
if (isNaN(n) || n === 0) return null;
if (n === 1) return 2;
if (n === 2) return 3;
if (n === 3) return 4;
if (n === 4) return 5;
return null;
}
function parseEmployeeInfo(rows: Record<string, string>[]): ParsedEmployeeInfo {
const info: ParsedEmployeeInfo = {};
for (const row of rows) {
const item = (row["item"] ?? "").toLowerCase().trim();
const value = (row["property"] ?? "").trim();
if (!value || value === "please select") continue;
if (item.includes("full name")) {
info.displayName = value;
} else if (item.includes("area of expertise") || item.includes("main focus")) {
info.areaOfExpertise = value;
} else if (item.includes("years of experience")) {
const n = parseFloat(value);
if (!isNaN(n)) info.yearsOfExperience = Math.round(n);
} else if (item.includes("portfolio") || item.includes("url") || item.includes("linkedin") || item.includes("artstation")) {
if (value.startsWith("http")) info.portfolioUrl = value;
}
}
return info;
}
function parseSkillSheet(rows: Record<string, string>[], mainSkillSet: Set<string>): SkillEntry[] {
const skills: SkillEntry[] = [];
let mainSkillCount = 0;
for (const row of rows) {
const category = (row["category"] ?? "").trim();
const item = (row["item"] ?? "").trim();
const property = (row["property"] ?? "").trim();
const mainSkillRaw = (row["main skillset"] ?? "").trim();
if (!item) continue;
const proficiency = mapProficiency(property);
if (proficiency === null) continue; // skip 0 / no experience
const isMainSkillCandidate = mainSkillRaw === "1" || mainSkillRaw === "2";
const isMainSkill = isMainSkillCandidate && mainSkillCount < 2;
if (isMainSkill) {
mainSkillSet.add(item);
mainSkillCount++;
}
skills.push({
skill: item,
...(category ? { category } : {}),
proficiency,
...(isMainSkillCandidate ? { isMainSkill: true } : {}),
});
}
return skills;
}
/**
* Parse a skill matrix workbook (xlsx ArrayBuffer) into structured data.
* Returns ParsedSkillMatrix with employeeInfo and merged skills array.
*/
export async function parseSkillMatrixWorkbook(data: ArrayBuffer): Promise<ParsedSkillMatrix> {
const XLSX = await getXLSX();
const workbook = XLSX.read(new Uint8Array(data), { type: "array" });
const employeeSheet = workbook.Sheets["Employee Information"];
const softwareSheet = workbook.Sheets["Software Skills"];
const technicalSheet = workbook.Sheets["Technical Skillset"];
const employeeRows = employeeSheet
? XLSX.utils.sheet_to_json<Record<string, string>>(employeeSheet, { raw: false, defval: "" })
: [];
const softwareRows = softwareSheet
? XLSX.utils.sheet_to_json<Record<string, string>>(softwareSheet, { raw: false, defval: "" })
: [];
const technicalRows = technicalSheet
? XLSX.utils.sheet_to_json<Record<string, string>>(technicalSheet, { raw: false, defval: "" })
: [];
const employeeInfo = parseEmployeeInfo(employeeRows);
// Track main skills across both sheets (max 2 total)
const mainSkillSet = new Set<string>();
const softwareSkills = parseSkillSheet(softwareRows, mainSkillSet);
const technicalSkills = parseSkillSheet(technicalRows, mainSkillSet);
// Merge: deduplicate by skill name (prefer higher proficiency)
const skillMap = new Map<string, SkillEntry>();
for (const s of [...softwareSkills, ...technicalSkills]) {
const existing = skillMap.get(s.skill);
if (!existing || s.proficiency > existing.proficiency) {
skillMap.set(s.skill, s);
}
}
return {
employeeInfo,
skills: Array.from(skillMap.values()),
};
}
/**
* Fuzzy match an areaOfExpertise string against a list of role names.
* Returns the best matching role name or null.
*/
export function matchRoleName(areaOfExpertise: string, roleNames: string[]): string | null {
if (!areaOfExpertise) return null;
const needle = areaOfExpertise.toLowerCase().trim();
// Exact match first
const exact = roleNames.find((r) => r.toLowerCase() === needle);
if (exact) return exact;
// Partial match (needle contains role name or vice versa)
const partial = roleNames.find(
(r) => needle.includes(r.toLowerCase()) || r.toLowerCase().includes(needle),
);
return partial ?? null;
}