# 03 — Estructura del Proyecto

---

## 1. Árbol de primer nivel

```text
/home/heimar/Repositorios/kuorum
├── .env                     # Configuración real del entorno (NO versionado)
├── .env.example             # Plantilla documentada de todas las variables
├── .htaccess                # Mitigación: bloquea .env y config por HTTP
├── .audit_controller_refs.csv   # Salida de una auditoría de referencias a controladores
├── composer.json / .lock
├── app/                     # Código de aplicación (MVC)
│   ├── config/              # Configuración estructural en PHP
│   ├── controllers/         # 75 controladores
│   ├── fonts/               # 6 fuentes manuscritas para firmas generadas como imagen
│   ├── layouts/             # 8 layouts + parciales (menus, shortcuts, flashes, logs, alerts)
│   ├── models/              # 127 modelos + 12 servicios/helpers
│   └── views/               # 291 vistas PHP, una carpeta por módulo
├── bin/                     # CLI: migrate, seed, make-migration, make-seeder
├── core/                    # Framework propietario "Klee"
│   ├── attributes/          # 26 tipos de atributo de modelo
│   ├── db/                  # Drivers PDO (KleePDO, MysqlPDO, OraclePDO, LdapPDO)
│   └── helpers/             # Helpers estáticos + fuente por defecto
├── database/
│   ├── migrations/          # 41 migraciones con nombre YYYY_MM_DD_NNNNNN_descripcion.php
│   ├── seeders/             # 39 semillas numeradas + support/
│   ├── Migrator.php         # Ejecutor de migraciones
│   ├── DatabaseStructureAuditor.php   # Validación estática de migraciones/semillas
│   ├── DemoReadinessVerifier.php      # Verifica cobertura de datos demo
│   └── README.md            # ✅ Documentación exacta y vigente del flujo de BD
├── docs/                    # Documentación (IGNORADA por git — ver §6)
│   ├── knowledge/           # ← esta base de conocimiento
│   ├── guias_modulos/       # Manuales funcionales de usuario (14 ficheros)
│   └── 0X_*.md              # ⚠️ Documentación técnica OBSOLETA / de otro producto
├── files/                   # Almacén de ficheros subidos y logotipos (escribible)
├── logs/                    # Logs de aplicación (vacío en el checkout actual)
├── public/                  # DocumentRoot esperado
│   ├── index.php            # Front controller
│   ├── check_session.php    # Endpoint de heartbeat de sesión
│   ├── .htaccess
│   ├── admin/               # Recurso residual (index.php + default.png)
│   ├── assets/              # CSS propio + quick-search.js
│   ├── js/                  # Copia de quick-search.js
│   └── metronic_html_v8.0.23_demo1/   # Plantilla completa de Metronic (assets + demos HTML)
├── services/                # Servicios de dominio desacoplados
│   ├── asistente_ia/        # AsistenteIAService, DatabaseContext, SQLGenerator, ResponseFormatter
│   ├── llm/                 # LLMInterface, OllamaProvider
│   └── prompt/              # PromptBuilder
├── storage/
│   └── cache/               # classmap.php + caché de consultas (NO versionado)
├── tests/                   # 13 ficheros PHPUnit + fixtures
└── vendor/                  # Dependencias Composer
```

---

## 2. Detalle por carpeta

### `app/config/` — Configuración estructural en PHP

| Fichero | Propósito | Quién lo usa | Qué NO debe contener |
|---|---|---|---|
| `Config.php` | Clase base con banderas, layouts, filtros de sesión, `syncConfigFromEnv()`, helpers de negocio (`getFrecuenciasMetasDisponibles`) | Toda la aplicación (`Controller extends ConfigEnv extends Config`) | Credenciales, endpoints, secretos → van en `.env` |
| `ConfigEnv.php` | Sobrescrituras por instalación. **Actualmente vacío a propósito** | `core/AutoLoad.php` | ⛔ **Nunca redeclarar una propiedad estática que gestione `.env`**: PHP crearía un almacenamiento separado y el `.env` sería ignorado en silencio |
| `ConfigEnv.local.php` | Variante para `localhost`/`127.0.0.1`. **No existe en el checkout actual** | `core/AutoLoad.php`, `bin/migrate`, `bin/seed` | — |
| `Menu.php` | Árbol del menú del backoffice (`$principal`) y del portal (`$public`) + renderizado a HTML de Metronic | `UsuariosModel::loadMenu()`, layouts | Lógica de negocio |
| `QuickActionsConfig.php` | Catálogo de 15 acciones para la búsqueda rápida, con permisos requeridos | `QuickSearchModel` | — |
| `Titles.php` | Array global `$titles` con prefijos de título por acción (`list`→`Lista `, `create`→`Creación `…) | `Controller::generateTitle()` | — |
| `GeneralDataArray.php`, `Localidades.php`, `Sedes.php`, `TiposSede.php` | Catálogos estáticos heredados del framework base | Uso residual | — |

