# Auditoría forense del paquete de producción anterior

## Alcance y reglas observadas

Esta investigación es exclusivamente diagnóstica. No se modificaron código, assets, fuentes, Git ni paquetes; no se generó ningún ZIP y no se desplegó. La única escritura persistente es este reporte solicitado.

Paquete investigado:

- Ruta: `output/production/flexi-platform-production-20260811-225203.zip`
- Tamaño: 11,725,618 bytes.
- SHA-256: `50f216fd0b1c40d39e818025dd1e99591d1e6dba688a39631b0497f1bfa10b18`.
- Fecha de creación observada en filesystem: 2026-08-11 18:52:08, zona local del contenedor (-04:00).
- Entradas: 6,370 archivos.

El ZIP se extrajo nuevamente en un directorio temporal limpio: `/tmp/flexi-forensic-oldzip.Adr4L4/`. Esta ruta es evidencia temporal de trabajo, no parte del proyecto.

## Resumen ejecutivo

La premisa de que las fuentes variables incorrectas “terminaron dentro” de este ZIP no se sostiene al inspeccionar el archivo real:

1. El ZIP anterior contiene **cero archivos tipográficos** (`.ttf`, `.otf`, `.woff`, `.woff2`, `.eot`).
2. Su `public/assets/css/site.css` contiene referencias a `font-family: Asap` y `font-family: Raleway`, pero **no contiene ninguna declaración `@font-face`**, ningún URL de Google Fonts y ningún `src` de fuente local.
3. Por ello, en una instalación limpia el navegador solo podía resolver esos nombres mediante fuentes instaladas en el sistema; si no estaban instaladas, utilizaba los fallbacks (`Arial`/`sans-serif`). El entorno actual tampoco tiene Asap ni Raleway instaladas en Fontconfig: `fc-match Asap` y `fc-match Raleway` resuelven a DejaVu Sans.
4. El staging preservado `/tmp/flexi-production-stage` contiene exactamente los mismos 6,370 archivos y hashes que el ZIP: 6,370/6,370 iguales, cero añadidos, cero omitidos a nivel de archivos y cero modificados.
5. El checkpoint `output/source/flexi-source/`, creado segundos antes, confirma que CSS, vistas, imágenes y JavaScript ya estaban en ese estado antes del empaquetado. Tampoco contiene fuentes.
6. Las fuentes variables auditadas posteriormente (`Asap-wdth-wght.ttf` y `Raleway-wght.ttf`) entraron en el árbol Laravel durante Fase D a las 22:24:23, **3 horas y 32 minutos después** de crear el ZIP anterior. Sus hashes coinciden con archivos de la reconstrucción WordPress, pero no aparecen en el ZIP ni en sus checkpoints.
7. Las carpetas de `storage` sí existían vacías en staging a las 18:49:46, pero el script de ZIP solo añadió archivos regulares. No llamó `ZipArchive::addEmptyDir()` y no había marcadores dentro de esas carpetas. Esa es la causa demostrada de su pérdida.

En consecuencia, el empaquetado anterior no sustituyó ni generó fuentes. Reprodujo fielmente un árbol que ya carecía de fuentes web autocontenidas. El fallo de carpetas vacías sí fue causado por la implementación del empaquetador.

## Evidencia disponible y ausencia de Git

### Git

No existe `.git` dentro de `/workspace/codex-workspace/flexi-laravel`, ni se encontró otro repositorio Git bajo `/workspace/codex-workspace`. Por tanto:

- no hay `git log` disponible;
- no hay `git reflog` disponible;
- no hay commit o tree Git identificable;
- no es posible atribuir los archivos a un hash de commit.

Esto no es una inferencia: el proyecto no está versionado como repositorio Git en este workspace. El checkpoint más próximo y verificable es la copia fuente generada en `output/source/flexi-source/`, junto con el staging preservado en `/tmp/flexi-production-stage`.

### Artefactos temporales preservados

Se localizaron los scripts exactos y el staging de Fase C:

- `/tmp/build-flexi-artifacts.php`, creado 18:49:22.
- `/tmp/flexi-production-stage`, preparado entre 18:49:46 y 18:50:36, con actualización final del manifiesto a las 18:52:02.
- `/tmp/package-flexi-production.php`, creado 18:50:09.
- `/tmp/finalize-flexi-hashes.php`, creado 18:51:12.
- `/tmp/clean-flexi-generated.php`, creado 18:51:56.
- `output/source/flexi-source-20260811-225202.zip`, creado 18:52:03.
- ZIP productivo investigado, creado 18:52:08.

