Ctrl K
ComunidadGuías…

lib/career-profile.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/** 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;
}