# Verificación local HECATE **no usa CI en la nube**. No hay `.github/workflows/`, ni CircleCI, ni GitLab CI. La calidad se garantiza en la máquina de quien escribe el código, con dos scripts del repositorio. ## 1. Por qué | Motivo | Detalle | | --- | --- | | **Soberanía** | El criterio de calidad es un fichero del repositorio, auditable y versionado. | | **Coste cero** | Sin minutos de CI, sin cuotas, sin runners que cambian de versión sin avisar. | | **Independencia** | Migrar el proyecto a otro *forge* no obliga a reescribir la verificación. | | **Reproducibilidad** | `pyproject.toml` fija ruff/mypy/pytest y `Cargo.toml` fija los lints de Rust. | | **Cobertura de la arquitectura real** | Al ser un sistema híbrido, la verificación cubre **los dos planos**: si solo se verificara Python, el código más crítico (el bucle de red en Rust) quedaría sin red. | La contrapartida aceptada: no hay una "insignia verde" pública generada por un servicio externo. La insignia del README es un SVG local (`docs/badges/local-verify.svg`) que documenta el modelo elegido, no un semáforo remoto. ## 2. Requisitos ### Plano de datos (Python) - Python 3.10 o superior. - Dependencias de desarrollo: ```bash python -m venv .venv source .venv/bin/activate # Windows (PowerShell): .venv\Scripts\Activate.ps1 pip install -e ".[dev]" ``` `[dev]` instala `ruff`, `mypy`, `pytest`, `pytest-cov` y `grpcio-tools`. ### Plano de control (Rust) - Rust **1.88** o superior (edición 2024; lo exige `tonic-prost` 0.14). ```bash # Windows: target GNU + linker de MinGW, sin Visual Studio winget install Rustlang.Rustup rustup default stable-x86_64-pc-windows-gnu winget install BrechtSanders.WinLibs.POSIX.MSVCRT # Linux / macOS curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` Si no tienes toolchain Rust, `SKIP_RUST=1 ./scripts/verify.sh` ejecuta solo el plano de datos (útil para editar documentación o el modelo). El hook `pre-push` **no** lo salta. ## 3. `scripts/verify.sh` ```bash ./scripts/verify.sh ``` Veintiuno pasos, en orden y con aborto al primer fallo: | # | Comando real | Qué se comprueba | Cómo se arregla | | --- | --- | --- | --- | | 1 | `python -m ruff check --no-fix .` | Lint de Python: imports muertos, comparaciones sospechosas, complejidad. | `python -m ruff check --fix .` | | 2 | `python -m ruff format --check .` | Formato de Python estable. | `python -m ruff format .` | | 3 | `python -m mypy` | Tipado estricto de `src/hecate`. | Corregir anotaciones; no añadir `# type: ignore` sin motivo. | | 4 | `pytest --cov=hecate --cov-fail-under=70` | **150 tests** del plano de datos y telomerasa, cobertura mínima. | Añadir tests; nunca bajar el umbral sin justificarlo. | | 5 | `bash scripts/gen-proto.sh --check` | Los stubs de Python coinciden con el `.proto`. | `bash scripts/gen-proto.sh` | | 6 | `emit_quant_fixture.py --check` | El fixture de cuantización sigue describiendo lo que hace el algoritmo. | `just fixture` | | 7 | `emit_acceleration_fixture.py --check` | El ratio de aceleración del ADR 0001 coincide en los dos planos. | `just fixture` | | 8 | `cargo fmt --all -- --check` | Formato de Rust. | `cargo fmt --all` | | 9 | `cargo clippy --workspace --all-targets -- -D warnings` | Lints del workspace con warnings como error. | Corregir el aviso; no silenciarlo sin justificar el `#[allow]`. | | 10 | `cargo clippy -- -W clippy::pedantic` | Sin avisos fuera de la lista justificada en `Cargo.toml`. | Igual: corregir o justificar en `Cargo.toml`. | | 11 | `cargo test --workspace` | **437 tests** de unidad, integración, propiedades y *doctests*. | Corregir el test o el código, nunca ambos a la vez. | | 12 | `cargo build --release -p hecate_node` | El binario compila y enlaza con el linker de GNU. | Revisar el toolchain (ver §2). | | 13 | `hecate-node bench` en debug | El binario de debug se niega a medir. | Si mide, el guardián `require_release` se rompió. | | 14 | `scripts/check_versions.py` | La versión es la misma en Cargo.toml, pyproject, CHANGELOG y CITATION. | `just versions` dice cuál está atrás. | | 15 | `scripts/check-scaffolding.sh` | Ficheros de política, visibilidad y recetas del `justfile`. | Crear el fichero que falte. | | 16 | `scripts/check-llms-txt.sh` | `llms.txt` y `.well-known/ai.txt` válidos. | Corregir la estructura, no el validador. | | 17 | `scripts/check_docker_lab.py` | El laboratorio Docker es coherente: sintaxis de los scripts, estructura del compose, servicios que los scripts esperan, puertos sólo en loopback y `.dockerignore`. **No levanta contenedores.** | Corregir el compose o el script; si falta PyYAML, instalar `.[dev]`. | | 18 | `scripts/check_metrics.py` | Las cifras de `README.md`, `llms.txt`, `.well-known/ai.txt` y `site/index.html` coinciden con `examples/metrics.json`, y el propio JSON describe el árbol actual. | `just sync-metrics` recuenta y propaga; nunca editar las cifras a mano. | | 19 | `scripts/check-css-classes.py` | Toda clase usada en el HTML del sitio tiene al menos una regla CSS. | Añadir la regla o quitar la clase del marcado, según cuál de las dos esté de más. | | 20 | `scripts/check_hosting_links.py --offline` | Los enlaces internos resuelven sin salir a Internet. | Corregir la ruta del enlace. | | 21 | comprobación de docs | `README.md`, `docs/*.md`, `docs/adr/` y `docker/` existen y superan `MIN_DOC_BYTES`. | Escribir documentación real, no esqueletos. | Los lints de Rust no son opcionales: el workspace declara `[workspace.lints.rust] unsafe_code = "forbid", missing_docs = "deny"` y `[workspace.lints.clippy] all = "deny"`. Un `#[allow]` sin justificación se detecta en revisión. El paso 17 merece una aclaración: comprueba el laboratorio **sin ejecutarlo**, porque la verificación obligatoria tiene que funcionar en una máquina sin Docker. Que los contenedores arranquen y que los siete pasos del failover pasen **no** está verificado aquí, y así se declara en [`docs/benchmarks/docker-lab.md`](benchmarks/docker-lab.md). Salida esperada: ```text HECATE · verificación local raíz: /ruta/a/HECATE rama: main commit: 1a2b3c4 python: .venv/bin/python (Python 3.14.6) cargo: cargo (cargo 1.99.1 (5f94df478 2026-08-27)) [1/21] ruff check · lint del plano de datos ✔ sin avisos de lint ... [6/21] paridad de cuantización · el fixture coincide con el algoritmo de Python ✔ el fixture de paridad de cuantización está al día [7/21] paridad de aceleración · el fixture coincide con el ADR 0001 ✔ el fixture de paridad de aceleración está al día [8/21] cargo fmt --check · formato de Rust ✔ formato consistente [9/21] cargo clippy · lints del workspace (warnings = error) ✔ clippy limpio [10/21] cargo clippy::pedantic · sin avisos no justificados ✔ pedantic limpio [11/21] cargo test · suite del plano de control (Rust + Tokio) ✔ tests de Rust en verde [12/21] cargo build --release · binario hecate-node ✔ binario de release compilado y enlazado [17/21] laboratorio Docker · coherencia estática (no se ejecuta) ✔ laboratorio Docker coherente [18/21] métricas · la documentación coincide con el recuento ✔ cifras alineadas en README, llms.txt, ai.txt y el sitio [19/21] CSS · toda clase del HTML tiene regla ✔ toda clase del HTML tiene regla CSS [21/21] documentación · README y docs TODO EN VERDE — plano de datos (Python) y plano de control (Rust) verificados la rama puede publicarse (hook pre-push satisfecho) ``` ### Variables de entorno | Variable | Por defecto | Uso | | --- | --- | --- | | `PYTHON` | autodetección (`.venv`, `python3`, `python`) | Forzar un intérprete concreto. | | `CARGO` | `cargo` (con `~/.cargo/bin` añadido al `PATH`) | Forzar un `cargo` concreto. | | `COVERAGE_MIN` | `70` | Umbral de cobertura exigido por pytest. | | `MIN_DOC_BYTES` | `200` | Tamaño mínimo aceptado por documento. | | `SKIP_RUST` | `0` | `1` omite los cinco pasos del plano de control y el chequeo de toolchain: quedan 17. | | `SKIP_PYTHON` | `0` | `1` omite los siete pasos del plano de datos: quedan 15. Con los dos a `1`, quedan 10. | | `VERIFY_HOOK` | vacío | Si está definida, imprime los milisegundos de cada paso. Es lo que permite responder «por qué tarda» con una columna en vez de con una suposición. | Ejemplos: ```bash COVERAGE_MIN=85 ./scripts/verify.sh PYTHON=/usr/bin/python3.12 ./scripts/verify.sh SKIP_RUST=1 ./scripts/verify.sh # solo plano de datos SKIP_PYTHON=1 ./scripts/verify.sh # solo plano de control SKIP_RUST=1 SKIP_PYTHON=1 ./scripts/verify.sh # solo SCM y documentación ``` ### Códigos de salida - `0`: todo en verde. - `1`: algún paso ha fallado (el mensaje indica cuál: `VERIFICACIÓN FALLIDA (código 1, paso 3/21)`). - `101`: fallo de `cargo` (compilación, clippy o test). El paso indica cuál. - `127`/mensaje de intérprete: no se encontró Python o `cargo`; exporta `PYTHON=...` / `CARGO=...`. ## 4. `scripts/install-hooks.sh` ```bash ./scripts/install-hooks.sh # instala o reinstala el hook ./scripts/install-hooks.sh --force # sobrescribe un pre-push ajeno (guarda .bak) ./scripts/install-hooks.sh --remove # desinstala el hook ``` Escribe `.git/hooks/pre-push` (o `core.hooksPath` si está configurado) con este comportamiento: 1. Localiza la raíz del repositorio. 2. Guarda en un temporal las líneas que git pasa por stdin. Hacen falta **dos** consumidores —el guardián de etiquetas y el cálculo de qué planos se han tocado— y un flujo no se lee dos veces: leerlo mal deja al segundo con una entrada vacía que parece «no hay nada que verificar». 3. Ejecuta `scripts/tag-guard.sh`: rechaza publicar una etiqueta `vX.Y.Z` que no apunte al commit publicado (o a un ancestro suyo, para permitir recolocar una etiqueta histórica). 4. Calcula **qué planos ha tocado el push** comparando la base con la punta, y apaga el que no ha cambiado. Es conservador a propósito: una referencia nueva, un borrado, un `git diff` que falle o una lista vacía dejan los dos planos encendidos. 5. Verifica que `scripts/verify.sh` es ejecutable y lo ejecuta con `SKIP_PYTHON`, `SKIP_RUST` y `VERIFY_HOOK=1`. Si falla, **aborta el push**. ## 4.bis Por qué el push de documentación no recompila Rust Verificar los dos planos en cada push significa compilar el workspace para publicar un cambio de una coma en un `.md`. Eso paga dos veces por una garantía que ya se pagó: si el push no toca `crates/`, el árbol de Rust es **el mismo** que ya pasó estas puertas. | Cambia algo en | Se verifica | | --- | --- | | `crates/`, `Cargo.toml`, `Cargo.lock`, `proto/` | Plano de control | | `python/`, `tests/python/`, `pyproject.toml`, `scripts/*.py` | Plano de datos | | `scripts/`, `.github/`, `justfile` | **Los dos** | | `docs/`, `README.md`, `site/` | Ninguno: quedan SCM y documentación | La fila de `scripts/` no es un descuido: **el instrumento que mide no se valida a sí mismo**. Si cambia `verify.sh`, `count-tests.sh` o un guarda, la medición se repite entera con el instrumento nuevo. Medido: un push que sólo toca documentación y `scripts/` pasó de **~10 min a 10,1 s** cuando se saltaron los dos planos. Un push que toca `scripts/` sigue tardando lo que tiene que tardar (~3 min), porque ahí sí hay que remirarlo todo. Si algún día hace falta publicar sin verificar (caso de fuerza mayor), existe `git push --no-verify`; el `README` y este documento recomiendan no usarlo. ### Windows Los hooks de git se ejecutan con el intérprete de shell disponible. En Windows funciona con **Git Bash** (incluido con Git for Windows): ```powershell & "C:\Program Files\Git\bin\bash.exe" .\scripts\install-hooks.sh & "C:\Program Files\Git\bin\bash.exe" .\scripts\verify.sh ``` `verify.sh` añade al `PATH` tanto `~/.cargo/bin` como el `mingw64/bin` de WinLibs si los encuentra, así que funciona lanzado desde un shell no interactivo (que es como git ejecuta los hooks). Si tu consola no tiene `bash`, ejecuta los mismos comandos de la tabla del apartado 3 a mano: son exactamente los que llama `verify.sh`. ## 5. Refuerzo opcional con pre-commit `.pre-commit-config.yaml` declara un único hook `local` que invoca `scripts/verify.sh` en la fase `pre-push`. No descarga nada de la red ni depende de servicios externos. ```bash pip install pre-commit pre-commit install --hook-type pre-push ``` Es una alternativa, no un sustituto: si no tienes `pre-commit`, el hook de `install-hooks.sh` hace lo mismo con cero dependencias. ## 6. Flujo recomendado antes de publicar ```bash python -m ruff check --fix . # arregla lo automático python -m ruff format . # formatea Python cargo fmt --all # formatea Rust ./scripts/verify.sh # verifica los dos planos git add -A git commit -m "feat: ..." git push # el hook vuelve a verificar (red de seguridad) ``` ## 7. Resolución de problemas | Síntoma | Causa habitual | Solución | | --- | --- | --- | | `no se encontró un intérprete de Python` | No hay `.venv` y `python3` no está en PATH. | Crear el venv o exportar `PYTHON`. | | `ModuleNotFoundError: ruff` | Dependencias de desarrollo no instaladas. | `pip install -e ".[dev]"`. | | `no se encontró cargo` | Toolchain Rust ausente o fuera del `PATH`. | `winget install Rustlang.Rustup`; o `SKIP_RUST=1`. | | `error: linker 'x86_64-w64-mingw32-gcc' not found` | Falta el linker de GNU en Windows. | `winget install BrechtSanders.WinLibs.POSIX.MSVCRT` y reiniciar el shell. | | `error: package ID specification 'hecate-node' did not match` | El nombre del paquete usa guion bajo. | Usar `-p hecate_node` (lo hace `verify.sh`). | | `clippy: error: unknown lint` tras actualizar Rust | Un lint nuevo se activó con `-D warnings`. | Corregir el aviso; el código generado por `tonic` ya está aislado en `mesh_proto::v1`. | | `los stubs generados no coinciden` (paso 5) | Se editó el `.proto` sin regenerar. | `bash scripts/gen-proto.sh` y commit de los `*_pb2*.py`. | | Tests de red fallan con `OSError: address already in use` | Puerto ocupado por otra ejecución. | Los tests usan puertos efímeros (`DEFAULT_BASE_PORT = 0`); cerrar procesos colgados. | | `mypy: error: Library stubs not installed` | Dependencia sin tipos. | Cubierto por `ignore_missing_imports`; actualizar el paquete si persiste. | | `VERIFICACIÓN FALLIDA` en el paso 10 | Documentación vacía o renombrada. | Completar el documento; `REQUIRED_DOCS` está en `scripts/verify.sh`. | | El hook no se ejecuta | `core.hooksPath` apunta a otro sitio. | `git config --get core.hooksPath`; reinstalar con `HECATE_HOOKS_DIR=...`. | ## 8. Qué NO se verifica aquí - **Rendimiento**: la latencia real depende del hardware; se mide con `docs/benchmarks/methodology.md`, no en `verify.sh`. - **Seguridad de dependencias**: ejecuta `cargo audit` / `pip-audit` aparte. - **Interoperabilidad entre máquinas**: los tests cubren el protocolo en loopback; una prueba entre dos equipos es manual (y recomendable antes de una release). - **Calidad del modelo**: los fragmentos se inicializan con una semilla. La verificación comprueba que las matemáticas y el contrato son correctos, no que el modelo sea bueno.