### `app/controllers/` — 75 controladores

- Un fichero por controlador, nombre `{Nombre}Controller.php`, clase con el mismo nombre.
- **Debe** extender `Controller`.
- **No debe** contener SQL literal. (Se incumple en varios sitios — ver `18_KNOWN_ISSUES.md`.)
- **No debe** contener HTML extenso; eso va en `app/views/`.
- Usado por: `core/AutoLoad.php` (despacho), `PermisosModel::hasAccess()` (instanciación para validar permisos).

### `app/models/` — 139 ficheros

Contiene tres cosas distintas, sin separación de carpetas:

| Tipo | Convención de nombre | Extiende | Ejemplos |
|---|---|---|---|
| Modelos de datos | `*Model.php` | `Model` | `ColaboradoresModel`, `MetasModel` |
| Alias de compatibilidad | `Reclutamiento*Model.php` | Otro modelo | `ReclutamientoVacantesModel extends RecruitmentVacanciesModel` |
| Servicios de dominio | `*Service.php` o nombre libre | *(nada)* | `TalentScoreService`, `NominaCalculoModel`, `ContratacionDesdeOfertaService`, `ScoringCandidatosService`, `GeneradorPlanesDesarrolloService`, `CalculadorProgresoPlanService`, `PlanesDesarrolloSchemaHelper`, `SincronizacionModel`, `AsistenteIAModel` |

> ⚠️ `NominaCalculoModel` y `AsistenteIAModel` se llaman "Model" pero **no extienden `Model`** ni tienen tabla. Son servicios.

### `app/views/` — 291 ficheros, 78 carpetas

Una carpeta por `$ViewFolder` del controlador. Convenciones observadas:

| Fichero | Papel |
|---|---|
| `list.php` / `index.php` | Listado (el CRUD genérico espera `list.php`) |
| `view.php` / `ver.php` | Detalle |
| `create.php` / `crear.php` | Alta |
| `edit.php` / `editar.php` | Edición |
| `_form.php` | Parcial de formulario compartido entre create y edit (prefijo `_`) |
| `_headboard.php` | Parcial de cabecera |
| `partials/` | Subcarpeta de parciales (solo en `colaboradores/`) |

> No hay una convención única: los módulos antiguos usan inglés (`list/create/edit/view`) y los nuevos español (`index/crear/ver`). Al trabajar en un módulo, **imita el que ya usa esa carpeta**.

Carpetas sin controlador asociado (huérfanas): `app/views/perfil_cargo/`, `app/views/archivos/` (solo un CSS), `app/views/_test/`, `app/views/test/`.

### `app/layouts/`

```
metronic.php          ← backoffice
metronic_public.php   ← portal del colaborador
metronic_empty.php    ← login y pantallas sin chrome
impresiones.php       ← comprobantes imprimibles
empty.php             ← respuestas AJAX
clear.php             ← fragmento sin envoltorio (View::stream_view)
demo.php              ← modo demo
_asistente_ia_widget.php  ← parcial incluido por metronic.php
alerts/metronic.php
flashes/{metronic, metronic_empty, metronic_public, demo, developr, developr_login, _metronic}.php
logs/{metronic, metronic_login, demo}.php
menus/{metronic, metronic_public, demo, developr}.php
shortcuts/{metronic, metronic_public, demo, developr}.php
```

Los parciales `developr*` son residuos de una plantilla anterior; `tests/LayoutConsistencyTest.php` verifica que **ningún controlador los seleccione**.