La conservación accidental del staging permite una comparación criptográfica completa, no aproximada.

## Línea temporal demostrable

| Hora local | Evidencia |
|---|---|
| 06:36:17 | Imágenes públicas copiadas/creadas en el árbol Laravel. |
| 06:48:34 | `site.css` anterior queda con mtime preservado; ya no contiene `@font-face`. |
| 06:48:45 | Captura Home de Fase A generada 11 segundos después con ese CSS. |
| 18:49:22 | Se crea el script de artefactos fuente. |
| 18:49:46 | Se materializan en staging los directorios vacíos de `storage`; también se copia el árbol de aplicación preservando mtimes. |
| 18:49:48–18:49:53 | Composer instala/regenera `vendor` productivo y autoload en staging. |
| 18:49:58–18:49:59 | Laravel genera `bootstrap/cache/services.php` y `packages.php`. |
| 18:50:09 | Se crea el script exacto del ZIP productivo. |
| 18:52:02 | Se actualiza/copia `SOURCE-CODE-MANIFEST.md` en staging y checkpoint fuente. |
| 18:52:03 | Se crea ZIP de código fuente. |
| 18:52:08 | Se crea `flexi-platform-production-20260811-225203.zip`. |
| 22:24:23 | Se copian por primera vez al árbol Laravel `Asap-wdth-wght.ttf` y `Raleway-wght.ttf` durante Fase D. |
| 23:16:49 | Esos dos archivos variables se retiran del directorio público durante la auditoría tipográfica y quedan preservados temporalmente. |

No hay evidencia de fuentes locales dentro de Laravel antes de las 22:24:23.

## Proceso exacto de construcción

### Generación del checkpoint fuente

`/tmp/build-flexi-artifacts.php` define explícitamente:

```php
$root='/workspace/codex-workspace/flexi-laravel';
$source="$root/output/source/flexi-source";
$copyRoots=['app','bootstrap','config','database/factories','database/migrations',
    'database/seeders','docs','public/assets','resources','routes','tests'];
```

Su función `copyTree()` usa `FilesystemIterator::SKIP_DOTS`, crea directorios y ejecuta `copy()` archivo por archivo. Así se creó el checkpoint fuente inmediatamente anterior. Este proceso copia `public/assets` tal como estaba; no compila, genera ni sustituye assets.

### Preparación del staging

No se encontró un único script conservado que documente los comandos exactos usados para poblar inicialmente `/tmp/flexi-production-stage`. La evidencia física demuestra:

- el staging es una copia del árbol Laravel para los archivos propios;
- sus mtimes públicos coinciden con el origen;
- `storage` fue creado vacío a las 18:49:46;
- `database-production.sql` fue añadido al staging;
- Composer escribió `vendor/` entre 18:49:48 y 18:49:53;
- se generaron caches de descubrimiento en `bootstrap/cache` a las 18:49:58–59.

No existe evidencia suficiente para afirmar si la copia inicial se realizó con `cp`, `rsync` o mediante una función PHP distinta. No aparece ese comando en `.bash_history`, y no debe inventarse.

### Composer

El staging contiene `vendor/` productivo y no contiene PHPUnit. Los timestamps de `vendor/composer` muestran regeneración de autoload. Composer no opera sobre `public/assets` y no hay diferencia criptográfica en dichos assets antes/después; por tanto no sustituyó fuentes, CSS, imágenes o JS.

### Creación del ZIP

El script exacto `/tmp/package-flexi-production.php` establece:

```php
$stage='/tmp/flexi-production-stage';
$it=new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($stage, FilesystemIterator::SKIP_DOTS)
);
foreach ($it as $file) {
    if (!$file->isFile()) continue;
    // exclusiones...
    $zip->addFile($file->getPathname(), 'flexi-platform/'.$relative);
}
```

Exclusiones exactas:

- `.env`;
- cualquier ruta con `PRODUCTION-SECRETS`;
- cualquier ruta con `database.sqlite`;
- archivos bajo `storage/logs/`.

No hubo reglas específicas de sustitución o limpieza para `public/assets`. El empaquetador no ejecuta Vite, npm, compilación CSS, generación de fuentes ni copia alternativa de assets.

