Saltar a contenido

Norte — fitz (core del lenguaje)

Backlog técnico surgido de la auditoría hecha para MatHelp (juego de matemática mobile-first, i18n es-AR + en, Postgres, 100% Fitz). Primer proyecto real de terceros apoyado de punta a punta en el stack.

Fecha del análisis: 2026-08-19 · Actualizado: 2026-08-20 (hallazgos de la fase F0) Versión auditada del core: v0.48.0 (repo) / v0.47.0 (binario compilado por el autor).

Documento vivo: marcá los checkboxes al implementar. IDs estables — no renumerar. El archivo hermano con las tareas del framework es fitz-liveviews/docs/norte-mathelp.md.

Nota de renumeración (2026-08-20): en la primera versión yo había usado FITZ-09 (Map.remove) y FITZ-10 (differ de paridad) como hallazgos propios. El autor asignó FITZ-09..12 a hallazgos nuevos de mayor impacto (con repro real sobre el compilador). Esos IDs ganan; mis dos hallazgos se renumeraron a FITZ-13 (Map.remove) y FITZ-14 (differ de paridad). Las referencias cruzadas están actualizadas.


Resumen ejecutivo

Auditoría original (A1–A8): 6 confirmados, 1 muta (A4), 1 refutado a medias (A5). Fase F0 sumó cuatro hallazgos que salieron de correr el compilador, no de leer docs — y tres de los cuatro son la misma clase de bug. Eso sube T2 (paridad) por encima de features nuevas.

  • FITZ-01 (rand) sigue siendo el único bloqueante para empezar MatHelp. Nada cambia acá.
  • FITZ-09 (codegen de T?) es un ALTO nuevo y urgente. Una función -> Str? con return null genera Rust roto (return () donde va return None, valor sin Some(...)). No es evitable en el código de la app: flv_cookie, dentro de fitz-liveviews, tiene el patrón exacto (lib.fitz:2430), así que cualquier app que dependa del framework y compile a nativo choca. Verificado: examples/admin importa el framework entero → no compila con fitz build, solo con fitz run. Consecuencia en MatHelp: el Docker corre el intérprete en vez del binario (se pierde ~9x + distroless).
  • FITZ-10 (Str + Any) y FITZ-06 (preload) completan la clase check✓/build✗. Tres divergencias interpretado↔compilado en una tarde. No es mala suerte: no hay nada que las detecte.
  • FITZ-14 (differ de paridad) sube de Medio a Alto y se mueve al Hito 1. Es la sugerencia del autor y la comparto: un test que corra el corpus de examples/ + boilerplates/ por las dos vías y diffee la salida hubiera cazado los tres solos. Un lenguaje que a veces se comporta distinto al compilar es un problema de confianza, no de features.
  • FITZ-11 (la imagen oficial de Docker no trae git) — con git deps como única forma de dep externa hoy, la imagen no puede construir ningún proyecto con dependencias. Fix de una línea.
  • A4 confirmado empíricamente sobre el binario: los format specs compilan ({n:,} → 1,234,567); la doc guide.md:1266 mentía y ya la corregí. Lo que queda de FITZ-04 es solo locale (1.234.567,00 argentino) — y para un juego de matemática, mostrar mal los decimales es enseñar mal.
  • A5 confirmado empíricamente: @post acepta form-urlencoded → login zero-JS. Comentario stale de examples/admin/src/auth.fitz ya corregido.

Recomendación de arranque: FITZ-01 (rand) para desbloquear el juego. En paralelo, el bloque de confianza: FITZ-14 (differ) + FITZ-09 (T?) + FITZ-10 (Str+Any) — los tres van juntos porque el differ es la red que protege a los otros dos (y a preload). FITZ-09 además destraba el binario nativo de todo lo que use fitz-liveviews, MatHelp incluido.


Tabla priorizada (unificada cross-repo, re-priorizada 2026-08-20)

Misma tabla en los dos archivos; acá se detallan solo las fichas de fitz core. Filas FLV-* viven en fitz-liveviews/docs/norte-mathelp.md.

# ID Tarea Estado Impacto Costo Riesgo Depende / desbloquea
1 FITZ-01 Módulo rand (CSPRNG + seeded) Confirmado Bloqueante M Ninguno —
2 FITZ-09 Codegen: -> T? emite None/Some Confirmado (repro) Alto S Bajo desbloquea FLV-10, native builds
3 FITZ-14 Differ de paridad run↔build [antes FITZ-10] Confirmado Alto (↑) M Ninguno protege 09/10/06
4 FITZ-11 git en la imagen oficial de Docker Confirmado (repro) Medio-Alto S Ninguno —
5 FITZ-10 Str + Any: check✓ / build✗ Confirmado (repro) Medio S Bajo —
6 FITZ-05 API de cookies (@cookie + Response.cookies) Ya resuelto Alto M Bajo —
7 FITZ-03 Módulo fs Confirmado Alto M Bajo habilita T1
8 FITZ-04 Formateo de números con locale Parcial (paridad OK) Alto M Bajo T1
11 FITZ-02 Servido de estáticos (@server(static_dir=)) Ya resuelto Medio M Bajo habilita T3
13 FITZ-13 Map.remove [antes FITZ-09] Ya resuelto Medio S Bajo desbloquea FLV-03
19 FITZ-06 .preload() en el intérprete (error claro) Ya resuelto (MVP) Medio M Bajo T2
20 FITZ-07 .is_in(<var>) → = ANY($n) Ya resuelto Bajo S Bajo —
21 FITZ-12 Paréntesis redundantes en el match generado Ya resuelto Bajo S Ninguno —
22 FITZ-08 ENUM nativo de Postgres Confirmado Bajo L Medio —