### `core/` — Framework propietario

| Fichero | Responsabilidad |
|---|---|
| `AutoLoad.php` | Bootstrap y despacho. **Fichero crítico #1** |
| `Controller.php` | Clase base de controladores, autorización, CSRF. **Fichero crítico #2** |
| `Model.php` | Clase base de modelos, persistencia, atributos. **Fichero crítico #3** |
| `Router.php` | Construcción de URLs y redirecciones (con verificación de permisos) |
| `View.php` | `render_view`, `stream_view`, `load_view` |
| `Db.php` | Fábrica de conexiones por driver |
| `db/KleePDO.php` | Interfaz que deben cumplir los drivers |
| `db/MysqlPDO.php` | Driver activo. Construcción de SQL |
| `db/OraclePDO.php`, `db/LdapPDO.php` | Drivers alternativos (Oracle incompleto) |
| `Atributo.php` | Clase base de los 26 tipos de atributo |
| `attributes/*.php` | Un fichero por tipo |
| `Lista.php` / `ListaAjax.php` | Generador de tablas DataTables server-side |
| `Cache.php` | Caché en ficheros con TTL y namespaces |
| `Env.php` | Parser de `.env` (`get`, `bool`, `int`, `has`) |
| `RateLimiter.php` | Limitación de intentos en sesión |
| `PasswordPolicy.php` | Validación de contraseñas (31 líneas) |
| `UserFlash.php` | Mensajes flash en sesión |
| `LogsConsole.php` | Log de depuración volcado al `console.log` del navegador |
| `Logger.php` / `LoggerConfig.php` / `LoggerManager.php` | Registro estructurado a fichero con rotación |
| `ErrorHandler.php` | Manejador de errores (solo si `APP_DEBUG=true`) |
| `QueueCss.php` / `QueueScripts.php` | Cola de CSS/JS por petición (`pushBefore`/`pushAfter`) |
| `Html.php` | Helpers de HTML (uso residual) |
| `Formvalidate.php` | Validador de formularios (uso residual) |
| `ModelArray.php` | Variante de modelo sobre arrays en memoria |
| `Instalador.php`, `Migracion.php`, `Htaccess.php` | Instalador legado incompleto |
| `helpers/*.php` | `DateHelper`, `TimeHelper`, `TextHelper`, `NumberHelper`, `MoneyHelper`, `PassHelper`, `QuitarTildesHelper`, `KHtml`, `KleePicture`, `PHPImage`, `DebugHelper` |
| `ajax.js`, `check_sessions.js` | JS servido desde `core/` (⚠️ fuera de `public/`) |

**Qué NO debe contener `core/`:** lógica de negocio de RR. HH., referencias a tablas concretas del dominio, o vistas.

### `database/`

| Elemento | Regla |
|---|---|
| `migrations/` | Nombre `YYYY_MM_DD_HHMMSS_descripcion_snake_case.php`. El fichero **retorna** un array con closures `up(PDO)` y `down(PDO)` (formato estándar), o declara funciones globales (formato histórico). **No se editan una vez aplicadas.** |
| `seeders/` | Prefijo numérico de 3 dígitos que **fija el orden**: `0xx` núcleo/catálogos, `1xx` módulos, `2xx` escenarios demo |
| `seeders/support/` | `Seeder.php` (clase base con `upsert()`), `ClavesDeNegocio.php` (columna natural de cada tabla), `SeederManifest.php` (perfiles), `SeederPipeline.php` (ejecutor) |
| `Migrator.php` | Ejecutor; registra en la tabla `migrations` (`id, migration, batch, executed_at`) |
| `DatabaseStructureAuditor.php` | Validación estática (`php bin/migrate validate` / `php bin/seed --validate`) |
| `DemoReadinessVerifier.php` | `php bin/seed --verify-demo` |
| `README.md` | ✅ Documentación exacta y vigente. **Léela antes de tocar BD.** |

### `bin/`

