Generación de horarios

Cheatsheet para ubicar hard/soft constraints, validaciones de health, precheck, failure reports y violaciones soft en un solo recorrido.

  • Hard / bloquea / falla
  • Warning / no bloquea
  • Soft / preferencia
  • Éxito / done

Tipo: cheatsheet operativa (análisis rápido)
Última revisión: 2026-07-23
Vista HTML: schedule-generation-overview.html (npm run docs:schedule-overview-html)
Detalle de flujo: schedule-generation-flow.md
Contrato de validación: schedule-generation-validation.md
Solver interno: kampus-schedule-generator-worker/docs/architecture.md

Documento de consulta rápida. Usa tablas para localizar dónde vive cada regla, qué bloquea y qué reporte sale. Para contratos profundos (V-04/V-05, copy H1, SSE), sigue los enlaces de arriba.


1

Flujo en una mirada

Health advisory POST task ¿Gate ERROR?

409 — no encola

No
Cola Precheck CP-SAT Admin hours done

Fallo en precheck / solver / admin → failureReport

Paso Repo ¿Bloquea encolado? Resultado si falla
Health advisory (GET) API → UI No Banner / lista de issues
Health gate (POST) API Sí (solo ERROR) 409 + issues[]
Payload integrity API 409 / rechazo
solverConfig en cola API → Worker No Usa defaults en código si falta/inválido
Regenerar (warmStart ± fixedBlocks) API → Worker → UI Sí (gate ERROR) Base intacto; task failed, 400 keys o 409
Precheck core + extended Worker No failureReport phase: precheck
CP-SAT clases (hard + soft) Worker No failureReport phase: solver
Admin hours Phase-2 Worker No failureReport phase: admin_hours
Éxito Worker → API → UI Schedule + constraintViolationsReport
Éxito regenerar Worker → API → UI Mismo _id (replace) + reporte nuevo
status task Significado
queued Encolada
processing Worker corriendo
done Schedule creado o reemplazado (+ violaciones soft opcionales)
failed Precheck / solver / admin / error (+ failureReport)

Regenerar: desde el detalle (task done o failed con resultScheduleId), re-encola la misma task con warmStart (hints desde scheduleBlocks + replaceScheduleId + locks opcionales fixedBlocks desde fixedBlockKeys). UI: fijar/desfijar bloques (ephemeral). Detalle: flow — Regeneración.


2

Hard constraints (CP-SAT clases)

Si se violan → no hay solución (SOLVER_UNSAT o precheck previo). No tienen peso: son factibilidad.

# Constraint Qué impone Módulo
H1 Cupo semanal Exactamente sessionsPerWeek sesiones por lección model_builder.py
H2 Unicidad de grupo A lo sumo 1 clase por grupo/slot model_builder.py
H3 Unicidad de docente A lo sumo 1 clase por docente/periodo de reloj model_builder.py
H4 Solape cross-TF docente Mismo docente no en periodos de reloj que se solapan (multi-etapa) model_builder.py
H5 Subject CANNOT Slot marcado CANNOT en materia → var = 0 model_builder.py
H6 Pair-block Distribución pair-first: máximo run 2 consecutivos; días activos = ceil(n/2); overflow/n≥10 exige par por día; multi-marco: adyacencia = intersección de todos los TF de la lección pair_constraints.py
H7 Capacidad de aula Σ grupos concurrentes ≤ groupsCapacity por periodo model_builder.py
H8 Solape cross-TF aula Misma capacidad en periodos de reloj que se solapan model_builder.py
H9 Opciones de aula Si hay rooms (n>0): exactamente n aulas distintas de rooms ∪ backupRooms (XOR de opciones) room_options.py + builder

Notas rápidas

Tema Comportamiento
Docente CANNOT No es hard: es soft avoid_cannot (peso alto)
Materia CANNOT es hard (H5)
Multi-grupo sincronizado Una sola var de asignación para todos los grupos de la lección
Grid del grupo course.timeFrameId → un solo timeframe por grupo

Hard — Phase-2 (horas admin)

# Constraint Qué impone
A1 Cupo admin Todas las sesiones requeridas por (adminHour, teacher)
A2 No solape con clases Periodos de clase del docente son bloque duro
A3 Unicidad admin A lo sumo 1 bloque admin por docente/periodo (incluye solapes)
A4 Gap fill duro Mientras haya hueco interior SESSION rellenable, no colocar en borde