## Comparación completa staging → ZIP

Se calculó SHA-256 de todos los archivos relativos en ambos árboles:

```text
staging: 6,370 archivos
ZIP extraído: 6,370 archivos
comunes: 6,370
hash idéntico: 6,370
hash diferente: 0
solo staging: 0
solo ZIP: 0
```

Esto prueba que el contenido de archivo del ZIP es una réplica exacta del staging. La única información no preservada son los directorios vacíos, porque no son archivos.

## Comparación checkpoint fuente → staging/ZIP

El checkpoint `output/source/flexi-source/` contiene 194 archivos propios. Frente al ZIP:

- 180 rutas comunes;
- 178 idénticas;
- 2 diferentes: `bootstrap/cache/packages.php` y `bootstrap/cache/services.php`, regenerados legítimamente durante Composer/package discovery;
- 14 archivos solo fuente: factories, seeders, tests y `phpunit.xml`, excluidos de producción;
- archivos adicionales en producción: `vendor/`, front controller/`.htaccess`, robots/sitemap y schema SQL limpio.

Para las áreas visuales prioritarias, el resultado es inequívoco:

| Área | Checkpoint fuente vs ZIP | Staging vs ZIP |
|---|---:|---:|
| `public/assets/css/` | 3 archivos, 0 diferencias | 3 archivos, 0 diferencias |
| `public/assets/fonts/` | 0 archivos en ambos | 0 archivos en ambos |
| `public/fonts/` | 0 archivos en ambos | 0 archivos en ambos |
| `resources/views/` | 50 archivos, 0 diferencias | 50 archivos, 0 diferencias |
| `public/assets/images/` | 10 archivos, 0 diferencias | 10 archivos, 0 diferencias |
| `public/assets/js/` | 3 archivos, 0 diferencias | 3 archivos, 0 diferencias |

No existen `public/css`, `public/fonts`, `public/images` o `public/js` alternativos en esos artefactos.

## Auditoría de fuentes del ZIP anterior

### Inventario

**No hay archivos de fuente en el ZIP anterior.** Por tanto no existe una tabla de metadata por archivo que completar: no hay ruta, SHA-256, family, subfamily, weight, width, ejes o PostScript name dentro del paquete.

Esta ausencia fue validada de tres formas:

1. inventario del ZIP por extensiones tipográficas;
2. búsqueda recursiva en el ZIP extraído;
3. comparación con staging y checkpoint fuente.

### CSS relacionado

El CSS sí nombra las familias, sin definirlas:

```css
body { font-family: Asap, Arial, sans-serif; }
.button { font: 700 13px Raleway, Arial, sans-serif; }
input, textarea { font: 400 14px Raleway, Arial, sans-serif; }
```

No existe `@font-face`, `fonts.googleapis.com` o `fonts.gstatic.com` en el layout o CSS público del ZIP. `resources/views/layouts/app.blade.php` enlaza únicamente `assets/css/site.css`.

### Respuesta específica

¿Las fuentes incorrectas ya estaban en el proyecto antes del ZIP?

**No como archivos.** El proyecto inmediatamente anterior ya tenía el defecto de referenciar Asap/Raleway sin proporcionar ninguna fuente web. Ese defecto preexistía al empaquetado y fue copiado sin cambios.

¿Fueron introducidas, sustituidas, generadas o seleccionadas durante packaging?

**No.** El proceso de packaging no introdujo ninguna fuente. El ZIP contiene cero fuentes y no altera `site.css`.

## Origen posterior de las fuentes variables

Las dos fuentes variables mencionadas en la auditoría tipográfica posterior no pertenecen al ZIP investigado:

| Archivo posterior | SHA-256 | Origen comprobado | Entrada en Laravel |
|---|---|---|---|
| `Asap-wdth-wght.ttf` | `7bf29bcab72f7d00de600e964a3fc206620050d8448ddd976822378d0fe18028` | `flexi/rebuild/.../fonts/asap/Asap-wdth-wght.ttf` | 22:24:23, Fase D |
| `Raleway-wght.ttf` | `8bbcc3eb8275c388f4bcd998832f8a4b943eadbaf6a595205312774b5951aefb` | `flexi/rebuild/.../fonts/raleway/Raleway-wght.ttf` | 22:24:23, Fase D |