Estado ∈ Confirmado · Parcial · Refutado · Ya resuelto Impacto ∈ Bloqueante · Alto · Medio · Bajo · Costo ∈ S (horas) · M (días) · L (semana+) (los # saltados son filas FLV-* — ver el archivo hermano para la tabla completa)


Fichas

FITZ-01 · Módulo rand

  • Implementado (2026-08-20) — módulo src/rand.rs (SplitMix64 fijo, no el crate rand), Value::RandGen + Type::RandGen, intérprete + codegen con paridad bit-a-bit (RAND_PRELUDE_CORE/_GLOBAL, getrandom inyectado en Cargo.toml para el path global), checker (rand = Type::Any, codegen re-deriva RandGen), LSP completions, ejemplo runnable 13w-random.fitz, test E2E de reproducibilidad + smoke verde. Limitaciones del MVP (follow-ups, NO divergencias silenciosas): (a) rand en codegen solo en el programa principal CERRADO 2026-08-22 (dogfooding MatHelp F2 — los generadores viven en gen_arith.fitz, módulo importado): el crate root emite RAND_PRELUDE_CORE/_GLOBAL + getrandom si CUALQUIER módulo usa rand.* (OR con loader.modules, patrón SMTP/Response/DB), y cada módulo importado emite use crate::{__FitzRng, ...} / los __fitz_rand_g_* globales. Idéntico fix para num (FITZ-04: NUM_PRELUDE + use crate::{__fitz_num_*}). LoadedModule suma uses_rand/uses_rand_global/uses_num; 6 sitios en src/codegen.rs; validado con MatHelp compilando a binario nativo. (b) un RandGen NO cruza un límite de función (ni param ni return): el checker no conoce RandGen como tipo nombrable CERRADO v0.58.1, 2026-08-22 (FITZ-01(b)): resolve_type_expr mapea "RandGen" → Type::RandGen, así que fn draw(g: RandGen, hi: Int) -> Int { return g.int(1, hi) } type-chequea y compila; infer_randgen_method tipa los 6 métodos (int→Int, float→Float, bool→Bool, choice→Result<T>, shuffle→List<T>, sample→Result<List<T>>) para que un g que llega por parámetro tenga métodos tipados. Verificado check/run/build/binario idénticos. El receptor de un método sigue prefiriendo una var local simple; © un match r.sample(...) { Ok(v) => v, Err(_) => "x" } heterogéneo en posición de valor cae al gap pre-existente de "bare Any en CLI" (usar matches homogéneos o en posición de statement).
  • Estado: Confirmado.
  • Evidencia: src/evaluator.rs:246-304 (builtin_names() — lista cerrada, no hay rand); src/lsp.rs:4204-4239 (módulos built-in, no hay rand); src/value.rs:699 (única fuente de azar = Uuid.v4()). rand_core en Cargo.toml:174 solo para salt de Argon2 / nonce SCRAM / UUID v4.
  • Impacto en un usuario real: el núcleo de MatHelp es "generar un ejercicio aleatorio según la destreza del chico". Sin azar no hay juego. Caso obvio también de simulaciones, sampling, jitter, shuffles.
  • Workaround hoy: xorshift32 propio sembrado con Uuid.v4() hasheado (~40 líneas). Anda, frágil, no reusable, y no es CSPRNG (no sirve para tokens).
  • Propuesta (API cerrada):
    // Global — CSPRNG (getrandom/OsRng). No reproducible.
    rand.int(min, max) -> Int             // inclusivo ambos extremos
    rand.float() -> Float                 // [0, 1)
    rand.bool() -> Bool
    rand.choice(xs) -> Result<T>          // Err si vacía
    rand.shuffle(xs) -> List<T>           // copia barajada (Fisher-Yates)
    rand.sample(xs, n) -> Result<List<T>> // sin repetir; Err si n > len
    rand.bytes(n) -> Bytes                // CSPRNG, tokens
    
    // Seeded — PRNG rápido, determinístico, NO cripto.
    let r = rand.seeded(12345)            // -> RandGen (valor con estado, como DbConn)
    r.int(1, 100)  r.float()  r.bool()  r.choice(xs)  r.shuffle(xs)  r.sample(xs, n)
    
  • Criterio de aceptación: rand.seeded(N) produce la misma secuencia en fitz run y fitz build, para siempre. Por eso el PRNG sembrado no debe delegar en StdRng del crate rand (algoritmo no estable entre versiones): fijar uno simple y bien especificado (PCG-XSH-RR 64/32 o SplitMix64), implementado idéntico en evaluador y codegen. El CSPRNG global usa getrandom. Esta garantía habilita guardar seed + índice y reconstruir una partida entera desde dos enteros.
  • Archivos a tocar: nuevo src/rand.rs (algoritmo compartido); src/evaluator.rs:246 (módulo + builtins + Value::RandGen); src/value.rs; src/types.rs; src/lsp.rs:4204; src/codegen.rs (preludio __fitz_rand_* + dispatch + Cargo.toml con getrandom); docs/guide.md.
  • Tests: @test de secuencia sembrada fija; unit del rango/sample/choice; E2E de paridad (ver FITZ-14 — el criterio de rand es paridad).
  • Docs: capítulo "Aleatoriedad" en docs/guide.md (panorama vecino; CSPRNG vs seeded; patrón replay).
  • Dependencias: ninguna. Desbloquea el arranque de MatHelp.
  • Notas de diseño: separar CSPRNG (rand.*) de seeded (rand.seeded()) es la decisión central. Descartado exponer StdRng (no reproducible) y un rand.seed_global() (estado global).

FITZ-09 · Codegen: las funciones que devuelven T? compilan mal

  • Implementado (2026-08-20) — fix en gen_return + test E2E de paridad. Smoke ~360 ejemplos verde.
  • Estado: Confirmado con repro mínimo (sobre el binario compilado, no docs).
  • Evidencia: repro de 20 líneas del autor — fn primera_parte(s: Str?) -> Str? con return null y return <valor> pasa fitz check ✓, corre en fitz run ✓, y falla fitz build con E0308. El Rust generado emite dos defectos: return () donde va return None, y el valor de retorno sin envolver en Some(...). El propio rustc sugiere el fix (help: try wrapping the expression in Some). La manifestación en el framework: flv_cookie(cookie: Str?, name: Str) -> Str? (fitz-liveviews/src/lib.fitz:2430) tiene el patrón exacto (null => { return null }, c => c, return null de tail) → el Rust generado fitz_liveviews.rs:1432 no compila. Verificado que examples/admin importa el framework entero (auth.fitz:17, dashboard.fitz:15, ...) con dep { path = "../.." } → examples/admin no compila con fitz build (solo fitz run).
  • Impacto en un usuario real (ALTO, no medio): no es evitable en el código de la app — cualquier proyecto que dependa de fitz-liveviews y compile a nativo choca, haga lo que haga. Y flv_cookie no es marginal: resuelve el locale y la sesión desde la cookie del handshake, lo que necesita toda app con i18n o auth. La app insignia del framework no compila a nativo. En MatHelp, el Dockerfile corre el intérprete en vez del binario (se pierde ~9x de performance y el runtime distroless; la versión compilada quedó comentada).
  • Workaround hoy: correr el intérprete en Docker (funciona, pero pierde perf + distroless). En el código propio, evitar -> T? con return adentro de un match — pero no evitable para el código de la librería.
  • Propuesta: aplicar la coerción nullable en posición de return dentro de una fn cuyo return type es Nullable. return null → return None; return <v> → return Some(<coerced v>); idem el tail. El codegen ya tiene la coerción (T → T?) ⇒ Some(...) y (Null → T?) ⇒ None para asignaciones y campos — el bug es que no la invoca en la posición de return.
  • Criterio de aceptación: (1) el repro compila con fitz build y GET / devuelve "a"; (2) fitz-liveviews compila entero, flv_cookie incluida, y examples/admin compila y corre igual que interpretado (ese es también el cierre de FLV-10); (3) el repro da salida idéntica por fitz run y fitz build.
  • Archivos a tocar: src/codegen.rs — la emisión de Stmt::Return (buscar gen_return/el brazo de Return): cuando el frame de return type es Type::Nullable(_), envolver el valor con la coerción Some(...) y emitir None para return null. Reusar el helper coerce(...) existente.
  • Tests: el repro como E2E en tests/compile_e2e.rs; unit de gen_return con ret Nullable (return null → None, return v → Some(v)); regresión de flv_cookie: un mini programa que importe fitz-liveviews y llame flv_cookie, que hoy no compila y debe compilar.
  • Docs: — (bug interno del codegen; sin cambio de superficie).
  • Dependencias: desbloquea FLV-10 (flv_cookie native), el native build de examples/admin, y el Dockerfile compilado de MatHelp. Lo caza FITZ-14 (differ).
  • Notas de diseño: rustc literalmente sugiere el fix — es la coerción nullable faltante en la única posición donde el codegen no la aplica. Fix chico (S), impacto alto. Debe landear con FITZ-14 para quedar protegido contra regresión.

FITZ-14 · Differ de paridad fitz run ↔ fitz build [antes FITZ-10]

  • Implementado (2026-08-20) — MVP: helper run_interpreter + run_build_parity_corpus_fitz14 con 13 ejemplos CLI-puros diffeados run↔build. Extensible. (Corpus curado, no los 360; ver notas.)
  • Estado: Confirmado (hallazgo estructural; sube de Medio a Alto con la fase F0).
  • Evidencia: en tests/compile_e2e.rs, todos los Command::new(&bin) (:2288, :2322, :2378, :2569, :2613, :11150) ejecutan el binario compilado; ninguno corre fitz run sobre el mismo fuente para diffear. El smoke GUIDE_EXAMPLES_COMPILE solo valida que compile. La paridad se asegura test por test hard-codeando la salida. La fase F0 encontró tres divergencias en una tarde (FITZ-09, FITZ-10, FITZ-06) — la prueba de que no hay red.
  • Impacto en un usuario real: los bugs "mismo código, dos comportamientos" son la clase que más rápido quema la confianza porque aparecen recién en producción — el usuario dockerizando a las once de la noche. Sin differ, el próximo lo encuentra un usuario, no el CI.
  • Workaround hoy: ninguno — es deuda de infraestructura de tests.
  • Propuesta: un harness que tome el corpus de examples/** + boilerplates/** (ya son ~360 archivos), corra cada uno CLI-puro por fitz run y por fitz build && ./bin, y assertee stdout idéntico bit-a-bit. Allowlist explícita para los que legítimamente difieren o no aplican (HTTP servers, tiempo, aleatoriedad no-sembrada).
  • Criterio de aceptación: un cargo test que falla si cualquier ejemplo del corpus produce salida distinta entre run y build. Corre en CI. Habría cazado FITZ-09, FITZ-10 y FITZ-06 solo.
  • Archivos a tocar: tests/parity_e2e.rs nuevo con run_and_build_diff(fuente); corpus + allowlist.
  • Tests: el harness es el test.
  • Docs: nota en docs/architecture.md (testing).
  • Dependencias: ninguna. Pilar de T2. Protege FITZ-01 (criterio = paridad), FITZ-09, FITZ-10, FITZ-06.
  • Notas de diseño: empezar por los ejemplos CLI-puros (deterministas). El corpus crece con cada feature nueva. Vale más que cualquier arreglo puntual — es lo que convierte "arreglamos tres bugs" en "no vuelve a pasar".

FITZ-11 · La imagen oficial de Docker no trae git

  • Implementado (2026-08-20) — git agregado al apt-get install de la imagen estándar (build-capable) en .github/workflows/release.yml. Se ejercita en el próximo tag de release. El --dep-override queda como enhancement opcional diferido (no era un bug confirmado). El -python runtime es run-only (sin cargo), no lo toca.
  • Estado: Confirmado en un build real.
  • Evidencia: el build del autor sobre ghcr.io/thegreekman76/fitz falla en RUN fitz build con could not invoke git (No such file or directory, os error 2). La imagen existe y baja bien, pero sin git adentro fitz build no puede resolver ninguna dep { git = ... }. Y hoy { git = ... } es la única forma de dep externa: el registry público no existe y { path = ... } no sirve en un container.
  • Impacto en un usuario real: la imagen oficial no puede construir ningún proyecto con dependencias — justo el caso que la imagen existe para resolver. MatHelp depende de fitz-liveviews por git.
  • Workaround hoy: etapa vendor sobre Alpine que clona fitz-liveviews + un fitz.docker.toml paralelo con la dep apuntada a { path = "/vendor/..." }. Funciona, pero obliga a dos manifiestos en sincronía — trampa clásica para un olvido.
  • Propuesta: (1) una línea en el Dockerfile de la imagen: apt-get install -y --no-install-recommends git ca-certificates (el ca-certificates para clonar por https). (2) Evaluar fitz build --dep-override nombre=ruta, que evitaría el manifiesto duplicado en cualquier build containerizado (y sirve a cualquier CI).
  • Criterio de aceptación: docker run ghcr.io/thegreekman76/fitz fitz build sobre un proyecto con una git dep resuelve la dep y compila. Con --dep-override, el mismo proyecto compila apuntando la dep a un path sin tocar el fitz.toml.
  • Archivos a tocar: el Dockerfile de la imagen oficial (repo fitz, docker/ o el workflow que la buildea); opcionalmente src/main.rs + src/manifest.rs para --dep-override.
  • Tests: smoke de CI que buildee un proyecto con git dep dentro de la imagen; unit de --dep-override.
  • Docs: docs/guide.md (cap de deployment / imagen Docker).
  • Dependencias: ninguna. Alto apalancamiento para cualquiera que dockerice (costo de una línea).
  • Notas de diseño: el --dep-override es la mejora ergonómica de fondo: hoy el registry no existe, así que los builds containerizados con deps son inevitablemente incómodos; el override los limpia sin esperar el registry.

FITZ-10 · Str + Any: lo acepta check, lo rechaza build [antes FITZ-13 no — nuevo ID del autor]

  • Implementado (2026-08-20) — Str + Any en gen_binop (coerce Any→Str, paridad con el intérprete) + detección de List<Any>/Map<_,Any> vía TypeInfo para emitir el preludio __FitzValue en CLI. Test E2E + smoke verde.
  • Estado: Confirmado.
  • Evidencia: repro del autor — let chars = [] (infiere List<Any>), chars.push(c) con c: Str, y luego out + chars[0] → fitz check ✓, fitz build ✗ con codegen: operador + no aplicable a Str y Any en codegen. El checker infiere Any para el [] vacío y no propaga hacia atrás desde el push, o el codegen es más estricto que el checker.
  • Impacto en un usuario real: Medio. Otra divergencia check✓/build✗ — el usuario se entera al compilar. Misma familia que FITZ-09 y FITZ-06.
  • Workaround hoy: anotar let chars: List<Str> = [].
  • Propuesta: consistencia — o (a) el checker infiere el tipo del elemento desde el primer .push() (back-propagation) y así chars[0] tipa Str, o (b) el codegen coacciona Str + Any como el intérprete. Preferible (a): que el checker no acepte lo que el build rechaza. Lo que no puede pasar es la divergencia.
  • Criterio de aceptación: el repro compila sin anotación (inferencia), o fitz check lo rechaza con un mensaje claro. Nunca pasar check y fallar build.
  • Archivos a tocar: src/types.rs (inferencia del elemento de un List<Any> vacío desde el push) y/o src/codegen.rs (operador + con operando Any).
  • Tests: el repro como caso de checker + E2E; lo caza además FITZ-14.
  • Docs: — (comportamiento interno).
  • Dependencias: ninguna. Parte de T2.
  • Notas de diseño: la back-propagation del tipo de elemento desde push/insert es lo más útil (mejora la inferencia en general), pero cualquier resolución que elimine la divergencia sirve. Lo importante es la consistencia checker↔codegen, no cuál de los dos cede.

  • Implementado (2026-08-20) — FASE A (leer con @cookie(name="X"), param Str/Str?, opcional into="alias", sobre @get/@post/@ws; parse_cookie_header; paridad run↔build; OpenAPI in: cookie) + FASE B (nominal built-in Cookie de 8 campos con defaults + campo Response.cookies: List<Cookie>; cada Cookie → un Set-Cookie; helper compartido serialize_set_cookie/__fitz_serialize_set_cookie con paridad bit-a-bit; fix del .insert→ .append en outcome_to_response para que múltiples Set-Cookie sobrevivan; LSP + guía cap 17 "Cookies y sesiones"). Tests: 5 unit http (serialización) + 1 E2E oneshot intérprete (valida el .append) + 1 E2E codegen raw-TCP (2 Set-Cookie, paridad) + 3 unit codegen del prelude + 1 LSP. Bonus (post-v0.49.0, 2026-08-20): cerrado el gap de paridad descubierto acá — parse_urlencoded_body ahora coerce el body form-urlencoded al type del handler (paralelo a parse_body/JSON y al __parse_urlencoded→__from_fitz_json del codegen), así el login zero-JS funciona igual en fitz run y fitz build (2 tests nuevos + smoke bit-a-bit).
  • 🟢 RESIDUAL CERRADO (2026-08-21): @cookie ya funciona sobre @ws. Fix completo (multi-archivo, con verificación end-to-end de paridad run ↔ build): (1) el checker de aridad check_ws_handler (src/types.rs) suma cookie_count a expected_params (paralelo a @header); (2) el runtime WS (src/http.rs, build_ws_method_router) clona route.cookies y bindea la cookie del header Cookie del handshake con parse_cookie_header, con manejo post-upgrade (nullable → Null; requerida faltante → cierra la conn, no 400); (3) el codegen del wrapper @ws (src/codegen.rs, gen_ws_handler_wrapper) emite el binding con __fitz_parse_cookie desde el HeaderMap del upgrade, con OR en el gate del move. Tests: 3 unit del checker (ws_handler_with_cookie_accepts_extra_param, ws_handler_cookie_and_header_and_auth_arity, ws_handler_missing_cookie_param_is_error) + 1 E2E de compilación (ws_handler_with_cookie_builds_fitz05) + smoke manual de paridad con un cliente WS que manda Cookie: lang=es en el handshake. El workaround del admin (@header(name="cookie") + locale_from_cookie) ya no es necesario — ahora @cookie(name="X") @ws(...) es directo.
  • Estado: Ya resuelto (FASE A + FASE B + paridad form-urlencoded en el intérprete). Form-urlencoded (mitad del A5 original): REFUTADO — soportado en fitz build y ahora también en fitz run con coerción al type.
  • Evidencia (cookies sin API): src/http.rs:1310-1341 (HandlerOutcome sin campo cookies); sin @cookie/Cookie en src/parser.rs/src/value.rs. docs/guide.md:11436 lista sessions cookie-based como futuro.
  • Evidencia (form-urlencoded — REFUTADO): src/http.rs:4077-4126 + codegen src/codegen.rs:33359, helper __parse_urlencoded (:36113). El autor lo verificó en un probe compilado. <form method=POST> nativo deserializa al mismo type que hoy recibe JSON, sin JS. (Comentario stale de examples/admin/src/auth.fitz ya corregido.)
  • Impacto en un usuario real: la cookie es el único mecanismo viable para el login de familia (el browser no manda Authorization en navegación normal ni en el handshake WS). Con esto + form-urlencoded (ya está), el login de MatHelp es HTML puro. Sin API, cada uno reescribe el parser de Cookie — riesgo de seguridad.
  • Workaround hoy: el del ejemplo admin (auth.fitz / i18n.fitz): concatenar Set-Cookie a mano y parsear Cookie con .split(";")/.split("="). Probado, pero es el parser que no debería reescribir cada uno.
  • Propuesta (API cerrada):
    @cookie(name="session")
    @get("/")
    fn home(session: Str?) -> Response { ... }   // también sobre @ws (upgrade es HTTP)
    
    type Cookie {                 // nominal built-in, como Request / File / Response
        name: Str
        value: Str
        path: Str = "/"
        http_only: Bool = false
        secure: Bool = false
        same_site: Str = "Lax"    // "Strict" | "Lax" | "None"
        max_age: Int? = null       // segundos; null = session cookie
        domain: Str? = null
    }
    return Response {
        status: 303,
        cookies: [ Cookie { name: "session", value: token, http_only: true, max_age: 86400 } ],
        headers: { "Location": "/" },
    }
    
  • Criterio de aceptación: @cookie(name=X) inyecta el valor parseado (o null) en @get/@post/@ws; cada Cookie del cookies se serializa a un Set-Cookie con flags correctos; paridad run ↔ build.
  • Archivos a tocar: src/types.rs (nominal Cookie + validación @cookie, paralelo a @header); src/value.rs; src/http.rs:1310 (cookies + parse_cookie_header + serialización + extractor); src/codegen.rs; src/lsp.rs; docs/guide.md cap 17.
  • Tests: unit de parse_cookie_header; E2E de serialización; E2E de login form-urlencoded → Set-Cookie → @cookie de vuelta.
  • Docs: docs/guide.md cap 17. Cruza con FLV-09.
  • Dependencias: ninguna. Habilita que el admin y docs/i18n.md dejen de parsear cookies a mano.
  • Notas de diseño: lectura por decorador (como @header), escritura por campo cookies (coherente con el Response sin estado). La mitad form-urlencoded no requiere trabajo — solo se documentó.

FITZ-03 · Módulo fs

  • Implementado (2026-08-20) — src/fs.rs con los 8 builtins + intérprete + codegen con paridad (FS_PRELUDE sobre std::fs, sin deps extra) + checker (fs = Any) + LSP + sección de guía "Filesystem" + test E2E de roundtrip run↔build. Detector genérico nuevo program_calls_module (reusable). MVP: main-program (cross-module fs = follow-up); sin sandbox; JSON parse nativo no existe (i18n usa formato clave=valor con .split, o interop Python).
  • Estado: Confirmado.
  • Evidencia: builtin_names() (src/evaluator.rs:246-304) sin fs/read_file/write_file. La única lectura de disco expuesta es load_env(path) (src/evaluator.rs:19017, parser :19065) — solo puebla env vars. El tipo File (src/types.rs:1618-1648) es de multipart/form-data, no abre archivos.
  • Impacto en un usuario real: rompe la arquitectura natural de i18n de MatHelp (catálogos locales/*.json leídos al boot). Sin fs, los catálogos son código compilado → agregar un idioma implica recompilar. También: templates de mail, seeds, CSV, logs — categoría entera que hoy solo se hace bajando a Python.
  • Workaround hoy: script externo que compila los .json a .fitz + @test de claves faltantes. Paso de build que no debería existir.
  • Propuesta (API cerrada):
    fs.read(path) -> Result<Str>          fs.read_bytes(path) -> Result<Bytes>
    fs.write(path, content) -> Result<Null>   fs.append(path, content) -> Result<Null>
    fs.exists(path) -> Bool               fs.list(path) -> Result<List<Str>>
    fs.remove(path) -> Result<Null>       fs.mkdir_all(path) -> Result<Null>
    
    Lectura en runtime (no compile-time): fs.read("locales/es-AR.json") al boot + json.loads.
  • Criterio de aceptación: los 8 builtins con paridad run ↔ build (mismos Ok/Err, mismos mensajes con el path citado); fs.read inexistente → Err, nunca panic; paths relativos al working dir del proceso.
  • Archivos a tocar: src/evaluator.rs:246 (módulo + 8 builtins); src/types.rs; src/lsp.rs:4204; src/codegen.rs (helpers __fitz_fs_*); docs/guide.md.
  • Tests: round-trip write→read→remove; inexistente → Err; fs.list; E2E de paridad.
  • Docs: docs/guide.md sección "Filesystem"; nota sobre working-dir en distroless.
  • Dependencias: ninguna. Habilita T1 (catálogos desde JSON) y feeds FLV-09.
  • Notas de diseño: MVP sin sandbox (backend nativo, fs es esperable); modelo Deno (--allow-read=) como capa opt-in futura. Streaming de archivos grandes (open() con seek) fuera del MVP.

FITZ-04 · Formateo de números con locale

  • Implementado (2026-08-20) — (1) doc stale de format specs YA corregida (guide.md:1266, apunta al módulo num); (2) módulo num (src/num.rs): num.format/num.percent/num.currency con es-AR + en-US, paridad bit-a-bit (NUM_PRELUDE misma lógica de agrupamiento) + checker (Any) + LSP + sección de guía + ejemplo 13x-num-locale.fitz (smoke + corpus de paridad FITZ-14). MVP: args posicionales (kwargs locale: = futuro); tabla de locales embebida (sin ICU).
  • Estado: Parcial. "Sin paridad run↔build" REFUTADO (confirmado empíricamente por el autor: {n:,} → 1,234,567 en el binario). "Sin locale" CONFIRMADO.
  • Evidencia (paridad — REFUTADO): src/codegen.rs:40167-40257 + helpers :13445-13489 implementan grouping/exponente/general/char/percent; docs/guide.md:5042-5078 documenta el cierre "Fm". La tabla stale de docs/guide.md:1266 ya la corregí (2026-08-20). Probe del autor: fitz build de {n:,}|{r:.1%} → "1,234,567|42.0%".
  • Evidencia (sin locale — CONFIRMADO): esa misma salida 1,234,567 / 42.0% en es-AR debería ser 1.234.567 / 42,0 %. __fitz_fmt_grouping(n, sep) inserta cada 3 con el sep elegido, pero el decimal es siempre .; no hay formato europeo ni moneda.
  • Impacto en un usuario real: en Argentina 1.234,5 y $ 1.250. Un juego de matemática que muestra 1,234.5 le está enseñando mal al chico. Pedagógico, no cosmético.
  • Workaround hoy: fmt_num(locale, x) con .replace(). Default silencioso = número mal formateado.
  • Propuesta (API cerrada):
    num.format(1234.5, locale: "es-AR")             // "1.234,5"
    num.percent(0.42, locale: "es-AR", digits: 1)   // "42,0 %"
    num.currency(1250, locale: "es-AR", code: "ARS")// "$ 1.250,00"
    
    Separador decimal + de miles + símbolo de moneda + posición cubren el 95%. Precedente: DateTime.in_tz(iana).
  • Criterio de aceptación: num.* con "es-AR" da formato argentino, con "en-US" inglés; paridad run ↔ build.
  • Archivos a tocar: src/evaluator.rs (módulo num); src/codegen.rs (__fitz_num_* con la misma tabla de locales); src/types.rs/src/lsp.rs. (La doc stale guide.md:1266 ya está corregida.)
  • Tests: @test es-AR / en-US; E2E de paridad.
  • Docs: docs/guide.md (interpolación) — ya corregido el falso "solo fitz run".
  • Dependencias: parte de T1.
  • Notas de diseño: tabla de locales embebida (es-AR, en-US, algunos más), sin ICU. Mantener los format specs existentes; num.* es la vía locale-aware.

FITZ-02 · Servido de archivos estáticos

  • Implementado (2026-08-20, v0.51.0) — @server(port, static_dir="./public", static_prefix="/static") con módulo compartido src/static_files.rs (Content-Type por extensión, ETag basado en contenido, HTTP-date, is_safe_relative), intérprete (src/http.rs: handler wildcard bajo el prefijo, If-None-Match → 304, Cache-Control, Last-Modified, path-traversal bloqueado lexical + canonicalize+containment), y codegen con paridad bit-a-bit (STATIC_PRELUDE_* con handler de disco Y de embed, .merge(__fitz_static_route()); los __fitz_static_* mirror literal de static_files.rs). Flag fitz build --embed-static hornea los assets en el binario con include_bytes! → sirve su propio frontend sin el dir en disco (distroless). Checker sin cambios (@server es opaco al checker). LSP completion de @server cita los kwargs nuevos. Guía cap 17 "Archivos estáticos" + nota deployment distroless + ejemplo 17m-static.fitz. Tests: 8 unit static_files + 2 unit http (resolved_static_prefix/if_none_match_matches)
  • 6 unit codegen (prelude/route/embed/collect) + 2 E2E (fitz02_static_disk_parity_content_type_etag_304_traversal paridad run↔build + 304 + traversal + missing + user-route; fitz02_embed_static_serves_without_dir_on_disk). Validado a mano con curl: fitz run, fitz build (disco, ETag bit-a-bit), fitz build --embed-static (sin public/). Cierra Hito ¾ entero. Habilita T3 (PWA instalable). MVP: sin directory index (un dir → 404); embed sin Last-Modified (no hay mtime en memoria); assets resueltos relativos al working dir del proceso (runtime) / del build (embed).
  • Estado: Ya resuelto.
  • Evidencia: src/evaluator.rs:1573-1744 (allowlist cerrada de kwargs de @server, error en :1740); grep ServeDir|static_dir|@static → 0. Boilerplates fullstack con nginx aparte (boilerplates/taskhub/docker-compose.yml:92-101).
  • Impacto en un usuario real: favicon, manifest.webmanifest (instalable → T3), CSS, sonidos. nginx al lado duplica la infra de una app que si no es un binario + Postgres.
  • Workaround hoy: "assets como rutas" (@get por archivo con Response/body_bytes + bytes_from_b64). Sin caching, sin ETag, llena el binario de rutas.
  • Propuesta (API cerrada): @server(3000, static_dir="./public", static_prefix="/static") con Content-Type por extensión, ETag/If-None-Match, Cache-Control, Last-Modified, path-traversal bloqueado. Evaluar fitz build --embed-static (assets dentro del binario → encaja con distroless).
  • Criterio de aceptación: GET a /static/foo.css sirve con Content-Type + ETag + 304; ../ rechazado; paridad run ↔ build; con --embed-static, sirve sin el dir en disco.
  • Archivos a tocar: src/evaluator.rs:1573 (kwargs); src/http.rs (ServeDir/handler + ETag/traversal); src/codegen.rs (montaje + --embed-static con include_bytes!); src/main.rs (flag); docs/guide.md.
  • Tests: E2E de estático (Content-Type/ETag/304); traversal rechazado; --embed-static.
  • Docs: docs/guide.md cap 17 + nota de deployment distroless.
  • Dependencias: habilita T3. Reduce/elimina el nginx de los boilerplates.
  • Notas de diseño: ServeDir de tower-http en runtime; include_bytes! en codegen. El embed es el diferencial real ("un binario que sirve su propio frontend").

FITZ-13 · Map.remove(key) [antes FITZ-09]

  • Implementado (2026-08-20, post-v0.49.0) — m.remove(key) -> Bool (true si existía), muta el Map in place (semántica de referencia compartida, visible por cualquier alias). Evaluator (map_remove, búsqueda lineal + Vec::remove preservando orden) + codegen con paridad bit-a-bit (gen_map_remove, .remove sobre el Vec<(K, V)>; Arc ligado a un local para no dropear el MutexGuard temporal) + checker (Map<K,V>.remove(K) -> Bool) + LSP (after-dot + signature catalog) + guía (sección métodos de Map). Tests: 1 unit evaluator (mutación vía alias) + 1 E2E de paridad run↔build (map_remove_parity_fitz13). Desbloquea FLV-03 (eviction del store de componentes en fitz-liveviews).
  • Estado: Confirmado (hallazgo derivado de FLV-03).
  • Evidencia: fitz-liveviews/src/lib.fitz:2206-2210 (el store no puede evictar: "Map has no remove yet"); fitz-liveviews/docs/components.md:398. Métodos de Map en el core: get/has/keys/values/len, sin remove.
  • Impacto en un usuario real: indirecto — bloquea la eviction de estado de componentes (FLV-03). Sin Map.remove, el store del framework crece para siempre (leak lento). MatHelp, con chicos que abandonan partidas, lo siente.
  • Workaround hoy: ninguno limpio del lado del framework. MatHelp mitiga persistiendo a Postgres.
  • Propuesta (API cerrada): m.remove(key) -> Bool (true si existía).
  • Criterio de aceptación: borra la entrada y devuelve si existía; paridad run ↔ build. Habilita flv_drop_instance/eviction en fitz-liveviews.
  • Archivos a tocar: src/evaluator.rs (dispatch de Value::Map); src/codegen.rs (.remove() sobre el Vec<(K,V)> cuidando el orden); src/types.rs/src/lsp.rs; docs/guide.md.
  • Tests: unit del remove; E2E de paridad.
  • Docs: docs/guide.md (métodos de Map).
  • Dependencias: desbloquea FLV-03. Prerequisito de menor costo / mayor apalancamiento cross-repo.
  • Notas de diseño: el Map interno es Vec<(K,V)> con orden de inserción; remove es búsqueda lineal + retain. Barato.

FITZ-06 · .preload() en el intérprete

  • Implementado — MVP "error-claro-primero" (2026-08-20, post-v0.49.0). La premisa original del norte ("no-op silencioso") quedó STALE: desde v0.47.0 el dispatch cambió y .preload() en fitz run ya daba un error genérico ("QueryBuilder has no method preload"), no un no-op silencioso. Ahora da un error DEDICADO en ambos dispatches (QueryBuilder + Type directo) que apunta a fitz build + workarounds (navigation method user.posts(db) o .where separado). Helper preload_not_in_interpreter_error. 1 unit test (fitz06_preload_in_interpreter_gives_dedicated_error). El criterio de aceptación se cumple ("aborta con mensaje claro, nunca no-op silencioso"). Follow-up (Costo M, NO bloquea): implementar el eager loading real en el intérprete para paridad con el codegen (cargar las relaciones en fitz run).
  • Estado: Ya resuelto (MVP error-claro; paridad real = follow-up).
  • Evidencia: CHANGELOG.md:71-72; src/evaluator.rs:14670-14673 (cae al _ => Ok(None), no-op silencioso, ni error dedicado); implementado en codegen (src/codegen.rs:23814, :24933).
  • Impacto en un usuario real: el panel del padre lista perfiles con progreso (Family → Profile → Mastery). Funciona compilado, falla en fitz run. Peor: no-op silencioso (relaciones vacías sin error). Es una trampa (T2).
  • Workaround hoy: en fitz run, queries de la relación a mano o desarrollar con fitz build.
  • Propuesta: implementar .preload() en el intérprete (paridad), o — MVP barato — que fitz run emita error claro en vez del no-op silencioso.
  • Criterio de aceptación: .preload() carga las relaciones en fitz run (paridad), o aborta con mensaje claro. Nunca no-op silencioso.
  • Archivos a tocar: src/evaluator.rs:14670 (rama "preload").
  • Tests: E2E .preload() en fitz run carga o falla con el mensaje esperado. Lo caza FITZ-14.
  • Docs: docs/db-orm.md.
  • Dependencias: parte de T2.
  • Notas de diseño: el error-claro-primero elimina la trampa en horas mientras la implementación real espera turno.

FITZ-07 · .is_in(<var>) → = ANY($n)

  • Implementado (2026-08-20, post-v0.49.0) — is_in(<var>) con una variable List<T> del scope externo del closure emite "col" = ANY($N::<oid>[]), bindeando la lista entera como UN solo parámetro array (el OID sale del tipo escalar de la columna). La lista literal sigue emitiendo IN ($1, $2, ...). Evaluator (scalar_pg_oid + resolución del var vía env
  • fitz_list_to_pg_array) + codegen con paridad bit-a-bit (orm_scalar_pg_info_from_type_expr
  • binding array inline que lockea el Arc del var y mapea cada elem con __IntoPgValue::into_pg). Doc-comment corregido (ya no miente). Tests: 2 unit del translator (var Int → = ANY($1::int8[]), var Str → text[]) + 2 existentes (literal IN + empty false) intactos + 1 E2E real Postgres (orm_where_is_in_variable_fitz07). Validado run↔build bit-a-bit contra Postgres local. Doc db-orm.md sec is_in reescrita + tabla de var-support actualizada.
  • Estado: Confirmado.
  • Evidencia: src/evaluator.rs:17280-17307 y src/codegen.rs:20877-20911 — is_in solo matchea Expr::List literal; emite IN ($1,$2,...), no ANY($n). Doc-comment stale en src/evaluator.rs:17207-17209 promete soporte de variable sin cumplirlo.
  • Impacto en un usuario real: el motor adaptativo necesita Mastery.where(fn(m) => m.skill_code.is_in(pendientes)) con pendientes: List<Str> calculada. Hoy no se puede.
  • Workaround hoy: db.query("... = ANY($1)", [pendientes]) crudo — pierde type-safety.
  • Propuesta: aceptar variable List<T> → "col" = ANY($n) (lista entera a un solo parámetro array). La lista literal sigue igual.
  • Criterio de aceptación: is_in(pendientes) compila y emite = ANY($n); literal sin regresión; paridad; doc-comment :17207 corregido.
  • Archivos a tocar: src/evaluator.rs:17280, src/codegen.rs:20877, doc-comment :17207.
  • Tests: los existentes + variante var; E2E real Postgres.
  • Docs: docs/db-orm.md.
  • Dependencias: ninguna. Bajo (hay workaround), pero cierra un doc-comment que miente.
  • Notas de diseño: = ANY($n) es la forma canónica; mantener la lista literal por su camino.

FITZ-12 · Paréntesis redundantes en el match generado

  • Implementado (2026-08-20, post-v0.49.0) — helper strip_stmt_match_parens (scanner balanceado que skipea strings, fail-safe: solo strippea un (match … ) completamente parentizado) aplicado en gen_return y gen_assign (las 3 ramas: reasignación, let _, let mut). Un let x = match … / return match … deja de emitir los paréntesis externos; las posiciones de operando/receptor ((match …).foo(), 1 + (match …)) los conservan (el helper solo se llama desde los emisores de statement). 2 tests (helper + output sin (match n) + smoke real (main.rs generado con 0 (match, binario corre OK).
  • Estado: Confirmado (cosmético).
  • Evidencia: rustc avisa unnecessary parentheses around ... match (ej. let mut volver: String = (match referer.clone() { ... });). Un build de MatHelp emite 194 warnings, buena parte de este patrón.
  • Impacto en un usuario real: Bajo. El ruido tapa los warnings que sí importan (y hoy, con FITZ-09 sin arreglar, cuesta ver los errores reales entre 194 warnings).
  • Workaround hoy: ninguno (es ruido, no rompe).
  • Propuesta: no envolver el match en paréntesis cuando es el RHS de un let/return/asignación (el match de Rust ya es una expresión válida en esas posiciones).
  • Criterio de aceptación: un build de un programa con match como valor no emite el warning unnecessary parentheses.
  • Archivos a tocar: src/codegen.rs — la emisión de match como expresión (buscar dónde se envuelve en (...)).
  • Tests: unit que el Rust generado para let x = match ... no tiene los paréntesis externos.
  • Docs: — (interno).
  • Dependencias: ninguna. Barato; limpia el ruido de warnings.
  • Notas de diseño: cuidar los casos donde el paréntesis sí hace falta (p.ej. (match ...).método() o dentro de un binop) — solo sacarlo en posición de statement/RHS directo.

FITZ-08 · ENUM nativo de Postgres

  • Implementado — candidata segunda tanda (prioridad baja; @check_constraint cubre)
  • Estado: Confirmado (prioridad baja).
  • Evidencia: boilerplates/api-orm-full/src/models.fitz:24-26 (no soportado en ORM MVP). @check_constraint SÍ existe (src/types.rs:3162-3228, src/migrations.rs:916-941).
  • Impacto en un usuario real: mode (quiz | truefalse | ...) queda como Str sin tipo enum. Bajo.
  • Workaround hoy: Str + @check_constraint("mode IN (...)") — existe y funciona con migración/drift.
  • Propuesta: ENUM nativo (CREATE TYPE ... AS ENUM) en ORM + migraciones. Costo L. No pedido.
  • Criterio de aceptación: campo enum materializado como CREATE TYPE + columna, con migración/drift.
  • Archivos a tocar: src/types.rs, src/migrations.rs, src/codegen.rs, src/db.rs.
  • Tests: E2E real Postgres con enum.
  • Docs: docs/db-orm.md.
  • Dependencias: ninguna. No prioritario — @check_constraint cubre.
  • Notas de diseño: documentar el patrón Str + @check_constraint como camino recomendado hoy es más barato que el enum nativo.

FITZ-21 · Loader: un test que se llama igual que el módulo que importa se auto-importa (ciclo)

  • Cerrado 2026-08-23 (fitz v0.59) — el Loader (src/evaluator.rs) gana entry_file: Option<PathBuf> (el path canónico del archivo entry, que se evalúa directo y nunca entra al stack loading). En el loop de canonicalización de load_module se computa self_path = loading.last() .or(entry_file) y se saltea el candidato que canonicaliza al self, así la resolución sibling-first cae a import_root (src/foo.fitz) en vez de ciclar/re-cargarse. eval_with_base_import_root_and_deps + install_loader_with_root reciben el entry_file; el runner de fitz test (eval_test_source) pasa fs::canonicalize(path). Un ciclo mutuo A→B→A sigue detectándose (self=B, candidato=A). Tests: 1 unit (fitz21_test_entry_homonymous_import_resolves_to_src_sibling) + 1 cli_e2e (fitz21_test_homonymous_import_resolves_to_src_not_cycle).
  • Estado: Confirmado (repro). Descubierto en MatHelp F3 (motor adaptativo); re-pegado en MatHelp F4 (2026-08-22): tests/keypad.fitz con from keypad import ... (apuntando a src/keypad.fitz) → ciclo; hubo que renombrar a tests/teclado.fitz. Sigue siendo fricción real cada vez que se agrega un módulo con su test homónimo.
  • Impacto: Bajo (workaround trivial: renombrar). Pero es un mensaje confuso.
  • Evidencia: tests/engine.fitz con from engine import ... (apuntando a src/engine.fitz) falla fitz test con Error — ciclo de imports detectado: tests/engine.fitz -> tests/engine.fitz. El fallback import_root (FITZ-20) resuelve primero el hermano en tests/ (que es el propio archivo) antes de caer a src/; como el hermano existe (es él mismo), nunca llega al src/.
  • Repro mínimo: src/foo.fitz con fn bar() => 1; tests/foo.fitz con from foo import bar + un @test. fitz test → ciclo.
  • Workaround (aplicado en la app): nombrar el test distinto del módulo (tests/motor.fitz en vez de tests/engine.fitz).
  • Propuesta: en la resolución sibling-first, saltar el archivo que se está cargando (self): si el único candidato hermano es el propio archivo en curso, caer al import_root fallback.
  • Archivos a tocar: el loader del runner de fitz test (resolución import_root).
  • Criterio de aceptación: tests/engine.fitz con from engine import X resuelve a src/engine.fitz, no a sí mismo.

FITZ-22 · fitz check no caza aridad incorrecta en llamada a fn importada (check✓/build✗)

  • Cerrado 2026-08-23 (fitz v0.59) — nuevo pre-scan pre_scan_imported_fn_signatures (src/main.rs, paralelo a W12/B10) que, por cada from m import f, resuelve la firma real de fn f (extract_fn_signatures en src/types.rs), la mapea bajo el binding local (alias-aware) y la mete en TypeEnv.imported_fn_sigs. El handler Stmt::FromImport del checker la registra como Type::Function (con defaults/varargs) en vez de Type::Any → el call site valida aridad/tipos con la maquinaria existente (regla 5.3.2). Un param nominal no importado por el caller resuelve a Any (gradual-safe, sin falso positivo); la aridad igual se valida. Sin falsos positivos: fitz check examples/admin (importa todo el framework, multi-módulo) queda limpio. Tests: 7 unit (fitz22_*: aridad, tipos, alias, default, varargs, nominal-no-importado) + 2 cli_e2e (fitz22_check_catches_wrong_arity_imported_fn + correct-arity guard).
  • Estado: Confirmado (repro). Clase T2 (paridad check/build). Descubierto en MatHelp F3.
  • Impacto: Medio — es exactamente el patrón "pasa check, rompe build" que T2 quiere erradicar. Un refactor de firma de una fn muy usada cross-módulo no se caza en fitz check; recién aparece en fitz build (que sí valida aridad en el codegen).
  • Evidencia: en MatHelp, gen_for (10 params, en src/gen_arith.fitz) llamada con 6 args desde src/live_game.fitz (persistir_practica): fitz check dijo "sin errores"; fitz build cortó con codegen: Error ... gen_for expects 10 argument(s), got 6.
  • Repro mínimo: src/m.fitz con fn f(a: Int, b: Int) -> Int => a + b; otro módulo con from m import f y let x = f(1). fitz check pasa; fitz build falla por aridad.
  • Hipótesis (a confirmar por el core): el checker tipa las fns importadas de forma laxa (Any) y no valida aridad cross-módulo; el codegen sí resuelve la firma real y valida.
  • Propuesta: el checker debería resolver la firma Function{params, ret} de una fn importada (ya tiene la info del módulo) y validar aridad/tipos en el call site, como lo hace para fns del mismo módulo (regla 5.3.2).
  • Archivos a tocar: src/types.rs (resolución de fns importadas + Expr::Call).
  • Criterio de aceptación: una llamada con aridad incorrecta a una fn importada es error de fitz check, no solo de fitz build.

FITZ-26 · fitz check no valida field-access sobre el retorno primitivo de una fn importada cross-módulo (check✓/run✗)

  • Cerrado (v0.61.0, 2026-09-22). Descubierto en MatHelp, Lote A (emails de registro), 2026-09-22.
  • Fix: pre_scan_imported_fn_signatures (src/main.rs) ahora sigue re-exports transitivos. El sibling plano YA se cazaba (el pre-scan resuelve la firma directa → FITZ-24 dispara); el hueco real era el re-export (fitz-liveviews expone flv vía from internal import flv en su lib entry). Cuando la fn no es un fn directo del módulo importado, el nuevo helper recursivo resolve_imported_fn_sig busca el from <sub> import <fn> que la re-exporta y resuelve la firma en el sub-módulo (guard de profundidad = 4 contra ciclos; helper read_parse_module_program compartido). Con la firma poblada, f("x") tipa Str y el field-access .raw dispara el error de FITZ-24. Repro confirmado: sibling plano ya erraba; path dep también; re-export transitivo daba 0 errores → ahora erra. Tests: 2 cli_e2e (fitz26_check_catches_field_access_on_reexported_fn_str_return + regression guard ..._legitimate_use_passes).
  • Estado: Confirmado (repro). Clase check✓/run✗ — la MISMA familia que FITZ-24, pero cross-módulo.
  • Impacto: Medio. FITZ-24 (v0.60.0) cerró el field-access sobre primitivos concretos locales (let s: Str = "x"; s.raw → error de check). Pero cuando el receptor es el retorno de una fn importada cuya firma es primitiva, el checker no lo caza: pasa fitz check y explota en runtime con field access .raw on a value of type Str.
  • Evidencia: en MatHelp emails.fitz, flv(fam.display_name).raw — flv está en fitz-liveviews con firma fn flv(s: Str) -> Str (devuelve Str, no un Html; el .raw va sobre html(...), no sobre flv(...)). fitz check dio 0 fallas; en runtime el handler de registro devolvió 500.
  • Repro mínimo: m.fitz con fn f(s: Str) -> Str => s; otro módulo con from m import f y let r = f("x").raw. fitz check pasa; fitz run rompe con field-access sobre Str.
  • Hipótesis: FITZ-22 (v0.59) ya resuelve la firma de la fn importada (imported_fn_sigs), pero el refinamiento de FITZ-24 (field-access sobre primitivos concretos en Expr::Field) no dispara cuando el receptor es el retorno de esa fn — probablemente porque ese nodo no llega tipado como el primitivo concreto al chequeo de field, o la firma importada se sigue tratando laxa en ese punto.
  • Propuesta: cuando el receptor de un Expr::Field es una llamada a fn importada con retorno primitivo resuelto (Str/Int/Float/Bool/…), aplicar la misma regla de FITZ-24 (error si no es un tipo con campos). Reusa imported_fn_sigs de FITZ-22.
  • Archivos a tocar: src/types.rs (Expr::Field + resolución del tipo del receptor cuando es Expr::Call a fn importada).
  • Criterio de aceptación: f("x").raw con f importada que retorna Str es error de fitz check.
  • Workaround en MatHelp: ninguno necesario — el código correcto usa flv(x) directo (Str ya escapado). El ticket es para cerrar el hueco del checker, no un bug de la app.

FITZ-27 · spawn(fn()) desde un handler @post traga en silencio los errores de runtime de la task

  • Cerrado (v0.61.0, 2026-09-22). Descubierto en MatHelp, Lote A (emails de registro), 2026-09-22.
  • Fix: eval_spawn_call (src/evaluator.rs) — en el camino in-memory del tokio::spawn, cuando el resultado de la task es Err, se emite un log estructurado emit_log_record("error", "spawned task failed", …) con el nombre de la fn spawnada y el mensaje del error, ANTES de que el Value::Future se dropee sin awaitear (fire-and-forget). Paralelo a cómo @cron (cron_jobs.rs) y @background persistente (background_jobs.rs) reportan fallos de sus tasks. Si el caller SÍ awaitea el Future, el error también propaga (doble señal aceptada, como fire-and-forget). Test: E2E fitz27_spawned_task_runtime_error_is_logged_not_swallowed (spawn de una fn con 1/0, sin .await, + sleep().await para dar tiempo; verifica stderr contiene spawned task failed + el nombre de la fn, y el programa completa normal).
  • MatHelp: cuando MatHelp bumpee a v0.61.0 puede revertir el workaround de .await inline en registro_post a spawn(signup_emails(fam)) — los errores de la task ya no se tragan. (El trace_id del request originante en el log queda como mejora futura: el SpanContext es task-local de tokio y no se propaga automáticamente al spawn.)
  • Estado: Confirmado (repro). Clase observabilidad/confiabilidad del runtime del intérprete.
  • Impacto: Alto para debugging. Un spawn(g()) disparado desde un handler HTTP @post, si g tiene un error de runtime, la task muere sin log y el handler devuelve normal (el 303 salió perfecto). El bug quedó invisible: parecía que el spawn "no corría"; en realidad corría, fallaba y el error se perdía. Recién al pasar a .await inline el error propagó (500) y se pudo diagnosticar.
  • Evidencia: en MatHelp auth.fitz registro_post, spawn(signup_emails(fam)) donde signup_emails tenía el bug de FITZ-26 (flv(...).raw): registro devolvía 303, cero logs, cero error — la task moría en silencio. Con .await inline: 500 visible + diagnóstico.
  • Repro mínimo: un handler @post que hace spawn(g()) donde g es @background async fn con un error de runtime garantizado (p. ej. .raw sobre un Str). El POST responde OK; no hay log del panic/error de la task.
  • Hipótesis: el tokio::spawn interno del intérprete descarta el Result/panic del JoinHandle sin loguearlo. Comparar con el path @ws/@cron, que sí parecen loguear (o al menos el spawn de @cron reporta fallos).
  • Propuesta: loguear (nivel error, estructurado, con el nombre de la fn spawnada y el trace_id del request originante si aplica) cualquier error/panic de una task spawnada, en vez de tragarlo. Idealmente unificado con cómo @cron/@background persistente reporta fallos.
  • Archivos a tocar: src/evaluator.rs (eval_spawn_call / el wrapper del tokio::spawn).
  • Criterio de aceptación: un error de runtime dentro de una fn spawnada produce un log de error visible; el spawn deja de ser un agujero negro.
  • Workaround en MatHelp: registro_post usa .await inline en vez de spawn (best-effort, rápido por el guard de mailer, correlacionado por trace_id). Revertir a spawn(signup_emails(fam)) cuando FITZ-27 se cierre (para no bloquear el alta con la latencia de Resend).

FITZ-28 · Un type usado como body de un handler @post, definido DESPUÉS del handler en el mismo módulo, hace 500 en runtime (check✓/run✗)

  • Cerrado (v0.61.0, 2026-09-22). Descubierto en MatHelp, Lote B (recuperación de clave), 2026-09-22.
  • Fix: nuevo helper preregister_type_defs(program, env) (src/evaluator.rs) que registra los Stmt::TypeDef top-level ANTES del loop de evaluación top-down, en AMBOS entry points: eval_with_base_import_root_and_deps (el main) y load_module (módulos importados — el caso real de MatHelp recuperar.fitz). Es seguro: registra el Value::Type con resolved_defaults VACÍO → no ejecuta código de usuario (los defaults siguen lazy, evaluados por struct-lit / body coercion con el env correcto), así NO cambia el orden de evaluación de defaults (PreF8.3). El loop top-down re-define el mismo valor idempotente. Con el type disponible, register_http_route resuelve el body param eager (deja de guardar None → deja de deserializar el body como Map libre → el field-access typed del handler funciona → 200 en vez de 500). Test: fitz28_body_type_defined_after_handler_resolves_and_returns_200 (src/http.rs, vía run_oneshot_with_body); repro real confirmado: sin el pre-scan → 500 con field access .name on a value of type Map; con el pre-scan → 200.
  • Estado: Confirmado (repro decisivo). Clase check✓/run✗ del intérprete (registro de rutas HTTP).
  • Impacto: Medio-alto en DX. fitz check pasa, pero el POST devuelve 500 antes de entrar al handler (ni el primer log.info del cuerpo dispara). El 500 no imprime detalle (lo traga el panic-catch del wrapper HTTP), así que es dificilísimo de diagnosticar — costó ~4 reboots aislarlo.
  • Evidencia: en recuperar.fitz los type PedirForm { email } / type ResetForm { password } estaban al FINAL del archivo, después de los @post que los reciben como body. fitz check 0 fallas; POST /recuperar → 500 sin ningún log. Al mover los dos type ARRIBA (antes de los handlers, como en auth.fitz) → 200 y todo el flujo anda. Fue el único cambio entre el 500 y el 200.
  • Contraste: un handler @get con solo path params ({id}/{token}, sin body) sobre el mismo módulo NO se ve afectado (respondía 200 con los type abajo). El problema es exclusivo del body type (que el registro de la ruta resuelve al arrancar, y el forward-ref no está resuelto).
  • Repro mínimo: un módulo con @post("/x") async fn h(input: Body) -> Response { ... } y, MÁS ABAJO en el mismo archivo, type Body { campo: Str = "" }. fitz check pasa; el POST hace 500.
  • Hipótesis: el registro de rutas del evaluador resuelve el Value::Type del body param al procesar el decorator del handler (top-down). Si el type se define después, en ese punto no está en el env → queda sin resolver, y la coerción del body urlencoded/JSON a ese type panica en el request. Los type deberían hoistearse (pre-scan del módulo) antes de registrar las rutas, igual que se pre-registran las firmas de fns.
  • Propuesta: pre-escanear los Stmt::TypeDef del módulo y registrarlos ANTES de procesar los decoradores @get/@post/@put/@delete/@ws que puedan referenciarlos como body/param. Alternativa mínima: que el 500 por body-type no resuelto sea un error claro (nombrando el type) en vez de un panic mudo — o mejor, que fitz check lo cace (check✓/run✗).
  • Archivos a tocar: src/evaluator.rs (registro de rutas / resolución del body type del handler).
  • Criterio de aceptación: un body type definido después del handler en el mismo módulo funciona en runtime (o, como mínimo, fitz check lo reporta en vez de romper con 500 mudo).
  • Workaround en MatHelp: definir los body types ANTES de los handlers que los usan (convención ya presente en auth.fitz; aplicada en recuperar.fitz). Sin deuda de código pendiente — es una convención de orden, no un workaround feo.

FITZ-29 · Un Map<Str, Any> literal construido en un helper que retorna Result<Map<Str, Any>> no envuelve los valores en __FitzValue en fitz build (check✓/build✗)

  • Cerrado (v0.61.0, 2026-09-22). Descubierto en MatHelp, Lote D1 (export de datos), 2026-09-22.
  • Fix: gen_return (src/codegen.rs), bloque de propagación Ok/Err — cuando el return es Ok(Expr::Map) y el inner-type del Result esperado es Map<_, Any> (o Nullable(Map<_,Any>)), se llama gen_map_lit_with_hint(pairs, span, Some(tok)) en vez de gen_expr(inner), propagando el hint que envuelve cada entrada en __FitzValue (shape Vec<(FV, FV)>). Replica el patrón ad-hoc de gen_assign/gen_module_top_let (v0.55/v0.56); el hueco era exactamente el return Ok({...}). Repro confirmado: un map homogéneo por contenido (todos Str) con destino Map<Str,Any> → pre-fix 6× E0308, post-fix compila. Test: fitz29_map_str_any_in_result_ok_builds_and_matches_interpreter (paridad run↔build). Hueco paralelo NO cubierto (deuda residual, ver deudas-post-5b): return Ok([...]) con destino Result<List<Any>> homogéneo — no existe gen_list_lit_with_hint; requiere un helper nuevo. El caso Map (el reportado) queda cerrado.
  • Estado: Confirmado (repro real). Clase check✓/build✗ del codegen. Familia de los fixes v0.10.4/v0.10.5/W1/v0.55/v0.56 sobre Map<Str, Any>, pero por un camino nuevo no cubierto.
  • Impacto: Medio. fitz check + fitz run OK (el intérprete arma el Map heterogéneo sin problema); fitz build rompe con error[E0308]: expected __FitzValue, found String por cada entrada del map.
  • Evidencia: en MatHelp cuenta.fitz, async fn build_export(fam) -> Result<Map<Str, Any>> con return Ok({"email": fam.email, "display_name": ..., "locale": ...}) (y listas de sub-maps). El codegen representa Map<Str, Any> como Vec<(__FitzValue, __FitzValue)> pero emitió los valores como String crudo — no los envolvió en __FitzValue::Str(...). 6× E0308.
  • Hipótesis: el hint Map<_, Any> que dispara gen_map_lit_with_hint (que sí envuelve en __FitzValue) NO se propaga a través de Ok(...) al map literal interno cuando la fn retorna Result<Map<Str, Any>>. Los casos cubiertos (v0.55/v0.56) eran let x: Map<Str,Any> = {...} y top-level de módulo; el return Ok({...}) en una fn -> Result<Map<Str,Any>> es el hueco.
  • Propuesta: propagar el hint Map<_, Any> desde el return type Result<Map<_,Any>> a través del Ok(...) hacia el map literal (y análogamente a través de List/nested).
  • Workaround en MatHelp: reemplazar el Map<Str, Any> por tipos nominales (type ExportData {...}) que el codegen serializa a JSON limpio. Cerrado en la app (mejor tipado igual).

FITZ-30 · Un type definido en un MÓDULO y retornado como JSON desde un handler de ESE módulo no recibe impl __ToFitzJson si tiene un campo nominal anidado no importado al main (check✓/build✗)

  • Cerrado (v0.61.0, 2026-09-22). Descubierto en MatHelp, Lote D1 (export de datos), 2026-09-22.
  • Fix (Opción A — auto-registro, paralelo a v0.45/FITZ-15): nuevo pase auto_register_module_type_field_nominals(env, modules) (src/codegen.rs), enganchado en generate_main_rs junto a auto_register_relation_targets / auto_register_imported_fn_ret_nominals, mergeado a synth_rel_targets. Worklist BFS que, sembrando desde los CAMPOS de todos los type_sigs de los módulos, mintea (con env.declare_nominal) cada nominal anidado transitivamente alcanzable que algún módulo DEFINE pero el env del main no tiene. Con el nominal registrado, remap_imported_nominals deja de degradarlo a Type::Any, has_opaque_field da false, y emit_helpers_for_imported_types emite impl __ToFitzJson para el tipo (antes lo skipeaba → E0599). Sólo destraba nominales que algún módulo define; los genuinamente opacos siguen degradando (sin regresión). Test E2E fitz30_module_type_nested_nominal_gets_to_json_impl (proyecto worker+main: Outer { inner: Inner } retornado por @get, main importa el módulo por namespace sin importar Inner/Outer). Repro confirmado: pre-fix E0599, post-fix buildea. MatHelp puede revertir el workaround W17 (importar todos los tipos anidados al entry).
  • Estado: Confirmado (repro real, leído el codegen). Clase check✓/build✗. Extensión de W9/W17 (v0.10.7) a tipos planos (no-@table).
  • Impacto: Medio-alto (build roto). error[E0599]: ExportDataData: __ToFitzJson is not satisfied.
  • Causa raíz (leída en src/codegen.rs): generate_module_rs_with_bindings NO llama gen_type_http_impls para los type_defs del módulo — solo el main lo hace (loop en ~9389) y las impls de tipos de módulos las emite emit_helpers_for_imported_types (desde el main). Ese emisor, al procesar un tipo de módulo con un campo nominal anidado (family: ExportFamily, sibling del mismo módulo), corre remap_imported_nominals: si ExportFamily NO está importado en el main, degrada el campo a Type::Any → has_opaque_field → saltea el impl __ToFitzJson de ExportData → E0599. La lista List<ExportProfile> cae en la excepción W11 (List legítimo) y no dispara el skip, pero el nominal anidado directo sí.
  • Repro mínimo: módulo m.fitz con type Inner { x: Str } + type Outer { inner: Inner } + @get("/x") async fn h() -> Result<Outer> { ... }. El main NO importa Inner/Outer. fitz check OK; fitz build → OuterData: __ToFitzJson is not satisfied.
  • Propuesta: (a) que generate_module_rs_with_bindings emita gen_type_http_impls para los tipos locales del módulo cuando el módulo tiene HTTP (en vez de delegar 100% al main); o (b) auto-registrar los nominales anidados alcanzables desde tipos retornados por handlers (paralelo a auto_register_relation_targets de v0.45, que ya lo hace para relations @table).
  • Workaround en MatHelp: importar TODOS los tipos anidados al entry — from cuenta import ExportFamily, ExportProfile, ExportSession, ExportData en main.fitz (patrón W17). Cerrado en la app.

FITZ-31 · Una variable Fitz con nombre de keyword RESERVADA de Rust (priv, move, ref, …) rompe fitz build (check✓/build✗)

  • Cerrado (v0.61.0, 2026-09-22). Descubierto en MatHelp, Lote E1 (páginas legales), 2026-09-22.
  • Fix: helper sanitize_var_ident(name) (src/codegen.rs) — emite r#<name> para keywords de Rust que admiten raw identifier (priv, move, ref, dyn, impl, trait, enum, unsafe, box, yield, …), y renombra con prefijo __fitz_kw_ las 6 que NO lo admiten (crate/self/Self/super/true/false). Lista canónica RUST_KEYWORDS + is_rust_keyword compartida con validate_rust_ident. Aplicado en TODOS los sitios de binding de variable: let decl + reassign + uso (Expr::Ident), params de fn (se removió el validate_rust_ident que erraba — ahora compilan), todos los for-loops (range/List/tuple/Map), match bindings (Ident/Ok/Err), closure/method/ callback params, capture rebind, transaction callback, y destructure. El scope-map interno mantiene el nombre Fitz original como clave; sólo la emisión Rust se sanea → decl y uso consistentes por construcción → byte-idéntico para nombres normales (smoke ~290 verde). Los nombres de fn/type + campos de struct + top-level consts con nombre reservado quedan con el error claro de validate_rust_ident (deuda residual menor, ver deudas-post-5b). Elegido "que compile" (paridad run↔build total) sobre "error claro". Tests: 5 unit (sanitize_var_ident + emisión) + 2 E2E de paridad (fitz31_reserved_rust_keywords_as_vars_build_and_match_interpreter + ..._in_match_binding_...).
  • Estado: Confirmado (repro real). Clase check✓/build✗ — el codegen no sanea nombres de variable que colisionan con keywords reservadas de Rust.
  • Impacto: Bajo-medio pero muy confuso. fitz check + fitz run OK (priv es un identificador Fitz válido); fitz build corta con error: expected identifier, found reserved keyword \priv``.
  • Evidencia: en MatHelp brand.fitz::footer, let priv = flv(t(locale, "legal.priv.titulo")) → el codegen emitió let priv = ...; en Rust → priv es keyword reservada de Rust → 2 errores.
  • Repro mínimo: let priv = "x" + usarla; fitz check/run OK, fitz build rompe. Idem con otras reservadas de Rust que NO son keywords de Fitz: move, ref, dyn, impl, trait, enum, unsafe, where, box, become, final, override, virtual, yield, macro, union, etc.
  • Hipótesis: el codegen mapea 1:1 el nombre de la variable Fitz al de Rust sin escapar/renombrar las keywords reservadas de Rust. Fitz ya rechaza sus PROPIAS keywords como nombres, pero el conjunto de reservadas de Rust es más grande.
  • Asimetría observada (MatHelp E1 + E5): los params de fn/handler SÍ están chequeados con un error claro del codegen (param: name \ref` is a reserved Rust keyword — rename in Fitz), pero las **variablesletlocales NO** (let priv = ...se cuela hasta rustc con el error críptico). El fix debería cubrir ambos (params + locales + probablemente campos de type) con la lista completa de reservadas de Rust, emitiendor#` o renombrando.
  • Propuesta: en el codegen, cuando un identificador de variable local es una keyword (estricta o reservada) de Rust, emitir el raw identifier r#<name> (o un prefijo __fitz_<name>). Cubrir la lista completa de reservadas de Rust.
  • Workaround en MatHelp: renombrar la variable (priv → pol_priv). Cerrado en la app.

Épicos transversales

T1 · La cadena del i18n

Del lado de fitz core: FITZ-03 (fs → catálogos desde JSON), FITZ-04 (locale num → 1.234.567,00), FITZ-05 (cookie de idioma con API real). Del lado de fitz-liveviews: FLV-01 (<html lang>) + FLV-09 (docs). Tratarlo como un épico único con dueño.

T2 · Paridad fitz run ↔ fitz build — SUBE

La fase F0 confirmó que esta clase es el problema estructural, no una feature suelta:

Hallazgo fitz check fitz run fitz build
FITZ-09 · T? ✓ ✓ ✗
FITZ-10 · Str + Any ✓ ✓ ✗
FITZ-04 · format specs ✓ ✓ ✓ (los docs mentían — corregido)
FITZ-06 · preload ✓ ✗ silencioso ✓

Tres divergencias en una tarde. FITZ-14 (differ) sube de Medio a Alto y se mueve al Hito 1: hubiera cazado las tres solo. Un lenguaje que a veces se comporta distinto al compilar es un problema de confianza, no de features — y la confianza es lo que hace que alguien apueste un proyecto real. Miembros: FITZ-09, FITZ-10, FITZ-06 (los bugs) + FITZ-14 (la red).

T3 · Mobile como ciudadano de primera

Del lado de fitz core: FITZ-02 (static → manifest.webmanifest → instalable) — cerrado en v0.51.0. El resto (reconnect, viewport, touch, cards) vive en fitz-liveviews (FLV-04, FLV-01, FLV-05; FLV-06 ya resuelto).


Orden de ataque sugerido (re-priorizado 2026-08-20)

Hito 1 — Arranque + confianza. FITZ-01 (rand) para desbloquear el juego. En paralelo el bloque de paridad: FITZ-14 (differ) + FITZ-09 (T?) + FITZ-10 (Str+Any). El differ landea con/antes de los fixes de codegen para dejarlos protegidos. FITZ-09 además destraba el binario nativo de todo lo que use fitz-liveviews (MatHelp, admin).

Hito 2 — Deployment desbloqueado + login zero-JS + i18n correcto. FITZ-11 (git en la imagen, 1 línea) + FITZ-05 (cookies) + FITZ-03 (fs) + FITZ-04 (locale num).

Hito 3 — Mobile + eviction. ✅ CERRADO (v0.50.0 + v0.51.0) FITZ-02 (static → T3) [v0.51.0] + FITZ-13 (Map.remove → desbloquea FLV-03) [v0.50.0]. (Los quick wins de liveviews van en su propio hito — ver archivo hermano.)

Hito 4 — Robustez y ruido. FITZ-06 (preload en intérprete o error claro) + FITZ-07 (is_in var) + FITZ-12 (paréntesis). FITZ-08 (enum PG) fuera salvo demanda concreta.


Refutados / fuera de alcance

ID Por qué no va Evidencia
A4 (mitad paridad) Los format specs ,/_/%/e/g/c compilan en fitz build (confirmado por el autor sobre el binario). Solo queda locale (→ FITZ-04). Doc guide.md:1266 corregida 2026-08-20. src/codegen.rs:40167-40257; probe: {n:,} → 1,234,567
A5 (mitad form) @post acepta form-urlencoded (confirmado por el autor). Login = <form method=POST> nativo sin JS. Comentario auth.fitz corregido 2026-08-20. src/http.rs:4077; src/codegen.rs:36113
FITZ-08 Prioridad baja. Str + @check_constraint funciona con migración/drift. src/types.rs:3162; src/migrations.rs:916

Qué se puede construir HOY con este repo (v0.47.0/0.48.0, sin cambios)

✅ Sale limpio, sin workaround

  • HTTP server + rutas + JSON body + <form method=POST> nativo (form-urlencoded, confirmado) → login sin JS.
  • Auth JWT + Argon2id (jwt.encode/hash.password/hash.verify).
  • ORM Postgres nativo (@table, .where(closure), .insert/.update/.delete, agregados).
  • WebSockets tipados + LiveViews con diffs por WS.
  • @cron/@background/spawn, env/load_env, @check_constraint.
  • Format specs completos ({n:,}, {r:.1%}, etc.) — compilan a nativo (la doc que decía lo contrario ya está corregida).

⚠️ Sale, pero con workaround — AISLALO detrás de un módulo con la firma futura

Necesidad Workaround HOY Qué tarea lo elimina Costo de migrar
Ejercicios aleatorios rng.fitz con la firma de rand.seeded(). Guardá seed + índice desde el día 1 FITZ-01 Trivial
Binario nativo (Docker) Correr el intérprete en el container (versión compilada comentada en el Dockerfile) FITZ-09 Trivial: descomentás el build cuando T? compile
Build en la imagen oficial con deps Etapa vendor + fitz.docker.toml paralelo (dos manifiestos) FITZ-11 Fácil: borrás el manifiesto paralelo cuando la imagen traiga git
Cookie de sesión/idioma cookies.fitz (set_cookie/read_cookie) copiado del admin FITZ-05 Fácil
Números es-AR fmt.fitz con la firma de num.format/num.percent/num.currency. Ningún número crudo en la UI FITZ-04 Fácil
Catálogos i18n locale_<code>.fitz generados por script desde JSON FITZ-03 Fácil
Assets estáticos ✅ assets.fitz con un @get por archivo → @server(static_dir=) (v0.51.0) FITZ-02 —
is_in con lista calculada db.query(... = ANY($1)) crudo, aislado en una función FITZ-07 Trivial
List<Any> inferido anotar let xs: List<Str> = [] FITZ-10 Trivial

🚫 No sale hoy de ninguna forma

  • Binario nativo de una app que use fitz-liveviews (FITZ-09/FLV-10). Hasta que T? compile, el Docker corre el intérprete. No es bloqueante para shippear (el intérprete anda) pero sí pierde ~9x + distroless.
  • Nada más es 100% imposible: el único bloqueante para empezar MatHelp es rand (FITZ-01), y hasta eso tiene workaround frágil.

🪤 Trampas conocidas

  • Funciones -> T? con return no compilan (FITZ-09). Si tu Dockerfile compila y falla con E0308 sobre Option, es esto. Corré el intérprete mientras.
  • Str + Any pasa check y falla build (FITZ-10). Anotá el tipo del List vacío.
  • .preload() en fitz run = no-op SILENCIOSO. Usá fitz build para esa parte o join manual.
  • La imagen oficial no trae git (FITZ-11) → no resuelve git deps. Vendor stage mientras.
  • 194 warnings de paréntesis redundantes (FITZ-12) tapan los errores reales — ojo al leer el output de build.
  • El doc guide.md:1266 ya no miente (corregido): los format specs compilan.

📐 Recomendaciones de arquitectura para MatHelp (fitz core)

  1. rng.fitz — todo el azar con la firma de rand.seeded(). Guardá seed + índice desde el día 1.
  2. Docker: corré el intérprete ahora, dejá el build compilado comentado — un solo comentario para descomentar cuando cierre FITZ-09.
  3. Un solo fitz.docker.toml + vendor stage si dockerizás con deps (hasta FITZ-11); tené un test que verifique que el manifiesto paralelo y el fitz.toml no divergen.
  4. cookies.fitz/fmt.fitz/i18n.fitz/assets.fitz/db_queries.fitz — cada limitación detrás de un módulo con la firma futura. Ningún número crudo en la UI. is_in/preload en funciones con nombre.
  5. Anotá los List/Map vacíos (let xs: List<Str> = []) para no chocar con FITZ-10 al compilar.

El principio: workarounds fáciles de borrar. El más caro de arrastrar es el del intérprete-en-Docker (FITZ-09) — pero es un comentario; el día que T? compile, descomentás y ganás perf + distroless.


Hallazgos de F1 (auth y perfiles) — 2026-08-21, core v0.56.0

Dos divergencias más de la clase check✓/build✗ (misma familia que FITZ-09/10), las dos en match sobre Result. Encontradas construyendo el login + sesión JWT de MatHelp. Ambas tienen workaround trivial ya aplicado en la app (aislado y comentado con el ID); el autor pidió cerrarlas en el core.

FITZ-15 — un nominal retornado por una fn importada, no en scope del módulo consumidor, se degrada a Any en codegen

  • CERRADO en v0.57 (src/codegen.rs: auto_register_imported_fn_ret_nominals
  • Paso 2 en emit_main_rs_body). Test fitz15_cross_module_fn_ret_nominal_infers_type_without_importing
  • W20 convergió a la solución completa (tipo concreto Vec<Patch> en vez de omitir la anotación). Verificado: repro CLI + match + Result<List<Foo>>, 4195 unit + smoke 290 ejemplos verdes, y MatHelp compila a binario nativo en HTTP sin el workaround (perfiles.fitz sin importar Family). La limpieza del workaround en MatHelp se hace al bumpear su fitz a v0.57.
  • Síntoma: fitz check pasa; fitz build corta con field access .id over Any: only supported on instances of custom types.
  • Diagnóstico (corregido tras repro): NO es sobre match ni arms divergentes — eso fue incidental. El bug es cross-module: un valor cuyo tipo es un nominal retornado (transitivamente) por una fn importada, cuando el nominal no está importado en el módulo que lo consume, se degrada a Any en el codegen. El checker lo resuelve (tiene el TypeEnv global del grafo de módulos); el codegen tiene env por-módulo que no incluye el nominal.
  • Repro mínimo (d:\fitz\target\scratch-fitz15\proj\):
    // models.fitz:  type Foo { id: Int = 0 }
    // data.fitz:    from models import Foo
    //              async fn get_foo() -> Result<Foo> { return Ok(Foo { id: 5 }) }
    // main.fitz — NO importa Foo:
    from data import get_foo
    async fn run() -> Result<Int> {
        let f = get_foo().await?     // f: Foo, pero Foo no está en scope → Any
        return Ok(f.id)              // <- ".id over Any" en build (check pasa)
    }
    
    Con match o con ? da igual. List<Foo> cross-module rompe en rustc con __FitzValue (sub-caso, ver W19/W20). En MatHelp fue read_session (de auth.fitz) devolviendo Family (de models.fitz) usado en perfiles.fitz, que no importaba Family.
  • Workaround REAL: importar el nominal en el módulo consumidor (from models import Foo). Basta el import — anotar el binding NO alcanza (let f: Foo = … da unknown type Foo en check si no está importado). En MatHelp: se sumó Family al from models import … de perfiles.fitz. (La anotación let fam: Family que también se puso es redundante y se saca al cerrar esto.)
  • Fix esperado: extender el auto-registro de nominales cross-module que ya hace auto_register_relation_targets (v0.26.1, src/codegen.rs, para targets de @has_many/@belongs_to) para que también recorra los ret types de las fns importadas que el módulo usa (y sus nominales anidados en Result/List/Option), auto-registre esos TypeId + copie fields + emita el use <mod>::{T, TData}. Es la misma maquinaria; falta el trigger nuevo.

FITZ-16 — el arm Ok(v) => v de un match sobre Result<Map<Str,__FitzValue>> no coacciona a Str

  • CERRADO en v0.57 (src/codegen.rs: gen_match coacciona cada arm Any con coerce(Any → primitivo) cuando el LUB es Str/Int/Float/Bool, reusando el __fv_to_* de Map<Str,Any>.keys()). Test fitz16_match_result_any_arm_coerces_to_primitive. Verificado: repro Str+Int paridad fitz run↔binario, 4195 unit + smoke 290 verdes. Habilita la forma natural match claims.get(k) { Ok(v)=>v, Err(_)=>"" } sin el workaround Err(_) => return "" + anotación. Limpieza en MatHelp pid_from_token.
  • Síntoma: fitz build corta con E0308 expected String, found __FitzValue sobre el arm Ok(v) => v de un match claims.get(k), donde claims viene de jwt.decode (→ Map<Str,__FitzValue>).
  • Repro (MatHelp src/auth.fitz, pid_from_token):
    let claims = jwt.decode(token, jwt_secret())?   // Map<Str, __FitzValue>
    return match claims.get("pid") {
        Ok(v) => v,          // <- __FitzValue, la fn retorna Str: E0308
        Err(_) => "",
    }
    
  • Observación: la coerción __FitzValue → Str SÍ se aplica cuando (a) el binding está anotado : Str y (b) el arm Err diverge (dejando Ok como único arm productivo). Falla cuando ambos arms producen valor (Ok(v)=>v vs Err(_)=>""), porque el match debe unificar __FitzValue con String y el codegen no inserta la conversión en el arm Ok.
  • Workaround aplicado: hacer diverger el Err + anotar el binding — let pid: Str = match claims.get("pid") { Ok(v)=>v, Err(_)=>return "" }. Idéntico patrón que read_session para el claim email.
  • Fix esperado: cuando un arm de match produce __FitzValue (Map/List Any) y el tipo destino del match/binding es un primitivo concreto, emitir la conversión (__fv_to_string/etc, la misma que ya usa Map<Str,Any>.keys() desde v0.55) en el arm, no solo en el camino con arm divergente.

FITZ-17 · Checker: bloque { } sin return con tipo de retorno no-nullable → null silencioso — CERRADO

  • Cerrado 2026-08-22 (fitz v0.58) — helper recursivo block_always_returns/ stmt_always_returns/expr_always_returns en src/types.rs (baja sobre if con ambas ramas, match con todos los arms, loop divergente, return/break/continue); el brazo Stmt::FnDef de check_stmt emite un error cuando el tipo de retorno declarado NO es Any/Null/nullable, el handler no es HTTP-like, y el cuerpo puede caer por el final. Cuerpos arrow (=> expr) desugaran a [Stmt::Return] → pasan; un match/if cuyos arms todos return pasa. Blast radius verificado ≈ 0 (todos los ejemplos del core Y la lib de fitz-liveviews usan return/=>; lib 4195 + smoke 290 verdes).
  • Síntoma: una fn con cuerpo de bloque { } (no arrow =>) cuyo último statement es una expresión "pelada" (sin return) declara -> T no-nullable, pasa fitz check ✓, y devuelve null en runtime — tanto fitz run como el codegen. El return implícito del último expr NO existe en cuerpos de bloque (sí en =>), pero el checker no exige el return.
  • Repro (mínimo, single-file fitz run):
    fn a() -> Int { 5 }                                          // -> null, no 5
    fn s(op: Str) -> Str { match op { "a" => "X", _ => "?" } }   // -> null
    print("{a()}")     // null
    
    fn a() -> Int { return 5 } y fn a() -> Int => 5 andan bien.
  • Por qué importa (T2, check✓/run-da-null): la clase más peligrosa — no hay error en ninguna etapa, la fn simplemente rinde null y el bug aparece lejos del origen (un .field sobre el null, una comparación que nunca matchea). En MatHelp lo pegaron op_symbol y build_exercise (ambas con un match pelado como cuerpo); costó bisectar porque el síntoma visible (gen_mix → null) estaba a tres saltos de la fn culpable.
  • Fix esperado: el checker debe rechazar un cuerpo de bloque que no garantiza return en todos los caminos cuando el tipo de retorno declarado NO es nullable (ni Null). Análisis de retorno estándar: cada rama terminal (if/match sin fall-through, o el último stmt) debe ser un return, o break/continue/loop infinito, etc. Mensaje sugerido: "la función declara -> Int pero un camino del cuerpo no retorna un valor (¿falta return?)". Alternativa más grande (soportar return implícito del último expr en bloques {}) se descarta: rompe la distinción semántica => vs {}.
  • No es workaround en la app: el idiom correcto ya es return <expr> (o el arrow). MatHelp está corregido; el valor de la ficha es cerrar el gap del checker para el próximo usuario.

FITZ-18 · Codegen: getrandom no se inyecta si el Cargo.toml ya menciona el substring (auth + rand global) — CERRADO

  • Cerrado 2026-08-22 — dogfooding de MatHelp F2.
  • Síntoma: un programa que usa auth (Argon2 → rand_core = { features = ["getrandom"] }) y rand global (rand.int/rand.bytes) compila mal: el prelude emite getrandom::getrandom(...) pero el dep getrandom = "0.2" nunca entra al Cargo.toml → E0433 cannot find module or crate getrandom.
  • Causa: inject_getrandom_dep chequeaba toml.contains("getrandom") como idempotencia. El feature ["getrandom"] de rand_core (que arrastra el auth) contiene ese substring → falso positivo → la función retorna sin agregar la línea de dep real.
  • Fix: el chequeo ahora matchea la LÍNEA de dep (getrandom =/getrandom al inicio de línea, trim), no el substring. Solo dispara ante un dep getrandom real, no ante el nombre de un feature.

FITZ-19 · Codegen: un @ws que usa ? no cerraba con Ok(()) (tail + return explícito, E0308) — CERRADO ENTERO

  • Tail fall-through cerrado 2026-08-22 (fitz v0.58) — gen_top_fn (src/codegen.rs) ahora, cuando is_ws_handler && body_has_try, emite #[allow(unreachable_code)] en la firma + un Ok(()) de cola (paralelo al None tail-fall de middleware). Antes solo compilaba un @ws que terminara en un loop {} divergente; ahora un handler que cae por el final (gate con match, limpieza tras el loop) cierra con Ok(()). El Ok(()) es código muerto (eliminado por rustc) cuando el body diverge.
  • Residual del return explícito cerrado 2026-08-23 (fitz v0.59) — hallazgo de la verificación de FLV-10 (fitz build examples/admin). El fix del tail NO traducía un return/return null explícito intermedio: un Err(_) => { return } (el auth gate de los @ws de admin — empleados_socket/departamentos_socket) emitía return () en vez de return Ok(()) → E0308, bloqueando el build nativo de admin (que nunca se había buildeado — solo fitz run; la nota vieja "admin usa el idiom limpio" era incorrecta). Fix en gen_return (src/codegen.rs): cuando ret_expected == Null && e == Expr::Null && ret_stack.last() == Result → emite return Ok(<null→ok>) (= Ok(())). Airtight-safe: ese patrón antes emitía return () → E0308 → no compilaba, así que solo arregla código antes roto. Test: fitz19_ws_handler_bare_return_in_match_arm_builds (compile_e2e)
  • fitz build examples/admin verde. Cierra el Fix esperado de abajo.
  • Síntoma (histórico): un handler @ws que usa ? (sobre ws.send) se compila como -> Result<(), String>. Un return PELADO (sin valor) adentro emitía return () en vez de return Ok(()) → E0308 mismatched types.
  • Fix (ambos casos): en un handler @ws con tipo de retorno Result sintetizado, emitir Ok(()) tanto en el tail fall-through (v0.58) como en un return explícito pelado (v0.59).

FITZ-20 · Loader: un archivo de tests/ no podía importar módulos de src/ — CERRADO

  • Cerrado 2026-08-22 (fitz v0.58) — hallazgo de dogfooding de MatHelp F2 (tests de generadores).
  • Síntoma: un from gen_arith import X en tests/foo.fitz NO resolvía — fitz test computaba base_dir = tests/ y el import_root fallback era el mismo dir, así que buscaba tests/gen_arith.fitz. La única vía para tocar código de src/ era from <pkg> import X (auto-self-import del [lib].entry), pero Fitz NO reexporta imports, así que el [lib].entry tenía que DEFINIR el símbolo — forzando o wrappers o apuntar el [lib].entry a un módulo concreto (raro: el "lib" del paquete siendo el módulo de generadores).
  • Fix: el runner de fitz test ahora pasa el dir del [lib].entry/[bin].main (src/) como import_root fallback del loader. TestSource suma import_root: Option<PathBuf> (None en single-file --file); discover_test_sources_from_manifest lo deriva del entry; eval_test_source lo threadea a la nueva evaluator:: eval_with_base_import_root_and_deps(program, base_dir, import_root, dep_registry) (que instala el loader con import_root separado vía install_loader_with_root). Ahora from <src-module> import X resuelve a src/<module>.fitz (base_dir primero, después import_root). MatHelp revirtió el [lib].entry a un src/lib.fitz limpio y sus tests importan gen_arith directo.
  • Deuda residual (menor): sigue sin haber re-export de imports; un test que quiera un símbolo de un submódulo profundo (no un hermano directo de src/) cae fuera del fallback single-level. Suficiente para el caso 90%.

FITZ-23 · Codegen: list.push(<async>.await) sostiene el MutexGuard cruzando el .await (future !Send, rompe handlers HTTP) — CERRADO

  • Cerrado 2026-08-27 (fitz v0.59.1) — hallazgo de dogfooding de MatHelp (tablero familiar: filas.push(fila_familiar(locale, p).await)).
  • Síntoma: list.push(<expr con .await>) compilaba con fitz check y corría en un contexto CLI, pero al usarlo (directa o indirectamente) desde un handler @get/@post/@ws rompía fitz build con error[E0277]: the trait bound '... {__handler_X}: Handler<_, _>' is not satisfied. La causa: el codegen emitía (xs.clone()).lock().unwrap().push((make(i)).await). En Rust, en una llamada a método receptor.metodo(arg), el receptor se evalúa antes que el arg, así que el MutexGuard temporal (.lock().unwrap(), !Send) queda vivo mientras se evalúa el arg — y si el arg tiene un .await, el guard cruza el await → el future del handler pasa a !Send → axum rechaza el Handler.
  • Reproducción mínima: un async fn con xs.push(f(i).await) en un for, llamado desde un @get. fitz build → E0277.
  • Fix (src/codegen.rs::gen_list_push): patrón "compute first, lock last" — cuando expr_contains_await(&args[0]), se bindea el valor a un local ANTES de tomar el lock: { let __push_v = <arg>; (xs).lock().unwrap().push(__push_v) }. El .await corre antes de lockear, así el guard nunca cruza el await. Gateado por expr_contains_await → el caso común (sin await) emite byte-idéntico. Es el mismo patrón que ya usaban gen_index_assign (xs[i] = v, que además evita el deadlock de re-entrancia) y gen_map_remove (bindea __k primero).
  • Alcance: push era el ÚNICO método de List/Map que insertaba el arg inline con el guard vivo. Los demás mutadores con args ya eran seguros: insert_at/ remove_at snapshotean (.lock().clone()), remove bindea la key, index-assign bindea __val/__idx. El field-assign de Nominal (obj.f = <arg>.await) NO tiene el bug: en una asignación Rust evalúa el RHS antes que el place del LHS, así que el await corre antes de tomar el lock (verificado: compila).
  • Tests: 2 unit en codegen.rs (fitz23_list_push_await_arg_hoists_before_lock
  • fitz23_list_push_no_await_stays_byte_identical) + 1 E2E de compilación en tests/compile_e2e.rs (fitz23_list_push_await_in_handler_chain_builds, el patrón exacto de MatHelp: push con await en un loop, dentro de una async fn llamada por un @get).
  • MatHelp: revirtió el workaround en src/parent.fitz::tablero_familiar (el let fila = fila_familiar(...).await intermedio) a filas.push( fila_familiar(locale, p).await) directo.

FITZ-24 · fitz check no valida field-access (.campo) sobre tipos primitivos concretos (check✓/run✗) — CERRADO

  • Cerrado 2026-08-28 (fitz v0.60) — hallazgo de dogfooding de MatHelp (editar perfil: h_join(...).raw, con h_join(List<Html>) -> Str).
  • Diagnóstico real (la hipótesis inicial de "firmas de deps git" era ERRADA): el checker NO validaba el field-access .campo (sin paréntesis) sobre NINGÚN tipo primitivo concreto — ni siquiera un Str LOCAL anotado. let s: Str = "x"; let r = s.raw pasaba fitz check (lo tipaba gradual → Any) pero el runtime lo rechaza (field access .raw on a value of type Str — only allowed on custom type instances or modules). La brecha era universal: no dependía de deps ni de cross-módulo. El h_join(...).raw de MatHelp la destapó porque h_join devuelve Str y la forma canónica correcta es SIN .raw (h_join ya concatena a Str) o html("...{ops}...").raw.
  • Asimetría que confirmó el bug: un METHOD-CALL sobre un primitivo SÍ se cazaba (s.bogus() → "Str does not have method bogus" vía infer_method_call); un FIELD-ACCESS bare (s.raw) se colaba como Any silencioso — el arm Expr::Field del checker tenía un fallthrough _ => Type::Any que tragaba todos los primitivos.
  • Fix (src/types.rs, handler Expr::Field en synthesize_expr): el match del receptor pasa a obj_ty.base() (desenvuelve Nullable un nivel, así User?.name resuelve contra los fields de User) con arms explícitos: Nominal (valida fields, ya existía — campo desconocido sigue devolviendo Any silencioso, sin falso positivo), PyAny → PyAny (chaining), Any → Any (escape gradual — cubre los módulos nativos http/log/db/... que se bindean como Any), y el _ final (todo tipo concreto sin fields: Str/Int/Float/Bool/List/Map/Result/Range/ Bytes/Date/...) emite el error espejando el mensaje del runtime. Expr::TupleField (.0/.1) es un variant separado → no lo toca.
  • Sin falsos positivos: Nullable(Nominal).campoExistente valida (no error); Nullable(Nominal).campoInexistente sigue Any silencioso (comportamiento previo); method-calls intactos; built-ins con fields (HttpClientResponse/ Request/Response/File/SmtpResult/Cookie) son Type::Nominal → caen en el arm Nominal y se validan igual que antes. fitz check de MatHelp entero (30+ módulos) queda en 0 errores; smoke GUIDE_EXAMPLES_COMPILE verde.
  • Tests: 8 unit en types.rs::tests::fitz24_* (Str/Int/List field-access → error; Str? nullable-de-primitivo → error; regresión: method-call válido, method-call desconocido, Nominal, Nullable-Nominal).
  • Criterio de aceptación (cumplido): let s: Str = "x"; s.raw es error de fitz check, no un panic de runtime.