Si falla → INSUFFICIENT_ADMIN_HOUR_SLOTS (phase: admin_hours).


3

Soft constraints (objetivo CP-SAT)

Si se violan → el horario igual se genera. Todos los softs de esta tabla pesan en el objetivo CP-SAT. El panel de avisos al admin solo muestra un subconjunto accionable — ver §7.

Pesos por defecto (tabla abajo). Overrides: kampus-admin → Mongo → campo solverConfig del mensaje de cola (ver §3b). Jerarquía: 10000 (nivel 4 / critical) → 100 (nivel 3 / warning) → 1 (nivel 2 / info). Explícitos de nivel 4 pesan más que derivados de familia (nivel 3). La separación (~100× entre niveles) evita que muchas warnings compensen una crítica.

Constraint Peso default Severidad Qué desalienta / prefiere Instrumentado
avoid_cannot 10000 critical Asignar en franja CANNOT del docente (o materia si llega al soft)
avoid_same_day_subject_pairs 10000 critical Pares noDayMatchSubjectIds el mismo día (mismo grupo)
avoid_related_subject_adjacency 10000 critical Materias “familia” (ej. Biología / Lab. Biología) en SESSION consecutivos
minimize_teacher_gaps 100 warning Huecos interiores SESSION del docente (crédito admin en Fase 1; métrica final en log post Fase 2)
separate_extra_session 100 warning Singleton de overflow demasiado cerca del bloque doble (max(0, 3−d))
minimize_teacher_load_variance 100 warning Desviación de carga diaria del docente vs promedio en días balanceables
avoid_room_contention 100 warning Compartir aula aunque groupsCapacity lo permita
avoid_related_subject_same_day 100 warning Pares de familia derivados (mismo grupo) el mismo día; flag deriveNoDayMatchFromFamilies
prefer_can_over_maybe 1 info Preferir CAN sobre MAYBE
avoid_pair_across_break 1 info Bloque doble que cruza un BREAK en el eje del timeframe
spread_lesson_days 1 info Lección activa en dos días calendario consecutivos (reemplaza stub balance_subjects_per_day)
prefer_room_backup_tier 1 info Usar aulas de backupRooms (tier > 0)

Monitoreo: si en colegios reales avoid_same_day_subject_pairs a 10000 sacrifica demasiado otras preferencias, bajar a nivel 3 (100) desde kampus-admin.

Soft — Phase-2 (orden de prioridad)

Prioridad Objetivo Notas
1 Rellenar huecos interiores SESSION Reward dominante (~10000) + hard A4
2 Preferir CAN sobre MAYBE
3 Repartir sesiones en la semana Por work item y por docente
4 Orden determinista de slots Reproducibilidad

3b. Configuración del solver (kampus-admin)

Tuneo global de límites CP-SAT, early stop, pesos soft y flags. No requiere redeploy del worker.

Pieza Rol
UI kampus-admin → sección «Configuración del solver»: formulario dinámico desde GET, guardar con PUT, badge «modificado» + reset al default por campo
API GET/PUT /solver-configuration (JWT admin). Catálogo tipado en código (22 claves) + overrides en Mongo (scope: 'global')
Cola Al encolar/reintentar, la API embebe solverConfig efectivo (defaults ⊕ overrides) en el payload; queda en queuedPayloadJson
Worker Precedencia: valor del mensaje (validado/clampeado) → default en código. Env SOLVER_* / SOFT_WEIGHT_* / DERIVE_* deprecadas (solo warning al arranque)

Siguen en env del worker (infra): RABBITMQ_*, KAMPUS_API_* / WORKER_API_KEY, LOG_*, WORKER_GRACEFUL_SHUTDOWN_SECONDS (default fijo 60 s), PRECHECK_EXTENDED_PROFILE.

Contrato completo de claves: schedule-generation-flow.md (§ construcción del mensaje / solverConfig).


4

Validaciones de health (hoy)

Fuente: kampus-apiConfigurationHealthService.
Advisory: GET /institutions/configuration-health.
Gate: mismo set ERROR en POST /schedules/generation-tasks.

