jfastframework
Documentación English PyPI GitHub

Changelog

Formato: Keep a Changelog. Versionado: SemVer con identificadores de pre-release según PEP 440. Mientras la API esté en pre-alpha, los servicios fijan la versión exacta (jfastframework==0.1.0a5); un pin de release compatible (~=) empieza a tener sentido en 0.2.

Renumeración

Las entradas de abajo estaban numeradas originalmente de 0.1.0 a 0.7.0. Esa numeración exageraba la madurez del código. En ese momento no se había publicado nada; el formato del archivo de workspace está por cambiar; el backend de cola de Redis no implementa el visibility timeout que documenta su propio contrato; los backends de RabbitMQ y Kafka nunca se corrieron contra un broker real.

Por eso el paquete vuelve a empezar en 0.1.0a1. La historia se conserva textual como log de desarrollo -- registra qué se construyó y cuándo -- pero esos números nunca fueron releases y nunca fueron instalables. pip install jfastframework no resuelve un pre-release sin --pre, así que la herramienta de packaging impone la advertencia en vez de una frase en un README.

La madurez a nivel de subsistema vive en STATUS.md, que es el archivo para leer antes de depender de cualquier parte de esto.

[Unreleased]

[0.1.0a12] - 2026-09-30

Lo que se encontró al construir un SaaS nuevo desde cero sobre 0.1.0a11, corregido. La suite no lo veía porque cada caso necesitaba el camino de un usuario nuevo: un campo único opcional, una imagen de producción, el frontend generado en un navegador, una subida, una migración corrida en el host, una llamada a un modelo desde un worker. Llegó con ello una adición: enums en --fields.

Corregido

Lo que encontró una mesa de ayuda construida desde cero sobre esta versión, en contenedores de producción, corregido antes de publicarla:

Agregado

[0.1.0a11] - 2026-09-30

Sale de construir dos servicios reales sobre 0.1.0a10, Cuadra y Dictamen, y de lo que tuvieron que rodear. El peor hallazgo rompía una regla que este proyecto no había escrito: los contratos le decían a un módulo que publicara un evento en vez de llamar a otro módulo, y en el stack por defecto ese evento no llegaba a ningún lado -- outbox.publish respondía 201 y la fila se reintentaba hasta morir. Así que este release trata sobre todo de que el camino recomendado sea el que funciona: eventos que llegan a un suscriptor sin broker, un worker que existe, cuentas con las que un SaaS puede abrir, un paso de un cliente a varios que es un comando y no una cacería, un deadline en cada llamada a algo que se puede colgar, y código generado que pasa sus propios controles.

Para cada cambio incompatible de abajo, corre jfast upgrade --check: lista los que aplican a tu proyecto, con archivo y línea, y el arreglo.

Cambios incompatibles

Agregado -- eventos y trabajo en segundo plano

Agregado -- telemetría

Agregado -- cuentas

Agregado -- un cliente hoy, varios mañana

Agregado -- deadlines, breakers y simulacros de falla

Agregado -- código generado que pasa sus propios controles

Agregado -- servicios en Go en un workspace de Python

"Contrato sí, framework no": el scaffold de Go recibe lo que un servicio en Go necesita para convivir con los de Python -- sigue siendo solo biblioteca estándar, todo middleware de net/http plano que envuelve tal cual un engine de Gin, Echo o Chi.

Agregado -- CI

Cambiado

Corregido

Rendimiento y escala, medidos

Cada número de aquí está en docs/scaling.md, nueva, con la máquina donde se midió (una laptop Apple M5 con otras suites corriendo) y el script que lo produce.

Sin hacer, y con nombre

[0.1.0a10] - 2026-09-29

Tres cosas que un proyecto deja atrás en su primer mes de uso real: una recuperación que mezclaba tenants, módulos que se hablaban por SQL crudo porque los contratos prohibían cualquier otra forma, y un modelo de tenants sin lugar para "cada cuenta es la suya".

Cambios incompatibles

Corregido

Rendimiento

