# Validación CI Sistema — Validadores y Scripts Completos

Código fuente completo de los validadores y el script ejecutable.

---

## Validador de Agentes

```javascript
// validation/validators/agente.validator.js
import { parse } from 'yaml';
import { readFileSync } from 'fs';

const CAMPOS_AGENTE = {
  obligatorios: ['name', 'description'],
  opcionales: {
    model:          ['claude-opus-4-5', 'claude-sonnet-4-6', 'claude-haiku-3-5'],
    permissionMode: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
    color:          ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'cyan', 'white'],
  },
  condicionales: {
    tools: {
      valoresPermitidos: [
        'Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob',
        'WebSearch', 'WebFetch', 'TodoRead', 'TodoWrite',
        'Task', 'Skill', 'mcp__context7__resolve-library-id',
        'mcp__context7__get-library-docs',
      ],
    },
  },
};

export function validarAgente(rutaArchivo) {
  const contenido = readFileSync(rutaArchivo, 'utf8');
  const errores = [];

  const match = contenido.match(/^---\n([\s\S]*?)\n---/);
  if (!match) {
    return [{ campo: 'frontmatter', error: 'No se encontró frontmatter YAML' }];
  }

  let frontmatter;
  try {
    frontmatter = parse(match[1]);
  } catch (e) {
    return [{ campo: 'frontmatter', error: `YAML inválido: ${e.message}` }];
  }

  for (const campo of CAMPOS_AGENTE.obligatorios) {
    if (!frontmatter[campo] || String(frontmatter[campo]).trim() === '') {
      errores.push({ campo, error: `Campo obligatorio ausente o vacío: '${campo}'` });
    }
  }

  if (frontmatter.description && frontmatter.description.length < 20) {
    errores.push({
      campo: 'description',
      error: `Description demasiado corta (${frontmatter.description.length} chars, mínimo 20)`,
    });
  }

  for (const [campo, valoresPermitidos] of Object.entries(CAMPOS_AGENTE.opcionales)) {
    if (frontmatter[campo] !== undefined) {
      if (!valoresPermitidos.includes(frontmatter[campo])) {
        errores.push({
          campo,
          error: `Valor inválido '${frontmatter[campo]}'. Permitidos: ${valoresPermitidos.join(', ')}`,
        });
      }
    }
  }

  if (frontmatter.tools) {
    const toolsArray = String(frontmatter.tools).split(',').map(t => t.trim());
    for (const tool of toolsArray) {
      if (!CAMPOS_AGENTE.condicionales.tools.valoresPermitidos.includes(tool)) {
        errores.push({ campo: 'tools', error: `Tool desconocida: '${tool}'` });
      }
    }
  }

  const nombreEsperado = path.basename(rutaArchivo, '.md');
  if (frontmatter.name && frontmatter.name !== nombreEsperado) {
    errores.push({
      campo: 'name',
      error: `name '${frontmatter.name}' no coincide con nombre de archivo '${nombreEsperado}'`,
    });
  }

  return errores;
}
```

---

## Validador de Skills

```javascript
// validation/validators/skill.validator.js
import { statSync, readFileSync } from 'fs';
import path from 'path';

export function validarSkill(directorioSkill) {
  const errores = [];
  const rutaSkill = path.join(directorioSkill, 'SKILL.md');

  try { statSync(rutaSkill); } catch {
    errores.push({ skill: path.basename(directorioSkill), error: 'SKILL.md no encontrado' });
    return errores;
  }

  const contenido = readFileSync(rutaSkill, 'utf8');

  if (contenido.trim().length === 0) {
    errores.push({ skill: path.basename(directorioSkill), error: 'SKILL.md está vacío' });
    return errores;
  }

  const match = contenido.match(/^---\n([\s\S]*?)\n---/);
  if (!match) {
    errores.push({ skill: path.basename(directorioSkill), error: 'Sin frontmatter YAML' });
    return errores;
  }

  let frontmatter;
  try { frontmatter = parse(match[1]); } catch (e) {
    errores.push({ skill: path.basename(directorioSkill), error: `Frontmatter inválido: ${e.message}` });
    return errores;
  }

  if (!frontmatter.name) errores.push({ skill: path.basename(directorioSkill), error: "Falta 'name'" });
  if (!frontmatter.description) errores.push({ skill: path.basename(directorioSkill), error: "Falta 'description'" });

  const lineas = contenido.split('\n').length;
  if (lineas < 50) {
    errores.push({ skill: path.basename(directorioSkill), error: `Muy corto (${lineas} líneas)`, severidad: 'advertencia' });
  }

  return errores;
}
```