Copy admin: title / detail / fixHints usan vocabulario de la grilla (franja de clase, Disponible / Disponibilidad parcial / No disponible, horario base, conjunto de aulas), tono . Los códigos de la tabla siguen en inglés; no exponer SESSION / CAN / MAYBE / pool al administrador.

ERROR — bloquean generación

Code Qué valida Ámbito
COURSE_MISSING_TIMEFRAME Curso con grupos sin timeFrameId Curso
COURSE_TIMEFRAME_INVALID timeFrameId no existe en DB Curso
SUBJECT_NO_SCHEDULABLE_SLOTS Sin SESSION utilizables (multi-marco: intersección exacta vacía post-CANNOT) Materia
SUBJECT_SESSIONS_EXCEED_ALLOWED_SLOTS sessionsPerWeek > capacidad (multi-marco: |intersección| − CANNOT) Materia
ASSIGNMENT_NO_COMMON_SLOTS Teacher ∩ subject = vacío (multi-grupo: marcos de todos los grupos) Asignación
ASSIGNMENT_INSUFFICIENT_COMMON_SLOTS Slots comunes insuficientes (single-group; multi-grupo parcial → worker) Asignación
TEACHER_DEMAND_EXCEEDS_STAGE_CAPACITY Demanda del docente > capacidad de la etapa Docente × etapa
GROUP_DEMAND_EXCEEDS_CAPACITY Demanda del grupo > slots del TF del curso Grupo
GROUP_DEMAND_BELOW_CAPACITY 0 < demand < capacity (grupo con materias incompleto vs horario base) Grupo
TEACHER_ADMIN_HOUR_SESSIONS_EXCEED_ALLOWED_SLOTS Horas admin no caben en slots elegibles Admin hour
TEACHER_ADMIN_HOUR_REFERENCES_INVALID_TIMEFRAME Admin hour apunta a TF inválido Admin hour
SUBJECT_ROOM_POOL_INSUFFICIENT Pool rooms ∪ backupRooms insuficiente para n primarias Materia
SUBJECT_ROOM_CONFIGURATION Duplicados o solape entre rooms / backupRooms Materia

WARNING — no bloquean (health-only)

Code Qué valida Notas
TEACHER_CROSS_STAGE_OVERLOAD Demanda global del docente > periodos SESSION de reloj únicos (CAN/MAYBE) Espejo de capacidad worker; multi-etapa
SHARED_ROOM_POOL_PRESSURE (V-06) Throughput semanal mismo-día en pool de aulas compartido Heurística; no es V-05
DUPLICATE_SUBJECT_IN_GROUP (V-08) Dos Subjects distintos con mismo nombre normalizado en un grupo Warning de configuración

Modelo course-centric

Toda regla de slots/capacidad en health resuelve:

course.timeFrameIdtimeFramesById[id]

Marcos a nivel institución, asignables por curso (varios cursos de la misma etapa pueden usar marcos distintos). Cola: timeFrames[] en la raíz; no hay stages[].timeframe.

GROUPED/COMBINED multi-marco: la lección conjunta solo usa franjas con el mismo triplete (día, inicio, fin) en todos los marcos (intersección exacta; sin solape parcial). Create/update de materia y PUT /time-frames/:id/courses409 si esa intersección (menos CANNOT) es menor que sessionsPerWeek. Health aplica la misma capacidad de intersección.


5

Precheck worker (antes de CP-SAT)

No bloquean encolado. Si fallan → task failed + failureReport.

Code Capa Qué detecta
PAIR_BLOCK_CAPACITY Core (V-04) Lección n≥2 sin suficientes días con par consecutivo (post-CANNOT; incluye regla n≥10)
NO_COMPATIBLE_GROUP_SLOTS Extended Lección multi-grupo con cero slots alineados
LESSON_QUOTA_UNSATISFIABLE Extended Slots alineados insuficientes para el cupo multi-grupo
ROOM_WINDOW_CONTENTION Extended (V-05) Grupos concurrentes > groupsCapacity en átomo de reloj (demanda inevitable)
COURSE_TIMEFRAME_NOT_IN_INDEX Core Curso con grupos cuyo timeFrameId no resuelve en timeFrames[] (payload inconsistente)
FIXED_BLOCKS_INFEASIBLE Fixed blocks Locks de regeneración inválidos, ambiguos o conflictivos (solo si warmStart.fixedBlocks)
Code post-UNSAT Cuándo
ROOM_WINDOW_CONTENTION (diagnostic) Enrichment tras UNSAT (criterio amplio)
ROOM_CAPACITY_SHORTAGE Fallback si V-05 diagnostic no emitió
TEACHER_PERIOD_SHORTAGE Post-UNSAT (o precheck solo si PRECHECK_EXTENDED_PROFILE=legacy)

