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) yFITZ-10(differ de paridad) como hallazgos propios. El autor asignóFITZ-09..12a 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?conreturn nullgenera Rust roto (return ()donde vareturn None, valor sinSome(...)). 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/adminimporta el framework entero → no compila confitz build, solo confitz 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) — congitdeps 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 docguide.md:1266mentía y ya la corregí. Lo que queda de FITZ-04 es solo locale (1.234.567,00argentino) — y para un juego de matemática, mostrar mal los decimales es enseñar mal. - A5 confirmado empíricamente:
@postaceptaform-urlencoded→ login zero-JS. Comentario stale deexamples/admin/src/auth.fitzya 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 craterand),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-derivaRandGen), LSP completions, ejemplo runnable13w-random.fitz, test E2E de reproducibilidad + smoke verde. Limitaciones del MVP (follow-ups, NO divergencias silenciosas): (a)CERRADO 2026-08-22 (dogfooding MatHelp F2 — los generadores viven enranden codegen solo en el programa principalgen_arith.fitz, módulo importado): el crate root emiteRAND_PRELUDE_CORE/_GLOBAL+getrandomsi CUALQUIER módulo usarand.*(OR conloader.modules, patrón SMTP/Response/DB), y cada módulo importado emiteuse crate::{__FitzRng, ...}/ los__fitz_rand_g_*globales. Idéntico fix paranum(FITZ-04:NUM_PRELUDE+use crate::{__fitz_num_*}).LoadedModulesumauses_rand/uses_rand_global/uses_num; 6 sitios ensrc/codegen.rs; validado con MatHelp compilando a binario nativo. (b)unCERRADO v0.58.1, 2026-08-22 (FITZ-01(b)):RandGenNO cruza un límite de función (ni param ni return): el checker no conoceRandGencomo tipo nombrableresolve_type_exprmapea"RandGen" → Type::RandGen, así quefn draw(g: RandGen, hi: Int) -> Int { return g.int(1, hi) }type-chequea y compila;infer_randgen_methodtipa los 6 métodos (int→Int,float→Float,bool→Bool,choice→Result<T>,shuffle→List<T>,sample→Result<List<T>>) para que ungque 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; © unmatch 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 hayrand);src/lsp.rs:4204-4239(módulos built-in, no hayrand);src/value.rs:699(única fuente de azar =Uuid.v4()).rand_coreenCargo.toml:174solo 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 enfitz runyfitz build, para siempre. Por eso el PRNG sembrado no debe delegar enStdRngdel craterand(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 usagetrandom. Esta garantía habilita guardarseed + índicey 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.tomlcongetrandom);docs/guide.md. - Tests:
@testde 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 exponerStdRng(no reproducible) y unrand.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?conreturn nullyreturn <valor>pasafitz check✓, corre enfitz run✓, y fallafitz buildconE0308. El Rust generado emite dos defectos:return ()donde vareturn None, y el valor de retorno sin envolver enSome(...). El propiorustcsugiere 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 nullde tail) → el Rust generadofitz_liveviews.rs:1432no compila. Verificado queexamples/adminimporta el framework entero (auth.fitz:17,dashboard.fitz:15, ...) con dep{ path = "../.." }→examples/adminno compila confitz build(solofitz 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_cookieno 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?conreturnadentro de unmatch— pero no evitable para el código de la librería. - Propuesta: aplicar la coerción nullable en posición de
returndentro de una fn cuyo return type esNullable.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?) ⇒ Nonepara 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 buildyGET /devuelve"a"; (2)fitz-liveviewscompila entero,flv_cookieincluida, yexamples/admincompila y corre igual que interpretado (ese es también el cierre de FLV-10); (3) el repro da salida idéntica porfitz runyfitz build. - Archivos a tocar:
src/codegen.rs— la emisión deStmt::Return(buscargen_return/el brazo deReturn): cuando el frame de return type esType::Nullable(_), envolver el valor con la coerciónSome(...)y emitirNoneparareturn null. Reusar el helpercoerce(...)existente. - Tests: el repro como E2E en
tests/compile_e2e.rs; unit degen_returncon retNullable(return null→None,return v→Some(v)); regresión deflv_cookie: un mini programa que importe fitz-liveviews y llameflv_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:
rustcliteralmente 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_fitz14con 13 ejemplos CLI-puros diffeadosrun↔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 losCommand::new(&bin)(:2288,:2322,:2378,:2569,:2613,:11150) ejecutan el binario compilado; ninguno correfitz runsobre el mismo fuente para diffear. El smokeGUIDE_EXAMPLES_COMPILEsolo 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 porfitz runy porfitz 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 testque falla si cualquier ejemplo del corpus produce salida distinta entrerunybuild. Corre en CI. Habría cazado FITZ-09, FITZ-10 y FITZ-06 solo. - Archivos a tocar:
tests/parity_e2e.rsnuevo conrun_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) —
gitagregado alapt-get installde la imagen estándar (build-capable) en.github/workflows/release.yml. Se ejercita en el próximo tag de release. El--dep-overridequeda como enhancement opcional diferido (no era un bug confirmado). El-pythonruntime es run-only (sin cargo), no lo toca. - Estado: Confirmado en un build real.
- Evidencia: el build del autor sobre
ghcr.io/thegreekman76/fitzfalla enRUN fitz buildconcould not invoke git (No such file or directory, os error 2). La imagen existe y baja bien, pero singitadentrofitz buildno 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
vendorsobre Alpine que clona fitz-liveviews + unfitz.docker.tomlparalelo 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(elca-certificatespara clonar por https). (2) Evaluarfitz 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 buildsobre 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 elfitz.toml. - Archivos a tocar: el Dockerfile de la imagen oficial (repo
fitz,docker/o el workflow que la buildea); opcionalmentesrc/main.rs+src/manifest.rspara--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-overridees 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 + Anyengen_binop(coerce Any→Str, paridad con el intérprete) + detección deList<Any>/Map<_,Any>víaTypeInfopara emitir el preludio__FitzValueen CLI. Test E2E + smoke verde. - Estado: Confirmado.
- Evidencia: repro del autor —
let chars = [](infiereList<Any>),chars.push(c)conc: Str, y luegoout + chars[0]→fitz check✓,fitz build✗ concodegen: operador + no aplicable a Str y Any en codegen. El checker infiereAnypara el[]vacío y no propaga hacia atrás desde elpush, 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]tipaStr, o (b) el codegen coaccionaStr + Anycomo 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 checklo rechaza con un mensaje claro. Nunca pasar check y fallar build. - Archivos a tocar:
src/types.rs(inferencia del elemento de unList<Any>vacío desde elpush) y/osrc/codegen.rs(operador+con operandoAny). - 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/insertes 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.
FITZ-05 · API de cookies (@cookie + Response.cookies)¶
- Implementado (2026-08-20) — FASE A (leer con
@cookie(name="X"), paramStr/Str?, opcionalinto="alias", sobre@get/@post/@ws;parse_cookie_header; paridad run↔build; OpenAPIin: cookie) + FASE B (nominal built-inCookiede 8 campos con defaults + campoResponse.cookies: List<Cookie>; cadaCookie→ unSet-Cookie; helper compartidoserialize_set_cookie/__fitz_serialize_set_cookiecon paridad bit-a-bit; fix del.insert→.appendenoutcome_to_responsepara 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_bodyahora coerce el bodyform-urlencodedaltypedel handler (paralelo aparse_body/JSON y al__parse_urlencoded→__from_fitz_jsondel codegen), así el login zero-JS funciona igual enfitz runyfitz build(2 tests nuevos + smoke bit-a-bit). - 🟢 RESIDUAL CERRADO (2026-08-21):
@cookieya funciona sobre@ws. Fix completo (multi-archivo, con verificación end-to-end de paridadrun↔build): (1) el checker de aridadcheck_ws_handler(src/types.rs) sumacookie_countaexpected_params(paralelo a@header); (2) el runtime WS (src/http.rs,build_ws_method_router) clonaroute.cookiesy bindea la cookie del headerCookiedel handshake conparse_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_cookiedesde elHeaderMapdel upgrade, con OR en el gate delmove. 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 mandaCookie: lang=esen 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 buildy ahora también enfitz runcon coerción altype. - Evidencia (cookies sin API):
src/http.rs:1310-1341(HandlerOutcomesin campocookies); sin@cookie/Cookieensrc/parser.rs/src/value.rs.docs/guide.md:11436lista sessions cookie-based como futuro. - Evidencia (form-urlencoded — REFUTADO):
src/http.rs:4077-4126+ codegensrc/codegen.rs:33359, helper__parse_urlencoded(:36113). El autor lo verificó en un probe compilado.<form method=POST>nativo deserializa al mismotypeque hoy recibe JSON, sin JS. (Comentario stale deexamples/admin/src/auth.fitzya corregido.) - Impacto en un usuario real: la cookie es el único mecanismo viable para el login de familia (el
browser no manda
Authorizationen 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 deCookie— riesgo de seguridad. - Workaround hoy: el del ejemplo admin (
auth.fitz/i18n.fitz): concatenarSet-Cookiea mano y parsearCookiecon.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 (onull) en@get/@post/@ws; cadaCookiedelcookiesse serializa a unSet-Cookiecon flags correctos; paridadrun↔build. - Archivos a tocar:
src/types.rs(nominalCookie+ 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.mdcap 17. - Tests: unit de
parse_cookie_header; E2E de serialización; E2E de login form-urlencoded →Set-Cookie→@cookiede vuelta. - Docs:
docs/guide.mdcap 17. Cruza con FLV-09. - Dependencias: ninguna. Habilita que el admin y
docs/i18n.mddejen de parsear cookies a mano. - Notas de diseño: lectura por decorador (como
@header), escritura por campocookies(coherente con elResponsesin estado). La mitad form-urlencoded no requiere trabajo — solo se documentó.
FITZ-03 · Módulo fs¶
- Implementado (2026-08-20) —
src/fs.rscon los 8 builtins + intérprete + codegen con paridad (FS_PRELUDEsobrestd::fs, sin deps extra) + checker (fs= Any) + LSP + sección de guía "Filesystem" + test E2E de roundtrip run↔build. Detector genérico nuevoprogram_calls_module(reusable). MVP: main-program (cross-module fs = follow-up); sin sandbox; JSON parse nativo no existe (i18n usa formatoclave=valorcon.split, o interop Python). - Estado: Confirmado.
- Evidencia:
builtin_names()(src/evaluator.rs:246-304) sinfs/read_file/write_file. La única lectura de disco expuesta esload_env(path)(src/evaluator.rs:19017, parser:19065) — solo puebla env vars. El tipoFile(src/types.rs:1618-1648) es demultipart/form-data, no abre archivos. - Impacto en un usuario real: rompe la arquitectura natural de i18n de MatHelp (catálogos
locales/*.jsonleídos al boot). Sinfs, 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
.jsona.fitz+@testde claves faltantes. Paso de build que no debería existir. - Propuesta (API cerrada):
Lectura en runtime (no compile-time):
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>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.readinexistente →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.mdsecció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,
fses 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ódulonum(src/num.rs):num.format/num.percent/num.currencycon es-AR + en-US, paridad bit-a-bit (NUM_PRELUDEmisma lógica de agrupamiento) + checker (Any) + LSP + sección de guía + ejemplo13x-num-locale.fitz(smoke + corpus de paridad FITZ-14). MVP: args posicionales (kwargslocale:= futuro); tabla de locales embebida (sin ICU). - Estado: Parcial. "Sin paridad run↔build" REFUTADO (confirmado empíricamente por el autor:
{n:,}→1,234,567en el binario). "Sin locale" CONFIRMADO. - Evidencia (paridad — REFUTADO):
src/codegen.rs:40167-40257+ helpers:13445-13489implementan grouping/exponente/general/char/percent;docs/guide.md:5042-5078documenta el cierre "Fm". La tabla stale dedocs/guide.md:1266ya la corregí (2026-08-20). Probe del autor:fitz buildde{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 ser1.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,5y$ 1.250. Un juego de matemática que muestra1,234.5le 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):
Separador decimal + de miles + símbolo de moneda + posición cubren el 95%. Precedente:
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"DateTime.in_tz(iana). - Criterio de aceptación:
num.*con"es-AR"da formato argentino, con"en-US"inglés; paridadrun↔build. - Archivos a tocar:
src/evaluator.rs(módulonum);src/codegen.rs(__fitz_num_*con la misma tabla de locales);src/types.rs/src/lsp.rs. (La doc staleguide.md:1266ya está corregida.) - Tests:
@testes-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 compartidosrc/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 destatic_files.rs). Flagfitz build --embed-statichornea los assets en el binario coninclude_bytes!→ sirve su propio frontend sin el dir en disco (distroless). Checker sin cambios (@serveres opaco al checker). LSP completion de@servercita los kwargs nuevos. Guía cap 17 "Archivos estáticos" + nota deployment distroless + ejemplo17m-static.fitz. Tests: 8 unitstatic_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_traversalparidad 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(sinpublic/). 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); grepServeDir|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" (
@getpor archivo conResponse/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")conContent-Typepor extensión,ETag/If-None-Match,Cache-Control,Last-Modified, path-traversal bloqueado. Evaluarfitz build --embed-static(assets dentro del binario → encaja con distroless). - Criterio de aceptación: GET a
/static/foo.csssirve con Content-Type + ETag + 304;../rechazado; paridadrun↔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-staticconinclude_bytes!);src/main.rs(flag);docs/guide.md. - Tests: E2E de estático (Content-Type/ETag/304); traversal rechazado;
--embed-static. - Docs:
docs/guide.mdcap 17 + nota de deployment distroless. - Dependencias: habilita T3. Reduce/elimina el nginx de los boilerplates.
- Notas de diseño:
ServeDirdetower-httpen 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::removepreservando orden) + codegen con paridad bit-a-bit (gen_map_remove,.removesobre elVec<(K, V)>; Arc ligado a un local para no dropear elMutexGuardtemporal) + 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 paridadrun↔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: "Maphas noremoveyet");fitz-liveviews/docs/components.md:398. Métodos deMapen el core:get/has/keys/values/len, sinremove. - 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. Habilitaflv_drop_instance/eviction en fitz-liveviews. - Archivos a tocar:
src/evaluator.rs(dispatch deValue::Map);src/codegen.rs(.remove()sobre elVec<(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 deMap). - Dependencias: desbloquea FLV-03. Prerequisito de menor costo / mayor apalancamiento cross-repo.
- Notas de diseño: el
Mapinterno esVec<(K,V)>con orden de inserción;removees 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()enfitz runya daba un error genérico ("QueryBuilder has no methodpreload"), no un no-op silencioso. Ahora da un error DEDICADO en ambos dispatches (QueryBuilder + Type directo) que apunta afitz build+ workarounds (navigation methoduser.posts(db)o.whereseparado). Helperpreload_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 enfitz 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 enfitz 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 confitz build. - Propuesta: implementar
.preload()en el intérprete (paridad), o — MVP barato — quefitz runemita error claro en vez del no-op silencioso. - Criterio de aceptación:
.preload()carga las relaciones enfitz run(paridad), o aborta con mensaje claro. Nunca no-op silencioso. - Archivos a tocar:
src/evaluator.rs:14670(rama"preload"). - Tests: E2E
.preload()enfitz runcarga 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 variableList<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 emitiendoIN ($1, $2, ...). Evaluator (scalar_pg_oid+ resolución del var víaenv 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 (literalIN+ emptyfalse) intactos + 1 E2E real Postgres (orm_where_is_in_variable_fitz07). Validado run↔build bit-a-bit contra Postgres local. Docdb-orm.mdsec is_in reescrita + tabla de var-support actualizada. - Estado: Confirmado.
- Evidencia:
src/evaluator.rs:17280-17307ysrc/codegen.rs:20877-20911—is_insolo matcheaExpr::Listliteral; emiteIN ($1,$2,...), noANY($n). Doc-comment stale ensrc/evaluator.rs:17207-17209promete soporte de variable sin cumplirlo. - Impacto en un usuario real: el motor adaptativo necesita
Mastery.where(fn(m) => m.skill_code.is_in(pendientes))conpendientes: 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:17207corregido. - 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 engen_returnygen_assign(las 3 ramas: reasignación,let _,let mut). Unlet 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.rsgenerado con 0(match, binario corre OK). - Estado: Confirmado (cosmético).
- Evidencia:
rustcavisaunnecessary 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
matchen paréntesis cuando es el RHS de unlet/return/asignación (elmatchde Rust ya es una expresión válida en esas posiciones). - Criterio de aceptación: un build de un programa con
matchcomo valor no emite el warningunnecessary parentheses. - Archivos a tocar:
src/codegen.rs— la emisión dematchcomo 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_constraintcubre) - Estado: Confirmado (prioridad baja).
- Evidencia:
boilerplates/api-orm-full/src/models.fitz:24-26(no soportado en ORM MVP).@check_constraintSÍ existe (src/types.rs:3162-3228,src/migrations.rs:916-941). - Impacto en un usuario real:
mode(quiz | truefalse | ...) queda comoStrsin 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_constraintcubre. - Notas de diseño: documentar el patrón
Str + @check_constraintcomo 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) ganaentry_file: Option<PathBuf>(el path canónico del archivo entry, que se evalúa directo y nunca entra al stackloading). En el loop de canonicalización deload_modulese computaself_path = loading.last() .or(entry_file)y se saltea el candidato que canonicaliza al self, así la resolución sibling-first cae aimport_root(src/foo.fitz) en vez de ciclar/re-cargarse.eval_with_base_import_root_and_deps+install_loader_with_rootreciben elentry_file; el runner defitz test(eval_test_source) pasafs::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.fitzconfrom keypad import ...(apuntando asrc/keypad.fitz) → ciclo; hubo que renombrar atests/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.fitzconfrom engine import ...(apuntando asrc/engine.fitz) fallafitz testconError — ciclo de imports detectado: tests/engine.fitz -> tests/engine.fitz. El fallbackimport_root(FITZ-20) resuelve primero el hermano entests/(que es el propio archivo) antes de caer asrc/; como el hermano existe (es él mismo), nunca llega alsrc/. - Repro mínimo:
src/foo.fitzconfn bar() => 1;tests/foo.fitzconfrom foo import bar+ un@test.fitz test→ ciclo. - Workaround (aplicado en la app): nombrar el test distinto del módulo (
tests/motor.fitzen vez detests/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_rootfallback. - Archivos a tocar: el loader del runner de
fitz test(resoluciónimport_root). - Criterio de aceptación:
tests/engine.fitzconfrom engine import Xresuelve asrc/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 cadafrom m import f, resuelve la firma real defn f(extract_fn_signaturesensrc/types.rs), la mapea bajo el binding local (alias-aware) y la mete enTypeEnv.imported_fn_sigs. El handlerStmt::FromImportdel checker la registra comoType::Function(con defaults/varargs) en vez deType::Any→ el call site valida aridad/tipos con la maquinaria existente (regla 5.3.2). Un param nominal no importado por el caller resuelve aAny(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 enfitz build(que sí valida aridad en el codegen). - Evidencia: en MatHelp,
gen_for(10 params, ensrc/gen_arith.fitz) llamada con 6 args desdesrc/live_game.fitz(persistir_practica):fitz checkdijo "sin errores";fitz buildcortó concodegen: Error ... gen_for expects 10 argument(s), got 6. - Repro mínimo:
src/m.fitzconfn f(a: Int, b: Int) -> Int => a + b; otro módulo confrom m import fylet x = f(1).fitz checkpasa;fitz buildfalla 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 defitz build.
É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.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?conreturnno compilan (FITZ-09). Si tu Dockerfile compila y falla conE0308sobreOption, es esto. Corré el intérprete mientras. Str + Anypasachecky fallabuild(FITZ-10). Anotá el tipo delListvacío..preload()enfitz run= no-op SILENCIOSO. Usáfitz buildpara 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:1266ya no miente (corregido): los format specs compilan.
📐 Recomendaciones de arquitectura para MatHelp (fitz core)¶
rng.fitz— todo el azar con la firma derand.seeded(). Guardáseed + índicedesde el día 1.- Docker: corré el intérprete ahora, dejá el build compilado comentado — un solo comentario para descomentar cuando cierre FITZ-09.
- 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 elfitz.tomlno divergen. 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/preloaden funciones con nombre.- Anotá los
List/Mapvací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). Testfitz15_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.fitzsin importarFamily). La limpieza del workaround en MatHelp se hace al bumpear su fitz a v0.57. - Síntoma:
fitz checkpasa;fitz buildcorta confield access .id over Any: only supported on instances of custom types. - Diagnóstico (corregido tras repro): NO es sobre
matchni 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 aAnyen el codegen. El checker lo resuelve (tiene elTypeEnvglobal 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\):Con// 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) }matcho con?da igual.List<Foo>cross-module rompe en rustc con__FitzValue(sub-caso, ver W19/W20). En MatHelp fueread_session(deauth.fitz) devolviendoFamily(demodels.fitz) usado enperfiles.fitz, que no importabaFamily. - 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 = …daunknown type Fooen check si no está importado). En MatHelp: se sumóFamilyalfrom models import …deperfiles.fitz. (La anotaciónlet fam: Familyque 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 enResult/List/Option), auto-registre esosTypeId+ copie fields + emita eluse <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_matchcoacciona cada armAnyconcoerce(Any → primitivo)cuando el LUB es Str/Int/Float/Bool, reusando el__fv_to_*deMap<Str,Any>.keys()). Testfitz16_match_result_any_arm_coerces_to_primitive. Verificado: repro Str+Int paridadfitz run↔binario, 4195 unit + smoke 290 verdes. Habilita la forma naturalmatch claims.get(k) { Ok(v)=>v, Err(_)=>"" }sin el workaroundErr(_) => return ""+ anotación. Limpieza en MatHelppid_from_token. - Síntoma:
fitz buildcorta conE0308 expected String, found __FitzValuesobre el armOk(v) => vde unmatch claims.get(k), dondeclaimsviene dejwt.decode(→Map<Str,__FitzValue>). - Repro (MatHelp
src/auth.fitz,pid_from_token): - Observación: la coerción
__FitzValue → StrSÍ se aplica cuando (a) el binding está anotado: Stry (b) el armErrdiverge (dejandoOkcomo único arm productivo). Falla cuando ambos arms producen valor (Ok(v)=>vvsErr(_)=>""), porque el match debe unificar__FitzValueconStringy el codegen no inserta la conversión en el armOk. - 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 queread_sessionpara el claimemail. - Fix esperado: cuando un arm de
matchproduce__FitzValue(Map/ListAny) y el tipo destino del match/binding es un primitivo concreto, emitir la conversión (__fv_to_string/etc, la misma que ya usaMap<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_returnsensrc/types.rs(baja sobreifcon ambas ramas,matchcon todos los arms,loopdivergente,return/break/continue); el brazoStmt::FnDefdecheck_stmtemite un error cuando el tipo de retorno declarado NO esAny/Null/nullable, el handler no es HTTP-like, y el cuerpo puede caer por el final. Cuerpos arrow (=> expr) desugaran a[Stmt::Return]→ pasan; unmatch/ifcuyos arms todosreturnpasa. Blast radius verificado ≈ 0 (todos los ejemplos del core Y la lib de fitz-liveviews usanreturn/=>; lib 4195 + smoke 290 verdes). - Síntoma: una fn con cuerpo de bloque
{ }(no arrow=>) cuyo último statement es una expresión "pelada" (sinreturn) declara-> Tno-nullable, pasafitz check✓, y devuelvenullen runtime — tantofitz runcomo el codegen. El return implícito del último expr NO existe en cuerpos de bloque (sí en=>), pero el checker no exige elreturn. - 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()}") // nullfn a() -> Int { return 5 }yfn a() -> Int => 5andan bien. - Por qué importa (T2, check✓/run-da-null): la clase más peligrosa — no hay error en ninguna
etapa, la fn simplemente rinde
nully el bug aparece lejos del origen (un.fieldsobre el null, una comparación que nunca matchea). En MatHelp lo pegaronop_symbolybuild_exercise(ambas con unmatchpelado 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
returnen todos los caminos cuando el tipo de retorno declarado NO es nullable (niNull). Análisis de retorno estándar: cada rama terminal (if/matchsin fall-through, o el último stmt) debe ser unreturn, obreak/continue/loop infinito, etc. Mensaje sugerido: "la función declara-> Intpero un camino del cuerpo no retorna un valor (¿faltareturn?)". 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"] }) yrandglobal (rand.int/rand.bytes) compila mal: el prelude emitegetrandom::getrandom(...)pero el depgetrandom = "0.2"nunca entra al Cargo.toml →E0433 cannot find module or crate getrandom. - Causa:
inject_getrandom_depchequeabatoml.contains("getrandom")como idempotencia. El feature["getrandom"]derand_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 =/getrandomal inicio de línea, trim), no el substring. Solo dispara ante un depgetrandomreal, 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, cuandois_ws_handler && body_has_try, emite#[allow(unreachable_code)]en la firma + unOk(())de cola (paralelo alNonetail-fall de middleware). Antes solo compilaba un@wsque terminara en unloop {}divergente; ahora un handler que cae por el final (gate conmatch, limpieza tras el loop) cierra conOk(()). ElOk(())es código muerto (eliminado por rustc) cuando el body diverge. - Residual del
returnexplí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 unreturn/return nullexplícito intermedio: unErr(_) => { return }(el auth gate de los@wsde admin —empleados_socket/departamentos_socket) emitíareturn ()en vez dereturn Ok(())→E0308, bloqueando el build nativo de admin (que nunca se había buildeado — solofitz run; la nota vieja "admin usa el idiom limpio" era incorrecta). Fix engen_return(src/codegen.rs): cuandoret_expected == Null && e == Expr::Null && ret_stack.last() == Result→ emitereturn Ok(<null→ok>)(=Ok(())). Airtight-safe: ese patrón antes emitíareturn ()→ 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/adminverde. Cierra elFix esperadode abajo.- Síntoma (histórico): un handler
@wsque usa?(sobrews.send) se compila como-> Result<(), String>. UnreturnPELADO (sin valor) adentro emitíareturn ()en vez dereturn Ok(())→E0308 mismatched types. - Fix (ambos casos): en un handler
@wscon tipo de retorno Result sintetizado, emitirOk(())tanto en el tail fall-through (v0.58) como en unreturnexplí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 Xentests/foo.fitzNO resolvía —fitz testcomputababase_dir = tests/y elimport_rootfallback era el mismo dir, así que buscabatests/gen_arith.fitz. La única vía para tocar código desrc/erafrom <pkg> import X(auto-self-import del[lib].entry), pero Fitz NO reexporta imports, así que el[lib].entrytenía que DEFINIR el símbolo — forzando o wrappers o apuntar el[lib].entrya un módulo concreto (raro: el "lib" del paquete siendo el módulo de generadores). - Fix: el runner de
fitz testahora pasa el dir del[lib].entry/[bin].main(src/) comoimport_rootfallback del loader.TestSourcesumaimport_root: Option<PathBuf>(Noneen single-file--file);discover_test_sources_from_manifestlo deriva del entry;eval_test_sourcelo threadea a la nuevaevaluator:: eval_with_base_import_root_and_deps(program, base_dir, import_root, dep_registry)(que instala el loader conimport_rootseparado víainstall_loader_with_root). Ahorafrom <src-module> import Xresuelve asrc/<module>.fitz(base_dir primero, después import_root). MatHelp revirtió el[lib].entrya unsrc/lib.fitzlimpio y sus tests importangen_arithdirecto. - 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 confitz checky corría en un contexto CLI, pero al usarlo (directa o indirectamente) desde un handler@get/@post/@wsrompíafitz buildconerror[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étodoreceptor.metodo(arg), el receptor se evalúa antes que el arg, así que elMutexGuardtemporal (.lock().unwrap(),!Send) queda vivo mientras se evalúa elarg— y si el arg tiene un.await, el guard cruza el await → el future del handler pasa a!Send→ axum rechaza elHandler. - Reproducción mínima: un
async fnconxs.push(f(i).await)en unfor, llamado desde un@get.fitz build→ E0277. - Fix (
src/codegen.rs::gen_list_push): patrón "compute first, lock last" — cuandoexpr_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.awaitcorre antes de lockear, así el guard nunca cruza el await. Gateado porexpr_contains_await→ el caso común (sin await) emite byte-idéntico. Es el mismo patrón que ya usabangen_index_assign(xs[i] = v, que además evita el deadlock de re-entrancia) ygen_map_remove(bindea__kprimero). - Alcance:
pushera 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_atsnapshotean (.lock().clone()),removebindea 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 entests/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(ellet fila = fila_familiar(...).awaitintermedio) afilas.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, conh_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 unStrLOCAL anotado.let s: Str = "x"; let r = s.rawpasabafitz 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. Elh_join(...).rawde MatHelp la destapó porqueh_joindevuelve Str y la forma canónica correcta es SIN.raw(h_join ya concatena a Str) ohtml("...{ops}...").raw. - Asimetría que confirmó el bug: un METHOD-CALL sobre un primitivo SÍ se cazaba
(
s.bogus()→ "Strdoes not have methodbogus" víainfer_method_call); un FIELD-ACCESS bare (s.raw) se colaba comoAnysilencioso — el armExpr::Fielddel checker tenía un fallthrough_ => Type::Anyque tragaba todos los primitivos. - Fix (
src/types.rs, handlerExpr::Fieldensynthesize_expr): el match del receptor pasa aobj_ty.base()(desenvuelveNullableun nivel, asíUser?.nameresuelve contra los fields deUser) con arms explícitos:Nominal(valida fields, ya existía — campo desconocido sigue devolviendoAnysilencioso, sin falso positivo),PyAny → PyAny(chaining),Any → Any(escape gradual — cubre los módulos nativoshttp/log/db/... que se bindean comoAny), 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).campoExistentevalida (no error);Nullable(Nominal).campoInexistentesigueAnysilencioso (comportamiento previo); method-calls intactos; built-ins con fields (HttpClientResponse/Request/Response/File/SmtpResult/Cookie) sonType::Nominal→ caen en el arm Nominal y se validan igual que antes.fitz checkde MatHelp entero (30+ módulos) queda en 0 errores; smokeGUIDE_EXAMPLES_COMPILEverde. - 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.rawes error defitz check, no un panic de runtime.