Especificación del formato de schema

Todo lo que puede declarar un schema en /DataLab/config/upload_schemas/{OP_ID}. Esta es la referencia para crear o endurecer operaciones. Los cambios se propagan a los wizards en vivo (el loader usa onValue) — sin redeploy.

Regla de oro: el op_id del nodo debe coincidir EXACTO con el id del catálogo. Un schema escrito en una clave con typo (ej. ASIST_CTRL_TIEMP vs ASIST_CTRL_TIEMPOS) queda huérfano y la UI seguirá mostrando SCHEMA MIN.

Estructura completa

{
  "op_id": "MOV_INT_ADMIN",             // = clave del nodo
  "area_id": "ORI",
  "title": "Movilidad internacional administrativos",
  "description": "…",

  // ── Hoja Excel ──
  "sheetName": "P_Admin_int_sal",       // pestaña REAL del template
  "sheetAliases": ["p_admin_int_sal", "datos", …],   // fuzzy match
  "headerRow": 18,                      // fila (1-indexed) de las cabeceras

  // ── Destino de datos ──
  "dataArrayField": "movilidad_int_admin",   // clave del array en el reporte
  "dataPathTemplate": "DataLab/ciclos_de_carga/ori/{cicloKey}/data/movilidad_int_admin",
  "configPath": "DataLab/config/ciclo_mov_int_admin",
  "codigoPrefix": "MIA",                // prefijo ÚNICO de certificados

  // ── Ciclo ──
  "defaultCicloKey": "2026-B",          // semestral: AAAA-A|B · mensual: AAAA-MM · anual: AAAA
  "cicloGranularity": "semestral",      // semestral | mensual | anual
  "reportNature": "acumulado",          // opcional: reportes acumulados al corte
  "reportNote": "…",

  // ── Validación ──
  "requiredColumns": ["semestre", "tipo_documento", …],  // ⚠ KEYS CANÓNICAS
  "criticalFields":  ["semestre", "tipo_documento", …],  // no pueden ir vacíos
  "dateColumns": ["fecha_inicio_movilidad"],             // reciben coerción de fecha
  "fields": [ … ],                // ver §Campos
  "fieldValidations": [ … ],      // ver §Validaciones
  "controlledValues": { … },      // ver §Catálogos
  "integrityRules": [ … ],        // ver §Reglas
  "additionalSheets": [ … ],      // ver §Multi-hoja
  "templateUrl": "https://…"      // se PRESERVA al reescribir el schema
}

Campos (fields[])

{
  "key": "numero_documento",       // snake_case canónico — así viaja a RTDB
  "label": "Número documento",     // para UI
  "type": "text",                  // text | number | date | currency | boolean
  "currency": "COP",               // solo type=currency (COP | USD)
  "required": true,                // el valor no puede faltar
  "requiredColumn": true,          // la COLUMNA debe existir en el Excel
  "min": 0, "max": 500,            // cotas para type=number
  "aliases": ["NÚMERO DOCUMENTO", "NUMERO DOCUMENTO", "IDENTIFICACIÓN"]
}
Aliases: el matcher normaliza con trim → lowercase → espacios→_ y prueba primero la key, luego cada alias. Incluye SIEMPRE variantes con y sin tilde, y los typos literales del template oficial (TPO DOCUMENTO, DEPENDENCIA ADMINSITRATIVA, FUENTE FINANACIACIÓN existen en producción y deben aliarse tal cual).

Validaciones (fieldValidations[])

type: "pattern"

{ "key": "semestre", "type": "pattern",
  "pattern": "^(20[2-3]\\d)\\s?-?\\s?(A|B|I|II|1|2|a|b|i|ii)$",
  "msg": "Formato: 2026-A, 2026-B, …",   // alineado con lo que el pattern acepta
  "nullable": false }                     // true ⇒ vacío es válido

Patrones de la casa (copiar tal cual):

UsoPattern
Semestre^(20[2-3]\d)\s?-?\s?(A|B|I|II|1|2|a|b|i|ii)$
Año^20\d{2}$
Documento^[1-9]\d{5,14}$ (6-15 dígitos, sin ceros a la izquierda)
Texto no vacío^\S.*\S$|^\S$ (permite 1 carácter, rechaza espacios en los bordes)
Moneda COP^\d{1,3}(\.\d{3})*(,\d{1,2})?$|^\d+([.,]\d{1,2})?$
Entero ≥ 0^(0|[1-9]\d*)$
Hora 24h/12h^\s*(([01]?\d|2[0-3]):[0-5]\d|(0?[1-9]|1[0-2]):[0-5]\d\s?(AM|PM|am|pm|a\.m\.|p\.m\.))\s*$
SNIES^\d{3,10}$
Promedio 0–5^(5([.,]0+)?|[0-4]([.,]\d{1,3})?)$
Duración con unidad^\s*\d+([.,]\d+)?\s*(día(s)?|Día(s)?|…|año(s)?|AÑO(S)?)\s*$ (unidad OBLIGATORIA, variantes de caja explícitas)
Los patterns son case-sensitive y sin flags: si necesitas tolerar “Semanas”, “SEMANAS” y “semanas”, enumera las variantes en el alternation. El validador NO aplica i.

type: "enum"

{ "key": "tipo_documento", "type": "enum",
  "values": ["CC", "cc", "C.C.", "CÉDULA DE CIUDADANÍA", …],
  "caseInsensitive": true,
  "nullable": false,
  "msg": "Tipo de documento debe estar en el catálogo" }
Antipatrón detectado en auditoría: definir controlledValues sin su fieldValidation type:"enum" deja el catálogo como documentación muerta — cualquier texto pasa. Siempre en pareja.

type: "date-range"

{ "key": "fecha_grado", "type": "date-range",
  "min": "2010-01-01",
  "max": "{today+30d}",      // placeholders: {today} {today±Nd} {today±Ny}
  "nullable": false, "msg": "…" }

Catálogos (controlledValues{})

Mapa key → valores permitidos. Convenciones:

Reglas de integridad (integrityRules[])

kindSemánticaEjemplo
cross-field-orderfieldA op fieldBfecha_inicio < fecha_final
cross-field-conditionalsi condition entonces requirediscapacidad=SI ⇒ tipo_discapacidad no vacío
cross-field-sumΣfields op targetcantidades desagregadas ≤ cantidad total
cross-field-year-matchaño de compareTo = fieldanio ≙ año(fecha_formulacion)
cross-field-vs-cicloKeycampo ≙ {cicloKey} de la rutaanio = ciclo anual de carga

Las reglas usan severity: warning|info — advierten sin bloquear la carga.

Multi-hoja (additionalSheets[])

Para operaciones cuyo template trae más de una pestaña de datos:

Checklist para publicar un schema

  1. Confirmar op_id EXACTO contra el catálogo (los ids largos se truncan en la UI).
  2. sheetName y headerRow tomados del template REAL (varían: 18, 19, 20).
  3. requiredColumns con keys canónicas, todas presentes en fields[].key.
  4. Cada controlledValues con su validación enum + caseInsensitive.
  5. Solo columnas identificadoras como required — el resto opcional con nullable: true.
  6. Preservar templateUrl existente al reescribir (y nunca escribir undefined: RTDB lo rechaza).
  7. Quitar __minimal y verificar que el chip pase a SCHEMA OK.
  8. Agregar el dataArrayField a dataCandidates en procesar.html.
  9. codigoPrefix único en todo el catálogo.