lib/career-profile.mjs
Rama main · réplica de lectura del repositorio original.
/** Shared schema v1 validation and read-only loading for Master Career Profile. */
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { loadYaml } from './yaml-document.mjs';
import { getCareerOpsRoot } from '../path-resolver.mjs';
const STATUS = new Set(['needs_review', 'verified']);
export function validateProfile(profile) {
const errors = [];
if (!profile || typeof profile !== 'object' || profile.schema_version !== 1) errors.push('schema_version must be 1');
if (!profile?.candidate || typeof profile.candidate !== 'object') errors.push('candidate must be a mapping');
const ids = new Set();
const checkFact = (fact, path) => {
if (!fact || typeof fact !== 'object') { errors.push(`${path} must be a mapping`); return; }
if (typeof fact.id !== 'string' || !fact.id.trim()) errors.push(`${path}.id is required`);
else if (ids.has(fact.id)) errors.push(`duplicate id: ${fact.id}`);
else ids.add(fact.id);
if (typeof fact.text !== 'string' || !fact.text.trim()) errors.push(`${path}.text is required`);
if (!STATUS.has(fact.review_status)) errors.push(`${path}.review_status must be needs_review or verified`);
if (!fact.evidence || typeof fact.evidence.source !== 'string' || !fact.evidence.source.trim() ||
!Number.isInteger(fact.evidence.line) || fact.evidence.line < 1 ||
typeof fact.evidence.quote !== 'string' || !fact.evidence.quote.trim()) errors.push(`${path}.evidence requires source, positive line, and quote`);
};
const arrays = ['summary', 'experiences', 'projects', 'education', 'certifications', 'skills'];
for (const key of arrays) if (!Array.isArray(profile?.[key])) errors.push(`${key} must be a list`);
for (const key of ['summary', 'certifications', 'skills']) {
(Array.isArray(profile?.[key]) ? profile[key] : []).forEach((fact, i) => checkFact(fact, `${key}[${i}]`));
}
for (const key of ['experiences', 'projects', 'education']) {
(Array.isArray(profile?.[key]) ? profile[key] : []).forEach((entry, i) => {
const path = `${key}[${i}]`;
if (!entry || typeof entry.label !== 'string' || !entry.label.trim()) errors.push(`${path}.label is required`);
checkFact({ ...entry, text: entry?.label, review_status: entry?.review_status ?? 'verified' }, path);
if (!Array.isArray(entry?.facts)) errors.push(`${path}.facts must be a list`);
else entry.facts.forEach((fact, j) => checkFact(fact, `${path}.facts[${j}]`));
});
}
return errors;
}
/** Missing/invalid profiles fail; only the importer may initialize a new one. */
export function loadValidatedProfile({ dataRoot = getCareerOpsRoot(), file = join(dataRoot, 'data', 'career-profile.yml') } = {}) {
const profile = loadYaml(readFileSync(file, 'utf8'));
const errors = validateProfile(profile);
if (errors.length) throw new Error(`Invalid profile:\n- ${errors.join('\n- ')}`);
return profile;
}