6

Failure reports

Emitido en PATCH failed. Schema v1. UI: modal checklist (title / detail / recommendations / metrics opcionales).

Ejemplo de UI

Ejemplos de los 12 códigos del catálogo (precheck, solver, admin hours y processing).

Shape

Campo Uso
schemaVersion 1
kind infeasible | error
phase Ver tabla abajo
summary Conteo corto (Se encontraron N problemas.)
items[] Cards: code, title, detail, recommendations[], metrics?

Fases

phase Origen Códigos típicos
precheck check_feasibility() aborta PAIR_BLOCK_CAPACITY, NO_COMPATIBLE_GROUP_SLOTS, LESSON_QUOTA_UNSATISFIABLE, ROOM_WINDOW_CONTENTION, COURSE_TIMEFRAME_NOT_IN_INDEX, FIXED_BLOCKS_INFEASIBLE
solver CP-SAT UNSAT (+ diagnostics) SOLVER_UNSAT, FIXED_BLOCKS_INFEASIBLE, ROOM_WINDOW_CONTENTION, ROOM_CAPACITY_SHORTAGE, TEACHER_PERIOD_SHORTAGE
admin_hours Phase-2 no coloca todo INSUFFICIENT_ADMIN_HOUR_SLOTS
processing Excepción / config stale PROCESSING_ERROR, SUBJECT_ROOM_CONFIGURATION

Catálogo de ítems (`items[].code`)

Code Phase metrics? Idea en una línea
PAIR_BLOCK_CAPACITY precheck Faltan días con franja doble
NO_COMPATIBLE_GROUP_SLOTS precheck A veces Multi-grupo sin slots comunes
LESSON_QUOTA_UNSATISFIABLE precheck Cupo multi-grupo imposible
ROOM_WINDOW_CONTENTION precheck / solver Demasiados grupos a la vez en un aula (1 ítem por aula)
COURSE_TIMEFRAME_NOT_IN_INDEX precheck No course.timeFrameId ausente del índice timeFrames[]
FIXED_BLOCKS_INFEASIBLE precheck / solver No Locks de regeneración inválidos o que hacen UNSAT
ROOM_CAPACITY_SHORTAGE solver Fallback de capacidad de aula
TEACHER_PERIOD_SHORTAGE solver (o legacy precheck) Docente sin periodos de reloj suficientes
SOLVER_UNSAT solver No UNSAT genérico sin diagnóstico estructurado
INSUFFICIENT_ADMIN_HOUR_SLOTS admin_hours No caben todas las horas admin
SUBJECT_ROOM_CONFIGURATION processing No rooms/backupRooms inválidos (payload stale)
PROCESSING_ERROR processing No Error inesperado

7

Constraint violations report (éxito)

Emitido en PATCH done. Schema v1. UI: toggle Avisos en el header de ScheduleDetail + panel lateral + “Ir al bloque”.
Vacío limpio permitido (violations: []). totalPenalty = suma de weight de las violaciones del payload; optimalityGap / solveTimeMs son métricas del solve.

Ejemplo de UI

Avisos del horario

  • Crítica

    María López quedó el martes en la franja de las 10:00 (Matemáticas, 5°A), en un bloque donde indicó no estar disponible.

+ 5 tipos más en el ejemplo ampliado

Ejemplo del panel con los tipos de aviso que puede ver el admin.

Avisos del horario

Tipos de aviso · 6