---

## Validador de Hooks

```javascript
// validation/validators/hook.validator.js
import { execSync } from 'child_process';

export function validarHook(rutaHook) {
  const errores = [];
  const contenido = readFileSync(rutaHook, 'utf8');

  try {
    execSync(`node --check "${rutaHook}"`, { stdio: 'pipe' });
  } catch (e) {
    errores.push({ hook: path.basename(rutaHook), error: `Sintaxis inválida: ${e.stderr?.toString().split('\n')[0]}` });
    return errores;
  }

  if (!contenido.includes('process.exit(')) {
    errores.push({ hook: path.basename(rutaHook), error: 'No tiene process.exit() explícito' });
  }

  const esObservacion = rutaHook.includes('observe');
  if (esObservacion && contenido.includes('process.exit(1)')) {
    errores.push({ hook: path.basename(rutaHook), error: 'Hook de observación no debe usar exit(1)' });
  }

  const esToolHook = rutaHook.match(/pre-|post-|observe/i);
  if (esToolHook && !contenido.includes('stdin') && !contenido.includes('readFileSync')) {
    errores.push({ hook: path.basename(rutaHook), error: 'No lee stdin', severidad: 'advertencia' });
  }

  if (contenido.includes('execSync') && !contenido.includes('timeout')) {
    errores.push({ hook: path.basename(rutaHook), error: 'execSync sin timeout', severidad: 'advertencia' });
  }

  return errores;
}
```

---

## Validador de Comandos

```javascript
// validation/validators/comando.validator.js
export function validarComando(directorioComando) {
  const errores = [];
  const nombreComando = path.basename(directorioComando);

  const archivosPermitidos = ['command.md', `${nombreComando}.md`, 'index.md'];
  const archivosEncontrados = archivosPermitidos.filter(f =>
    existsSync(path.join(directorioComando, f))
  );

  if (archivosEncontrados.length === 0) {
    errores.push({ comando: nombreComando, error: `Sin archivo de comando. Esperado: ${archivosPermitidos.join(' o ')}` });
    return errores;
  }

  const rutaArchivo = path.join(directorioComando, archivosEncontrados[0]);
  const contenido = readFileSync(rutaArchivo, 'utf8');

  if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(nombreComando)) {
    errores.push({ comando: nombreComando, error: `No está en kebab-case` });
  }

  if (contenido.startsWith('---')) {
    const match = contenido.match(/^---\n([\s\S]*?)\n---/);
    if (!match) errores.push({ comando: nombreComando, error: 'Frontmatter sin cerrar' });
  }

  const sinFrontmatter = contenido.replace(/^---[\s\S]*?---\n/, '').trim();
  if (sinFrontmatter.length < 10) {
    errores.push({ comando: nombreComando, error: 'Sin contenido de instrucciones' });
  }

  return errores;
}
```

---

## Validador de Reglas

```javascript
export function validarReglas(directorioReglas) {
  const errores = [];
  const archivos = globSync('**/*.md', { cwd: directorioReglas, absolute: true });

  for (const ruta of archivos) {
    const contenido = readFileSync(ruta, 'utf8').trim();
    if (contenido.length < 20) {
      errores.push({ regla: path.relative(directorioReglas, ruta), error: 'Vacía o muy corta' });
    }
  }

  const contenidos = archivos.map(f => ({
    nombre: path.basename(f, '.md'),
    texto: readFileSync(f, 'utf8').toLowerCase(),
  }));

  for (let i = 0; i < contenidos.length; i++) {
    for (let j = i + 1; j < contenidos.length; j++) {
      const a = contenidos[i], b = contenidos[j];
      const siempresA = extraerTerminosSiempre(a.texto);
      const nuncasB = extraerTerminosNunca(b.texto);
      const conflictos = siempresA.filter(t => nuncasB.includes(t));
      if (conflictos.length > 0) {
        errores.push({
          regla: `${a.nombre} <-> ${b.nombre}`,
          error: `Posible contradicción: SIEMPRE vs NUNCA ${conflictos[0]}`,
          severidad: 'advertencia',
        });
      }
    }
  }
  return errores;
}
```