Medido con ab contra un worker de uvicorn (tabla en docs/deploy.md#rendimiento): un servicio con auth, tenancy, métricas y logs pasó de 2,411 a 8,581 peticiones por segundo en el mismo endpoint; FastAPI con JWT y tenant hechos a mano da 9,494. JFast con sus plugins por defecto pasó de 4,228 a 12,443.

Agregado

Cambiado

[0.1.0a9] - 2026-09-28

Un 201 tiene que significar que la fila existe.

La sesión de la request confirmaba al desmontar la dependencia, y FastAPI corre el desmontaje de una dependencia con yield después de enviar la respuesta. Así que un commit fallido ya se había contestado como éxito, y un cliente que leía su propia escritura enseguida podía llegar antes que el commit. Todos los módulos generados conectaban la sesión de esa forma.

Las entradas de 0.1.0a6 a 0.1.0a8 solo están en inglés, en el CHANGELOG del repositorio.

Incompatible

Agregado

Agregado -- el resto de la historia de transacciones

Cambiado

Corregido

Corregido -- tablas del framework y autogenerate

Cambiado -- el CLI

Agregado -- lo que necesitó un servicio real

Salió de reconstruir un servicio de facturación (E-Cont) sobre el framework: cada una de estas piezas la había escrito él mismo, y cada una la tenía mal en el mismo lugar.

Agregado -- tareas recurrentes, retrasos en RabbitMQ, llamar a otros servicios

Corregido -- RabbitMQ

Agregado -- los frontends vienen en looks, y Nexora es el de por defecto

Cambiado -- modular por defecto

Sitio de documentación

Sin hacer, y nombrado

Tracing distribuido, y, en accounts, verificación de email, recuperación de contraseña y MFA. Los dos en PLAN-NEXT.md.

[0.1.0a8] - 2026-09-03

Traducción parcial: de 0.1.0a6 a 0.1.0a8 solo esta parte está en español. El registro completo está en el CHANGELOG en inglés.

Agregado

Cinco comandos que llevan a la CLI más allá de los primeros diez minutos de un proyecto. Cada uno responde algo que el framework ya podía responder y no respondía.

[0.1.0a5] - 2026-08-30

Veinte hallazgos de un segundo reporte externo, este contra 0.1.0a4. Nueve se verificaron a mano antes de tocar nada, dos resultaron peores de lo reportado, y dos se rechazaron — uno de ellos rechazado como defecto y resuelto como problema de nombre.

El eje es más angosto que el de la versión anterior. 0.1.0a4 construyó siete comandos de diagnóstico en un día y tres mienten: migration check aprobaba tres sentencias peligrosas, contracts check daba verde sobre un contrato que no gobernaba un solo archivo, y analyze no reportaba nada en ese mismo proyecto mientras jfast next decía que el check fallaba. Un diagnóstico que nadie cree se ignora; un diagnóstico que la gente cree y que miente sale más caro que el defecto que venía a encontrar, porque es la razón por la que dejaron de leer la migración a mano.

De ahí salieron dos reglas, y las dos viajan como test y no como nota:

  1. Ningún diagnóstico se da por bueno sin un caso donde debe fallar y falla.
  2. El test afirma la mitad que puede romperse, por la ruta que toma un usuario: no el string anunciado cuando lo que falla es el mapping de puertos, no contracts init cuando la gente corre jfast new service, no el middleware aislado cuando corre detrás de uvicorn.

1.287 tests pasan y 7 se saltan: cinco quieren un Redis real, dos son un mismo comando alcanzable por dos caminos a propósito.

Rompe

Un archivo que ayer matcheaba una capa y hoy no matchea ninguna no está gobernado por nada, forbid_packages incluido. Donde el alcance era intencional, ensanchá el patrón:

diff -paths = ["modules/*/repository.py"] +paths = ["modules/**/repository.py"]

** cruza directorios a propósito y lo dice, y **/ matchea también cero segmentos, así que modules/**/http.py sigue cubriendo modules/http.py. jfast upgrade --check lista bajo layer-globs-narrowed los archivos que cambiaron de mano, calculados contra tu árbol y no deducidos de los patrones: un contrato escrito todo con ** no cambia y no recibe reporte.

diff -created = datetime.now() +from jfastframework.time import now +created = now()

datetime.now(UTC) vale igual: la regla es sobre el argumento que falta, no sobre qué módulo usás. Un splat *args o **kwargs nunca se reporta, porque la sintaxis no puede decir si la zona va ahí adentro. Para postergar la regla entera, una línea en contracts.toml:

toml [rules.async_safety] naive_datetime = false

Eso deja prendida la mitad de async-blocking, que es la que ya tenías.

toml [plugin.database] session_timezone = "" # no toca el TimeZone del servidor

Va en el paquete de arranque y no como un SET después de conectar, a propósito: una conexión pooleada se toma a mitad de su vida, así que un DISCARD ALL, un RESET ALL o un reset de servidor de pgbouncer deshace una sentencia que corrió una vez y la sesión vuelve a la zona del servidor sin dejar nada en los logs.

Para proyectos generados antes de esto, la consecuencia es una falla nueva: contracts check reporta layer-unmatched (exit 5) para una capa que no matchea ningún archivo mientras archivos que debería haber reclamado quedan sin dueño. Apuntá la capa a las carpetas que tus módulos usan de verdad —

diff [layers.storage] -paths = ["modules/*/repository.py"] +paths = ["modules/*/infrastructure/*.py"]

— y jfast inspect te dice el layout de cada módulo. jfast contracts init --layout X --force también lo arregla y sobrescribe el archivo entero, con todas las capas, reglas y waivers que el proyecto agregó, así que acá es el último recurso y no el primero.

diff -async def rotate_refresh(self, token_id: str, *, family: str, ttl: int) -> bool: +async def rotate_refresh( + self, token_id: str, *, family: str, ttl: int, grace: int = 0 +) -> RefreshOutcome:

RefreshOutcome es Literal["rotated", "raced", "replayed"] en jfastframework.auth.store, donde MemoryTokenStore y RedisTokenStore son los ejemplos completos. Un store que no puede honrar una ventana de gracia devuelve "replayed" donde antes devolvía False. Solo afecta a proyectos que escribieron su propio store; los que vienen incluidos los lee el framework correctamente.

diff class PageResponse(BaseModel): - total: int + total: int | None

has_more responde "¿hay página siguiente?" sin necesidad del total. jfast upgrade --check lista los call sites bajo pagination-total-optional.

diff -docker exec -it shop_postgres psql -U shop +docker compose exec postgres psql -U shop

Agregado

jfastframework.time responde la mitad que es una pregunta de negocio. now() es el único reloj que lee el framework y siempre devuelve UTC aware; today(tz) y day_bounds(day, tz) deciden a qué día local pertenece un instante UTC. day_bounds devuelve un rango semiabierto [start, end) en UTC — nunca BETWEEN, cuyo límite superior cerrado o cuenta la medianoche dos veces o pierde el último microsegundo según la precisión de la columna — y resuelve los dos bordes con fold=0, que es lo que lo hace correcto tanto en el día local que no tiene medianoche (America/Santiago arranca el DST a las 00:00) como en el que tiene dos. in_zone() es presentación y nada más; parse() rechaza un string sin offset salvo que quien llama diga en qué zona lo escribieron.

Tres zonas, separadas porque son tres preguntas: el almacenamiento es UTC y no se configura; [app] timezone es la zona de negocio, lo que significa "hoy" para un reporte, un período de facturación o una cuota diaria; [plugin.tenancy.timezones] la pisa por tenant, que es el caso que existe cuando un mismo deploy atiende varios países, con una dependencia tenant_zone que cae en la zona de negocio y nunca en la del servidor. Cada nombre se valida al arrancar, así que un typo detiene el servicio en vez de correr los reportes de un tenant un día entero, una semana después.

"UTC" resuelve a datetime.UTC y no lee ningún archivo: zoneinfo lee /usr/share/zoneinfo también para esa clave, y un contenedor slim sin tzdata falla con cualquier nombre. Nombrar una zona real en una imagen así falla al arrancar, con un mensaje que dice qué paquete falta.

jfast doctor reporta db_timezone como par — en qué zona calcula un cliente sin fijar, y en cuál calcula este servicio — leído por dos conexiones, porque un parámetro de arranque se convierte en el valor de reset de la sesión y pg_settings.reset_val diría UTC sobre un servidor configurado con cualquier cosa. Que el servidor no esté en UTC no es una falla de este servicio y aun así merece una línea: cualquier otro cliente de esa base, psql o una herramienta de BI o una migración corrida a mano, está reportando otro día. Ver docs/timezones.md.

Sigue sin correr ninguna de las cuatro, a propósito: pytest ejecuta tu código por un tiempo no acotado y mypy en un árbol sin dependencias instaladas fabrica hallazgos, y cualquiera de las dos adentro convierte un hook de pre-commit en un build. Así que la solución es nombrar, no agregar. Toda corrida termina con

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

pase o falle, y --json lleva esas mismas cuatro bajo not_covered, cada una con lo que atrapa y el comando que la atrapa.

layers domain 4 files application 2 files infrastructure 3 files adapters 2 files shared 2 files

Una capa en 0 es el hallazgo, y es el que este checker reportaba tarde: layer-unmatched salta solo cuando los archivos que esa capa debería haber reclamado además quedan sin dueño, y una capa que pasa de 40 archivos a 3 en un refactor no levanta nada. Se cuenta a través de layer_for y no globeando crudo, así que una capa cuyos matches se los lleva todos un patrón más específico aparece con el cero que le corresponde.

Un hecho, un dueño, entre cuatro comandos: analyze reporta contract-governs-nothing llamando a check_coverage en vez de reimplementarlo — antes imprimía "sin hallazgos" en un proyecto cuyo contrato no gobernaba nada, mientras jfast next decía que el check fallaba — contracts diff lista lo que permite una capa vacía bajo ~ con el motivo, en vez de vender una caída como diez oportunidades de apretar el contrato, y next junta el par en un solo paso en lugar de cinco líneas sobre una sola cosa que hacer.

Arreglado — lo silencioso

jfast serve, jfast dev y el entrypoint del Dockerfile generado pasan --no-proxy-headers ahora: un solo resolvedor en el proceso, y es el que lee jfast.toml. Un peer que llega ya sustituido se detecta en vez de creerse — un proxy agrega la dirección desde la que recibió la conexión, nunca la propia, así que un peer de transporte legítimo no aparece en la cadena que está retransmitiendo — y esa petición se trata como si no tuviera cliente: un único balde del que un atacante no puede rotar, con el scheme degradado cuando el header pudo haberlo escrito. Los tests de las dos mitades corren contra un uvicorn real sobre un socket real, arrancado como viene de fábrica.

Todo ordenamiento que arma el repositorio escribe ahora NULLS LAST en las columnas nullables, porque el default no es uno solo — PostgreSQL pone los NULL últimos ascendente y primeros descendente, SQLite los pone primeros en los dos casos — y la comparación de keyset está escrita contra eso: IS NULL para un empate, y un paso más allá de un valor no nulo admite el bloque de NULL que lo sigue. El costo se dice en vez de esconderse: una columna de orden nullable degrada el index range scan a un index scan con filtro, medido en 840 buffers contra 4 con columnas NOT NULL, una página en la profundidad 100k sobre 200k filas, contra 1.049 de la misma página por OFFSET. Sigue siendo la más barata de las tres formas de paginar; ya no es plana.

Arreglado — corrección

[0.1.0a4] - 2026-08-30

Veinte defectos encontrados construyendo una aplicación real contra 0.1.0a3, más una auditoría de seguridad y una revisión de la CLI. Casi todos fallaban en silencio: un consumer suscrito a nada, un token refrescado que autenticaba sin permisos, un compose que se regeneraba idéntico ignorando un plugin, un contrato que prohibía el patrón que su propia documentación prescribe.

Rompe

sql ALTER TABLE invoices ALTER COLUMN created_at TYPE timestamptz USING created_at AT TIME ZONE 'UTC', ALTER COLUMN updated_at TYPE timestamptz USING updated_at AT TIME ZONE 'UTC';

El autogenerate de Alembic escribe la forma pelada, sin USING. Eso no falla: convierte por el cast implícito, leyendo cada valor guardado en el TimeZone del servidor, y desplaza la tabla entera en silencio en cualquier servidor que no esté en UTC.

Arreglado — lo silencioso

Arreglado — corrección

Agregado

Agregado

inspect es una pantalla: módulos, cómo está formado cada uno, qué sirve, si está cableado en la app. inspect module <name> va más hondo. analyze reporta ocho problemas estructurales, lo peor primero, cada uno con el arreglo en su propia línea: ciclos de import, un módulo que main.py nunca registra, dos routers reclamando un prefijo, shared/ importando un módulo, un plugin habilitado que nada provee. graph dibuja el grafo de dependencias entre módulos como texto, mermaid, dot o JSON.

Cada check es decidible desde la fuente. Nada adivina: 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. module-no-migration solo se dispara cuando ya hay revisiones, así que un proyecto recién generado no reporta nada.

Cambiado

bg-surface, bg-panel, bg-elevated, border-line, text-ink, text-ink-soft y text-ink-faint ahora salen de @theme inline, así que las utilidades emiten var(--ui-*) y un cambio de variable da vuelta la interfaz. Ningún componente lleva ya una clase dark:, salvo los cuatro tonos de estado de BaseBadge y ToastHost donde el color es el significado. color-scheme se define al lado, así que las barras de scroll y los date pickers también siguen el tema.

Arreglado

forbid_packages = ["sqlalchemy"] ahora viaja en layers.http (layers.adapters en hexagonal) en todas las plantillas de contrato. Los cuatro layouts se siguen generando y pasando su propio contrato; el import plantado ahora falla con layer-package: 'http' must not import 'sqlalchemy' y exit code 1.

Los proyectos existentes no cambian: contracts.toml es tuyo una vez generado. Agrega la línea para adoptar la regla.

Agregado

[0.1.0a3] - 2026-08-29

Documentación

Corregido

Agregado

Cambiado

Corregido

Agregado

[0.1.0a2] - 2026-08-28

Agregado

Corregido

Seis defectos que salieron en 0.1.0a1. Juntos significaban que un servicio generado no se podía instalar, no se podía construir en una imagen, no podía responder un GET, y no podía responder un PATCH. Cada uno está ahora cubierto por un test, y por scripts/smoke_docker.sh, que construye la imagen generada y la corre contra un PostgreSQL real -- el chequeo cuya ausencia dejó pasar a los seis.

Agregado

Un pre-release se fija exacto, porque ~=0.1 tampoco matchea con 0.1.0a1: una cláusula de release compatible se normaliza a >= 0.1, == 0.* y 0.1.0a1 ordena por debajo de 0.1.0, así que queda fuera de rango incluso con --pre. Cuando el framework llegue a un release final el pin pasa a ser ~=major.minor por sí solo.

Ahora falla un test si alguna plantilla de requirements vuelve a hardcodear una versión. La resolución en sí deliberadamente no se chequea en CI: al momento del release la versión que se está fijando todavía no está publicada, así que ese chequeo fallaría justo en el commit que es correcto.

Agregado

Corregido

Corregido

Agregado

Cambiado

Agregado

Cambiado

Corregido

[0.7.0] - 2026-08-28

Archivos, tenants, y los tres servicios de nube que una app desplegada termina buscando.

Agregado

Plugin storage - Discos con nombre y una visibilidad, modelados sobre los de Laravel: el código escribe a storage.disk("private") y dónde vive eso es configuración. - Drivers local y S3/MinIO detrás de un único protocolo StorageBackend. Un disco local escribe atómicamente (archivo temporal + replace) para que quien lee nunca vea un objeto parcial. - Un disco privado se niega a producir una URL permanente. temporary_url() firma la clave y la expiración con HMAC, comparadas en tiempo constante — firmar solo una de las dos convierte a un único enlace válido en una llave a todo el disco. - Toda clave se valida antes de llegar a un filesystem o a un bucket: traversal, rutas absolutas, backslashes y bytes nulos rechazados, .. resuelto primero. Los discos locales re-chequean después de resolver, porque un symlink adentro de la raíz igual puede apuntar afuera. - Las descargas van con Content-Disposition: attachment + nosniff. Un .html o .svg subido y servido inline corre el script de quien lo subió en tu origen. - Los enlaces expirados y los falsificados devuelven el mismo 403 con el mismo mensaje. - MinIO en el archivo de compose generado en el offset de puerto +6, opt-in.

Plugin tenancy - Resuelve el tenant desde un claim del token, un subdominio, un prefijo de ruta o un header, en ese orden de confianza. header no está en la lista por defecto y avisa en producción: X-Tenant-ID: acme está a un curl de distancia de los datos de otro tenant. - El parseo de subdominio rechaza hosts de múltiples labels, el dominio base pelado, y una lista reservada (www, api, admin, …). base_domain es obligatorio, o si no cada hostname parece un tenant. - require_tenant devuelve problem+json 403, con health, métricas y docs exentos para que las probes sigan pasando. - jfast workspace caddy --wildcard-tenants emite un bloque de sitio con comodín con TLS on-demand y el endpoint ask que lo controla. Sin ask, cualquiera que apunte DNS hacia ti quema tu rate limit de certificados.

Login social (auth) - Presets de Google, Microsoft y GitHub; cualquier otro proveedor por sus endpoints. - /auth/{provider}/start y /auth/{provider}/callback, con el state y el nonce viajando en una cookie httponly, samesite=lax y ambos verificados a la vuelta. - Los ID tokens se verifican por audiencia y emisor. Sin el chequeo de audiencia, un token emitido para la app de Google de cualquier otro loguea aquí. - @auth.on_identity es donde una identidad verificada se vuelve tu usuario. Que falte es un 500, no un 200 alegre. - OIDCIdentity.federated_id está calificado por proveedor, porque los ids de subject son únicos por proveedor y no globalmente.

Secrets - load_secrets() puebla os.environ desde AWS Secrets Manager o Google Secret Manager antes de create_app(). Un valor de entorno existente gana salvo que se sobrescriba; solo se loguean nombres, nunca valores; el JSON anidado se rechaza en vez de recibir un nombre aplanado impredecible.

Serverless - jfast deploy function <name> --target aws|gcp escribe el handler, el Dockerfile y un script de deploy — y no los corre. - Ambos targets corren la misma app ASGI que corre el contenedor. Privados por defecto en las dos nubes; --public opta por lo contrario y avisa.

Plugin notifications - Firebase Cloud Messaging sobre la API HTTP v1, con un backend console que loguea en vez de enviar para desarrollo y tests. - Los tokens de dispositivo no registrados se reportan de vuelta para poder borrarlos. - No está verificado contra un proyecto FCM real en CI; la construcción del payload sí.

Corregido

Agregado (interno)

[0.6.0] - 2026-08-27

Autenticación JWT, y manifiestos de Kubernetes derivados del contrato del servicio.

Agregado

Plugin auth - Verificación en tres modos: jwks (traer las claves públicas del emisor — el default, y el único sensato entre servicios), public_key (un PEM fijado), secret (HMAC, para un servicio único). - require_auth, require_scopes(...), require_roles(...), optional_auth como dependencias de FastAPI. 401 para "quién eres", 403 para "no puedes". - Cliente JWKS con caché, rotación ante un kid desconocido, y un rate limit en el refresh para que no se puedan usar kids falsificados para martillar al proveedor de identidad. Las claves cacheadas siguen funcionando durante una caída de JWKS; /ready reporta la antigüedad. - Emisión de tokens para un servicio que tiene su propio login, con rotación de refresh y detección de reuso: un refresh token reproducido revoca toda la familia de la sesión. - Revocación: POST /auth/logout deniega el jti y su familia de refresh, respaldado por Redis cuando el plugin cache está activo. El fallback en memoria se reporta a sí mismo como no compartido en vez de fingir. - GET /auth/me devuelve identidad y permisos — nunca el token, nunca los claims crudos.

Decisiones de seguridad, cada una con un test - Los algoritmos se fijan por configuración y se pasan explícitamente al decoder, así que alg: none y la confusión RS256→HS256 se rechazan las dos. Configurar algoritmos simétricos y asimétricos juntos se rechaza en el arranque: esa combinación es el ataque. - aud e iss se verifican — apagados por defecto en la mayoría de las librerías, y sin ellos un token para un servicio hermano se acepta aquí. - El margen de expiración es de 30 segundos, no de minutos. - Las razones de rechazo van al log; el cliente recibe un 401 pelado. - tenant_id ahora viene de un claim firmado, no del falsificable header X-Tenant-ID. Esta es la razón principal de seguridad para habilitar auth.

Kubernetes - jfast workspace k8s — un árbol de kustomize: Deployment, Service, ConfigMap, HPA y PodDisruptionBudget por servicio, un Ingress, overlays dev/prod. - jfast init pregunta si lo necesitas. - Las liveness probes van a /health, las de readiness a /ready — el contrato de dos endpoints es lo que evita que un parpadeo de la base de datos reinicie todos los pods sanos. Una startup probe permite 150s para un primer boot lento. - No-root, sistema de archivos raíz de solo lectura, capabilities descartadas, maxUnavailable: 0, y un PDB para que un drenaje de nodo no pueda llevarse todas las réplicas. - El Ingress sirve /api, la misma forma que el Caddyfile generado, así que el build del frontend es idéntico local y en el cluster.

No se genera, deliberadamente

Notas

[0.5.0] - 2026-08-27

Contratos por proyecto, y un quickstart que CI realmente ejecuta.

Agregado

Contratos - contracts.toml en cada servicio generado: alcance (owns / does_not_own), límites de capa, llamadas prohibidas, estructura requerida, interfaces declaradas, e invariantes que ningún checker puede verificar. - jfast contracts init | check | show --json | render | waivers. check sale con código distinto de cero, así que rompe un build en vez de imprimir consejos. - Un checker estático, basado en AST: límites de capa (imports relativos y absolutos), paquetes prohibidos por capa, llamadas prohibidas con la razón adjunta, y archivos requeridos por módulo. - Exoneraciones inline — # contracts: allow <reason> — con la razón obligatoria y jfast contracts waivers listando todas. - El contrato se valida antes que el código: dos capas reclamando una misma ruta, o un may_import nombrando una capa que no existe, se reportan como errores de contrato en vez de producir respuestas confiadas a la pregunta equivocada. - CONTRACTS.md se genera desde el mismo archivo, así que el documento y la regla que se aplica no pueden estar en desacuerdo. - .jfast/skills/respect-contracts/SKILL.md, y AGENTS.md ahora abre con jfast contracts show --json.

Primeros pasos - docs/local-setup.md — instalar desde un checkout, generar un proyecto, el ciclo que realmente usas, y los modos de falla que vale la pena conocer. - scripts/smoke_docs.sh corre esos comandos exactamente como están documentados, en CI. Documentación que nunca se ejecutó es una suposición.

Corregido

[0.4.0] - 2026-08-27

Servicios políglotas, colas y eventos, arranque de un solo comando, Caddy en el borde, y un sitio de documentación — más verificación real de build para todo lo que hasta ahora solo se había verificado con grep.

Agregado

jfast start - Un comando para el default opinado: un monolito modular en Python con PostgreSQL + pgvector, Redis, jobs en background y un módulo inicial, un frontend Vue, un Caddyfile y un archivo de compose de workspace. - Un monolito en vez de tres servicios a propósito: partirlo después es una jugada, despartirlo es una reescritura.

Servicios políglotas - docs/service-contract.md — el contrato que satisface todo servicio JFast sin importar el lenguaje: /health, /ready, X-Request-ID, problem+json, configuración JFAST_*, bloques de diez puertos, logs JSON en stdout. - jfast new service --language go — un servicio en Go con cero dependencias de terceros, que implementa el contrato en unas 300 líneas vendoreadas. CI corre go vet, go test, go build, arranca el binario y le hace curl. - jfastframework/languages.py — el registro de lenguajes. Solo necesitas el toolchain de los lenguajes que realmente uses.

gRPC - --grpc genera el contrato .proto (health, problem, un servicio de dominio) y reserva el offset de puerto +9. Solo el contrato: no se generan stubs y no se cablea ningún servidor, porque fijar una versión de protoc adentro de un scaffolder hace que los stubs generados no coincidan con lo que sea que tenga CI. Ver proto/README.md.

Colas - Plugin queue con un protocolo QueueBackend y tres backends: PostgreSQL (FOR UPDATE SKIP LOCKED, encolado transaccional), Redis (BLMOVE a una lista de procesamiento por worker), RabbitMQ (dead-letter exchange con un TTL para las demoras). - TaskRegistry y Worker: backoff exponencial acotado, dead-lettering, timeouts de job, drenaje de lo que está en vuelo al apagar, despertar inmediato al detener. - GET /queue/stats.

Eventos - Plugin events — publish/subscribe de Kafka con claves de partición, offsets commiteados después de manejar, e infraestructura en modo KRaft (sin ZooKeeper).

Borde y deploy del workspace - jfast workspace compose — un archivo de compose para cada servicio, sea cual sea su lenguaje, con datastores por servicio. - jfast workspace caddy — un Caddyfile que pone al workspace detrás de un solo hostname. Los backends viven bajo /api con o sin gateway, así que el build de producción del frontend sigue funcionando el día que aparezca uno. - Los frontends ganaron .env.production con VITE_API_URL=/api — relativo, así que no hay CORS ni un rebuild por entorno.

Sitio de documentación - docs-site/build.py renderiza el markdown propio del repositorio a un sitio estático y versionado; docs-site/check.py valida enlaces, anclas, assets, tokens de tema y artefactos de plantilla sin renderizar. - .github/workflows/pages.yml lo publica, reconstruyendo cada versión minor publicada desde su propio tag para que las versiones viejas sigan funcionando.

Corregido

Cambiado

No hecho, deliberadamente

[0.3.0] - 2026-08-27

Workspaces multi-servicio, un API gateway, generación de frontend, y el setup de migraciones/testing que el release anterior solo documentaba.

Agregado

Workspaces - jfast.workspace.toml y jfastframework.workspace: los servicios se registran solos, toman el siguiente bloque libre de diez puertos, y el archivo registra a qué debería llamar el frontend. - jfast workspace init | list | gateway | env.

API gateway - Plugin gateway: reverse proxy basado en prefijos con stripping de headers hop-by-hop, propagación de X-Request-ID, y 502/504 como problem+json. - Se genera automáticamente en cuanto un workspace tiene más de un backend. Con un solo backend deliberadamente no se genera. - No es un catch-all: solo los prefijos configurados se proxean, así que el gateway conserva sus propios /health, /ready y /metrics. La readiness no sondea a los upstreams, así que un reinicio no tumba a todo el sistema.

Frontends - jfast new service <name> --kind spa --frontend vue|react — proyecto Vite + Tailwind v4 con una página de inicio funcional que llama al /health del backend al cargar. - jfast new view <Name> — la estructura Modulo<Name>/{Components,Pages,Routes,Services}, registrada en el router y en la barra lateral en comentarios marcadores. - jfastframework.cli.patcher: parcheo idempotente, ruidoso, que preserva los marcadores. Re-correr el generador no duplica; un marcador faltante levanta con la ruta en vez de no hacer nada en silencio. - El framework se autodetecta desde el proyecto, así que no hay que repetir --frontend.

Migraciones y tests en los servicios generados - alembic.ini, migrations/env.py, script.py.mako y versions/. env.py lee el JFAST_DB_DSN propio de la app y auto-importa los modelos de cada módulo, así que el autogenerate no puede emitir una migración vacía en silencio. compare_type y compare_server_default están activos. - pytest.ini y un conftest.py con fixtures app / client.

Selección de datastores desde la terminal - jfast init — instalador interactivo: tipo, frontend, datastores, puerto. - jfast new service --with database,cache,qdrant,rag — la lista de plugins, los bloques [plugin.*], las claves del .env y los extras fijados se derivan todos de ahí. - El store de rag se infiere de los datastores elegidos, así que --with qdrant,rag no puede generar un servicio configurado para pgvector.

Cambiado

Corregido

[0.2.0] - 2026-08-27

Los datastores pasaron a ser una elección, y un frontend pasó a ser un servicio.

Agregado

Datastores - Protocolo VectorStore en jfastframework.vectors, con Chunk y SearchHit como vocabulario compartido. Cada store normaliza su score a similitud coseno en [0, 1]. - Plugin qdrant — cliente, health check, contenedor con puertos HTTP y gRPC. - Plugin mongo — cliente Motor y handle de base de datos. - rag ahora elige su store desde la configuración: pgvector, qdrant, o una ruta con puntos a tu propia clase. Lo mismo para el embedder. - InfraService.extra_ports para contenedores que exponen más de un puerto.

Frontends renderizados en el servidor - Plugin web — plantillas Jinja2, archivos estáticos, y render() con renderizado parcial de HTMX: una navegación del browser recibe la página, un hx-get recibe el fragmento, desde un solo handler. - Manejo de errores consciente de HTMX: un JFastError levantado durante una request HTMX devuelve un fragmento HTML en vez de problem+json, que HTMX si no metería en el DOM como texto crudo.

Generador - jfast new service <name> [--kind api|web] — scaffoldea un servicio entero. - jfast new module <name> [--layout layered|screaming] [--ui api|htmx]. - Layout module_screaming: dominio libre de framework, un archivo por caso de uso, almacenamiento y HTTP en los bordes, tests de dominio separados de los tests de casos de uso. - Overlay ui_htmx — compuesto sobre cualquiera de los dos layouts en vez de duplicado, así que tres árboles de plantillas cubren las cuatro combinaciones. - Dos entornos Jinja en el scaffolder: las plantillas .html.j2 usan [[ ]] para los valores de scaffolding para que el {{ }} de runtime que el browser necesita sobreviva. - Los nombres de tabla se pluralizan, lo que además esquiva las palabras reservadas de SQL con las que los sustantivos en singular no dejan de chocar (order, user, group). Se sobrescribe con --table.

Docs - docs/modules.md, docs/datastores.md.

Cambiado

Corregido

[0.1.0] - 2026-08-27

Primer alpha. Kernel y plugins incorporados.

Agregado

Notas