| Script | Uso |
|---|---|
| `bin/migrate {up\|down\|status\|validate}` | Migraciones |
| `bin/seed [perfil\|ClaseSeeder\|--list\|--validate\|--verify-demo]` | Semillas. Perfiles: `base`, `modulos`, `completo` (alias: `all`, `modules`, `demo`, `demo-complete`) |
| `bin/make-migration <descripcion>` | Genera un esqueleto de migración |
| `bin/make-seeder <Nombre>` | Genera un esqueleto de semilla |

> ⚠️ Solo `bin/seed` y `bin/make-seeder` tienen permiso de ejecución. `bin/migrate` y `bin/make-migration` se invocan con `php bin/migrate …`.

### `public/` — DocumentRoot

| Elemento | Nota |
|---|---|
| `index.php` | Front controller. Define `BASE_PATH`, `BASE_FILES`, `INC_DIR`, `DIR_LLAMADO`, `DIR_INDEX='public'` |
| `check_session.php` | Devuelve `1` si hay sesión; lo consulta `core/check_sessions.js` |
| `.htaccess` | `RewriteEngine Off`, `max_input_vars 5000`. **No hay reescritura de URLs** |
| `assets/kuorum-metronic-demo1.css` | Único CSS propio del proyecto |
| `js/quick-search.js` | **El único referenciado** por los layouts (`shortcuts/metronic.php` y `shortcuts/metronic_public.php`) |
| `assets/js/quick-search.js` | **Copia divergente y huérfana**: mismo tamaño (11 626 B) pero distinto contenido (md5 diferente). Nadie la carga |
| `metronic_html_v8.0.23_demo1/` | Plantilla completa de Metronic, incluidas sus 275 páginas HTML de demostración. Solo se usan `assets/` |
| `admin/` | Residuo (`index.php` + `default.png`) |

### `files/` — Almacén de ficheros

Subcarpetas por dominio: `adelantos_nomina`, `ayuda`, `beneficios`, `carrera_colaborador`, `configuraciones`, `folders`, `import`, `linea_etica`, `logos`, `metas`, `recruitment`, `servicio_colaborador`, `uploaded`, `usuarios`. Más los logotipos y favicon en la raíz.

Constante de acceso: `BASE_FILES` (definida en `public/index.php`). El layout referencia el favicon como `URL::base_url() . '/../files/' . $favicon`, lo que implica que `files/` debe ser **accesible por HTTP** desde fuera de `public/`.

**Requiere permisos de escritura del usuario del servidor web.**

### `storage/cache/`

- `classmap.php` — mapa clase→ruta generado en el arranque.
- Ficheros de caché de `core/Cache.php`.
- `demo_data_ready.flag` — marca de siembra del modo demo (TTL 6 h).

Ignorado por git. **Requiere escritura.** Si el directorio no es escribible, `Cache` y el classmap degradan silenciosamente (el autoload sigue funcionando por búsqueda de fichero).

### `tests/`

PHPUnit 9.6. Se ejecuta con `composer test` o `vendor/bin/phpunit tests`. No hay `phpunit.xml` → `[NO DETERMINADO EN EL CÓDIGO]` cuál es la configuración de bootstrap; los tests son de análisis estático de ficheros en su mayoría.

---

## 3. Ficheros críticos

Modificar cualquiera de estos afecta a **toda** la aplicación:

| Fichero | Riesgo si se rompe |
|---|---|
| `core/AutoLoad.php` | La aplicación no arranca. El orden de `require` es significativo |
| `core/Controller.php` | Se rompen la autorización y el CSRF de los 75 controladores |
| `core/Model.php` | Se rompe la persistencia de los 127 modelos |
| `core/db/MysqlPDO.php` | Se rompe todo el acceso a datos |
| `app/config/Config.php` | Configuración global; `syncConfigFromEnv()` es el único punto donde entra el `.env` |
| `app/config/Menu.php` | Se pierde la navegación de ambos portales |
| `core/Router.php` | Se rompen todos los enlaces y redirecciones |
| `core/View.php` | Se rompe el renderizado |
| `app/layouts/metronic.php` y `metronic_public.php` | Se rompe la interfaz completa de un portal |
| `app/models/PermisosModel.php` + `UsuariosModel.php` | Se rompe la autorización |
| `app/models/ColaboradoresModel.php` (676 líneas) | Entidad central; `validateUser()` concede permisos por código |
| `.env` | Sin `DB_NAME` la aplicación no conecta |
| `storage/cache/classmap.php` | Si contiene rutas obsoletas, hay clases que no cargan |

