Cómo contribuir: estilo, pruebas y commits
Esta página ordena lo que hace falta saber para que un cambio se acepte a la primera. El procedimiento completo, en inglés, está en CONTRIBUTING.md, y las reglas completas en AGENTS.md y server/AGENTS.md. Aquí no se repiten enteros: se explica el porqué de las que más tropiezos dan. Si algo de aquí es incorrecto o confuso, avísanos: sería una buena primera contribución.
El procedimiento
Sección titulada «El procedimiento»- Abre un issue primero si el cambio es grande o altera cómo se comporta algo, para acordarlo antes de que gastes tiempo. Para un arreglo pequeño, ve directo al pull request.
- Haz un fork y una rama desde
main. - Haz el cambio y ejecuta las comprobaciones hasta que pasen (la app, el servidor).
- Abre el pull request y explica por qué hace falta, no solo qué hace.
- Pasa la revisión. Es posible que pidamos cambios.
Un pull request por tema. Quien participa acepta el código de conducta. Para un fallo de seguridad no abras un issue público: sigue SECURITY.md.
Si trabajas con un asistente de IA, se acepta el resultado si quien abre el pull request entiende el cambio y responde de él. AGENTS.md lleva las instrucciones para el asistente.
Idioma, commits y licencia
Sección titulada «Idioma, commits y licencia»- Todo el repositorio está en inglés: código, identificadores, comentarios, mensajes de commit, README, issues y los textos de la interfaz en su idioma original. La excepción es
docs/, que está en español por ahora. Una pregunta en español o en inglés es bienvenida. - Commits: en inglés, en imperativo o descriptivos, como el historial que ya hay. Explican el porqué cuando no es evidente.
- Comentarios en el código: cortos, y dicen por qué, nunca repiten lo que hace el código.
- Licencia de lo que aportas: la de la parte que tocas. AGPL-3.0 o posterior para la app y el servidor, y MIT para
spec/. No hay que firmar ningún acuerdo.
Las reglas que más afectan a un cambio
Sección titulada «Las reglas que más afectan a un cambio»Cada una lleva su razón. Entenderla ayuda más que memorizarla.
- Toda escritura en el catálogo es una operación. Construye una lista de cambios y llama a
ops::recorddentro de una transacción; nunca hagasINSERT,UPDATEoDELETEdirectamente en las tablas del catálogo. Por qué: los dispositivos se intercambian operaciones, no filas, y eso es lo que hace que la sincronización converja. La única excepción essync::merge, que aplica las que llegan de otros dispositivos. - La lógica del dominio va en Rust (
crates/domain), no en la interfaz. React llama a comandos de Tauri y no ejecuta SQL. - Un campo o una entidad nuevos tocan varios sitios a la vez. Está explicado más abajo, paso a paso. Por qué: son dos implementaciones (Rust y PHP) de un mismo formato; si olvidas una, dejan de ponerse de acuerdo.
- Las reglas de una operación se definen en
spec/y se cubren con casos de conformidad que ejecutan tanto la implementación en Rust como la de PHP. Un caso de conformidad es una operación de ejemplo con el resultado que toda implementación debe dar. Por qué: es lo que impide que las dos se separen, y lo que permite que alguien escriba un cliente o un servidor compatible. Un cambio de regla va primero aspec/, con sus casos, y luego a las dos implementaciones. - La sincronización tiene que converger. Un cambio en cómo se aplican las operaciones necesita una prueba en
sync.rsque simule varios dispositivos y compruebe, conassert_converged, que acaban iguales. - Quién recibe una biblioteca se prueba primero contra un par hostil. Para cualquier regla que decida qué dispositivo o cuenta recibe, conserva o pierde datos de una biblioteca (emparejar, compartir, retirar el acceso, la tabla de acceso de la nube), escribe antes del arreglo la prueba del ataque y comprueba que falla: una app modificada, un par que conoce los ids, un aviso falsificado. Por qué: una revisión posterior es la segunda línea de defensa, no la primera.
- El servidor nunca se fía de un id del cliente. Los ids de biblioteca, operación y dispositivo de una petición se comparan, nunca sirven para buscar ni para decidir el acceso. El acceso sale solo de la cuenta, de la tabla de acceso y de los datos propios del servidor.
- Los servicios de fuera son opcionales y van detrás de interfaces. La app tiene que seguir funcionando cuando no están.
- Las pruebas no usan la red. Analizan respuestas grabadas en
app/src-tauri/tests/fixtures/. Las que necesitan red son#[ignore]y se llamanlive_*. - En Rust, no mantengas el acceso a la base abierto durante un
.await. ElMutexGuardde la base no debe cruzar una espera: lee lo que necesites en un bloque, suelta el guardián y espera después (miracommands::lookup_isbn). - Ningún texto de la interfaz se escribe a mano. Usa claves de i18n y añade la traducción en inglés y en español.
- Un cambio que una persona notaría necesita su línea en
app/src/whats-new.json. Una línea en español y otra en inglés, bajonext, escrita para personas (nada de internos ni de refactorizaciones). Si el cambio modifica lo que sale del dispositivo, lleva además un interruptor ligado al ajuste que lo controla.npm run check:whats-newfalla si la versión de la app no tiene entrada. - Ningún secreto en el repositorio: ni claves, ni contraseñas, ni tokens, ni de ejemplo. El repositorio es público y su historial tiene que estar limpio.
Reglas del servidor
Sección titulada «Reglas del servidor»Además de las anteriores, en server/ (más en server/AGENTS.md):
- Si Symfony ya tiene el mecanismo, usa el de Symfony. Cuerpos de petición como DTO con
#[MapRequestPayload], límites de peticiones con#[RateLimit], el usuario con#[CurrentUser], el reloj conClockInterface. - Un método devuelve su tipo o lanza: nunca
Foo|stringni un valor nulo para indicar un fallo. Y excepciones propias, no\RuntimeExceptiona secas. - Cada clase va en una carpeta que nombra su papel (
Controller/,Entity/,Dto/,Enum/…). Ninguna en la raíz desrc/salvoKernel.php. make checktiene que pasar, con PHPStan en el nivel máximo y sin archivo de base.
Añadir un campo o una entidad, paso a paso
Sección titulada «Añadir un campo o una entidad, paso a paso»Es el cambio que más veces se hace y donde más fácil es olvidar algo. Un campo nuevo (por ejemplo, un dato más de los juegos de mesa) pasa por estos sitios, juntos, en el mismo cambio:
- Una migración de SQLite en
app/src-tauri/migrations/: un archivoNNNN_nombre.sqlcon el número siguiente al último, y su líneaM::up(include_str!(…))enapp/src-tauri/src/db.rs. Sube tambiénschemaVersionenspec/sync.json: una prueba comprueba que coincide con el número de migraciones, porque dos dispositivos con versiones distintas no sincronizan. La app aplica las migraciones al arrancar, y a las bases de los usuarios que ya existen también. Por qué: nadie va a empezar de cero; el esquema tiene que poder evolucionar sobre datos reales. crates/domain/src/schema.rs: añade el campo a la lista de campos de su entidad confield("nombre", FieldType::…, obligatorio). Aquí se declara qué tipo de dato es y si es obligatorio.spec/operation.schema.json: el mismo campo, en el JSON Schema. Por qué: es lo que ve cualquier otro cliente o servidor, y el que validanpm run check:spec.- Casos de conformidad en
spec/conformance/: una operación válida con el campo nuevo, y las inválidas que correspondan (tipo equivocado, un obligatorio anull…), enoperation-validation.json. Si el campo cambia cómo se fusionan operaciones o los permisos, añade también un caso enmerge.jsono enpermissions.json. server/src/Enum/CatalogEntity.php: el espejo en PHP deschema.rs. Las pruebas del servidor ejecutan los mismos casos de conformidad y avisan de la mayoría de los desajustes.- La clase Doctrine de esa entidad, en
server/src/Entity/Catalog/, con su columna, y una migración del servidor (make migrationy luegomake migrate; quita los comentarios que genera y rellenagetDescription()). Por qué: el servidor guarda el catálogo en PostgreSQL con Doctrine, además de describirlo enCatalogEntity.
Después, lo que usa el campo: leerlo y escribirlo en app/src-tauri/src/catalog.rs (o donde toque), exponerlo en app/src/lib/api.ts y en el formulario, y los textos con claves de i18n en es.json y en en.json.
Para una entidad nueva (una tabla nueva), todo lo anterior y además:
- el caso nuevo en el
enum Entitydeschema.rsy enCatalogEntitydel servidor; - su sitio en las listas de entidades de la app, que tienen un tamaño fijo en el código: el orden de sincronización (
ORDERensync.rs, padres antes que hijos), el de las copias de seguridad (CONTENTSenbackup.rs) y el de borrado de una biblioteca (LIBRARY_CONTENTS_CHILDREN_FIRSTenops.rs, hijos antes que padres); - una prueba en
sync.rsde que converge entre dispositivos.
Comprueba al final con cargo test, npm run check:spec y make check.
Mejorar esta documentación
Sección titulada «Mejorar esta documentación»Esta guía está hecha con Starlight, un tema de documentación de Astro, a partir del Markdown de site/src/content/docs/. Es HTML estático: no se ejecuta nada en un servidor. Para verla mientras la editas:
cd sitenpm cinpm run dev # http://localhost:4321, se recarga al guardarnpm run build && npm run check:links # lo que comprueba la CICada página se escribe primero en español (es/) y después en inglés (en/), con la misma ruta. Los enlaces entre páginas son ../página/. Los detalles están en site/README.md. La carpeta docs/ no es esta guía: es el diario de desarrollo, en español.
Antes de abrir el pull request
Sección titulada «Antes de abrir el pull request»- Ejecuta las comprobaciones que correspondan a lo que tocaste.
- Pasa por la lista: ¿lleva prueba? ¿una línea de novedades, si se nota? ¿textos con i18n en los dos idiomas? ¿algo de
docs/que haya que actualizar? - Si cambia una decisión de arquitectura, se apunta en el registro de decisiones, con su estado. Una decisión que se cambia en silencio es la manera de que nadie sepa, tres meses después, qué se decidió ni por qué.
Los nombres que se repiten en el código (obra, ejemplar, miembro…) están en el glosario: mira allí antes de poner un nombre nuevo.
© 2026 Kosmotheka
Kosmotheka es software libre: la aplicación y el servidor, bajo la AGPL-3.0-or-later; la especificación de operaciones, bajo la MIT. El nombre y el logotipo no entran en la licencia.
kosmotheka.com · Política de marca · Política de privacidad · info@kosmotheka.com