TodasCríticas (3)Advertencias (1)Informativas (2)
  • Crítica

    María López quedó el martes en la franja de las 10:00 (Matemáticas, 5°A), en un bloque donde indicó no estar disponible.

    Ir al bloqueVer horario del profesor
  • Crítica

    “Matemáticas” y “Lengua” quedaron el mismo día (miércoles) en 4°B, aunque preferías separarlas.

    Ir al bloque
  • Crítica

    “Historia” y “Geografía” quedaron en franjas seguidas el viernes en 2°B.

    Ir al bloque
  • Advertencia

    “Matemáticas” y “Cálculo” (materias relacionadas) quedaron el mismo día (jueves) en 6°A.

    Ir al bloque
  • Informativa

    María López quedó en 5°A el jueves en la franja de las 11:00 (Inglés), en una franja donde solo marcó disponibilidad parcial.

    Ir al bloqueVer horario del profesor
  • Informativa

    “Educación Física” en 4°C usó un aula de respaldo (“Patio cubierto”) el viernes en la franja de las 08:00.

    Ir al bloque
Campo top-level Uso
totalPenalty Suma de weight de violations[] (0 si vacío)
optimalityGap Gap relativo del solve (null o 0 si OPTIMAL)
solveTimeMs Tiempo de solve
summary[] Conteo por constraintType
violations[] Avisos individuales con userMessage + deepLink
constraintType Severidad Fuente
avoid_cannot critical Vars en franjas CANNOT
avoid_same_day_subject_pairs critical Bool same-day (explícito)
avoid_related_subject_adjacency critical Bool adjacency
avoid_related_subject_same_day warning Bool same-day (familia derivada)
prefer_can_over_maybe info Vars en MAYBE
prefer_room_backup_tier info Opción con tier backup > 0

8

Matriz de decisión — ¿dónde se corta?

Situación ¿Cola? ¿Solver? Superficie
Health ERROR No No 409 / health UI
Health WARNING Banner advisory
Precheck fail No failureReport precheck
Solver UNSAT Sí (sin solución) failureReport solver
Admin hours fail Sí (clases OK) failureReport admin_hours
Éxito limpio done + reporte vacío
Éxito con softs done + constraintViolationsReport
Regenerar OK Sí (warmStart) Sí (hints ± locks) Mismo schedule _id; detalle in-place
Regenerar falla Sí / 409 gate / 400 keys Según fase Base intacto + failureReport (p. ej. FIXED_BLOCKS_INFEASIBLE) / 409
Preview swap No No Candidatos + hard blockers + soft delta
Confirm / undo No No CAS + currentHealthReport + auditoría

API ↔ worker (atajos)

Escenario API Worker
Curso sin TF / demanda grupo / overload etapa / assignment vacío 409 No llega
Asignación de marco rompe materia multi-curso 409 (PUT courses) N/A (config, no generación)
Pair-day imposible (n≥2) Encola PAIR_BLOCK_CAPACITY
Multi-grupo parcial / cero compatible Encola LESSON_QUOTA_* / NO_COMPATIBLE_*
Contención concurrente de aula Encola ROOM_WINDOW_CONTENTION (V-05)
course.timeFrameId no en timeFrames[] Encola (stale/bug) COURSE_TIMEFRAME_NOT_IN_INDEX
Keys de fijos huérfanas 400 No llega
Locks conflictivos / stale Encola FIXED_BLOCKS_INFEASIBLE
Pressure semanal de pool (V-06) WARNING Sin equivalente
Materia duplicada en grupo (V-08) WARNING Sin equivalente
Docente sobrecargado multi-etapa WARNING CROSS_STAGE Puede acabar en UNSAT + TEACHER_PERIOD_SHORTAGE

8b. Intercambio manual de bloques

Mutación interactiva sin solver. Contrato completo: flow — Intercambio manual · validación manual.

flowchart LR
  A[Seleccionar origen] --> B[Preview candidatos]
  B --> C{¿Hard OK?}
  C -->|No| D[Target bloqueado]
  C -->|Sí| E[Modal + soft delta]
  E --> F[Confirm CAS]
  F --> G[History]
  G --> H[Undo último swap]
