Gestión de notas académicas con reloj de clase en tiempo real y reserva de turnos por API.
Hecho con propósito educativo por Johan Sebastian Giraldo Hurtado · Licencia Apache 2.0
Durante la clase, el reloj corre en pantalla grande y los estudiantes reservan su turno para exponer. El docente administra todo desde una interfaz limpia, en tiempo real.
El estudiante elige un espacio libre y lo reserva: el turno pasa a Ocupado al instante, en tiempo real, sin recargar la página.
¿Prefieres verlo con audio y en alta calidad? Mira el video completo aquí.
Una sesión activa: el tiempo restante de la clase, grande y visible para todos.
Ver más capturas — login, estudiantes, materias e importación de planillas
|
|
Hoy existe un problema silencioso en las aulas: muchos estudiantes ya no repasan ni interiorizan los conceptos. Le piden a la inteligencia artificial que haga el trabajo por ellos, lo entregan, y el aprendizaje nunca ocurre. El conocimiento se delega — no se adquiere.
NoteOPs le da la vuelta a eso.
La idea es simple y poderosa: para poder sustentar su trabajo, el estudiante primero tiene que separar su turno haciendo él mismo una petición HTTP a la API. Con un curl desde la terminal o desde la web, pero hecho por él. Ese pequeño gesto cambia todo:
- 🧠 Antes de exponer, ya está aprendiendo. Entiende qué es una API, un endpoint, una petición, un identificador. Conocimiento técnico real, no delegado.
- 🎯 La sustentación deja de ser un trámite y se vuelve un reto práctico, técnico y hasta divertido.
- 👩🏫 El docente administra; el estudiante demuestra. Reservar el turno es la primera prueba de que entiende; sustentar el trabajo es la segunda.
Aprender haciendo. En vez de pedirle a la IA que resuelva, el estudiante ejecuta, observa y comprende. NoteOPs convierte un requisito administrativo —apartar un turno— en una oportunidad de aprendizaje genuina.
Y para el docente, además, es la herramienta que reemplaza la frágil planilla de Excel: cálculo automático de la nota definitiva, reloj de clase en tiempo real e importación de su planilla existente en segundos.
¿Eres docente? Levanta NoteOPs en tu máquina en menos de 5 minutos → Inicio rápido
¿Eres estudiante? Anímate al reto: aprende a separar tu turno con una petición real → Cómo un estudiante separa su turno
¿Te gusta el open source? Es Go + SvelteKit + PostgreSQL, todo en Docker. Las contribuciones son bienvenidas → Guía de contribución
⭐ Si la idea te gusta, deja una estrella en github.com/jsgiraldoh/noteops
- La aplicación de un vistazo
- ¿Por qué NoteOPs?
- Cómo un estudiante separa su turno
- API Reference
- Arquitectura
- Stack técnico
- Inicio rápido
- Importar tu planilla
- Comandos útiles
- CI/CD y seguridad
- Hoja de ruta
- Equipo de desarrollo (Claude Code)
- Guía de contribución
- Licencia
🎓 Esta es la sección estrella de NoteOPs. Aquí el estudiante deja de ser espectador: para apartar su turno de sustentación tiene que hacer su propia petición a la API. No te preocupes si nunca has usado una terminal — sigue los pasos y, sin darte cuenta, habrás aprendido cómo funciona una API real.
Todo el flujo de reserva es público: no necesitas usuario ni contraseña. Solo necesitas dos datos que te entrega tu docente:
| Dato | Qué es | Cambia |
|---|---|---|
SUBJECT_ID |
Identificador de la materia | Fijo durante todo el semestre |
STUDENT_ID |
Tu identificador como estudiante | Fijo durante todo el semestre |
Vas a usar curl, una herramienta de línea de comandos que envía peticiones HTTP. Viene incluida en macOS y Linux, y en Windows 10/11 modernos. Reemplaza los valores entre llaves {...} por los tuyos.
Tu docente activa la sesión al iniciar la clase. Con el SUBJECT_ID averiguas cuál es la sesión de hoy:
curl http://noteops.local/api/sessions/active?subject_id={SUBJECT_ID}Respuesta (HTTP 200):
{
"id": "f1a2b3c4-0000-1111-2222-333344445555",
"subject_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"starts_at": "2025-04-10T14:00:00Z",
"duration_min": 120,
"slot_min": 20,
"room": "Sala 201",
"active": true
}El campo id es tu SESSION_ID — guárdalo para los siguientes pasos.
Si recibes HTTP 404, la sesión todavía no está activa. Espera a que tu docente la inicie.
Con el SESSION_ID listas todos los turnos (slots) de la clase:
curl http://noteops.local/api/sessions/{SESSION_ID}/slotsRespuesta (HTTP 200) — un arreglo de turnos:
[
{
"id": "11111111-aaaa-bbbb-cccc-000000000001",
"number": 1,
"starts_at": "2025-04-10T14:00:00Z",
"duration_min": 20,
"student_id": "99999999-...-student-ocupado",
"reserved_at": "2025-04-10T13:45:10Z"
},
{
"id": "22222222-aaaa-bbbb-cccc-000000000002",
"number": 2,
"starts_at": "2025-04-10T14:20:00Z",
"duration_min": 20,
"student_id": null,
"reserved_at": null
}
]Un turno está libre cuando su student_id es null. Elige uno libre y copia su id — ese será tu SLOT_ID.
curl -X POST http://noteops.local/api/sessions/{SESSION_ID}/slots/{SLOT_ID}/reserve \
-H "Content-Type: application/json" \
-d '{"student_id": "{TU_STUDENT_ID}"}'Respuesta exitosa (HTTP 200):
{
"id": "22222222-aaaa-bbbb-cccc-000000000002",
"number": 2,
"starts_at": "2025-04-10T14:20:00Z",
"duration_min": 20,
"student_id": "tu-student-id-aqui",
"reserved_at": "2025-04-10T13:52:30Z"
}🎉 ¡Listo! Tu turno quedó reservado. El campo reserved_at confirma la hora exacta en que lo apartaste.
| Código | Significado | Qué hacer |
|---|---|---|
409 Conflict |
El turno ya fue reservado por otro, o la sesión ya no existe | Vuelve al Paso 1 y elige otro turno libre |
400 Bad Request |
Tu STUDENT_ID no existe o tiene formato inválido |
Verifica el dato con tu docente |
404 Not Found |
La sesión no está activa | Espera a que el docente la inicie |
💡 ¿Prefieres la interfaz gráfica? También puedes reservar desde la web. Pero te animamos a hacerlo con
curl: acabas de ejecutar una peticiónGETy unPOSTa una API REST real, leer una respuesta JSON e interpretar códigos HTTP. Eso es exactamente lo que hace una aplicación por dentro — y ahora lo hiciste tú.
La URL base local es http://noteops.local/api. Los endpoints marcados como 🔓 públicos no requieren autenticación; el resto requieren la cabecera Authorization: Bearer <token> obtenida en el login.
📘 Documentación interactiva (Swagger / OpenAPI). Con el sistema corriendo, abre http://noteops.local/swagger/index.html para explorar todos los endpoints, ver los esquemas de request/response y probarlos desde el navegador. Para los endpoints protegidos, usa el botón Authorize y pega el JWT obtenido en
/api/auth/login(formatoBearer <token>).Por seguridad, la UI de Swagger solo se expone cuando
APP_ENV != production; en producción la ruta/swaggerresponde404. El contrato OpenAPI en crudo está disponible enhttp://noteops.local/swagger/doc.json.
| Método | Ruta | Descripción |
|---|---|---|
🔓 GET |
/api/health |
Verifica que el servicio está arriba |
🔓 POST |
/api/auth/login |
Inicia sesión y devuelve un JWT |
# Login
curl -X POST http://noteops.local/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "docente@universidad.edu.co", "password": "••••••••"}'{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": "uuid", "full_name": "Docente Ejemplo", "role": "teacher" }
}Usa el token en las siguientes peticiones protegidas:
curl http://noteops.local/api/subjects \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."| Método | Ruta | Descripción |
|---|---|---|
GET |
/api/subjects |
Lista las materias del docente |
POST |
/api/subjects |
Crea una materia |
PATCH |
/api/subjects/:id |
Actualiza una materia |
DELETE |
/api/subjects/:id |
Elimina una materia (en cascada) |
GET |
/api/subjects/:id/students |
Estudiantes inscritos en la materia |
GET |
/api/subjects/:id/grades |
Notas completas: cortes, actividades y estudiantes |
GET |
/api/subjects/:id/final-grades |
Nota definitiva por estudiante |
POST |
/api/subjects/:id/enroll |
Inscribe un estudiante en la materia |
POST |
/api/subjects/:id/import |
Importa una planilla completa (ver Importar tu planilla) |
# Crear materia
curl -X POST http://noteops.local/api/subjects \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Sistemas Operativos", "period": "2025-1", "group_name": "A", "faculty": "Ingeniería"}'| Método | Ruta | Descripción |
|---|---|---|
POST |
/api/students |
Crea un estudiante |
PATCH |
/api/students/:id |
Actualiza nombre, correo o código |
| Método | Ruta | Descripción |
|---|---|---|
POST |
/api/grades |
Registra o actualiza una nota (upsert) |
PATCH |
/api/grades/:id/comment |
Edita el comentario de una nota |
# Registrar una nota (0.0 a 5.0)
curl -X POST http://noteops.local/api/grades \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"enrollment_id": "uuid", "activity_id": "uuid", "value": 4.5, "comment": "Buen trabajo"}'La nota definitiva se recalcula automáticamente en la base de datos (vista student_final_grades) a partir de los pesos de cada corte y actividad. No hay que calcularla a mano.
| Método | Ruta | Acceso | Descripción |
|---|---|---|---|
POST |
/api/sessions |
🔒 | Crea una sesión y genera sus turnos |
POST |
/api/sessions/:id/activate |
🔒 | Activa la sesión (arranca el reloj) |
POST |
/api/sessions/:id/deactivate |
🔒 | Finaliza la sesión (la elimina junto con sus turnos) |
GET |
/api/sessions/active?subject_id=uuid |
🔓 | Sesión activa de una materia |
GET |
/api/sessions/:id/slots |
🔓 | Lista los turnos de una sesión |
POST |
/api/sessions/:id/slots/:slotID/reserve |
🔓 | Reserva un turno (ver Cómo un estudiante separa su turno) |
# Crear una sesión de 120 min con turnos de 20 min → genera 6 turnos
curl -X POST http://noteops.local/api/sessions \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"subject_id": "uuid", "starts_at": "2025-04-10T14:00:00Z", "duration_min": 120, "slot_min": 20, "room": "Sala 201"}'{
"session": { "id": "uuid", "duration_min": 120, "slot_min": 20, "active": false },
"slots": [
{ "number": 1, "starts_at": "2025-04-10T14:00:00Z", "duration_min": 20, "student_id": null },
{ "number": 2, "starts_at": "2025-04-10T14:20:00Z", "duration_min": 20, "student_id": null }
]
}| Protocolo | Ruta | Acceso | Descripción |
|---|---|---|---|
🔓 WS |
/ws/session/:id |
🔓 | Emite el estado del reloj de la sesión, un tick por segundo |
Cada tick es un mensaje JSON con esta forma:
{
"session_id": "uuid",
"elapsed_sec": 642,
"remaining_sec": 6558,
"duration_min": 120,
"is_active": true
}| Campo | Significado |
|---|---|
elapsed_sec |
Segundos transcurridos desde el inicio |
remaining_sec |
Segundos restantes (0 si la sesión expiró) |
duration_min |
Duración total configurada |
is_active |
false cuando la sesión expiró o fue finalizada |
El componente Clock.svelte consume este flujo para pintar el reloj grande de la clase.
NoteOPs es un conjunto de servicios orquestados con Docker Compose detrás de un único reverse proxy (Traefik). El navegador solo habla con Traefik, que enruta por path y hostname hacia el frontend o el backend.
Navegador / curl
│ :80 / :443
┌──────────▼───────────┐
│ Traefik v3 │ routing por host y path, TLS
└────┬─────────────┬────┘
/api,/ws │ │ /
┌──────────▼──┐ ┌────▼─────────┐
│ Backend │ │ Frontend │
│ Go + Gin │ │ SvelteKit │
│ :8080 │ │ :3000 │
└───┬──────┬───┘ └──────────────┘
SQL (pgx)│ │ estado WS
┌──────▼──┐ ┌─▼────────┐
│PostgreSQL│ │ Redis │
│ :5432 │ │ :6379 │
└──────────┘ └──────────┘
Servicios auxiliares:
• MinIO :9001 almacenamiento de archivos (compatible S3)
• Adminer :8081 administrador web de la base de datos (desarrollo)
Docente en el navegador
└─▶ SvelteKit (POST /api/grades)
└─▶ Traefik (enruta /api → backend)
└─▶ Go/Gin handler (valida JWT, hace binding del JSON)
└─▶ Repository (UPSERT con pgx en PostgreSQL)
└─▶ Vista student_final_grades (recalcula la definitiva)
◀── Grade como JSON
◀── 200 OK
◀── el store de Svelte se actualiza
◀── la tabla de notas se re-renderiza
El cálculo de la nota definitiva no vive en el backend: es una vista SQL (student_final_grades) que suma valor × peso_actividad × peso_corte. La base de datos es la única fuente de verdad del resultado.
noteops/
├── backend/ Go + Gin — API REST + WebSocket
│ ├── cmd/server/ Punto de entrada (main.go, rutas)
│ └── internal/
│ ├── config/ Carga de variables de entorno
│ ├── handlers/ HTTP handlers, hub WebSocket, errores seguros
│ ├── middleware/ Autenticación JWT, logger
│ ├── models/ Structs de dominio y DTOs
│ ├── repository/ Queries SQL con pgx (interfaz Repo + tests)
│ └── service/ Lógica de negocio (slots, reloj, agregados)
├── frontend/ SvelteKit + TypeScript
│ └── src/
│ ├── lib/api/ Clientes HTTP tipados
│ ├── lib/stores/ Estado reactivo (auth, clock, session, notify)
│ ├── lib/components/ Clock, SlotGrid, GradeCell, modales
│ └── routes/ /, /session, /students, /subjects, /import, /login
├── workers/ Python — agentes IA (futuro)
├── infra/
│ ├── traefik/ traefik.yml
│ └── postgres/ init.sql · rollback_to_admin.sql · 02_seed_data.sql*
├── .github/workflows/ ci.yml · release.yml
├── .claude/ CLAUDE.md + skills del equipo (roles)
├── docker-compose.yml Stack completo (perfiles local / registry)
├── docker-compose.seed.yml Override para cargar datos de ejemplo
└── Makefile Atajos de desarrollo y operación
* 02_seed_data.sql es opcional y está excluido de git (datos privados).
| Capa | Tecnología | Versión | Por qué |
|---|---|---|---|
| Backend | Go + Gin | 1.23 | Concurrencia nativa para el WebSocket, binario estático de ~15 MB en imagen scratch, tipado fuerte |
| Acceso a datos | pgx | v5 | Driver PostgreSQL de alto rendimiento, sin ORM — control total del SQL |
| Frontend | SvelteKit | 2.x | Compila a JS sin runtime; bundle mínimo y reactividad simple |
| Base de datos | PostgreSQL | 16 | Vista SQL para la nota definitiva, pgcrypto para el hash de contraseñas |
| Cache / WS | Redis | 7 | Estado de sesiones WebSocket entre instancias |
| Archivos | MinIO | latest | Compatible con S3, self-hosted |
| Proxy | Traefik | v3 | TLS automático con Let's Encrypt, routing por labels de Docker |
| DB Admin | Adminer | 4 | UI web ligera (~10 MB) para inspeccionar la base en desarrollo |
| Contenedores | Docker + Compose | 24+ / v2 | Un comando levanta todo; entornos local y producción idénticos |
Go + Gin para el backend. El hub de WebSocket mantiene una goroutine por cliente conectado durante la clase; Go maneja esa concurrencia sin bloquear. Además, el binario compilado produce una imagen Docker de ~15 MB (base scratch) y el tipado estático atrapa en compilación errores que en JavaScript solo aparecerían en runtime.
pgx sin ORM. Las queries se escriben a mano con parámetros posicionales ($1, $2). El precio es más código; la ganancia es transparencia total sobre qué SQL se ejecuta, cero magia, y poder apoyar el cálculo de notas en una vista de PostgreSQL en lugar de en el código.
SvelteKit para el frontend. Compila a vanilla JS sin enviar un runtime al navegador, lo que produce un bundle pequeño ideal para el aula. Su modelo de stores reactivos encaja naturalmente con el reloj en tiempo real alimentado por WebSocket.
Prerequisitos: Docker 24+ y Docker Compose v2.
# 1. Clonar
git clone https://github.com/jsgiraldoh/noteops.git
cd noteops
# 2. Configurar entorno (al menos cambia JWT_SECRET y DB_PASSWORD)
cp .env.example .env
# 3. Hostname local (una sola vez)
echo "127.0.0.1 noteops.local" | sudo tee -a /etc/hosts
# 4. Levantar
make upgit clone https://github.com/jsgiraldoh/noteops.git
cd noteops
copy .env.example .env
Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "127.0.0.1 noteops.local"
docker compose --profile local up -d --buildLa aplicación queda en http://noteops.local, la documentación interactiva de la API en http://noteops.local/swagger/index.html y Adminer en http://localhost:8081.
Credenciales por defecto:
| Campo | Valor |
|---|---|
admin@noteops.local |
|
| Contraseña | admin123 |
Cambia la contraseña del admin y el
JWT_SECRETantes de exponer el sistema.
| Comando | Qué hace |
|---|---|
make up |
Build local conservando los datos existentes |
make fresh |
Arranque limpio: borra el volumen y deja solo el schema + el usuario admin |
make fresh-seed |
Arranque con datos de ejemplo desde infra/postgres/02_seed_data.sql |
| Variable | Requerida | Default | Descripción |
|---|---|---|---|
DATABASE_URL |
✅ | — | Conexión a PostgreSQL: postgres://user:pass@host:5432/db?sslmode=disable |
JWT_SECRET |
✅ | — | Secreto para firmar JWT. Mínimo 32 caracteres. Genera con openssl rand -hex 32 |
APP_ENV |
❌ | development |
production activa el modo release de Gin |
APP_PORT |
❌ | 8080 |
Puerto interno del backend |
REDIS_URL |
❌ | redis://redis:6379 |
Conexión a Redis |
PUBLIC_API_URL |
❌ | http://noteops.local/api |
URL del API embebida en el frontend (build time) |
PUBLIC_WS_URL |
❌ | ws://noteops.local |
URL del WebSocket embebida en el frontend (build time) |
NoteOPs lee la planilla de Excel que ya usas y crea todo automáticamente. Sube el archivo desde la sección Importar de la interfaz (endpoint POST /api/subjects/:id/import).
Formato esperado — una hoja por materia, con la estructura institucional:
| Ubicación | Contenido |
|---|---|
| Hoja | Una por materia (el nombre de la hoja es la materia) |
| Fila 6, col E | Nombre del espacio académico |
| Fila 8 | Período y grupo |
| Fila 12 | Porcentajes (pesos) de actividades y cortes |
| Fila 14 en adelante | Listado de estudiantes (código y nombre) |
El importador detecta y crea:
- Materias con su período, grupo y facultad
- Estudiantes e inscripciones
- Estructura de notas: cortes y actividades con sus pesos
- Notas existentes (opcional)
Al cargar el archivo verás un preview por materia antes de confirmar. Puedes elegir entre dos modos:
- Solo estructura — crea materias, estudiantes, cortes y actividades, sin notas.
- Estructura + notas — además importa las calificaciones ya registradas en el Excel.
El cálculo de la nota definitiva se valida contra la planilla original: el caso real del estudiante de referencia da exactamente
4.75, igual que en Excel.
# ── Entorno ──────────────────────────────────────────────
make up # Build local conservando datos
make fresh # Arranque limpio (schema + admin)
make fresh-seed # Arranque con datos de ejemplo
make dev # Solo infra (DB, Redis, MinIO)
make down # Apagar todos los servicios
make logs # Logs en tiempo real
make ps # Estado de los contenedores
# ── Base de datos ────────────────────────────────────────
make shell-db # Abrir psql en el contenedor de postgres
make rollback # Limpiar la BD dejando solo el usuario admin
# ── Calidad ──────────────────────────────────────────────
make test # Tests unitarios backend + check del frontend
make test-integration # Tests de integración con testcontainers (requiere Docker)
# ── Imágenes y release ───────────────────────────────────
make build # Build local de las imágenes Docker
make push # Push de imágenes a GHCR (requiere login)
make release VERSION=v1.0.0 # Crea el tag y dispara el workflow de releaseEl pipeline de integración continua (.github/workflows/ci.yml) corre en cada Pull Request y en cada push a main/develop, con la seguridad integrada en fases ordenadas:
Fase 0 secrets gitleaks (escaneo de secretos en el historial)
Fase 1 backend-test go vet → unit → integration → govulncheck
frontend-check npm ci → check → build
Fase 2 codeql SAST de Go y TypeScript
│
Fase 3-5 image ◀─────────────┘ build (una vez) → Trivy → push :latest (solo main)
El job image espera a que pasen todos los gates anteriores. En un Pull Request las imágenes se construyen y escanean pero no se publican; solo un push a main publica :latest en GHCR. Las versiones etiquetadas (vX.Y.Z) las publica release.yml al crear un tag.
No hay deployment automático. El despliegue continuo se retiró del pipeline; las imágenes quedan disponibles en GHCR para desplegarlas donde quieras.
| Gate | Herramienta | Acción |
|---|---|---|
| Secretos | gitleaks | Bloquea |
| Pruebas unitarias e integración | go test | Bloquea |
| Dependencias Go | govulncheck | Bloquea (solo vulns en código realmente usado) |
| Imágenes Docker | Trivy (CRITICAL/HIGH) | Bloquea, ignora CVEs sin parche disponible |
| Análisis de código | CodeQL | Reporta a Security → Code scanning |
Todas las herramientas son open source y no requieren cuentas ni tokens de pago — CodeQL es gratuito en repositorios públicos.
| Estado | Funcionalidad |
|---|---|
| ✅ | Backend Go + Gin: materias, estudiantes, cortes, actividades, notas |
| ✅ | Cálculo automático de la nota definitiva (vista SQL) |
| ✅ | Frontend SvelteKit: notas, estudiantes, materias |
| ✅ | Sesiones de clase con reloj en tiempo real (WebSocket) |
| ✅ | Reserva pública de turnos por API (el reto del estudiante) |
| ✅ | Importación de planillas Excel (estructura + notas) |
| ✅ | CI con seguridad: gitleaks, govulncheck, CodeQL, Trivy |
| ✅ | Tests unitarios y de integración (testcontainers) |
| ✅ | Documentación interactiva de la API (Swagger / OpenAPI) |
| ⏳ | Exportar notas a Excel compatible con el formato institucional |
| ⏳ | Autenticación completa: registro de docentes y cambio de contraseña |
| ⏳ | Endurecer producción: restringir CORS y validar el origen del WebSocket |
| ⏳ | Workers Python: análisis de notas con agentes de IA |
NoteOPs se desarrolla con un equipo de roles definidos como skills de Claude Code en .claude/skills/. Cada rol tiene un propósito y se invoca según la tarea:
| Rol | Skill | Responsabilidad |
|---|---|---|
| Developer | dev |
Implementa cambios en backend y frontend |
| QA Engineer | qa |
Tests unitarios y de integración |
| Arquitecto | architect |
Mantiene el README y las decisiones técnicas |
| Ing. de Documentación | docs |
Documenta código, endpoints y ADRs |
| Release Engineer | release |
Versionado SemVer y publicación de releases |
| Diseñador UX/UI | ux |
Consistencia visual y experiencia de uso |
| Security Engineer | security |
Auditoría de vulnerabilidades y fallos de seguridad |
| DevSecOps Engineer | devsecops |
Seguridad en el pipeline CI/CD |
| Marketing & Community | marketing |
Copys, anuncios y gestión de la comunidad |
Las contribuciones son bienvenidas. El proyecto trabaja con trunk-based development sobre main.
- Haz un fork del repositorio.
- Crea tu rama desde
main:git checkout -b feature/mi-aporte. - Sigue las convenciones del proyecto (Conventional Commits:
feat:,fix:,docs:,test:,chore:,ci:). - Asegúrate de que
make testpase sin errores. - Abre un Pull Request describiendo el qué y el por qué.
Al abrir el PR, el CI ejecutará automáticamente los gates de seguridad y las pruebas. Consulta .claude/CLAUDE.md para las convenciones detalladas del proyecto y CONTRIBUTING.md para el flujo completo.
Distribuido bajo la licencia Apache 2.0.
Copyright 2025 Johan Sebastian Giraldo Hurtado
Licensed under the Apache License, Version 2.0.
Consulta el archivo LICENSE para los términos completos.










