jfastframework
Documentación English PyPI GitHub

Leer un proyecto de vuelta

jfast inspect     # qué hay aquí
jfast analyze     # qué está mal
jfast graph       # qué depende de qué
jfast check       # todo lo anterior, más el resto, con un solo exit code

check es una línea de tu pipeline, no el pipeline: no corre ningún linter, ningún type checker ni ningún test — ver Qué NO chequea check.

Tres comandos que responden lo que un mantenedor pregunta de verdad en el mes seis, y que la CLI no sabía responder: era muy buena en los primeros diez minutos de un proyecto y se quedaba muda después.


El hueco que cierran

jfast describe responde "qué es este servicio" construyendo la app. Resuelve el grafo de plugins, importa cada plugin y lee la configuración viva. Esa respuesta es la autoritativa y tiene dos problemas.

No está disponible justo cuando la quieres. Una dependencia que falta, un error de sintaxis, un refactor a medias: los momentos en que más necesitas saber qué tienes enfrente son los momentos en que describe no puede correr.

Y no dice nada de los módulos. Genera dos y ninguno aparece en su salida:

jfast new module invoice && jfast new module order
jfast describe --json | grep -c invoice
0

El framework conocía el layout del módulo, las reglas de capas y el cableado, y no tenía forma de contarte nada de eso. Así que cada agente que tocaba un proyecto terminaba en grep, y cada persona también.

Estos tres leen el sistema de archivos. Un poco menos autoritativos, siempre disponibles, y saben qué es un módulo.


jfast inspect

  shop 0.1.0 (local)

  modules (2)
    invoice  layered/api   /invoices
    order    layered/api   /orders

  plugins      observability, metrics, database
  shared       2 files
  migrations   0
  contract     contracts.toml
  frontend     -

  ✓ nothing to report

Una pantalla. El comando para correr primero en un servicio desconocido, y el que un agente debería correr antes de editar nada.

jfast inspect module order    # un módulo en detalle
jfast inspect --json          # lo mismo, como datos
jfast inspect --path ../shop  # desde afuera

inspect module agrega la lista de archivos, los paquetes que importa, las tablas que declara, y si tiene tests y README.

Nunca importa tu código. Todo sale de parsear la fuente, así que funciona en un proyecto cuyas dependencias no están instaladas y en uno que hoy no arranca.


jfast analyze

Todo lo estructuralmente mal, lo peor primero, cada cosa con el arreglo en su propia línea:

  CRITICAL
    module-cycle: import cycle: invoice -> order -> invoice
      Modules in a cycle are one module with folders between them: neither can
      be extracted into a service, and a change to one breaks the other in a
      way no test covers. Keep the query in one direction, through the other
      module's public.py, and turn the other direction into an event.

  HIGH
    modules/ghost/  module-unregistered: module 'ghost' declares routes but
                    main.py never imports it
      The endpoints do not exist at runtime. Nothing fails: the tests in the
      module pass, the server starts, and the route 404s.

Qué revisa

Código Severidad Qué atrapa
module-cycle critical Dos módulos que se importan entre sí. Ninguno se va a poder extraer nunca.
module-unregistered high Un módulo con rutas que main.py nunca importa. Los endpoints sencillamente no existen.
route-conflict high Dos routers en un mismo prefijo. Gana el que se registra primero; las rutas del otro quedan inalcanzables y FastAPI no avisa.
shared-imports-module high shared/ volviendo hacia un módulo, lo que convierte el grafo de dependencias en un círculo.
plugin-unknown high Un plugin habilitado en jfast.toml que nada provee. La app se niega a arrancar.
contract-governs-nothing high Una capa de contracts.toml cuyos paths no matchean ningún archivo acá. Sus reglas no aplican a nada, y contracts check pasa sin hacer cumplir nada.
module-no-migration medium Un módulo cuya tabla ninguna revisión crea. Falla en la primera consulta y en ningún lado antes.
cross-module-import medium Un módulo importando a otro por fuera de su public.py. Importar modules/<otro>/public.py es como los módulos deben hablarse y no se reporta acá; contracts check decide si está declarado.
code-outside-module low Un .py en la raíz que no pertenece a nada.

Por qué la lista es corta

Cada check es decidible desde el texto de la fuente. Nada de esto adivina.

Es un límite deliberado y cuesta cobertura real: detección de N+1, código muerto y dependencias sin usar son cosas que una pasada más difusa podría reportar, y todas cosas en las que a veces se equivocaría. Un checker que acierta nueve de cada diez veces queda silenciado después del segundo falso positivo, y con él se van los hallazgos verdaderos. Diez checks que nadie discute valen más que treinta que nadie corre.