Pieza Contrato corto
Identidad blockId (físico), lessonInstanceId ("<subjectId>:<lessonIndex>"), lessonOccurrenceId (bundle); worker los emite en cada solve factible
Endpoints POST .../block-swap-previewsPOST .../block-swapsGET .../changesPOST .../changes/:id/undo
Hard Solapes grupo/docente/admin, capacidad de aula, franja exacta, materia No disponible, duración, integridad de bundle, legacy ambiguo
Soft Impacto introduced / resolved; confirm sigue permitido
Pair-block Excluido del evaluador manual (sigue hard en CP-SAT)
Health Schedule.currentHealthReport vigente ≠ constraintViolationsReport histórico de la task
Concurrencia revision CAS + configurationRevision + idempotency key
Undo Solo el último swap vigente; regenerate crea barrera
Legacy Único → normaliza al confirm; ambiguo → deshabilitado / 422
UI targets checking / eligible / blocked / incompatible (etiqueta + aria-*, no solo color)
Preview perf Medido en PR6 (~360 bloques): p95 local ≈ 415–475 ms; SLO no pinneado
Hard manual Soft manual (delta)
SWAP_GROUP_OVERLAP MANUAL_AVOID_TEACHER_CANNOT
SWAP_TEACHER_OVERLAP MANUAL_AVOID_TEACHER_MAYBE
SWAP_TEACHER_ADMIN_OVERLAP MANUAL_AVOID_SUBJECT_MAYBE
SWAP_ROOM_CAPACITY MANUAL_AVOID_SAME_DAY_SUBJECT_PAIRS
SWAP_SLOT_MISSING MANUAL_AVOID_RELATED_SUBJECT_ADJACENCY
SWAP_SUBJECT_CANNOT MANUAL_AVOID_RELATED_SUBJECT_SAME_DAY
SWAP_DURATION_MISMATCH MANUAL_AVOID_ROOM_CONTENTION
SWAP_BUNDLE_INCOMPLETE MANUAL_PREFER_ROOM_BACKUP_TIER
LEGACY_BLOCK_BUNDLE_AMBIGUOUS

Fixtures: fixtures/schedule-generation/block-swaps/.

UI (Schedule Manager): modo intercambio en Grupo/Etapa; modal de confirmación con delta soft; panel Historial con undo del último swap; avisos prefieren currentHealthReport.

Ejemplo de UI

Confirmar intercambio

Origen

Lunes 08:00–09:00

Destino

Martes 09:00–10:00

Se intercambian las posiciones manteniendo materia, docentes y aulas de cada clase.

Nuevos avisos Crítica

  • Docente no disponible en esa franja

    Después del intercambio, el docente quedaría en una franja marcada como no disponible.

Se resolverán Resuelto

  • Materias relacionadas el mismo día

    Las materias relacionadas dejarían de coincidir el mismo día.

Los avisos blandos no bloquean la confirmación. Al confirmar se revalida el horario completo.

Cancelar Confirmar intercambio

Confirmación A↔B con «Nuevos avisos» y «Se resolverán»; los softs no bloquean.

Confirmar intercambio

Origen

Lunes 08:00–09:00

Destino

Martes 09:00–10:00

Se intercambian las posiciones manteniendo materia, docentes y aulas de cada clase.

Nuevos avisos Crítica

  • Docente no disponible en esa franja

    Después del intercambio, el docente quedaría en una franja marcada como no disponible.

Se resolverán Resuelto

  • Materias relacionadas el mismo día

    Las materias relacionadas dejarían de coincidir el mismo día.

Los avisos blandos no bloquean la confirmación. Al confirmar se revalida el horario completo.

Cancelar Confirmar intercambio
9

Mapa de archivos clave