El Asap posterior es variable v3.002, OS/2 weight 400, width class 5 y ejes `wght` 100–900 / `wdth` 75–125, PostScript `Asap-Regular`. Raleway posterior es variable v4.026, OS/2 weight 100, width class 5 y eje `wght` 100–900, PostScript `Raleway-Thin`.

Esos metadatos explican el fallo auditado en Fase D cuando ambos binarios se declararon repetidamente como caras estáticas. No explican el ZIP de las 18:52 porque los archivos todavía no habían sido copiados a Laravel.

## Comparación de `site.css`

### Hashes

| Artefacto | SHA-256 |
|---|---|
| Checkpoint inmediatamente anterior `output/source/flexi-source/public/assets/css/site.css` | `affc29e0cfaabb726b4ef4b70cf969b73f925c4ef8199b5200ca7bf5ae233e51` |
| Staging `/tmp/flexi-production-stage/public/assets/css/site.css` | `affc29e0cfaabb726b4ef4b70cf969b73f925c4ef8199b5200ca7bf5ae233e51` |
| ZIP anterior extraído `public/assets/css/site.css` | `affc29e0cfaabb726b4ef4b70cf969b73f925c4ef8199b5200ca7bf5ae233e51` |

### Diff relevante

```text
sin diferencias
```

El mtime preservado de `site.css` es 06:48:34; la captura Home de Fase A fue creada a las 06:48:45. Por tanto ese mismo CSS ya se usaba antes de Fase C y antes del packaging. No hay un `site.css` anterior “correcto” adicional conservado dentro del proyecto o de Git con el cual demostrar cuándo se eliminó una carga web de Asap/Raleway.

## ¿Era copia del árbol servido en 127.0.0.1:8090?

La pregunta debe separarse entre contenido demostrado y proceso de servidor no preservado.

### A. ¿Mismo árbol?

**El contenido propio sí coincide con el árbol Laravel/checkpoint inmediatamente anterior**, salvo las diferencias deliberadas de producción (tests/factories/seeders fuera; `vendor` productivo y SQL dentro; caches Composer regeneradas).

No hay log de proceso, PID, cwd o comando del servidor 8090 correspondiente exactamente a las 18:52. Por ello no puede demostrarse formalmente qué directorio estaba atendiendo ese puerto en ese instante. README indica el comando previsto `php artisan serve --host=0.0.0.0 --port=8090`, pero eso no constituye una captura del proceso.

### B. ¿Mismos CSS?

**Sí.** Checkpoint, staging y ZIP tienen los tres CSS públicos con hashes idénticos. `site.css` coincide exactamente.

### C. ¿Mismas fuentes?

**Sí respecto al árbol/checkpoint: ninguno contenía fuentes.** No se puede afirmar qué fuentes del sistema operativo pudo resolver un navegador particular en una sesión anterior; eso es estado externo al árbol y no quedó registrado.

### D. ¿Mismas vistas?

**Sí.** Las 50 vistas de `resources/views/` coinciden por SHA-256 entre checkpoint, staging y ZIP.

### E. ¿Mismas imágenes?

**Sí.** Las 10 imágenes de `public/assets/images/` coinciden por SHA-256 entre checkpoint, staging y ZIP.

La evidencia del usuario —misma apariencia incorrecta en cPanel y homelab— es consistente con este análisis: ambos ejecutaron el mismo paquete sin fuentes web autocontenidas.

## Por qué desaparecieron las carpetas vacías

El staging demuestra que sí existían:

- `storage/framework/views`;
- `storage/framework/sessions`;
- `storage/framework/cache/data`;
- `storage/logs`;
- `storage/app/public`.

Todas estaban vacías y fueron creadas a las 18:49:46. El script de producción:

1. usa `RecursiveIteratorIterator`;
2. ignora todo elemento que no sea archivo mediante `if(!$file->isFile()) continue;`;
3. solo llama a `ZipArchive::addFile()`;
4. nunca llama a `ZipArchive::addEmptyDir()`;
5. no colocó `.gitkeep`/`.keep` dentro de `storage`.

Por eso ninguna ruta `flexi-platform/storage/` aparece en el ZIP.

No fue una limitación inevitable del formato ZIP. ZIP puede conservar directorios vacíos. Fue consecuencia directa del algoritmo del empaquetador y de la ausencia de archivos marcadores. `bootstrap/cache` sí sobrevivió porque contenía `.gitignore`, `services.php` y `packages.php`.

## Qué evidencia no existe