---

## 4. Relaciones entre carpetas

```mermaid
graph LR
    PUB["public/"] --> CORE["core/"]
    CORE --> CFG["app/config/"]
    CORE --> CTRL["app/controllers/"]
    CTRL --> MOD["app/models/"]
    CTRL --> SRV["services/"]
    CTRL --> VW["app/views/"]
    VW --> LAY["app/layouts/"]
    LAY --> ASSETS["public/metronic_.../assets"]
    MOD --> CORE
    SRV --> MOD
    CTRL --> FILES["files/"]
    CORE --> STOR["storage/cache/"]
    CORE --> LOGS["logs/"]
    BIN["bin/"] --> DB["database/"]
    DB --> MYSQL[("MySQL")]
    MOD --> MYSQL
    TESTS["tests/"] -.analiza.-> CTRL
    TESTS -.analiza.-> DB
    TESTS -.analiza.-> LAY
```

Dependencias **prohibidas** (no existen hoy y no deben introducirse):
- `core/` → `app/models/*Model` de dominio (excepto `UsuariosModel`, `PermisosModel`, `LogModulesModel`, `LogAccionesModel`, `DemoController`, que `core/Controller.php` y `core/Model.php` ya referencian — es una dependencia inversa existente y conocida)
- `app/views/` → acceso directo a `DB::getConnection()`
- `app/models/` → `View::` o `echo` de HTML

---

## 5. Convenciones de nombres observadas

| Elemento | Convención | Ejemplo |
|---|---|---|
| Controlador | `PascalCase` + `Controller` | `BeneficiosSolicitudesController` |
| Modelo | `PascalCase` + `Model` | `BeneficiosSolicitudesModel` |
| Servicio | `PascalCase` + `Service` | `TalentScoreService` |
| Tabla | `snake_case` plural (mayoritariamente) | `beneficios_solicitudes` |
| Vista SQL | `vista_` + nombre de tabla | `vista_beneficios_solicitudes` |
| Columna | `PascalCase` | `FechaSolicitud`, `IdColaborador`, `ColaboradorId` |
| Carpeta de vistas | `snake_case` | `beneficios_solicitudes/` |
| Acción | `camelCase` + `Action` | `cambiarEstadoAction()` |
| Constante de estado | `ESTADO_MAYUSCULA` con valor `snake_case` en minúscula | `const ESTADO_EN_REVISION = 'en_revision'` |
| Migración | `YYYY_MM_DD_NNNNNN_snake_case.php` | `2026_02_23_000039_create_nomina_tables.php` |
| Semilla | `NNN_PascalCaseSeeder.php` | `120_NominaSeeder.php` |

> ⚠️ **Inconsistencia real y activa:** las claves foráneas usan dos estilos según la antigüedad del módulo — `IdColaborador` (módulos antiguos: metas, evaluaciones, planes de desarrollo, vacaciones) y `ColaboradorId` (módulos nuevos: asistencia, beneficios, adelantos, capacitación, tickets, carrera, sucesión, reconocimientos). **No unificar sin migración.** Al escribir código, comprueba primero cuál usa la tabla.

---

## 6. ⚠️ `docs/` está excluido del control de versiones

`.gitignore` contiene:

```
# Documentation files in docs folder (tracked locally, not in repo)
docs/**/*.md
```

**Consecuencia:** esta base de conocimiento **no se sube al repositorio** con la configuración actual. Si se desea versionarla hay que:

```bash
# opción A — excepción en .gitignore
echo '!docs/knowledge/**/*.md' >> .gitignore

# opción B — forzar el add
git add -f docs/knowledge/
```

Otras entradas relevantes del `.gitignore`: `.env`, `vendor/`, `.vscode/`, `storage/`, `sftp.json`, `*.log`.

---

## Documentos relacionados
- [02_ARCHITECTURE.md](02_ARCHITECTURE.md)
- [13_CONFIGURATION.md](13_CONFIGURATION.md)
- [14_DEVELOPMENT_GUIDELINES.md](14_DEVELOPMENT_GUIDELINES.md)