---

## Catálogo Automático

```javascript
export async function generarCatalogo(raizSistema) {
  const ahora = new Date().toISOString();
  const agentes  = globSync('agentes/**/*.md', { cwd: raizSistema });
  const skills   = globSync('habilidades/*/SKILL.md', { cwd: raizSistema });
  const hooks    = globSync('hooks/**/*.js', { cwd: raizSistema });
  const comandos = globSync('comandos/*/', { cwd: raizSistema });
  const reglas   = globSync('reglas/**/*.md', { cwd: raizSistema });

  return {
    generado_en: ahora,
    totales: { agentes: agentes.length, skills: skills.length, hooks: hooks.length, comandos: comandos.length, reglas: reglas.length },
    agentes: agentes.map(a => ({ nombre: path.basename(a, '.md'), ruta: a, tamano_bytes: statSync(path.join(raizSistema, a)).size })),
    skills: skills.map(s => ({ nombre: path.basename(path.dirname(s)), ruta: s, lineas: readFileSync(path.join(raizSistema, s), 'utf8').split('\n').length })),
  };
}
```

---

## Script de Validación Ejecutable

```javascript
#!/usr/bin/env node
// scripts/validar-sistema.js
// Uso: node scripts/validar-sistema.js [--fix] [--catalog]

import path from 'path';
import { fileURLToPath } from 'url';
import { globSync } from 'glob';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RAIZ = path.resolve(__dirname, '..');

const args = process.argv.slice(2);
const MODO_FIX     = args.includes('--fix');
const GENERAR_CAT  = args.includes('--catalog');

let totalErrores = 0;
let totalAdvert  = 0;

function reporte(tipo, items, validador) {
  console.log(`\n${'='.repeat(50)}`);
  console.log(`Validando ${tipo} (${items.length})...`);

  for (const item of items) {
    const errores = validador(item);
    const criticos = errores.filter(e => e.severidad !== 'advertencia');
    const advert   = errores.filter(e => e.severidad === 'advertencia');

    if (criticos.length > 0) {
      console.error(`  FAIL ${path.basename(item)}`);
      criticos.forEach(e => console.error(`       ERROR: ${e.error}`));
      totalErrores += criticos.length;
    }
    if (advert.length > 0) {
      console.warn(`  WARN ${path.basename(item)}`);
      advert.forEach(e => console.warn(`       WARN: ${e.error}`));
      totalAdvert += advert.length;
    }
    if (criticos.length === 0 && advert.length === 0) {
      console.log(`  OK   ${path.basename(item)}`);
    }
  }
}

reporte('Agentes',   globSync('agentes/**/*.md', { cwd: RAIZ, absolute: true }), validarAgente);
reporte('Skills',    globSync('habilidades/*', { cwd: RAIZ, absolute: true }),   validarSkill);
reporte('Hooks',     globSync('hooks/**/*.js', { cwd: RAIZ, absolute: true }),   validarHook);
reporte('Comandos',  globSync('comandos/*', { cwd: RAIZ, absolute: true }),      validarComando);

console.log(`\n${'='.repeat(50)}`);
console.log(`RESUMEN: ${totalErrores} errores, ${totalAdvert} advertencias`);

if (GENERAR_CAT) {
  const catalogo = await generarCatalogo(RAIZ);
  writeFileSync(path.join(RAIZ, '.claude', 'catalogo.json'), JSON.stringify(catalogo, null, 2));
  console.log('Catálogo generado en .claude/catalogo.json');
}

process.exit(totalErrores > 0 ? 1 : 0);
```

### Uso en CI (GitHub Actions)

```yaml
name: Validar integridad del sistema SWL
on:
  push:
    branches: [main]
  pull_request:
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run validate:catalog
```