module-no-migration es la forma que esa regla toma en la práctica. Solo se dispara cuando ya hay revisiones y la tabla de ese módulo no está en ninguna. Sin migraciones, el proyecto simplemente no llegó a ese paso, que es como se ve uno recién generado, y recibir a un proyecto nuevo con un hallazgo es la forma de enseñarle a la gente a ignorar la herramienta.

Contra contracts check

El contrato aplica las reglas que declaraste dentro de un archivo: qué capa puede importar qué, qué llamadas están prohibidas, qué no puede bloquear. analyze reporta sobre la forma del proyecto entre archivos. Corre los dos; fallan por razones distintas y con exit codes distintos.

analyze no corre contracts check, y contract-governs-nothing no es una excepción a eso. Lee una sola cosa de contracts.toml: si las capas matchean algún archivo. En qué layout están los archivos y si el contrato lo describe son hechos sobre cómo están acomodados los archivos, decidibles sin abrir ninguno: la pregunta propia de este comando. Lo que una capa permite adentro de un archivo sigue siendo de contracts check, y reemitir sus hallazgos acá le daría a un mismo reporte dos dueños, dos severidades y dos remedios, que es la forma en que dos comandos terminan contradiciéndose.

Quedarse callado era la opción peor, y es lo que esto reemplaza. En un servicio generado con el contrato layered y después llenado con módulos hexagonales, contracts check fallaba con tres violaciones layer-unmatched, jfast next reportaba contracts check fails (3 violations) — y jfast analyze imprimía ✓ no findings sobre el mismo directorio. El hallazgo se produce llamando a check_coverage, la función del checker, así que los dos comandos reportan ahora el mismo conteo y las mismas frases.

jfast analyze --fail-on critical   # solo lo peor
jfast analyze --fail-on never      # reporta, nunca falla
jfast analyze --json               # conteos y hallazgos, como datos

Por defecto es --fail-on high.


jfast graph

  invoice
    └─ (no module dependencies)
  order
    └─ invoice

jfast workspace graph dibuja servicios. Este dibuja los módulos dentro de uno, que es el grafo que decide si un módulo se va a poder extraer alguna vez: un módulo que nadie importa es un servicio esperando a nacer, y un ciclo son dos módulos que nunca van a ser ninguna de las dos cosas.

jfast graph --module billing        # solo sus aristas
jfast graph --format mermaid        # para pegar en un README
jfast graph --format dot            # para graphviz
jfast graph --format json

jfast check

Todos los checks estáticos que tiene este framework, en un comando y un solo exit code.

Todos los checks de esta página ya existían. Lo que no existía era una sola cosa que correr, así que CI corría tres, un agente corría el que se acordaba, y los dos que nadie cableó no corrían nunca.

No corre tu linter, tu type checker ni tus tests, y cada corrida lo dice en sus últimas dos líneas. Leé Qué NO chequea check antes de reemplazar algo con esto.

  shop

  ✓ config      pass   shop (local)
  ✓ plugins     pass   3 enabled, 20 installed
  ✗ analyze     fail   high 1
  ✗ contracts   fail   high 1
  ✓ migrations  pass   0 revisions read, no database
  ✓ deploy      pass   compose renders, 2 services

  ANALYZE  (validation failure, exit 1)
  HIGH
    modules/order/  module-unregistered: module 'order' declares routes but
                    main.py never imports it
      The endpoints do not exist at runtime. Nothing fails: the tests in the
      module pass, the server starts, and the route 404s.

  CONTRACTS  (contract violation, exit 5)
  HIGH
    modules/invoice/service.py:43  forbid-call: print() is not allowed here
      Use the structured logger; print output has no request id and no level.

  4 pass, 2 fail in 0.13s
  exit 5  (contract violation)

  not checked here: lint, formatting, types, tests
  ruff check .  ruff format --check .  mypy .  pytest
jfast check                          # todo, salida para humanos
jfast check --json                   # todo, como datos
jfast check --ci                     # estricto: cualquier hallazgo falla, y también cualquier skip
jfast check --only contracts,analyze # un subconjunto
jfast check --fail-on critical       # el mismo umbral que `jfast analyze`
jfast check --multitenant-ready      # en lugar de la batería: qué rompería pasar a multitenant
jfast check --path ../shop           # desde afuera

Contra doctor y workspace validate

doctor pregunta algo sobre tu máquina: si la configuración resuelve y si cada plugin habilitado importa en este intérprete. Su respuesta cambia cuando cambias de virtualenv y nunca cuando cambias una línea de código.

check pregunta algo sobre el repositorio: si lo que está commiteado es consistente consigo mismo. Corre las dos preguntas de doctor como sus dos primeros checks, porque una instalación rota vuelve mentira toda respuesta posterior — pero doctor se queda, porque cuando estás depurando tu propia laptop quieres la respuesta de dos segundos y no la batería entera.

jfast workspace validate lee un solo archivo, el grafo de recursos del workspace, y su alcance es un workspace y no un servicio. check lo corre dentro de deploy.