No es posible demostrar con los artefactos disponibles:

- un commit exacto, porque no existe repositorio Git;
- el comando exacto de copia que pobló staging (`cp` frente a `rsync` u otro), porque no está en historial ni en un script conservado;
- el cwd/PID exacto del servidor 8090 a las 18:52;
- qué archivo tipográfico de sistema pudo usar un navegador en una revisión visual anterior;
- un checkpoint anterior que contenga la versión visual que el revisor recuerda como correcta;
- cuándo, antes de 06:48:34, se decidió referenciar Asap/Raleway sin incorporarlas como webfonts.

Sí existe evidencia suficiente para concluir que el ZIP no cambió los archivos visuales al empaquetarlos.

## Conclusión inequívoca

### CAUSA RAÍZ:

El defecto tipográfico del ZIP anterior fue que el árbol fuente ya declaraba `Asap` y `Raleway` en `site.css` **sin incluir archivos de fuentes, sin reglas `@font-face` y sin enlace a Google Fonts**. En una instalación limpia el navegador caía a fuentes del sistema. El empaquetador copió ese defecto exactamente; no introdujo las fuentes variables auditadas después.

El incidente tipográfico posterior de Asap Regular/Raleway Thin fue distinto: durante Fase D, a las 22:24:23, se copiaron desde la reconstrucción dos fuentes variables y se mapearon como caras estáticas. Eso ocurrió después del ZIP investigado.

### MOMENTO EN QUE OCURRIÓ:

El defecto “familias sin webfonts” existía como mínimo a las **06:48:34 del 11-08-2026**, mtime preservado de `site.css`, antes de la captura Fase A de 06:48:45 y muchas horas antes del packaging.

La introducción de las fuentes variables ocurrió a las **22:24:23**, durante Fase D, no durante el packaging de 18:52.

No hay evidencia para fechar con mayor precisión cuándo se originó el CSS font-less antes de 06:48:34.

### ARCHIVOS AFECTADOS:

En el ZIP anterior:

- `public/assets/css/site.css`: nombres de familia sin fuente suministrada.
- `resources/views/layouts/app.blade.php`: carga únicamente `site.css`, sin stylesheet tipográfico externo.
- ausencia total de `public/assets/fonts/` o equivalente.
- ausencia de `storage/framework/views`, `storage/framework/sessions`, `storage/framework/cache/data` y demás directorios vacíos.

No se alteraron durante packaging las vistas, imágenes, CSS o JavaScript: sus hashes coinciden.

### ¿FUE EL HOSTING?:

**NO.** El mismo ZIP reproduce el problema en homelab; además, el paquete carece objetivamente de fuentes web y depende de fallbacks del sistema. El hosting no sustituyó archivos del ZIP.

### ¿FUE EL EMPAQUETADO?:

**NO para la tipografía.** Staging y ZIP son idénticos en los 6,370 archivos; `site.css`, vistas, imágenes y JS no cambiaron. Packaging no añadió, generó ni sustituyó fuentes.

**SÍ para la pérdida de directorios vacíos.** El script añadió únicamente archivos y omitió todos los directorios; las carpetas existían en staging pero no tenían marcadores.

### ¿CÓMO EVITAR QUE SE REPITA?:

1. Versionar el proyecto en Git y etiquetar cada estado visual aprobado.
2. Crear el paquete desde un checkout/checkpoint inmutable, no desde un árbol mutable sin commit.
3. Incluir fuentes locales estáticas auditadas y verificar SHA-256/metadata/PostScript en CI.
4. Auditar recursos de red y `CSS.getPlatformFontsForNode`; no aceptar solamente `getComputedStyle().fontFamily`.
5. Comparar por hash el árbol servido y el staging antes de comprimir.
6. Crear un manifiesto del ZIP y comprobar que CSS, vistas, imágenes, JS y fuentes coinciden con el checkpoint aprobado.
7. Hacer que el empaquetador agregue directorios explícitamente con `addEmptyDir()` o use marcadores seguros.
8. Extraer cada ZIP en un directorio limpio y ejecutar smoke tests desde **esa extracción**, incluyendo assets, tipografía y escritura de sesiones/vistas/cache.
9. Fallar el build si no existen las cinco rutas estructurales requeridas o si alguna fuente declarada no fue incluida.
10. No considerar screenshots previas como prueba del paquete: generar screenshots finales ejecutando el ZIP extraído.

