# SCM y modularización del workspace Este documento fija dónde vive cada crate, qué garantía ofrece cada uno y cómo se asciende un crate de experimental a core. Es la referencia cuando alguien se pregunta «¿puedo cambiar esta API?». ## Estructura del repositorio ```text HECATE/ ├── Cargo.toml # workspace: members, versión única, lints ├── justfile # superficie de tareas (just check / audit / build) ├── pyproject.toml # paquete Python; la versión viene de Cargo.toml ├── llms.txt # descripción canónica para LLMs ├── .well-known/ai.txt # manifiesto legible por máquina ├── CITATION.cff # metadatos de citación ├── CHANGELOG.md # Keep a Changelog ├── CONTRIBUTING.md # ramas, commits y flujo de PR ├── proto/hecate.proto # contrato único (DataPlane + Telomerase) ├── crates/ │ ├── core/ # API congelada para v1.0 │ │ ├── hecate-node/ # binario CLI (serve, plan, zoo, guard, bench…) │ │ ├── mesh-proto/ # tipos generados del .proto │ │ ├── mesh-discovery/ # mDNS, registro de nodos │ │ ├── mesh-profiler/ # perfilado real del nodo │ │ ├── mesh-router/ # enrutamiento y failover │ │ ├── mesh-scheduler/ # reparto de capas, model zoo, selector │ │ ├── mesh-worker-net/ # framing, pool, clientes gRPC │ │ └── mesh-orchestrator/ # clúster, crecimiento, host_guard │ └── experimental/ # API inestable (hoy vacío, ver su README) ├── python/hecate/ # plano de datos (PyTorch, tokenizador, LoRA) ├── tests/ │ ├── rust/ # integración de los dos planos │ └── python/ ├── scripts/ # verificación y herramientas (sin CI externa) └── docs/ # arquitectura, SCM, auditorías, benchmarks ``` ## Por qué `core` y `experimental` El repositorio tiene nueve paquetes y un solo mantenedor. Sin una separación explícita, la pregunta «¿puedo cambiar esta firma?» no tiene respuesta, y en la práctica se acaba tratando todo como estable, lo que congela también lo que aún se está tanteando. La separación resuelve eso de la forma más barata posible: **la ubicación es el estado**. No hay una lista de «crates estables» en un documento que se pueda desincronizar del árbol; hay dos directorios. | Grupo | Qué ofrece | Qué se exige para cambiarlo | | --- | --- | --- | | `core` | API congelada para v1.0: lo que un tercero puede compilar sin romperse en un minor | ADR en `docs/adr/` + revisión en PR | | `experimental` | Nada: la API puede cambiar entre commits menores | `just check` en verde; misma calidad, distinta promesa | Importante: **experimental no significa descuidado**. Un crate de `experimental` pasa exactamente los mismos `just check`, `scripts/verify.sh` y lints que uno de `core`. La diferencia es la promesa de compatibilidad, no el nivel de exigencia. ## Promoción de `experimental` a `core` Los tres criterios son acumulativos. 1. **Tests en verde de sus caminos principales**, e imprescindiblemente: - concurrencia: si lanza tareas, cada `tokio::spawn` tiene camino de cancelación; - apagado: si abre sockets, hay test de que se cierran; - error: los caminos de fallo devuelven error tipado, no `panic!`. 2. **Auditoría de hardware real.** Medido con `hecate-node profile` en al menos un nodo físico, con la procedencia de cada campo declarada en `docs/benchmarks/hardware-real.md`. Un crate ejercitado sólo contra mocks **no** se promociona: el criterio existe porque el perfilador ya se equivocó una vez clasificando un nodo a partir de números de `debug`. 3. **ADR aprobado** en `docs/adr/` que fije la API pública (tipos, traits, errores) y justifique por qué merece la garantía de compatibilidad. El trámite es mecánico: ```bash git mv crates/experimental/ crates/core/ # actualizar `members` en el Cargo.toml del workspace just check ``` No hay que tocar ninguna lista más: si algo funcionaba por la ruta antigua, es que estaba mal referenciado y el compilador lo dirá. ## Versionado La versión se declara **una sola vez**, en `Cargo.toml`: ```toml [workspace.package] version = "0.6.0" ``` Los ocho crates la heredan con `version.workspace = true`. `pyproject.toml` **no** tiene versión literal: la lee del mismo fichero con la fuente `regex` de hatchling. ```toml [project] dynamic = ["version"] [tool.hatch.version] source = "regex" path = "Cargo.toml" pattern = '(?m)^version = "(?P[^"]+)"' ``` ### Por qué no se usa maturin La técnica habitual para tener una sola versión entre Rust y Python es `version-source` de maturin. **Aquí no aplica**: maturin empaqueta extensiones de Rust que se importan desde Python, y HECATE no tiene ninguna. El plano de control es un binario independiente y el plano de datos es Python puro; la frontera entre ambos es gRPC, no PyO3. Por eso el backend es hatchling (`[build-system] requires = ["hatchling"]`) y el equivalente a `version-source` es su fuente `regex`, que apunta al mismo `Cargo.toml`. Consecuencia práctica: si algún día se expone Rust a Python con PyO3, el backend pasa a maturin y esta sección cambia. Mientras no haya PyO3, añadir maturin sólo añadiría un requisito de compilación que hoy no hace falta. ### Deriva de versión `scripts/check_versions.py` comprueba los cuatro sitios que deben contar la misma historia: | Sitio | Qué se comprueba | | --- | --- | | `Cargo.toml` | es la fuente; se lee con `tomllib` | | `pyproject.toml` | `dynamic = ["version"]` y el patrón captura la versión correcta | | `CHANGELOG.md` | existe la sección `## []` | | `CITATION.cff` | `version:` coincide | Se ejecuta en `just audit` y en `scripts/verify.sh`, así que una versión a medias falla antes de salir del equipo. ### Etiquetas Cada release lleva una etiqueta anotada, no ligera: ```bash git tag -a v0.6.0 -m "v0.6.0 — consolidación: SCM, modularización y visibilidad" git push origin main --tags # publica commits y etiquetas en la organización ``` Una etiqueta ligera es sólo un puntero; la anotada guarda quién, cuándo y por qué, que es lo que hace falta cuando dentro de un año se pregunta qué había en esa release. ## Reglas de dependencia entre crates El grafo es unidireccional y no tiene ciclos: ```text hecate-node → mesh-orchestrator → mesh-scheduler → mesh-profiler ↘ mesh-router → mesh-scheduler ↘ mesh-discovery ↘ mesh-worker-net → mesh-proto ``` Reglas que se comprueban en revisión: - `mesh-proto` no depende de ningún otro crate del workspace: es el contrato. - `mesh-worker-net` no conoce el planificador ni el orquestador: mueve bytes. - `mesh-profiler` no abre sockets de nodos ajenos: mide el nodo local. - Ningún crate de `core` depende de uno de `experimental`. Si lo necesita, la dependencia está al revés y hay que promover el experimental primero. ## Superficie de tareas `justfile` es el único punto de entrada. `scripts/verify.sh` sigue siendo el guardián que ejecuta el hook `pre-push`, y `just audit` lo invoca. Las dos superficies no se solapan por capricho: `just` es para humanos (nombres cortos, valores por defecto), `verify.sh` es para el hook (sin dependencias, sin `just` instalado, ejecutable en una máquina recién clonada).