En CI va solo check.

Los siete checks

Cada uno lleva el nombre del comando que ya lo corría. No hay un segundo vocabulario, y el nombre es lo que recibe --only.

Check Qué corre Exit code al fallar
config JFastConfig.load, la misma carga que hace doctor 2
plugins registry.discover(), registry.build() y la lista de no importables 3
analyze jfast analyze: la estructura entre archivos 1
tenancy ajustes de tenant que se contradicen entre sí o con el código (multitenancy) 1
contracts jfast contracts check: las reglas que declaraste 5
migrations jfast migration check, en estático: sin base de datos 4
deploy workspace.validate() y renderizar docker-compose 1

Nada de esto levanta un contenedor, abre un socket ni habla con una base. Eso es lo que hace al comando seguro en un hook de pre-commit, y también es su límite: deploy prueba que el compose se puede generar y que es consistente consigo mismo, no que las imágenes bajen.

Avisos: el entorno sobrescribe jfast.toml

Una cosa que check reporta sin nunca fallar por ella. Para un ajuste que es del entorno (Quién gana), una variable puesta en este proceso que no coincide con jfast.toml aparece bajo config, con archivo, línea y variable, las credenciales enmascaradas:

  ✓ config      pass   shop (local)  (1 environment override)
  ...
  CONFIG  the environment overrides jfast.toml (reported, never a failure)
    jfast.toml:52  [plugin.mail] backend = 'console' is overridden by JFAST_MAIL_BACKEND='smtp' from the environment

En --json son los notices del check (severidad medium, código environment-overrides-file) y el conteo notices de arriba. Nunca ponen un status ni un exit code, --ci incluido: toda otra respuesta aquí es la misma en cualquier máquina, y esta es sobre la máquina -- corrido con el entorno del despliegue lista exactamente lo que lista el WARNING de arranque; en una laptop, lo de la laptop. Un default del archivo que el despliegue reemplaza suele ser el diseño funcionando, y fallar ahí empujaría a un equipo a borrar el default o a copiar los valores de producción al CI.

Qué NO chequea check

jfast check no corre ningún linter, ningún formateador, ningún type checker ni ningún test. Nada en su salida cambia cuando cualquiera de ellos falla:

# un import sin usar, un archivo mal formateado, un str asignado a un int
# y un test que falla, todo junto
ruff check .            # 17 errors
ruff format --check .   # 4 files would be reformatted
mypy .                  # 2 errors
pytest                  # 1 failed

jfast check             # 6 pass — idéntico byte a byte al proyecto limpio
jfast check --ci        # exit 0

Y no es un bug que se arregle corriéndolos. pytest ejecuta tu código por un tiempo no acotado contra lo que decida levantar un fixture, y mypy en un árbol cuyas dependencias no están instaladas reporta imports faltantes que tu propia configuración habría silenciado — cualquiera de los dos adentro de este comando convierte un hook de pre-commit en un build, y un checker que fabrica hallazgos queda silenciado en una semana. check responde si el repositorio es consistente consigo mismo; esos cuatro responden si el código está bien. Dos preguntas, dos comandos.

Así que el costo se paga a la vista. Cada corrida termina con los cuatro que te deja a vos, pase o falle:

  not checked here: lint, formatting, types, tests
  ruff check .  ruff format --check .  mypy .  pytest

y --json lleva lo mismo bajo not_covered, porque un script que lee "ok": true no puede leer un pie de página:

{
  "ok": true,
  "not_covered": [
    {"what": "lint", "catches": "unused imports, undefined names, unreachable code",
     "command": "ruff check ."},
    {"what": "formatting", "catches": "a diff nobody agreed to review",
     "command": "ruff format --check ."},
    {"what": "types", "catches": "a str where an int was declared", "command": "mypy ."},
    {"what": "tests", "catches": "whether any of it works", "command": "pytest"}
  ]
}

La línea que va en CI es la línea entera:

jfast check --ci && ruff check . && ruff format --check . && mypy . && pytest

Un exit code para muchos checks

Seis checks, un número. Cuando fallan varios, el código sale del fallo que invalida la mayor parte del reporte, no de la severidad más alta:

2 config  >  3 plugins  >  4 migrations  >  5 contracts  >  1 analyze/deploy

Un jfast.toml que no parsea vuelve conjetura a todas las demás respuestas, así que gana. Un plugin que no importa sigue, por la misma razón. Migrations le gana a contracts porque un esquema con el que el código no está de acuerdo falla en producción en la primera query y es el resultado que no puedes dejar pasar a un deploy; una violación de contrato, en cambio, se ve en el diff y falla sin daño. Contract le gana a analyze porque un contrato es una regla que alguien escribió, mientras que los hallazgos de analyze son la clase más amplia y menos específica: van al final, para que cualquier código más informativo le gane.