Área Dónde
Health rules kampus-api/src/modules/configuration-health/rules/
Health codes .../constants/configuration-health-codes.constants.ts
Solver config (catálogo+DB) kampus-api/src/modules/solver-configuration/
Solver config UI kampus-admin/.../features/solver-configuration/
Enqueue + payload kampus-api schedules generation tasks service (solverConfig, warmStart)
Regenerar + replace in-place regenerateDoneTask + createSchedule(replaceScheduleId)
Fixed blocks (locks) API fixedBlockKeyswarmStart.fixedBlocks; worker feasibility/fixed_blocks.py + warm_start.py
Precheck worker/.../scheduling/feasibility/
Hard + soft CP-SAT worker/.../scheduling/solver/ + soft_constraints/
Warm start hints worker/.../scheduling/warm_start.py
Job solver resolve worker/.../core/config.py (resolve_job_solver_config)
Admin hours worker/.../scheduling/admin_hours*.py
failureReport worker/.../queues/schedule_generator/failure_report.py
constraintViolationsReport worker/.../queues/schedule_generator/constraint_violations_report.py
Modal failure UI schedule-manager/.../failure-report-modal/
Toggle Avisos + panel softs schedule-manager/.../schedule-detail/ + constraint-violations-*
Botón Regenerar + fijar bloques schedule-manager/.../schedule-detail/ + schedule-calendar-selection-bar/
Swap preview/confirm/undo kampus-api schedules block-swap endpoints + evaluador manual
Modo intercambio UI schedule-manager/.../schedule-detail/ + selection-bar + calendar/matrix
Modal confirmar intercambio schedule-manager/.../block-swap-confirm-modal/
Historial + undo UI schedule-manager/.../schedule-changes-panel/
Identity producer (worker) worker/.../solver/extraction.py + schedule_transform.py → POST scheduleBlocks
Health vigente del schedule Schedule.currentHealthReport (≠ task constraintViolationsReport)
Historial de cambios ScheduleChange append-only (swap / undo / regenerate)
Fixtures swap _kampus_brain/fixtures/schedule-generation/block-swaps/

Changelog

Date Change
2026-07-24 PR 6: verificación cross-repo + fixtures ampliados; p95 preview reportado sin SLO; mock modal MANUAL_AVOID_TEACHER_CANNOT = critical (§8b)
2026-07-23 PR 5 Schedule Manager UX: modo intercambio, modal confirmación, historial/undo; preview block-swap-confirm-modal (§8b/§9)
2026-07-23 PR 4 API swap engine shipped: endpoints + evaluador manual; pair-block sigue excluido del path interactivo (§8b)
2026-07-23 Worker emite identidades de bloque en POST (blockId / lessonInstanceId / lessonOccurrenceId); §8b/§9
2026-07-23 §8b intercambio manual: preview/confirm/undo, hard/soft, pair-block excluido, health vigente vs histórico
2026-07-23 §4 ERROR: GROUP_DEMAND_BELOW_CAPACITY (0 < demand < capacity; health + gate) junto a exceeds
2026-07-23 Copy admin health/failureReport: vocabulario grilla (sin SESSION/CAN/MAYBE en UI); códigos siguen en inglés
2026-07-18 Bloques fijados: fixedBlocks / FIXED_BLOCKS_INFEASIBLE en §1/§5/§6/§8/§9; catálogo failure = 12 códigos; UI Bloquear/Desbloquear
2026-07-18 Regeneración (warm start): camino regenerate en §1 + matriz §8; replace in-place; enlace a flow
2026-07-18 §3b: config del solver en kampus-admin / Mongo / solverConfig en cola; env SOFT_WEIGHT_*/SOLVER_* deprecadas
2026-07-16 Soft weights: defaults 10000/100/1 (~100× entre niveles) para que warnings no compensen críticas
2026-07-15 Worker PR 2: H6 pair-block multi-marco = intersección de adyacencia; fixture factible corregido (par consecutivo alineado)
2026-07-15 GROUPED/COMBINED multi-marco: intersección exacta; 409 por intersección insuficiente (deja de rechazar solo por multi-TF)
2026-07-13 Marcos por curso: multi-TF por etapa; 409 asignación; precheck COURSE_TIMEFRAME_NOT_IN_INDEX; catálogo failure = 11 códigos
2026-07-13 §7: solo tipos de aviso del payload admin (sin columna Reportable); mock UI alineado
2026-07-13 Soft report: filtro admin en worker; totalPenalty = suma del payload
2026-07-12 Polish visual HTML: layout flat, tipografía, tablas y previews más legibles
2026-07-12 Previews UI: mocks con catálogo completo §6 (10 códigos) y §7 (12 constraintType)
2026-07-11 Soft weights final: defaults 600/80/10 y severidades alineadas (nivel 4 critical → 2 info); monitoreo same-day a 600
2026-07-11 Soft distribution epic (KMP-111–116): nuevos softs instrumentados; stubs balance_subjects_per_day / variance reemplazados
2026-07-11 Created — cheatsheet ágil: flujo, hard/soft, health, failure + violations reports
2026-07-11 HTML interactivo permanente: docs/schedule-generation-overview.html vía npm run docs:schedule-overview-html