El número único es un resumen, nunca la respuesta completa. --json lleva codes — el código de cada check que falló — para que un script no tenga que deducir los otros cinco:

{
  "ok": false,
  "exit_code": 5,
  "exit_meaning": "contract violation",
  "codes": [1, 5],
  "failed": ["analyze", "contracts"],
  "skipped": [],
  "complete": true
}

Un skip no es un pass

Un check que no pudo correr se reporta como skip, con el motivo, en los dos modos de salida:

  · contracts   skip   no contracts.toml in /tmp/shop (jfast contracts init)

  4 pass, 1 fail, 1 skip in 0.07s
  · skipped: contracts -- not a pass

En --json es status: "skip" con un reason no nulo, su nombre está en skipped y no en passed, y complete es false. Una batería que reporta verde en silencio sobre un check que no corrió es peor que no tener batería: convierte "no miré" en "miré y estaba bien".

Bajo --ci, un skip falla, con exit 3 — error de entorno. Eso es lo que significa un skip en CI: al runner le faltaba algo que el check necesitaba, y el pipeline no está revisando lo que crees que revisa. En local un skip es normal e informativo; en CI es un agujero.

--ci --allow-skips es la única forma de pasar, y es explícita a propósito: un equipo que de verdad no puede proveer algo escribe el flag en el pipeline, donde un revisor lo ve, en vez de no enterarse nunca del agujero.

Un skip solo decide el exit code cuando no falló nada más. Una corrida que además viola su contrato sale con 5, no con 3: hay un defecto concreto que reportar, y culpar al runner por él sería incorrecto.

Cuánto cuesta

Sobre un servicio recién generado, los checks tardan 120–185 ms, y el proceso completo 0.65–0.9 s de reloj: casi toda la diferencia es el arranque y los imports del propio Python, que cualquier herramienta en un hook de pre-commit paga igual.

Medido por check, sobre ese mismo servicio:

Check Costo
config ~1 ms
migrations ~3–12 ms
contracts ~30 ms
plugins ~45–85 ms
analyze ~60–100 ms (incluye plugins)
deploy ~60–90 ms (incluye plugins)

plugins es el lento y arrastra al resto: registry.discover() importa cada plugin instalado. Corre una vez por invocación y su resultado se le pasa a analyze — que necesita el conjunto instalado para reportar plugin-unknown — y a deploy, en vez de recalcularlo. Todo lo demás es parseo de AST, que escala con la cantidad de archivos .py y no con el tamaño de las dependencias del proyecto.

--only contracts,migrations es el subconjunto que evita el descubrimiento de plugins por completo, en ~35–45 ms. Esa es la forma a poner en un hook de pre-commit si la corrida completa alguna vez deja de sentirse instantánea, con la batería completa igual en CI.

Una advertencia sobre migrations: sin base de datos lee todas las revisiones, no solo las pendientes — cuáles están aplicadas es un dato que solo tiene la base — y trata cada tabla como poblada, que es la lectura conservadora. En un repositorio con historia larga de migraciones, jfast migration check --dsn ... es la respuesta más acotada, y --only puede sacar este check de la batería.


Exit codes

Estándar en toda la CLI, documentados y con tests. Un script que tiene que leer inglés para saber por qué falló un comando es un script que se rompe cuando cambia la redacción.

Código Significa
0 éxito
1 fallo de validación: un check encontró algo mal
2 error de configuración: falta un archivo o no parsea
3 error de entorno: Docker, un contenedor, una base
4 riesgo o fallo de migración
5 violación de contrato
6 error de entrada del usuario
7 error de compatibilidad o versión

Distinguir 1 de 5 es lo que le permite a CI decir por qué falló sin parsear texto:

jfast analyze || case $? in
  1) echo "structure" ;;
  2) echo "config" ;;
esac

Son parte de la API pública. Un código nunca cambia de significado; un modo de fallo nuevo recibe un número nuevo.


Para un agente

jfast inspect --json          # what exists
jfast graph --format json     # what depends on what
jfast analyze --json          # what is wrong, with the fix in `why`
jfast contracts show --json   # the rules
jfast contracts check --json  # the violations
jfast check --json            # todos los checks estáticos, con un solo exit code

jfast check --json es la última llamada de jfast antes de reportar un cambio como terminado: es la única que falla cuando algo no llegó a revisarse, así que "pasó" no puede significar "no corrió". Lee skipped y complete antes de leer ok — y lee not_covered, que nombra el linter, el formateador, el type checker y los tests que no corrió. "ok": true no es "el cambio está terminado"; es "el repositorio es consistente consigo mismo".

Cinco comandos, sin leer fuente, sin adivinar convenciones. El campo why de cada hallazgo es la parte que importa: a un agente al que le das una violación sin remedio tiende a satisfacer al checker en vez de arreglar el diseño.