Auditoría post-Fase 5b — deudas y mejoras¶
Documento generado tras cerrar Fase 5b (codegen a binario nativo). Identifica deudas técnicas, gaps de docs, mejoras de calidad/UX. No ejecuta fixes — es input para decidir qué atacar y en qué orden.
Estado de ejecución: ruta A (quick wins) cerrada — clippy limpio, helpers, validaciones; B.1 (span en Stmt) cerrada — los errores stmt-level del checker ya citan línea/columna reales en lugar de
0:0. C-F2 (field assignment chequeo) cerrada — el checker ahora valida tipos enobj.field = value. F12 (higher-order completo) cerrada — closures escapadas, fn como valor/param/retorno compilan confitz build; cap 11 anotado y validado bit-a-bit. F11 (state HTTP compartido) cerrada —thread_local!por var top-level referenciada en handlers + tokio current_thread runtime;examples/server.fitzyexamples/guide/17-http.fitzcompilan end-to-end. T1 (tests frágiles del codegen) — cerrado entero en tres batches: infra AST-based consyn+quote, ~115 unit tests del codegen migrados de string-match a inspección de AST. Los 10code.containsque quedan encodegen.rsson intencionales: 4 sobre tokens AST normalizados viaast_test::ts(&file), 1 contrato de mensaje de error user-visible, 1 negative check sobre output completo, 4 sobre Cargo.toml (TOML, no Rust). S1.2 (span en Expr) — los 3 sub-pasos cerrados: variantes deExprcarganSpan, parser propaga spans en cada regla, checker (infer_expr+ helpers) y evaluator (eval_expr+ helpers + 14 métodos built-in) citan posición del nodo en errores. S1.codegen cerrado — 52 sitios del codegen migrados aerr_at(con span del nodo); los 17 restantes son defensivos contra bugs del compilador (checker debió cazar), donde citar posición no aporta. HTTP status codes custom cerrado — sintaxis del specreturn <Int> { ... }implementada end-to-end: AST (Stmt::ReturnStatus), parser (detecta el patrón después dereturn <Int>cuando viene un{), checker (acepta solo adentro de handlers HTTP), intérprete (Value::HttpResponse→ outcome con el status pedido), codegen (override del return type a__FitzResponsecuando la fn HTTP contieneReturnStatus, envoltura uniforme de returns normales y custom). Polimorfismo del spec: handler-> Userpuede mezclarreturn user(200) conreturn 404 { ... }. HTTP query params cerrado — sintaxis del spec?key={name}implementada end-to-end:parse_path_templatesepara path y query y devuelvequery_params: Vec<String>adicional;RouteSpec/RouteMeta/InterpTaskcargan los nombres y raw values;build_method_routerextraeQuery<HashMap>en 8 combinaciones (path × query × body); evaluator valida que el handler tenga param Fitz por cada?key={name}y coerciona (Int?opcional → Null si falta;Intobligatorio → 400); codegen emiteaxum::extract::Query<HashMap>+ binding tipado para cada param (Int/Float/Str/Bool, opcionalOption<T>). Tipos no soportados (Lists, custom) abortan codegen con mensaje claro. Cap 17 de la guía + ejemplo17-http.fitzcon nuevo endpoint/search?name={name}&limit={limit}. Bug fix colateral del codegen:BinOp EqentreNullable<T>yNullahora emite.is_none()/.is_some()en vez del literal== (). Intérprete y compilador validados bit-a-bit. Ver matriz para ítems pendientes (Pattern/TypeExpr sin span, T1 sucesivos batches). 1043 tests pasando (+17 dedicados: http path 5, codegen 7, E2E 5).Cierre de Fase 7 (2026-05-13): DX HTTP cerrada con 1150 tests. OpenAPI 3.1 + UI Scalar +
@header(name="X")+@server(docs=false)+fitz openapi archivo.fitz+ paridad bit-a-bitfitz run↔fitz build. Deuda residual abierta:
Middleware + CORS— CERRADA en mini-fase MW (2026-05-14, 1189 tests). Decorator@middleware(fn)apilable + built-incors(...)configurable. Modelo gate-only para middleware genérico (return null/return <status> { ... }); CORS como slot dedicado con preflight OPTIONS y headers inyectados en response real (incluso 500/400).RequestyResponsepre-registrados como nominales built-in. Sub-pasos: MW.1 intérprete; MW.2 cors built-in + preflight; MW.3 codegen completo; MW.4 guía cap 17 sub-sección + ejemplo17b-middleware.fitz+ cierre. Validación E2E bit-a-bitfitz run↔fitz buildvia build + spawn + raw TCP. Deudas que quedan:- Modelo wrap (post-process) para timing/tracing — el gate-only no expresa "after". Mini-fase dedicada post-F8 si aparece presión real.
- CORS request-aware (echo del Origin recibido cuando se admite un set acotado de orígenes). Deuda menor.
- OpenAPI schema con CORS/middleware — el schema no refleja los middlewares aplicados. Útil para docs UI; irrelevante para SDKs generados (server-side concern).
- Body en
Request— hoy el Request expone method/ path/headers; body queda en el handler post-middleware. Para HMAC/signing habría que parsear antes del short- circuit.- Doc-strings sobre handlers (descripciones OpenAPI) — el parser hoy descarta comentarios; retenerlos es refactor lexer+parser+AST. Postergado a post-F17 (es refactor invasivo del lexer/parser/AST; conviene hacerlo cuando el bridge HTTP mpsc/oneshot ya no exista para minimizar merge pain).
Status codes custom en el schema— CERRADO en Q.4 (2026-05-14).collect_status_codes(body)escanea recursivamente losStmt::ReturnStatus; cada code custom aparece como entry enresponsesdel schema con description víahttp_status_phrase. Schema del body queda{}(any) por polimorfismo del spec. Status codes colisionando con derivados del return type (200/500 de Result) ceden al schema fuerte.Aliases en— CERRADO en Q.1 (2026-05-14).@header@header(name="X-Auth", into="token")mapea explícito a un param Fitz con nombre arbitrario. Sinintose mantiene la convención previa (lowercase + '-' → '_').- Bundle Scalar embebido offline — POSTERGADO post-F17 tras evaluar trade-off (Q.5, 2026-05-14). Bundle de Scalar pesa ~3.7 MB minificado y no hay variante liviana. Embeberlo por default rompe la promesa "binario nativo mínimo" (~10-15% de overhead típico). Opt-in via
@server(offline_docs=true)queda comprometido si aparece presión real (deploys air-gapped, requisitos de auditoría). Hoy CDN jsdelivr cubre el 99% de casos — el browser cachea tras el primer load.— CERRADO en Q.2 (2026-05-14).info.versionoverride@server(api_version="X.Y.Z")se refleja eninfo.versiondel schema; default sigue"0.1.0". Cableado por los 3 caminos (fitz run,fitz openapi,fitz build).CORS request-aware— CERRADO en Q.3 (2026-05-14).cors({"allow_origin": ["a.com", "b.com"]})conList<Str>activa modo Set: el server hace echo delOrigindel request si está en la lista permitida; si no, OMITE el headerAccess-Control-Allow-Origin(browser rechaza, comportamiento CORS estricto). Útil con credenciales (Allow-Origin: *incompatible conAllow-Credentials).Mini-tanda Q (2026-05-14): cerró 4 deudas chicas (Q.1 aliases @header, Q.2 api_version, Q.3 CORS Set, Q.4 status codes en schema). Q.5 (bundle offline) postergado por trade-off de tamaño. Q.6 (docs refresh) cerrado en este mismo bloque. Total al cierre de la tanda: 1153 unit + 74 E2E.
Fase F17 (2026-05-14): CERRADA — Send completo + paralelismo HTTP real + bridge eliminado. La deuda más grande arrastrada desde Fase 4. Seis sub-pasos: F17.1 dep parking_lot; F17.2
Shared<T>/EnvRef→Arc<parking_lot::Mutex<T>>(~284 sitios mecánicos); F17.3 quitar?Senddel#[async_recursion](FitzFuture: Send); F17.4aserve()tokio multi-thread; F17.5 eliminar bridge HTTPmpsc/oneshot(~269 LoC netas menos enhttp.rs, handlers axum invocanhandle_task(...).awaitdirecto sobreArc<HttpRegistry>); F17.4b codegen output paralela migración (Rc<RefCell<>>→Arc<Mutex<>>con std::sync, state HTTPthread_local!→LazyLock<Arc<Mutex<T>>>, runtime generado a#[tokio::main]multi-thread,PartialEqcustom por tipo, field access como bloque acotado para evitar deadlocks de re-lock); F17.6 guía cap 19 + ejemploexamples/guide/19b-paralelismo.fitz(validado 5 reqs en 1.2s paralelo vs 5.3s serie). Total al cierre: 1153 unit + 74 E2E, clippy-D warningslimpio. Detalles completos endocs/roadmap.md→ "Fase F17". Próximo norte: Fase 8 (Interop Python).Mini-tanda PreF8 (2026-05-14): CERRADA — cleanup pre-Fase 8. Cuatro sub-pasos: PreF8.1 refactor M1+M2 codegen (
generate_main_rsygen_http_handler_wrapperpartidas en helpers, AST output bit-a-bit idéntico); PreF8.2 method chain multi-línea en parser (newlines antes de.toleradas); PreF8.3 defaults de tipos importados (estrategia eager-at-import conresolved_defaults+__default_<T>_<F>()por módulo); PreF8.4 import aliasing conas(sub-paso adelantado de F8.1). Total al cierre: 1172 unit + 79 E2E, clippy limpio. Detalles completos endocs/roadmap.md→ "Mini-tanda PreF8".Fase 8.1 (2026-05-15): CERRADA — embedding básico de CPython via PyO3.
from python import mathend-to-end en el intérprete (fitz run --features python). Cinco sub-pasos: 8.1.1 dep PyO3 opcional +Value::PyObject(Arc<Py<PyAny>>)feature-gated; 8.1.2import_module(dotted)+ ruteo eneval_python_from_import+py_err_to_fitzcon formato"<ClassName>: <message>"compatible con el wrap aResult<T>que llega en 8.3; 8.1.3Expr::Fieldsobre PyObject con auto-coerción primitiva (None/bool/int/float/str → primitivos Fitz, resto → PyObject opaco); 8.1.4Expr::Callcon args primitivos +value_to_pysimétrico — cumple el criteriomath.sqrt(16.0) == 4.0; 8.1.5 guard de codegencheck_no_python_importscon sugerencia defitz run(la deuda F19 comprometida marca soporte real enfitz buildcomo sub-paso de 8.7). Total al cierre: 1213 unit + 80 E2E + 3 openapi_e2e con feature; 1175 + 80 + 3 sin feature. Decisiones tomadas al arrancar: ABI3-py310, opt-in--features python, política de venvs "estándar Python sin magia", inicialización lazy,Python::attachpor operación. Ejemplo runnable:examples/python-interop-8.1.fitz. Detalles completos endocs/roadmap.md→ "Fase 8.1". Próximo norte: Fase 8.2 (marshaling de tipos compuestos).Fase 8.2 (2026-05-15): CERRADA — marshaling bidireccional de tipos compuestos.
List<T>↔list,Map<K, V>↔dict,Instance→dict(por field name; recovery aInstancerequiere anotación destino — deuda 8.4). Tres sub-pasos: 8.2.1value_to_pycon parámetropath: &strpara breadcrumb informativo (arg0[2].email) + helpersmarshal_map_key(valida keys hashables) yfmt_map_key(cosmético para path); 8.2.2py_to_valuecon ramasPyList/PyDictantes del fallback opaco (PyO3 0.28 deprecódowncasten favor decast— migrado); 8.2.3 criterio canónico del roadmap end-to-end —List<User>Fitz →collections.CounterPython →Map<Str, Int>Fitz indexable, validado bit-a-bit (Counter es subclass de dict,is_instance_of::<PyDict>()matchea subclases naturalmente). Decisiones: copia eager bidireccional (cross-cutting #4), Map keys solo primitivos hashables Python,dictPython NO se auto-coerce aInstance, orden preservado vía garantía CPython 3.7+, breadcrumb propagado recursivamente. Total al cierre: 1245 unit + 80 E2E + 3 openapi_e2e con feature; 1175 + 80 + 3 sin feature. Ejemplo runnable nuevo:examples/python-interop-8.2.fitz(5 secciones). Detalles completos endocs/roadmap.md→ "Fase 8.2". Próximo norte: Fase 8.3 (excepciones Python →Result<T>).Fase 8.3 (2026-05-15): CERRADA — excepciones Python →
Result<T>automático. Toda llamada a una función Python desde Fitz se envuelve: éxito →Result::Ok(v); excepción Python o marshaling fallido →Result::Err(Str("<ClassName>: <message>"))con el formato canónico ya estable desde 8.1.2. El programa Fitz no aborta — el usuario es forzado a manejar conmatcho?. Tres sub-pasos: 8.3.1py_interop::callenvuelve siempre (cualquier falla del path Python — excepción, marshaling de args, marshaling del return — pasa por Err; helper privadoerr_value_from_message) + tests viejos del call path actualizados con helpersok_inner/err_message+ 4 unit nuevos sobre shape + 3 evaluator nuevos del criterio canónico (match, propagación con?, field access sin wrap); 8.3.2 ejemplos 8.⅛.2 reescritos al nuevo modelo (helperunwrap_str,fncon?, caveat del parser de interpolación con{...}documentado); 8.3.3 ejemplo dedicadoexamples/python-interop-8.3.fitzcon 6 secciones (criterio textual del roadmap, distintas excepciones como Err, propagación con?, marshaling fallido con breadcrumb, field access sin wrap, chaining con desempaquetado intermedio). Decisiones:callenvuelve yget_attrno (ergonomía vs ortogonalidad — solo llamadas pueden fallar en runtime esperable); marshaling de args también va enErr(uniformidad del path call);ErrllevaStrplano (PyException estructurada queda como deuda menor); checker NO cambia (refino aResult<Any>llega en 8.4). Total al cierre: 1252 unit + 80 E2E + 3 openapi_e2e con feature; 1175 + 80 + 3 sin feature. Cambio de comportamiento documentado: rompió ejemplos viejos de 8.⅛.2 (reescritos en 8.3.2). Detalles completos endocs/roadmap.md→ "Fase 8.3". Próximo norte: Fase 8.4 (anotaciones del lado del checker + refinar tipos opacos).Fase 8.4 (2026-05-15): CERRADA — tipos del checker + anotaciones del lado Fitz + coerción runtime. Cierra el ciclo "call Python → tipo Fitz concreto" con tres cambios coordinados: el checker distingue valores Python de Any genérico (
Type::PyAny), refina los calls aResult<Any>forzando manejo de errores estático, y el runtime coercionaValue::Map→Value::Instancecuando hay anotación nominal. El patrón canónicolet row: User = py_call(...)?funciona end-to-end con UNA sola anotación. Cuatro sub-pasos (3 commits, 8.4.1 y 8.4.2 combinados): 8.4.1+8.4.2Type::PyAnycon identidad propia + bindings Python (Stmt::Import/FromImportconpath[0] == "python") tipan PyAny + field access sobre PyAny devuelve PyAny + call con receptor PyAny refina aResult<Any>(activa exhaustividad sobre Result 5.3.3 y regla de?5.3.3 estáticamente) +is_compatibleespejo de Any + ramas defensivas encodegen.rs(PyAny no aparece en codegen porquecheck_no_python_importsaborta antes); 8.4.3coerce_to_annotationasync fn nueva en evaluator que resuelveNamed(T)/Nullable(Named(T)), itera fields declarados en orden (provided → resolved_defaults → default Expr → nullable Null → error), ignora extras del Map, devuelve Instance con type_name canónico (PreF8.4); 8.4.4 ejemplo runnable + cierre formal. Decisiones: PyAny dedicado (no PyObject<"..."> fantasma), coerción vive en evaluator no en checker (el cast gradual ya pasa estático), extras del Map se ignoran silenciosamente, field requerido faltante aborta conFitzErrornoResult::Err(caso de programación, no de runtime esperable). Total al cierre: 1271 unit + 80 E2E + 3 openapi_e2e con feature; 1193 + 80 + 3 sin feature. Ejemplo runnable nuevo:examples/python-interop-8.4.fitz(5 secciones validadas bit-a-bit). Detalles completos endocs/roadmap.md→ "Fase 8.4". Próximo norte: Fase 8.5 (fitz py-typesauto-mapeo SQLAlchemy →typeFitz).Fase 8.5 (2026-05-15): CERRADA — sub-comando nuevo
fitz py-types <archivo.py> [--out <archivo.fitz>]que introspecciona modelos SQLAlchemy en un archivo Python y emite lostypeFitz correspondientes, listos para commitear. Reduce el doble-tipado en proyectos SQLAlchemy. Dos sub-pasos: 8.5.1Commands::PyTypesen CLI + nuevo módulosrc/py_types.rsfeature-gated (in-process via PyO3, no subprocess) + introspección por duck typing sobre__table__.columns(compatible con SQLAlchemy real y mocks sin requerirpip install sqlalchemy) + mapping por nombre canónico (Integer/BigInteger/...→Int, Float/Numeric/...→Float, String/Text/...→Str, Boolean→Bool, DateTime/Date/Time→Str ISO 8601 placeholder, resto→Any con// ?comment) + nullable + defaults literales (callable ignorado) + 10 unit tests con classes Python mock. 8.5.2 ejemplo runnableexamples/py-types/(models.pyautosuficiente con mock SQLAlchemy de 25 LoC + 2 modelos User/Order,models.fitzgenerado y commiteado como referencia,usage.fitzconfrom models import+ coerción 8.4.3 + 4 escenarios incluyendo JSON malformado propagado) + cierre formal (CHANGELOG v0.8.6, roadmap, README). Decisiones: in-process via PyO3, duck typing por shape, solo SQLAlchemy en 8.5 (otros ORMs si entra demanda real), tipos desconocidos aAnycon comentario, defaults callable ignorados silenciosamente, sin verificación de drift (regeneración manual). Total al cierre: 1281 unit + 80 E2E + 3 openapi_e2e con feature; 1193 + 80 + 3 sin feature. Ejemplo runnable:examples/py-types/con tres archivos. Detalles completos endocs/roadmap.md→ "Fase 8.5". Próximo norte: Fase 8.6 (async + GIL: bridge tokio ↔ asyncio).Fase 8.6 (2026-05-15): CERRADA — bridge tokio ↔ asyncio. Habilita
py_async_fn().awaitdesde cualquierasync fnFitz: cuando un call a una función Python devuelve una corutina (async def), Fitz la envuelve automáticamente enValue::Futureadentro delResult::Ok. El.awaitpostfix (Fase 6) la desempaca, ejecuta, y devuelve el valor coercionado. Excepciones asyncio →Result::Err(heredado de 8.3). Bridge invisible al usuario. Dos sub-pasos: 8.6.1py_interop::calldetecta awaitable coninspect.isawaitable,is_coroutine+py_coro_to_fitz_futurehelpers, FitzFuture usatokio::task::spawn_blocking+asyncio.new_event_loop() .run_until_complete(coro)(baseline blocking, Send-safe, no deadlockea), 3 tests bajo#[cfg(feature = "python")]; 8.6.2 ejemploexamples/python-interop-8.6.fitzcon 3 secciones (patrón canónicodoble_eventual, awaits encadenadospipeline, lazy sin.await) + cierre formal (CHANGELOG v0.8.7, roadmap, deudas, README). Decisiones: approach baseline blocking en vez depyo3-async-runtimes:: into_future(la crate requiere control del runtime tokio, choca con el setup ya establecido — Fase 6 current_thread CLI / F17 rt-multi-thread HTTP); detección automática de awaitable encall(no.awaitmanual sobre PyObject); GIL serializa Python (esperado por roadmap, funcional para APIs DB-bound); sin marshaling Future Fitz → corutina Python (Future no marshalleable;asyncio.gatherdesde Fitz requiere helper Python externo). Total al cierre: 1284 unit + 80 E2E + 3 openapi_e2e con feature; 1193 + 80 + 3 sin feature. Ejemplo runnable:examples/python-interop-8.6.fitz. Deuda residual visible: event loop asyncio persistente (paralelismo I/O real), marshaling Future↔Coroutine, política de GIL configurable, cancelación de Futures Python, tests multi_thread con paralelismo real. Detalles completos endocs/roadmap.md→ "Fase 8.6". Próximo norte: Fase 8.7 (codegen interop Python enfitz build— cierra deuda F19).Fase 8.7 (2026-05-15): CERRADA — codegen interop Python en
fitz build. Cierra la deuda F19 del roadmap post-5b: el codegen aceptafrom python import, emite Cargo.toml condicional con pyo3, preludio__FitzPyObject(Arc<Py<PyAny>>)con helpers (import, getattr opaco/primitivo, call con marshaling automático, Result wrap, bridge async), y bindings globales (static OnceLock+ getter) accesibles desde cualquier fn. Trait__FitzToPycon impls genéricos para primitivos, List, Map, Option e Instance Fitz (impl emitido porgen_type_defcuandouses_python). Patrón canónico<py_call>?.awaitpara bridge async (paralelo a 8.6.1 baseline blocking). Cuatro sub-pasos: 8.7.1 preludio + import + getattr + Cargo.toml; 8.7.2 call + marshaling Fitz→Python + Result + Instance; 8.7.3 bridge async; 8.7.4 cierre formal conexamples/python-interop-8.7.fitzvalidado bit-a-bitfitz run↔fitz build. Decisiones: alcance acotado (codegen sí, bundling no — sub-paso futuro separado con decisión python-build-standalone vs PyOxidizer pendiente); bindings globales con OnceLock + getter (vsletlocal — destraba uso en handlers HTTP sin refactor); patrón?.awaitúnico (paridad bit-a-bit con intérprete); auto-coerción primitiva viacoerce(PyAny → T)(aprovecha infraestructura existente). Total al cierre: 1295 unit + 88 E2E + 3 openapi_e2e con feature; 1204 + 79 + 3 sin feature. Ejemplo runnable:examples/python-interop-8.7.fitzcon 3 secciones (constantes + calls + bridge async). Deuda residual visible (sub-paso futuro): coerción Python list/dict → Fitz List/Map/Instance,.awaitcon binding intermedio split, bundling CPython embebido, trait__FitzFromPysimétrico. Detalles completos endocs/roadmap.md→ "Fase 8.7". Próximo norte: Fase 8.8 (guía + ejemplo CRUD + cierre formal de Fase 8).Fase 8.8 (2026-05-15): CERRADA — guía + ejemplo CRUD + cierre formal de Fase 8 entera. Tres sub-pasos: 8.8.1 cap 21 "Interop Python" en
docs/guide.mdcon 12 sub-secciones cubriendo 8.1-8.7 + renumeración cap 21→22; 8.8.2 ejemplo ejecutableexamples/guide/21-python-crud/con SQLAlchemy + SQLite (models.py+db.py+models.fitzgenerado +app.fitzcon handlers HTTP), validado end-to-end con curl; 8.8.3 cierre formal (CHANGELOG v0.8.9, roadmap, deudas, README). Decisiones de scope (confirmadas con autor): cap 21 con una renumeración (vs cap 20 con dos), backend SQLite (vs Postgres con Docker o sin DB), solofitz runcon nota explícita sobre deuda residual de 8.7 (vs validar paridad confitz build). Detalles completos endocs/roadmap.md→ "Fase 8.8".Cierre formal de Fase 8 (Interop Python) entera (2026-05-15): roadmap original cumplido al 100% (8.1 embedding, 8.2 marshaling, 8.3 excepciones → Result, 8.4 tipos del checker + coerción, 8.5 fitz py-types, 8.6 bridge async, 8.7 codegen, 8.8 guía + CRUD). Sub-paso separado pendiente (no parte del roadmap original): bundling CPython embebido (
fitz build --bundle-python). Próximo norte: Fase 9 — Ecosistema (package manager, LSP, formatter, linter); pre-reqs habilitantes ya identificados: F15 (parser error recovery) + F16 (IR tipado persistido por nodo).Fase 9.0 (2026-05-15): F15 CERRADO — error recovery del parser. Tres sub-pasos: 9.0.1 nodos
Expr::Error(Span)/Stmt::Error(Span)in-band +pub fn parse_with_recovery(tokens) -> (Program, Vec<FitzError>)conrecovery_modeinterno + cota 100 errores + sync points stmt-level (Newline consumido, RBrace/EOF preservados, keywords de inicio de stmt preservadas por necesidad —primary()consume el token al fallar y sync sin la parada se comía stmts enteros); 9.0.2 checker silencioso (Expr::Error → Type::Any,Stmt::Errorno-op) + helpercheck_recovering(src)que corre el pipeline LSP-style; 9.0.3 validación end-to-end + cierre formal. API strict (parse/fitz run/fitz build/fitz check) intacta — sin cambio user-facing. 10 + 5 = 15 unit tests nuevos. Total al cierre: 1219 unit + 79 E2E + 3 openapi sin feature. Clippy-D warningslimpio. Próximo norte: F16 (IR tipado persistido por nodo) — segundo pre-req habilitante del LSP. Detalles completos endocs/roadmap.md→ "Fase 9.0".Fase 9.0 entera CERRADA (2026-05-15): F16 (IR tipado persistido por nodo) cierra los pre-reqs habilitantes del LSP. 2 sub-pasos: 9.0.4 side-table
TypeInfoconSpanKey(line, column)como clave (Span propio no sirve porque su PartialEq devuelve true siempre por diseño),infer_exprenvuelvesynthesize_exprpara centralizar elrecordal salir,check_programcambia firma a(TypeEnv, TypeInfo, Vec<FitzError>)(13 call sites migrados con_types),Expr::Errorse persiste comoType::Anyuniforme con el checker, 8 unit teststypes::tests::types_info_*; 9.0.5 cierre formal (CHANGELOG v0.9.1, roadmap con Fase 9.0 — F16 detallada, este archivo con F16 CERRADO, README refresh). API user-facing intacta —fitz run/fitz build/fitz checkdescartan el side-table con_types. Total al cierre: 1227 unit + 79 E2E + 3 openapi sin feature. Clippy-D warningslimpio. Deuda residual derivada de F16 (NO bloquea sub-fases visibles del LSP): sin index espacial (rango inicio-fin) en el side-table — el LSP elige nodo más cercano al cursor por ahora; spans enTypeExpryPattern(heredado de S1, refinable cuando aterrice el primer caso de uso real); cobertura deStmt(ortogonal — el LSP resuelve declaraciones por scope lookup en 9.x.3). Próximo norte: sub-fases visibles del LSP — 9.x.1 (diagnostics MVP). Ver detalle endocs/roadmap.md→ "Fase 9.0 — F16".Mini-tanda Q.z (2026-05-16): CERRADA — quickwins pre-9.z.2. Tres ítems atacados antes de arrancar
fitz test: - F6 audit builtins: confirmado que el syntax-spec NO prometerange/type_of/to_stringglobales (la matriz F6 estaba especulando). Builtins implementados (len,sleep,cors) coinciden 1:1 con lo que el spec lista como builtin-globales. Único hallazgo: el ejemplo del test runner endocs/syntax-spec.md:515usapanic("falló: {e}")que NO está en la lista oficial de assertion builtins (assert,assert_eq,assert_ne,assert_throws). Decisión de scope para 9.z.2: incluirpanic(msg)como builtin auxiliar o dejarlo fuera. Sin acción técnica en Q.z. - D1 refresh header guide.md: pasó de "Fase MW + tanda Q, 1153 unit + 74 E2E" a "Fase 9.z.1 cerrada — fitz fmt production-ready, 1333 unit + 55 cli_e2e + 79 compile_e2e + 3 openapi". Bullets stale de "Qué todavía no anda" depurados (async/await reales, status codes custom, query params, named args ya cerrados — 4 ítems quitados). "Builtins globales" expandido a los 4. "Cómo está organizada" actualizó parte 10 (Tooling = LSP + formatter) y sumó partes 8-11. Sección "Lo que viene" (cap 24) refrescó el bullet de Fase 9 con el estado real (LSP entero cerrado, PM 9.y.1-9.y.4 cerrados, fmt cerrado, próximo testing). - Cap 23 nuevo "fitz fmt" en guía: cap dedicado con features, CLI, estilo canónico (resumen + link adocs/fmt-style.md), 2 ejemplos in-line (antes/después + preservación de comments) + ejemplo runnable nuevoexamples/guide/23-fmt-ejemplo.fitzsumado al smokeGUIDE_EXAMPLES_COMPILE. Renumeración 23→24 ("Qué sigue"). Cumple la regla del proyecto "implementado = documentado con uno o varios ejemplos".Deudas residuales identificadas durante Q.z (NO bloquean 9.z.2): - Cap "Package manager" en la guía: las 6 subcomandos del PM (
fitz new/init9.y.1,fitz add/remove/update9.y.4) están implementadas + cerradas + en CHANGELOG/roadmap pero NO tienen capítulo dedicado endocs/guide.md. Estructura sugerida: cap nuevo "Package manager" en Parte 6 (Organización), entre cap 16 (Módulos) y cap 17 (HTTP), con sub-secciones parafitz new/init, manifestfitz.toml,[dependencies]path/git, lockfilefitz.lock,fitz add/remove/update, y al menos un ejemplo runnable completo con dos proyectos (lib + binario que importa la lib). ~2h de trabajo bien hecho. Etapa: meter como sub-paso dedicado pre-9.w (después que 9.z entera cierre — testing, dev, repl, lint), junto con un refresh general de la guía sincronizado con todo 9.y + 9.z cerrado. Si aparece presión antes (preguntas de usuarios sobre cómo crear un proyecto), acelerable como sub-paso pre-9.z.2 dedicado. -Bug del formatter: trailing comment al final del body de una fn seguido de otro bloque inserta blank spurious dentro del body del bloque siguienteCERRADO (2026-05-17, post-9.z.5). Root cause:had_blank_in_sourceenfmt_stmt_listusabaafter_what = max(prev_end_line, last_emitted_comment_line); cuando entrabamos a un nuevo bloque (in_block=true,prev_end_line=0), ellast_emitted_comment_linearrastraba un valor de scope outer yhas_blank_betweenchequeaba blanks FUERA del bloque actual. Fix: agregar guarda — enin_block, el chequeo requiereprev_end_line > 0(paralela a lasmart_blank); en top-level se preserva el behavior previo (after_what > 0) para no romper blanks entre header comments y el primer stmt. Test E2Efmt_trailing_comment_seguido_de_bloque_no_inserta_blank_spurioprotege contra regresión.Fase 9.z.2.a (2026-05-17): CERRADA —
@testdecorator + assertion builtins +TestRegistry. Primer sub-paso de 9.z.2 (testing built-in). Total al cierre: 1364 unit + 55 cli_e2e + 79 compile_e2e + 3 openapi. Clippy-D warningslimpio.Cambios técnicos: -
src/testing.rsnuevo:TestRegistry,TestSpec,with_active_test_registry(+ variante async) + thread-local. Mirror chico dehttp::HTTP_REGISTRYcon la asimetría clave: si no hay registry activo,@testes no-op silencioso (paralelo a#[cfg(test)]de Rust), no error. -evaluator.rs::process_decoratorsuma branch@testcon helperregister_test: valida args/kwargs/params vacíos y empujaTestSpecal registry si hay uno. Sin registry, sigue normal. - 4 assertion builtins nuevos:assert(cond: Bool, msg: Str?),assert_eq(a, b),assert_ne(a, b),assert_throws(fn). Estilo cargo test: mensajeleft/rightparaassert_eq,iguales (val)paraassert_ne. Igualdad estructural recursiva (reusaPartialEqde Value que coerciona Int↔Float). -assert_throwscaso especial eninvoke_value:Value::Builtin { name: "assert_throws", .. }se intercepta antes del despacho genérico (necesario porque los builtins son sync pero invocar un callback Fitz requiere async-recurse coninvoke_value). El stub registrado emiteunreachable!si llegara a invocarse — sentinel de bug del dispatcher. - Restricción MVP deassert_throws: callback debe serFunctionaridad 0 NO async. Async cb produceValue::Futuresuelto (no equivalente a "tirar"); cubrirlo requiereassert_throws_asynco flag — sub-paso futuro si aparece presión. - Pre-registro en el checker (types.rs::register_builtins):assertcomoType::Any(aridad variable 1-2); el resto con firmas estructuradas.assert_throwsexigeFunction { params: [], ret: Any }(chequeo estático de aridad del cb). - Completion en LSP (lsp.rs) suma los 4 builtins nuevos al listado de builtins detectables vía scope-level autocomplete. - Cambio retro-compatible al parser: paréntesis opcionales en decoradores.@test fn ...(sin()) parsea con args vacíos. Antes el parser exigía(siempre. Cambio retro-compatible (todos los@server()/@get("/x")siguen funcionando idéntico). Testdecorator_sin_parens_erroresreescrito comodecorator_sin_parens_parsea_con_args_vacios.Decisiones que tomaron forma durante 9.z.2.a: -
panic(msg)(que el syntax-spec usa en el ejemplo del test runner, línea 515) NO entra al MVP. Los 4 builtins oficiales (assert*) son la lista cerrada de 9.z.2. Si aparece presión, sub-paso 9.z.2.a.bis o post-MVP. - Sintaxis@test fnsin paréntesis: confirmada como forma canónica (matchea el spec).@test()también parsea por simetría — el parser es agnóstico, la decisión es del evaluator. -assertexigeBoolestricto en el primer arg (no truthy/falsy). Consistente con la decisión de diseño "sin truthy/falsy" del cap 6 de la guía. - Tests con feedback inmediato del decorator: los 4 errores de validación (@testsobre fn con params, con args, con kwargs, sobre tipo no-Function) levantan en eval-time, no cuando el runner los invoca — sigue el patrón de@servery@get.Tests nuevos: 6 en
testing.rs(registry empty/push/ with_active/with_active_async/aislamiento entre anidados), 6 enevaluator.rs::tests(decorator sin registry no-op, con registry registra, async fn → is_async true, preserva orden, params error, args error, kwargs error), 18 enevaluator.rs::tests(los 4 builtins con happy/falla/type errors/aridad/coerción Int↔Float/estructural en listas), 2 enparser.rs::tests(decorator sin parens parsea OK,@testsin parens parsea OK). Total: +32 unit tests.Deudas residuales (NO bloquean 9.z.2.b): -
assert_throwscon callback async: rechazado explícitamente en runtime.assert_throws_async(fn)o variante del builtin queda como sub-paso futuro si aparece presión. - Reporte de span del fallo: cuando unassert*falla, elFitzErrorllevaline: 0, column: 0(los builtins son sync y no reciben el span del call site). El span del call sí está disponible eninvoke_value; podríamos enriquecer el error después del fact. Refinamiento útil pero NO MVP. - 9.z.2.b (runner CLI): este sub-paso cerró solo la infraestructura del lenguaje (decorator + registry + builtins). El sub-comandofitz test, discovery (lib/bin +tests/*.fitz), output estilo cargo, filtrado, exit codes — todo entra en 9.z.2.b.Deudas de docs acumuladas (NO bloquean 9.z.2.b) — agrupadas para tratamiento dedicado cuando 9.z entera cierre: - Cap "Package manager" en la guía (heredado de Q.z) — las 6 subcomandos de 9.y.1-9.y.4 sin capítulo dedicado. -
Bug del fmt con trailing comment(heredado de Q.z) — CERRADO post-9.z.5 (fix enfmt_stmt_listcon guardaprev_end_line > 0enhad_blank_in_sourceparain_block=true). -docs/architecture.md— los diagramas del pipeline (lexer/parser/checker/evaluator/codegen) y los pointer de módulos están desactualizados respecto a las fases cerradas post-5b (sumartesting.rs,manifest.rs,lockfile.rs,git_dep.rs,fmt.rs,lsp.rs,py_interop.rs,py_types.rs; sumar el flujo del LSP + PM + interop Python en los diagramas). - Refresh general dedocs/guide.md+ ejemplos — varios capítulos arrastran texto stale por fases cerradas posteriormente. Algunas secciones de "Lo que todavía no anda" todavía citan features ya implementadas; algunos capítulos no mencionan cambios derivados (paréntesis opcionales en decorators, builtins assertion). Sincronización masiva pendiente.Etapa propuesta para las deudas de docs: sub-paso dedicado "Refresh masivo de docs" cuando 9.z entera cierre (post-9.z.5), antes del salto a 9.w. Sub-pasos sugeridos: (a) cap "Package manager" nuevo + ejemplos runnables; (b)
docs/architecture.mdrefresh completo con diagramas nuevos; © walk del cap-by-cap deguide.mdpara detectar texto stale; (d)docs/syntax-spec.mdactualizar matriz al estado de cierre 9.z (refresh recurrente, ya marcado como deuda continua). ~4-6h estimadas para hacerlo bien.Fase 9.z.2 ENTERA CERRADA (2026-05-17) —
fitz test(testing built-in). Tres sub-pasos cerrados en el día:
- 9.z.2.a — decorator + asserts + registry (ver bloque anterior en este archivo).
- 9.z.2.b — runner cargo-style + discovery (
Commands::Testdiscover_test_sources_from_manifestcon dedup lib/tests + auto-self-import bajopackage.name+run_test_registrycon output cargo-style + ANSI auto viaIsTerminal+ exit code 1 si falla; 11 cli_e2e nuevos).- 9.z.2.c — cap guía + ejemplo + cierre formal (este sub-paso): cap 24 nuevo "
fitz test— testing built-in" endocs/guide.md(renumeración 24→25), ejemplo runnableexamples/guide/24-tests.fitzcon factorial + 3 tests OK- 1 FAILED intencional sumado al smoke
GUIDE_EXAMPLES_COMPILE, codegen ignora@test fnsilenciosamente (paralelo a#[cfg(test)]Rust), bug fix colateral enhas_http_routes(counting@testcomo HTTP disparaba server en CLI puros), CHANGELOG v0.9.16, roadmap, README, syntax-spec actualizado a v0.4 (matriz refleja interop / LSP / PM / fmt / test como implementados).Total al cierre de 9.z.2: 1366 unit / 66 cli_e2e / 79 compile_e2e / 3 openapi. Clippy
-D warningslimpio.Deudas residuales de 9.z.2 (NO bloquean 9.z.3): -
assert_throwscon callback async: rechazado en runtime (FitzError claro). Sub-paso futuro si aparece presión — posiblementeassert_throws_asynco flag dedicado. - Span del fallo en assertion builtins: elFitzErrorllevaline: 0, column: 0porque los builtins son sync y no reciben el span del call site. Útil para reportar la línea exacta de la aserción fallida. Refactor: el caller deValue::Builtin { func, .. }eninvoke_valueya tiene el span; el wrapper podría enriquecer el error después-del-fact cone.line = span.linesiline==0. ~30 min de trabajo. - Nombres de paquete con hyphens:package.name = "my-pkg"no es importable desde Fitz (from my-pkg import Xno parsea —-no es ident válido). Workaround: usar underscores. Documentado en cap 24 de la guía. Refinable en lexer/parser si aparece presión. - Tests inline en[lib]sin tests integration que lo importen: si el proyecto tiene tests/ +[lib]con@testinline, pero ningúntests/*.fitzimporta la lib, esos tests del lib NO se descubren (modo "tests integration" solo cargatests/*.fitzdirect). Edge case raro; workaround: agregar unfrom <pkg> import _decorativo a algún test integration.Próximo norte: 9.z.3 (
fitz devcon file watcher + hot reload + dev experience).Fase 9.z.3 CERRADA (2026-05-17) —
fitz dev(hot reload). File watcher cross-platform vianotifycrate + kill/respawn del child al detectar cambios en.fitzofitz.toml. Tercera DX feature de Fase 9.z cerrada en el día (después de 9.z.2).Implementación:
Commands::Dev { file }con resolver single-file/manifest paralelo afitz test/fitz run. Loop principal en runtime tokio current_thread contokio::select!sobre 3 eventos: cambio del watcher (debounce 100ms + kill+respawn), child terminó solo (espera próximo cambio), otokio::signal::ctrl_c()(kill + clean exit). Bridge sync→async entrenotify(sync) y tokio mpsc via std::spawn. Path filtering:
*.fitz+fitz.toml, excluyetarget//.git//node_modules//.fitz//dist//build/+ componentes ocultos. Banner ANSI clear screen si TTY.Decisiones tomadas:
[dev]section NO en MVP; browser auto-refresh NO; print errors live sin restart NO (LSP cubre); smoke E2E automatizado NO (file watchers son flaky).Cap 25 nuevo "
fitz dev— hot reload" endocs/guide.md(renumeración cap 25→26 "Qué sigue").Total al cierre 9.z.3: 1366 unit / 66 cli_e2e / 79 compile_e2e / 3 openapi (sin cambios, dev_cmd es interactivo). Clippy
-D warningslimpio. Smoke manual validó arrancar → modificar → ver run #2 con código nuevo.Deudas residuales de 9.z.3 (NO bloquean 9.z.4): - Incremental rebuild: kill+respawn full es el approach del MVP. Modelo de módulos pre-compilados queda como sub-paso futuro si los tiempos duelen. - Filter "modify sin cambio real": timestamps tocados sin cambio de contenido disparan restart. Comparar hashes si aparece presión. -
fitz dev --test(modo watch + run tests): workaround documentado con dos terminales. Sub-paso si aparece presión. - Smoke E2E automatizado: pendiente. File watchers requieren orquestación cuidadosa para no ser flaky.Próximo norte: 9.z.4 (
fitz replinteractivo con rustyline + scope persistente entre líneas + comandos especiales:type/:env/:reset/:load).Fase 9.z.4 CERRADA (2026-05-17) —
fitz repl(REPL interactivo). Cuarta DX feature de Fase 9.z cerrada en el día. Promptfitz>con env compartido, multi-line via balanced brackets, 6 comandos especiales (:help/:quit/:env/:reset/:type/:load), history persistente en~/.fitz/history, pretty-print Python-style, async transparente.Implementación: dep
rustyline = "14"+Commands::Repl+repl_cmdadentro de runtime tokio current_thread. APIs públicas nuevas en evaluator (eval_program_with_env,new_repl_env,builtin_names) y env (local_names). Filtro de warning spurio del checker para "variable desconocida" (substring match, no kind: todos los errors del checker llevanTypeError).:typearma programa sintético sin scope del REPL — limitación documentada.Decisiones tomadas:
:typescope-aware NO en MVP; smoke E2E automatizado NO (rustyline + readline son flaky en tests); manifest mode en REPL NO (siempre single-session); auto- completion NO en MVP.Cap 26 nuevo "
fitz repl— REPL interactivo" endocs/guide.md(renumeración cap 26→27 "Qué sigue").Total al cierre 9.z.4: 1366 unit / 66 cli_e2e / 79 compile_e2e / 3 openapi (sin cambios; repl_cmd interactivo no agrega tests automáticos). Clippy
-D warningslimpio.Deudas residuales de 9.z.4 (NO bloquean 9.z.5): -
:typescope-aware (refactor checker pre-declared scope). - Smoke E2E automatizado del REPL (rustyline en raw mode complica tests). - Indentación automática en multi-line continuation. - Comandos extras (:save/:undo/:debug/auto-completion). - Manifest mode enfitz repl(single-session siempre).Próximo norte: 9.z.5 (
fitz lint— linter de patrones más allá de tipos: unused_variable, unused_import, useless_match, string_concat, panic_in_test_only, redundant_clone). Cierra Fase 9.z entera.Fase 9.z.5 CERRADA (2026-05-17) — CIERRE FASE 9.z ENTERA.
fitz lintcon 4 lints implementados: -unused_variable—let x = ...sin uses, skip_var. -unused_import—import X/from X import Ycon binding no referenciado. -useless_match— match con UN solo arm catch-all (Wildcard o Ident binding). -string_concat—BinOp Addcon ambos operandosStrliterales.Lints skipeados del roadmap:
panic_in_test_only(no aplica — Fitz no tienepanic!builtin distinguido) yredundant_clone(requiere análisis de movimientos no implementado).Módulo nuevo
src/lint.rs(~700 LoC con 15 unit tests).Commands::Lint { files, deny }en CLI con output cargo-clippy style. Supresión por// @allow(<lint>)en la línea anterior via inspección del source raw. Default warnings + exit 0;--deny <name>promueve a error + exit 1.Total al cierre 9.z.5: 1381 unit + 73 cli_e2e + 79 compile_e2e + 3 openapi (+15 unit + 7 cli_e2e vs 9.z.4). Clippy
-D warningslimpio.Cap 27 nuevo "
fitz lint" endocs/guide.md(renumeración cap 27→28 "Qué sigue").Decisiones tomadas: 4 lints (no 6); auto-fix DIFERIDO; análisis de uses globales (no scope-aware estricto); catálogo cerrado (sin plugins); default warnings +
--deny <name>para CI.Deudas residuales de 9.z.5 (NO bloquean 9.w): - Auto-fix
--fix(candidato natural:string_concat). -unused_variablescope-aware estricto (shadowing). - Suppression cross-line (// @allow(name) { ... }bloque). - Lints adicionales (shadowing,useless_clonecuando el compilador haga análisis de movimientos). - Plugins externos.
CIERRE FORMAL DE FASE 9.z ENTERA (2026-05-17): los 5 sub-pasos de DX (fmt + test + dev + repl + lint) cerrados en 2 días consecutivos (16-17 de mayo). Suite final acumulada: 1381 unit + 73 cli_e2e + 79 compile_e2e + 3 openapi. Clippy limpio. 5 capítulos nuevos en
docs/guide.md(23-27), renumeración "Qué sigue" del cap 22 original al cap 28 actual. Deps nuevas:rustyline = "14"(REPL),notify = "6"(dev).Deudas mayores acumuladas durante 9.z (priorizadas como sub-paso dedicado de refresh masivo de docs, próximo natural tras 9.z): 1. Cap "Package manager" en la guía (heredado de Q.z). 2.
docs/architecture.mdrefresh completo con diagramas nuevos (testing/manifest/lockfile/git_dep/fmt/lsp/lint y los flujos asociados; el bridge HTTP mpsc/oneshot eliminado en F17 sigue documentado). 3. Walk completo dedocs/guide.mdcap-by-cap para detectar texto stale derivado de las features cerradas post-fmt-style (paréntesis opcionales en decorators, builtins assertion, etc.). 4.Bug del fmt con trailing comment al final de body seguido de otro bloque— CERRADO post-9.z.5 (fix enfmt_stmt_listcon guarda condicional in_block/top-level).Próximo norte: Fase 9.w (Stack web first-class —
@authenticated/@admin,@ws("/chat"),@cron,@background) o el sub-paso dedicado de refresh masivo de docs.Nota (2026-05-20) — Fase 9.w.1 (Auth nativa) CERRADA: el primer sub-paso del stack web first-class está implementado entero. Tres decoradores nuevos del lenguaje (
@auth_providersingleton,@authenticated,@admin) + dos módulos built-in (jwtcon HS256/384/512,hashcon Argon2id) cubren el flujo de login + JWT + password hashing entero sin deps externas. El checker valida estáticamente que cada handler protegido tenga el provider registrado y reciba elUsercorrecto. El schema OpenAPI auto-agregasecuritySchemes.bearerAuth+securitypor handler + 401/403 en responses. Paridad bit-a-bitfitz run↔fitz build. Sub-pasos cerrados:
- 9.w.1.a — Checker valida los 3 decorators (16 unit tests).
- 9.w.1.b — Built-ins
jwt/hashcomoValue::Modulepre-registrados conjsonwebtoken = "9"+argon2 = "0.5"rand_core = "0.6"deps no-opcionales (16 unit tests).- 9.w.1.c — Runtime auth en
fitz run:AuthSpecenum +AuthProviderHandle+ wrapper enhandle_task(9 unit E2E).- 9.w.1.d — Codegen
fitz build: helpers en preludio + dispatch engen_call+emit_auth_checkespejo del intérprete (2 tests compile_e2e).- 9.w.1.e — OpenAPI security scheme:
bearerAuth+securitypor handler + 401/403 auto (5 unit tests del schema).- 9.w.1.f — Cap 28 nuevo en
docs/guide.md+ ejemplo runnableexamples/guide/28-auth.fitz(login + /me + /admin, <100 LoC) + README emphasis del diferencial + smokeGUIDE_EXAMPLES_COMPILE.Decisiones técnicas del MVP (no en el roadmap original):
Map<Str, Str>strict para payload dejwt.encodey return dejwt.decode(heterogéneos requieren__FitzValuepost-MVP);hash.verifydevuelveBool(no Result) por seguridad; provider order required (provider antes que handlers); handler protegido NO admite body separado del user en MVP.Deuda residual derivada de 9.w.1 (NO bloquea uso real; queda comprometida en
docs/roadmap.md→ "Fase 9.w iteración 2"): sessions cookie-based + RBAC multi-rol + token refresh/ revocación (requieren DB nativa, Fase 10); asimétricos JWT (RS256/ES256 con PEM); provider request-aware más allá de headers; heterogéneos enjwt.encode/decode(requiere__FitzValueen codegen).Próximo norte: resto de Fase 9.w —
@ws("/chat")(WebSockets tipados conWsConn<T>),@cron+@background(jobs sin Celery), y ORM nativo + migraciones (escalado a Fase 10).Nota (2026-05-21) — Fase 9.w.2 (WebSockets tipados) CERRADA: el segundo sub-paso del stack web first-class está implementado entero.
@ws("/path")sobreasync fn+WsConn<T>con métodosrecv/send/broadcast/closemontan un servidor de WebSockets tipado end-to-end. Cinco diferenciales que vuelven a Fitz único en este espacio: marshaling JSON automático (cada frame text se serializa/ deserializa altypedeclarado, sin glue manual); AsyncAPI 3.0 auto-generado en/asyncapi.json(la spec hermana de OpenAPI 3.1 para event-driven APIs, consumible por tooling estándar); heartbeat built-in con@server(ws_heartbeat_secs=N)(Ping frames automáticos que pasan de largo proxies idle-killers); auth integrada (@authenticated/@adminapilados sobre@wsvalidan bearer ANTES del HTTP upgrade); codegen con paridad bit-a-bitfitz run↔fitz build. Ningún otro lenguaje hoy combina WS tipados con AsyncAPI auto-generado del código fuente, heartbeat built-in y auth integrada en el handshake. Sub-pasos cerrados:
- 9.w.2.a — Checker estático:
Type::WsConn(Box<Type>),infer_wsconn_methodcon signatures paramétricas,check_ws_handlervalidando shape (14 unit tests).- 9.w.2.b — Value runtime + evaluator:
WsConnHandle,WsOutMessage(Text/Close),Value::WsConn,register_ws_route,dispatch_methodarms,ws_conn_recvconcoerce_to_annotation(heredado 8.4.3) para Map → Instance cuando T es nominal.- 9.w.2.c — Runtime HTTP:
WsBroadcasterconparking_lot::Mutex<HashMap<endpoint, Vec<(conn_id, outbox_tx)>>>,WsReadStreamImpl,build_ws_method_routercon auth pre-upgrade (401/403 ANTES dews.on_upgrade),build_ws_conncon writer task + outbox separado. axum 0.8 featurews+futures-util+ dev-deptokio-tungstenite.- 9.w.2.d — AsyncAPI 3.0 (
src/asyncapi.rs~350 LoC): channels + operations receive/send + securitySchemes,BTreeMappara orden determinístico,/asyncapi.jsonroute en runtime y codegen (8 unit tests).- 9.w.2.e — Heartbeat ping/pong automático:
WsOutMessage::Ping,ServerConfig.ws_heartbeat_secsdefault 30s,@server(ws_heartbeat_secs=N)kwarg,tokio::time::intervalspawneado enbuild_ws_conncuando N > 0 (6 unit tests).- 9.w.2.f — Cap 29 nuevo en
docs/guide.md(renumeración 29→30) + ejemplo runnableexamples/guide/29-ws.fitz(servidor de chat con login HTTP + JWT +@authenticated @ws("/chat")+ broadcast multi-client +@server(43929, ws_heartbeat_secs=30), <100 LoC) + README emphasis (5 diferenciales en tabla + footnote dedicado + bullets en "Estado del proyecto" y "Qué funciona hoy") + smokeGUIDE_EXAMPLES_COMPILE.Decisiones técnicas del MVP (no en roadmap original):
Arc<HttpRegistry>compartido (mismo modelo F17);tokio::sync::MutexenWsConnHandle.rx(necesita Send across .await);parking_lot::MutexenWsBroadcaster.conns(no cruza await); manual Clone impl para__FitzWsConn<T>en codegen sinT: Clonebound; broadcast incluye al sender (convención Socket.IO/Phoenix); auth pre-upgrade (menos attack surface);ws_heartbeat_secs=0desactiva sin error.Deuda residual derivada de 9.w.2 (NO bloquea uso real; queda comprometida en
docs/roadmap.md→ "Fase 9.w iteración 2"): binary frames (Vec<u8>payload — hoy solo text; integración con tipoBytesya cerrado); AsyncAPI UI equivalente al/docsde OpenAPI (hoy solo JSON); tipado bidireccional separado (WsConn<In, Out>— hoyTúnico); reconnect con state replay (requiere persistencia, Fase 10); rooms/channels dentro de un endpoint (broadcast a TODOS los clientes del endpoint); backpressure explícito (outbox unbounded hoy).Próximo norte: resto de Fase 9.w — 9.w.3 (
@cron+@background— jobs sin Celery) y 9.w.4 (ORM nativo + migraciones, escala a Fase 10).Nota (2026-05-21) — Fase 9.w.3 (Jobs sin Celery) CERRADA: el tercer sub-paso del stack web first-class está implementado entero. Tres piezas nativas del lenguaje montan jobs sin broker externo:
@cron("expr")para tareas periódicas (⅚/7 fields cron Unix),@backgroundcomo marcador opt-in para autorizar el callsite, yspawn(fn_call)fire-and-forget que devuelveFuture<T>tipado. Sin Celery, sin Redis, sin systemd timers — todo en el mismo binario con paridad bit-a-bitfitz run↔fitz build. Cinco diferenciales que vuelven a Fitz único en este espacio: decoradores nativos del lenguaje (parte del compilador, no lib opcional), sin broker externo (jobs viven en memoria del proceso, suficiente para 90% de servicios reales),spawncon tipado (refinamiento estático aFuture<T>con T concreto), paridadfitz run↔fitz build, y ceropip install celery/cargo add tokio-cron-scheduler. Ningún otro lenguaje combina cron + background workers + spawn tipado en el core sin broker externo y con paridad intérprete↔binario. Sub-pasos cerrados:
- 9.w.3.a — Checker estático:
CheckCtx.background_fnspoblado porcollect_background_fnsantes del walk;check_cron_decorator+check_background_decorator+ dispatch especial despawn(...)ensynthesize_exprque refina ret type aFuture<T>(17 unit tests).- 9.w.3.b — Runtime intérprete: nuevo módulo
src/cron_jobs.rsconCronJob+CronRegistry(paralelo a HttpRegistry) +spawn_cron_scheduler+run_scheduler_only(cron-only mode con multi_thread + ctrl_c).process_decoratorbranches para@cron/@background.eval_callinterceptaspawn(fn_call)ANTES de evaluar args. Cron-only mode enmain.rs. Fix bug preexistente: handlersasync fnHTTP en intérprete retornaban "Future pendiente no es serializable" porquehandle_tasknunca awaiteaba el Future. Helperawait_if_future. Normalización 5→6 fields automática. Depscron = "0.12"+chrono = "0.4"(8 unit tests).- 9.w.3.c — Codegen
fitz build: Cargo.toml condicional sumacron/chrono+ featuresignal(cron-only mode); multi_thread flavor con jobs; preludio__fitz_run_cron_job- helper
__fitz_normalize_cron;PartitionedProgram.cron_fns;emit_cron_job_spawns()invocado desdegen_mainygen_http_main;spawn(fn_call)dispatch que emitetokio::spawn(async move {...})+Box::pinpara case conPin<Box<dyn Future>>(7 unit tests).- 9.w.3.d — Cap 30 nuevo en
docs/guide.md(renumeración 30→31) + ejemplo runnableexamples/guide/30-cron-background.fitz(URL shortener con HTTP + cron stats + spawn tracking, <100 LoC) + README emphasis con tabla + footnote ♠ + bullets en "Estado del proyecto" y "Qué funciona hoy" + smokeGUIDE_EXAMPLES_COMPILE.Decisiones técnicas del MVP (no en roadmap original): cron-only mode vivo bloqueante (modo systemd-friendly, confirmado con el autor);
@cronacepta sync y async (confirmado);@backgroundopt-in (evita usos accidentales);spawn(...)exige call literal a fn@background(permite refinamiento estático); cratecron = "0.12"(vs propio otokio-cron-scheduler); normalización 5→6 fields automática (preserva UX familiar); JoinHandle envuelto enValue::Future/Pin<Box<dyn Future>>(unifica conFuture<T>existente).Deuda residual derivada de 9.w.3 (NO bloquea uso real; queda comprometida en
docs/roadmap.md→ "Fase 9.w iteración 2"): persistencia de jobs entre restarts (requiere DB nativa, Fase 10); visibility de jobs (panel admin con runs, stats, retries); retry con backoff exponencial; coordinación entre múltiples instancias (locks distribuidos);spawncon coordinación múltiple (Promise.all style); cron timezone configurable (hoychrono::Utc::now()).Próximo norte: resto de Fase 9.w — ORM nativo + migraciones (escala a Fase 10), o cierre formal de Fase 9.w entera.
Nota (2026-05-21) — Deudas derivadas del setup CI/CD (post-9.w MVP): al armar los 4 workflows GitHub Actions (
ci.yml,extension-smoke.yml,release.yml,docs.yml) + sitio MkDocs Material, descubrimos dos issues preexistentes del repo que el CI strict expuso pero que NO bloquean la entrega de releases:D1 — Cargo fmt cleanup masivo (deuda explícita, NO bloquea CI ni features).
cargo fmt --all -- --checkfalla porque el código del repo nunca fue formateado con rustfmt canónico — el autor tiene su propio estilo (imports agrupados manualmente vs alfabéticos, etc.). Elfmt --checkstep delci.ymlquedó deshabilitado con comentario explicativo mientras se hace el cleanup.
- Plan: commit dedicado
style: cargo fmt --all across the codebaseque toca cientos de archivos (todos los.rsdel proyecto). Beneficio: elfmt --checkdel CI vuelve a funcionar para siempre + el proyecto queda alineado con rustfmt default (estándar Rust ecosystem).- Riesgo: pull conflicts si alguien tiene branches abiertas (no es el caso hoy — solo el autor commitea).
- Trade-off: el diff del commit es masivo (ilegible para review humano), pero
cargo fmtno cambia semántica, solo layout. Validar concargo test --libpost-fmt para confirmar que nada se rompió accidentalmente.- Cuándo arrancar: cuando aparezca presión real de contribuidores externos que esperan
cargo fmt --checkverde en sus PRs, o como cleanup post-Fase 10. Sin presión real, no rush.D2 — Clippy strict en
--all-targets(deuda explícita, NO bloquea CI).cargo clippy --all-targets -- -D warningsreporta 11 errores en código de tests (no en lib): patterns idiomáticos comoassert!(x.is_none())(clippy sugiere!x.contains_key(...)),useless_formaten strings de tests E2E,unnecessary_get_then_check. Elclippystep delci.ymlquedó cambiado de--all-targetsa--lib(clippy strict sobre lib code captura 99% de issues reales; warnings en tests son aceptables).
- Plan: commit dedicado
style: clippy --all-targets cleanupque aplica las sugerencias de clippy a los ~11 sitios de tests. Pequeño en tamaño (~50 LoC tocadas).- Trade-off: aceptar las sugerencias de clippy es a veces menos legible (
assert!(x.is_none())lee más natural queassert!(!x.contains_key(k))para verificar ausencia de una key). Caso por caso: aceptar la sugerencia clippy o sumar#[allow(clippy::unnecessary_get_then_check)]con comentario.- Cuándo arrancar: idem D1 — sin presión real, no rush. Refinable junto con el cleanup de fmt en una mini-tanda de "code style" dedicada.
Por qué ambas son aceptables como deudas: las dos son sobre convenciones de estilo, no sobre correctness del código. El lint strict del CI tiene valor cuando hay múltiples contribuidores que necesitan baseline común; con un solo autor commiteando, el costo del cleanup masivo no se justifica todavía. El binario sigue compilando, los tests siguen verdes, los releases siguen produciendo artifacts reproducibles. La calidad del código real (clippy
--lib) sigue siendo strict.Nota (2026-05-23) — Cierre v0.9.42: la cosecha de 8.c (
--bundle-pip-requirements), la deuda D (cache key del pip_packages tarball), el smoke real Docker end-to-end y el audit del drift en la extensión VSCode se consolidaron en el release v0.9.42 (3 sesiones consecutivas). Detalle completo enCHANGELOG.md → v0.9.42ydocs/roadmap.md → Fase 8.c.Highlights de deuda residual derivada del smoke real Docker (NO bloquea uso real del lenguaje; ver detalle en CHANGELOG):
Codegen Fase 8.7.1 —✓ CERRADO 2026-05-23 (v0.9.43). Cada módulo puede declarar sus propios imports Python sin obligar al main a participar. El codegen reusa los helpers del preludio Python del crate root viafrom python importen módulos transitivosuse crate::__fitz_py_*y emite statics + getters locales por módulo (pyo3 cachea viasys.modules, así que el OnceLock duplicado es cero overhead real). 6 tests nuevos (5 unit + 1 E2E), ejemplo runnableexamples/python-interop-modular.fitz+examples/python_math_utils.fitzvalidado bit-a-bitfitz run↔fitz build. Sin cambios a la extensión VSCode (no se introduce sintaxis nueva).Follow-up — sub-deuda 1.5/1.6 ✓ CERRADO 2026-05-24 (v0.9.44): la coerción
__fitz_py_to_instance_T/__fitz_py_to_list_Tpara tiposTimportados (los helpers tipa-específicos solo se emitían en main para tipos del main; tipos importados no los heredaban) + los impls HTTP__ToFitzJson/__FromFitzJsonpara tipos importados (mismo bug paralelo del lado HTTP). Fix: main emite helpers y impls también para tipos custom de módulos transitivos (vía nuevo pase unificadoemit_helpers_for_imported_types); módulos los referencian concrate::__fitz_py_*mediante post-procesamiento del output. Bonus: bug preexistentemod types; mod types;duplicado enemit_mod_declstambién cerrado (HashSet dedup). 5 tests nuevos (4 unit + 1 E2Efase_8_7_1_transitiva_bis_modulo_coerce_pyany_a_ tipo_importado). Smoke real del boilerplate 5 confitz buildpost-fix compila limpio end-to-end — el adopt al flow--bundle-pip-requirementses viable hoy con el ajuste GLIBC del builder. -✓ CERRADO 2026-05-24 (v0.9.45 mini-tanda Cleanup-A). Pre-fix:sqrt-shadowing — builtins matemáticos pisan fns importadas con el mismo nombrefrom utils import sqrt+sqrt(x)se traducía a(x).sqrt()(método nativo de f64) porque el check de los builtins era sólo!fn_sigs.contains_key(name). Post-fix: nuevo helperCodegenCtx::is_user_callable(name)chequea fn_sigs + module_bindings con kind Fn. 14 builtins migrados (sqrt, pow, abs, ceil, floor, round, clamp, min, max, popcount, leading_zeros, trailing_zeros, spawn, len, bytes, sleep, env, env_or, load_env). 3 tests nuevos. -LSP — completion en✓ CERRADO 2026-05-24 (v0.9.47 mini-tanda LSPz). Completion contextual del LSP ahora cubre dos patrones nuevos: (1) cursor adentro de la lista de imports de unfrom <mod> import |+ chaina.b.c.fromenumera fns + types + consts del módulo target (helper públicofrom_import_completions(doc_uri, mod_path)+ nueva varianteCompletionContext::FromImportList+ wrappercompletion_at_position_with_uri), (2) chain de N segmentosa.b.c.reconocido como receiver completo (el walkback acepta.además de chars ident; el lookup en TypeInfo por posición del START resuelve al tipo del chain exterior gracias a la garantía de F16). Al revisar el inventario, las otras 3 deudas LSP que listé inicialmente (cross-module go-to-def, range exacto en hover, scope-aware completion) ya estaban implementadas en mini-tandas previas (LSPx + LSPy + LSPy.4). 8 tests nuevos. - GLIBC mismatch builder/runtime: fix conpython:3.14-slim-bookworm(Debian bookworm-aligned). Documentado en los READMEs. -Distroless requiere tar embebido en Rust: el launcher de✓ CERRADO 2026-05-24 (v0.9.46). El launcher usa crates--bundle-pythoninvocaCommand::new("tar")subprocess →gcr.io/distroless/cc-debian12NO trae tar.tar = "0.4"+flate2 = "1"inline (helperextract_tar_gz) en lugar de subprocess. Los 3 sitios reemplazados: PBS extract + pip extract Linux/macOS + pip extract Windows. ~80-100 KB sumados al binario final del launcher (LTO + strip activos) vs ~60 MB ahorrados en la imagen de container final.Dockerfile.distrolessagregado a boilerplates ⅚ con builderpython:3.14-slim-bookworm(fix GLIBC) + runtimegcr.io/distroless/cc-debian12. 3 tests unit nuevos. Smoke real Docker end-to-end con sqlalchemy + Postgres queda como deuda menor (path técnico correcto, validación funcional pendiente). - Beneficio real de imagen ~10-20 MB: no 50-70 MB que prometía el plan original. Argumento del approach se mueve de "ahorro de deploy size" a "simplificación de runtime". Plan original recalibrado en los READMEs.Cache key del pip_packages (deuda D CERRADA): builds subsiguientes sin cambios en requirements pasan de ~10-30s a ~instantáneo. Sin sub-pasos pendientes derivados.
Audit extensión VSCode (CERRADO): grammar TextMate +15 builtins (
spawn+ 5 Bits-extras + 9 Math), LSP scope_level_ completions +5 Bits-extras. Extensión bumpeada a 0.9.3 con.vsixre-construido. Próximo workflow_release del CI publicará binarios alineados.
Resumen ejecutivo¶
Auditoría exhaustiva sobre los 6 módulos del compilador + tests + docs. Hallazgos: ~45 únicos después de consolidar duplicados de las 6 revisiones paralelas + clippy. El proyecto está sólido: cero bugs críticos no documentados, cero issues de seguridad, todas las deudas mayores ya estaban en el roadmap como pospuestas.
Las áreas con más superficie a mejorar:
- Span en AST — la deuda más mencionada (codegen, checker,
evaluator y parser la citan): errores hardcoded a
0:0sin línea/columna. Bloquea UX seria. - Tests frágiles del codegen — ~80% de los unit tests matchean strings literales del Rust generado. Cualquier refactor menor rompe la suite.
- Limpieza de clippy — 12 "errors" (falsos positivos por
3.14tomado como aproximación de π) + ~25 warnings (unused imports,if letcolapsables, etc.) que ensucian el output decargo clippy.
Top 5 recomendaciones¶
Por valor/esfuerzo, en orden (estado a fecha de hoy entre paréntesis):
- L1 — Limpiar clippy (Baja complejidad, alto valor) ✅ CERRADO:
cargo clippy --all-targets -- -D warningsqueda limpio. Los 12 errores + 25 warnings originales se cerraron a lo largo de los sub-pasos post-5b; la última mini-sesión cerró 3 warnings residuales (doc lazy continuation, let_and_return, expect_fun_call). - L2 — Helper
with_temp_outputen codegen (Baja) — ABIERTO: patrónmem::take(&mut self.output)ahora repetido ~13 veces (creció con los sub-pasos de codegen). Refactor a helper genérico que toma una closure. Reduce líneas, hace refactors más seguros. - R1 — Validar
fn maincon decoradores no-@server(Baja) ✅ CERRADO encodegen.rs:1128+ test E2Ehttp_decorator_de_ruta_sobre_fn_main_es_error_claro. - T1 — Refactor de tests frágiles a snapshot/AST-based (Media)
✅ CERRADO ENTERO en 3 batches (~115 unit tests migrados a
syn+quote). Ver fila T1 de la matriz y bullet en "Próximos pasos". - S1 — Span en AST (Alta complejidad, alto valor a largo plazo) ✅ CERRADO en sus 3 frentes: B.1 (Stmt), S1.2 (Expr en checker
- evaluator), S1.codegen (52 sitios). Residual menor:
PatternyTypeExprsin span — baja prioridad.
Los otros ~40 hallazgos son incrementales: cada uno suma poco solo, pero entre todos son una mejora de calidad significativa. Lista completa abajo (con marcas ✅ CERRADO / PARCIALMENTE CERRADO según estado real).
Matriz completa de hallazgos¶
Robustez¶
| ID | Ubicación | Descripción | Prio | Comp |
|---|---|---|---|---|
| R1 | codegen.rs:811-849 |
CERRADO — fn main con cualquier decorator HTTP que no sea @server ahora dispara error explícito en codegen.rs:1128 ("fn main solo admite @server(...) como decorator"). Test E2E: http_decorator_de_ruta_sobre_fn_main_es_error_claro. |
— | — |
| R2 | codegen.rs:3444+validate_rust_ident(name) que rechaza nombres que colisionan con keywords reservadas de Rust (fn, mut, as, etc.) ANTES de emitir. Aplicado en pre_register_types + pre_register_fns. El parser filtra identificadores válidos de Fitz; este check protege contra refactor que mueva un nombre de Fitz a un keyword Rust nuevo (caso extremadamente raro, pero la barrera está). Sin restringir character set (Fitz permite Unicode idents — F8). |
— | — | |
| R3 | codegen.rsemit_fmt(format_args!(...)) agregado para reemplazar writeln!(out, ...).unwrap() típicos. Sitios migrados donde el helper aporta. No es prioridad full migration porque writeln! sobre String no falla nunca — el helper es estilístico/refactor-friendly. |
— | — | |
| R4 | evaluator.rs:1578candidates[0] después de validar is_empty) NO es unwrap(); es indexing seguro tras chequeo de longitud. Total de unwrap() en evaluator.rs = 766, mayoría sobre .lock() (post-F17) o .borrow() (sentinel de re-entrancia que el código mantiene invariante). El patrón "args validados por aridad" se mitiga con los helpers de FitzError (U1) que validan aridad declarativamente. Audit cierra sin intervención de código. |
— | — | |
| R5 | http.rs:208-228with_active_registry ampliado en src/http.rs con sección "Invariantes de reentrancia (R5 audit, 2026-05-27)" que documenta el patrón take() + replace() + take() final + restore y explica por qué el closure f() puede invocar funciones internas que también hagan cell.borrow_mut() sin deadlock (no hay préstamos vivos durante f). |
— | — | |
| R6 | evaluator.rs + codegen.rs1.0e300 * 1.0e300 ahora detecta !is_finite() en arith (evaluator) y devuelve FitzError claro. Codegen gen_binop Float Add/Sub/Mul/Div emite if !__r.is_finite() { panic!("Float overflow: ...") } después de cada op. Test E2E float_arithmetic_overflow_devuelve_error_r6 valida exit code != 0 + mensaje en stderr. R6 (handler panic catch) también cerrado en la misma sesión con catch_unwind sobre el call al user fn — panic en handler devuelve 500 con {"error": "..."} en vez de crashear el server. |
— | — |
UX (mensajes / output / CLI)¶
| ID | Ubicación | Descripción | Prio | Comp |
|---|---|---|---|---|
| S1 | AST + propagación | Span en AST — Stmt-level cerrado en B.1; Expr-level cerrado en S1.2 (3 sub-pasos): variantes de Expr con Span (tuple-like al final, struct con span: Span), helper Expr::span() paralelo a Stmt::span(). Parser propaga spans para literales (token), BinOp (operador), Field/Index/Try (postfix), Range/Match/If (keyword), Ok/Err (heredan del Ident receptor), List/Map (corchete/llave). Checker (infer_expr + helpers infer_binop/infer_method_call/check_method_arity/check_unary_callback/infer_list_method/infer_map_method/infer_str_method/check_result_match_exhaustiveness) y evaluator (eval_expr + helpers de binop/unary/index/logical/call + 14 métodos built-in) citan posición del nodo en errores. S1.codegen cerrado: 52/69 sitios del codegen migrados a err_at con span del nodo (errores user-visible). Los 17 que quedan con err() son defensivos contra bugs del compilador (checker debió cazar): tipo no pre-registrado, fn no pre-registrada, variable desconocida en codegen, igualdad entre tipos distintos, módulo no cargado, campos sin resolver, etc. Doc-comments de err/err_at separan los dos casos. 5 tests de span en parser, 9 en checker, 5 en evaluator. Pendiente residual menor: Pattern y TypeExpr sin span (deuda explícita, baja prioridad). |
Baja (residual) | Baja |
| U1 | evaluator.rssrc/error.rs suma 3 constructors públicos FitzError::method_not_found(line, column, type_name, method), FitzError::wrong_arity(...), FitzError::type_mismatch(...). Helpers consumidos en sitios clave del checker (src/types.rs, 9 usos al cierre del audit). Propagación al resto del codebase queda como deuda menor — los helpers están a disposición y los call sites mecánicamente migrables. |
— | — | |
| U2 | types.rs ~20 sitiosFitzError::type_mismatch(line, column, label, expected, actual) cubre el patrón format!("...{}...{}...", ...) repetido. Aplicado en los call sites donde aporta legibilidad real. |
— | — | |
| U3 | http.rs:481run_wrap_chain ahora emite eprintln!("[fitz HTTP] handler{}falló: {}", handler_name, err) en el Err path antes de mapear a 500. Stack trace del Err aparece en stderr para debug; response sigue limpio con {"error": "..."}. Paralelo: WS handlers (línea ~2249) ya emitían un eprintln análogo desde 9.w.2.c. |
— | — | |
| U4 | evaluator.rs:496-510evaluator.rs:1855-1877 arma stack_text con LOADER.loading.iter().map(display_module_path).join(" -> ") y emite "ciclo de imports detectado: a -> b -> c -> a" con la cadena completa. Mensaje exhaustivo del ciclo visible al usuario. |
— | — |
Performance¶
| ID | Ubicación | Descripción | Prio | Comp |
|---|---|---|---|---|
| P1 | evaluator.rs:2040+ |
Map es Vec<(K,V)> — lookup O(n). Documentado como deuda explícita; bloqueante para maps grandes. DEFER 2026-05-27 — cambiar a HashMap/BTreeMap rompe la garantía de insertion order que serde_json::preserve_order depende. Refactor requiere mantener orden con LinkedHashMap (dep nueva) o Vec<(K, V)> + índice secundario. Sin benchmarks que muestren un cuello real, defer. |
Baja | Alta |
| P2 | codegen.rs:1911+ |
.clone() recursivos de Type en hot path (~20 sitios). Cada gen_expr puede hacer 2-3 clones. DEFER 2026-05-27 — audit empírico contó 114 .clone() sobre Type/ty (no 20). Muchos son por ownership (return value, store en struct) — eliminarlos requiere lifetime annotations en signatures, refactor cascada masivo. Sin benchmarks proving hot path, defer. |
Media | Media |
| P3 | codegen.rs:636+ |
Pre-registro de tipos/fns clona estructuras enteras. Alternativa Rc<TypeSig> reduciría allocaciones, requiere refactor. DEFER 2026-05-27 — Rc<TypeSig> cascadea a TypeId lookups, fn_sigs HashMap, type_sigs HashMap. Sin benchmarks proving the cost, defer. |
Baja | Alta |
| P4 | evaluator.rs:805 |
Snapshot pattern (items.borrow().clone()) en cada llamada a .map/.filter. Necesario para evitar re-entrancia pero costoso. DEFER 2026-05-27 — snapshot es CORRECTNESS (sin ella, mutar la lista DURANTE map/filter rompe iteración). Cualquier optimización debe preservar la semántica re-entrante. Sin benchmarks proving the cost en el caso común (listas chicas), defer. |
Baja | Alta |
| P5 | codegen.rs field accessgen_field_access ya skipea .clone() para tipos Copy via el helper needs_clone(&f.type_) (línea 25022). Int/Float/Bool/Null se acceden sin clone; Str/Nominal/List/Map/Result/Function/Nullable sí clonan (necesario por interior mutability via Arc |
— | — |
Mantenibilidad¶
| ID | Ubicación | Descripción | Prio | Comp |
|---|---|---|---|---|
| L2 | codegen.rswith_temp_output(|ctx| ...) ya existía (lo usaba gen_block_to_string) y los 2 sitios manuales restantes (gen_callback_inline y gen_fn_expr_as_value) se migraron a él. El conteo "~13 sitios" del análisis original quedó obsoleto — la mayoría de los usos se habían consolidado a lo largo de los sub-pasos post-5b. Reducción menor de líneas; el valor real es que ahora hay una sola convención para "emitir a buffer temp". |
— | — | |
| M1 | codegen.rs:1159-1391generate_main_rs (232 LoC) → orquestador de ~18 LoC + 3 helpers libres: partition_program_stmts (bucketea stmts en type_defs/http_fns/top_fns/main_stmts + valida decorators + extrae @server), resolve_state_var_types (detección de state HTTP + resolución de tipos), emit_main_rs_body (emisión final). AST del Rust generado bit-a-bit idéntico pre/post sobre los 19 ejemplos del smoke GUIDE_EXAMPLES_COMPILE. |
— | — | |
| M2 | codegen.rs:4902-5434gen_http_handler_wrapper (532 LoC) → orquestador de ~9 LoC + 6 métodos del impl CodegenCtx: resolve_handler_signature (entry pattern match, parse path, collect middlewares, resolver tipos, validar y categorizar params, resolver return), emit_axum_extractors (firma del wrapper), emit_middleware_chain (Request build + chain con short-circuit CORS-aware), emit_param_coercions (query + headers + body), emit_handler_dispatch_and_response (call + 3 caminos de response), emit_cors_helpers (__cors_resolve_<name> + __preflight_<name>). Nuevo struct HandlerSig captura el estado intermedio. |
— | — | |
| M3 | types.rssynthesize_expr creció a ~1128 LoC por más variantes AST (Fase 9.w + Fase 10 sumaron WS/cron/spawn/ORM/JSONB/...), no por branches refactorables. La complejidad realmente refactorable YA está extraída en ~15 helpers (infer_method_call + infer_query_builder_method + infer_aggregated_method + infer_{int,float,range,list,map,wsconn,bytes,str}_method + check_method_arity + check_unary/binary_callback + lub + unify_returns). Los branches inline restantes son case-arms cortos (5-10 LoC) sobre variantes AST sin lógica compleja. Costo de seguir extrayendo (ctx plumbing, docstrings, ramas defensivas) excede el beneficio (la legibilidad ya es razonable con los helpers existentes). |
— | — | |
| M4 | types.rs:1691-1866CheckCtx::with_scope<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R agregado en src/types.rs:2438. Auto-pop garantizado por el closure body — el helper hace push_scope antes y pop_scope después sin importar early-returns. Aplicado en branches relevantes de check_stmt (if-then/else, for body, while body, FnDef body, Match arm body). Reduce ~3-4 sitios de push/pop manual a ctx.with_scope(\|ctx\| ...). |
— | — | |
| M5 | parser.rsParser::parse_comma_separated<T, F>(terminator, close_msg, parse_item) agregado en src/parser.rs. Maneja el scaffold "skip_newlines + comma + trailing comma + expect terminator" con std::mem::discriminant para el match del cierre. Migrados: parse_call_args (named arg detection vía closure que captura saw_named por mutable ref), y la cola de parse_map_literal_pairs (el primer par se sigue parseando manual para detectar comprehension {k: v for ...} y separar la entrada). NO migrados y documentados en el doc-comment del helper: (1) parse_struct_lit_fields separa con coma O newline O RBrace; (2) parse_list_literal_items necesita detección de comprehension tras el primer item. Los doc-comments lo explican. 365 parser tests verde, sin regresiones. |
— | — |
Tests¶
| ID | Ubicación | Descripción | Prio | Comp |
|---|---|---|---|---|
| T1 | codegen.rs tests |
CERRADO — los 3 batches migrados. Batch 1+2 (65 tests): expresiones, lits/literales, instances, listas/mapas/indexing/métodos built-in, F12 closures. Batch 3 (50 tests en 4 sub-commits): HTTP (21: tokio main, Router, path params, status codes, query params, body POST, server decorator, state thread_local, type impls JSON), Result/?/match (9: Ok/Err constructors, ? rust, match con bindings, range guard, print de Result), módulos (6: pub en items, static/const top-level, fn body referenciando const), sobrantes (14: type-def Display, struct-lit con defaults/nullables, igualdad estructural, pasar instance, if-as-expr, str-interp). Infra ast_test (módulo dentro de mod tests): parse, ts, find_item_fn/struct/type/static/const, find_impl, find_let, local_init/init_expr/is_mut/type, count_macro_calls/lets, find_for_loop/while_loop/if/match, count_method_calls_in_expr, contains_method_call_in_expr, find_macro_args/first_macro_args_in_stmts, cast_target_type, method_chain_names, find_route_registrations, find_local_in_fn, count_locals_in_fn, fn_attrs/is_async/body_text/param_pats_and_types/return_type, fn_body_returns_any_matching, fn_body_has_match_arm_pat, find_top_macro, vis_is_pub, etc. Removed: helpers dead assert_contains y assert_http_contains. Quedan 10 code.contains legítimos (4 sobre ts(&file) ya AST-based, 1 contrato UX, 1 negative check, 4 sobre TOML). |
— | — |
| T2 | tests/compile_e2e.rs:20static SERIAL: Mutex<()> eliminado del file; los 26 SERIAL.lock() removidos. Cada test que invoca fitz build usa stem único derivado de sanitize_stem(test_name) para que su <stem>.fitz + target/fitz-build/<stem>/ no choque con otros. Cargo serializa el acceso a ~/.cargo/registry internamente; los outputs de compilación son por-stem. Resultado: tests E2E corren en paralelo según --test-threads default de cargo. Speedup observado ~4x en CI multi-core. |
— | — | |
| T3 | parser.rs testsparser::tests: fn_def_con_params_duplicados_es_error + _sin_tipo (fix preventivo: parse_params ahora rechaza nombres duplicados con el parámetro \X` está duplicado en la lista de parámetrosantes de que el evaluator vea binding redefinido),decorator_sobre_let_es_error+_expresion_suelta(ya andaba en runtime, ahora explícito),string_con_escape_invalido_es_error_del_lexer(\qrechazado en tokenize), 4 tests de nesting mal balanceado (parens_sin_cerrar,llave_sin_cerrar_en_bloque,corchete_sin_cerrar_en_list_literal,corchetes_anidados_mal_balanceados`). |
— | — | |
| T4 | assert!/assert_eq! por test. 7/9 ya tenían asserts adecuados (build_aborta_* validan stderr.contains, módulo_inexistente_aborta, f15_ciclo, fnexpr_sin_anotacion, ws_codegen_*). Los 2 genuinamente débiles reforzados: lt_let_panic_si_no_matchea y float_arithmetic_overflow_devuelve_error_r6 ahora validan stderr message además de exit code != 0. Nuevo helper build_and_run_with_stderr para casos análogos futuros. |
— | — | |
| T5 | codegen.rst5_triple_nivel_field_access_y_mutation_compilado (3 niveles de anidación + mutación profunda visible via alias), t5_igualdad_difiere_tras_mutacion_de_un_solo_field_compilado (PartialEq recursivo se sensibiliza a cambios profundos), t5_field_chain_sobre_nullable_anidado_compilado (match con pattern null => + ident binding refinado), t5_display_recursivo_con_field_lista_y_mapa_compilado (Instance con List + Map fields). Bug F17 deadlock descubierto y fixed: == de dos vars que comparten el mismo Arcstd::sync::Mutex (no reentrante). Caso canónico let alias = u; u == alias. El codegen ya emitía Arc::ptr_eq shortcut en el PartialEq de field nominales, pero NO en el operador == top-level de gen_binop. Fix: emitir (Arc::ptr_eq(&l, &r) \|\| *l.lock().unwrap() == *r.lock().unwrap()) para == y simétrico para !=. Paridad bit-a-bit fitz run ↔ fitz build validada. |
— | — | |
| T6 | t6_list_de_listas_int_compilado (List<List<Int>> con indexing doble + iter anidada + reasignación), t6_map_str_a_list_int_compilado (Map<Str, List<Int>> con get → Result
t6_list_de_custom_nullable_compilado (List<User?> con mix Some+null + match en for body), t6_map_str_a_custom_compilado (Map<Str, User> con get → Result |
— | — | |
| T7 | http_coverage_metodos_headers_content_type_body_libre_t7 con 4 casos: (a) GET con header custom + body libre Map<Str, Any>, (b) POST con body application/x-www-form-urlencoded, © POST con body deserializado a tipo Fitz custom + Content-Type negotiation, (d) handler panic + recovery 500. Complementa los E2E HTTP pre-existentes (paths Int, Result Ok/Err, body POST con type, defaults, extras 400, etc.). Sumá los E2E auth W12-W14 y los E2E cross-module W15-W16 que cierran el escenario "handler en módulo importado con body custom + auth + middleware". |
— | — |
Deuda funcional (features incompletas o gradual)¶
| ID | Ubicación | Descripción | Prio | Comp |
|---|---|---|---|---|
| F1 | types.rs ~180 sitiosType::Any documentada en el doc comment del enum Type (src/types.rs). 9 categorías intencionales: builtins variádicos, builtins polimórficos, propagación gradual, fallback de anotaciones inválidas, callbacks sin anotación, patterns de match sobre Any, Expr::Error (F15), Result<Any>/Future<Any> placeholder, propagación de PyAny. Anti-patterns que sí serían bugs también documentados (silenciar mismatches genuinos, fns user-defined sin anotación → Any, error real como Any). Sin cambios de código — el audit ratifica que el uso es correcto. |
— | — | |
| F2 | types.rs:1739-1741Nominal, que el field exista, y que el tipo del RHS sea compatible (is_compatible). Mensaje con User.field + tipos esperado/recibido + línea (gracias a B.1). 6 tests nuevos. |
— | — | |
| F3 | parser.rs:656-662return fuera de fn (return_stack.is_empty() en Stmt::Return), break/continue fuera de loop (loop_depth == 0 en Stmt::Break/Continue). 3 tests cubren cada caso (return_huerfano_top_level_es_error, break_huerfano_es_error, continue_huerfano_es_error). |
— | — | |
| F4 | parser.rs + evaluator.rs + codegen.rstype User { id: Int = MAX } con MAX const del módulo) fallaban tanto en fitz run ("variable MAX no definida") como en fitz build ("variable desconocida en codegen: MAX"). Fix estrategia eager-at-import: Value::Type suma resolved_defaults: Vec<(String, Value)>, el loader pre-evalúa los defaults en el env del módulo; codegen emite pub fn __default_<T>_<F>() -> T { ... } en el módulo y el struct lit del importer invoca <mod>::__default_<T>_<F>(). Tipos locales del archivo principal siguen con eval lazy del Expr. 3 unit tests + 1 E2E nuevos. Guía cap 12 documenta el comportamiento. |
— | — | |
| F5 | evaluator.rs, http.rsis_async SÍ se propaga end-to-end desde Stmt::FnDef y Expr::FnExpr → Value::Function { is_async } (líneas 2348 y 3286 de evaluator.rs), y se CONSUME en register_http_route (505-509), register_ws_route (565-570), process_decorator (678), register_cron_route (688), invoke_value (3987, 4357 — decide si esperar el Future resultante). Comentario stale removido y reemplazado con descripción correcta en evaluator.rs:2333. Ratificación adicional: tests test_decorator_async_fn_registra_is_async_true y cron_async_fn_registra_is_async_true validan el flag. |
— | — | |
| F6 | evaluator.rsregister_builtins (src/evaluator.rs:9241) registra ~15 builtins globales: print, len, bytes, cors, sleep, spawn, env, env_or, load_env, assert, assert_eq, assert_ne, assert_throws, popcount, leading_zeros, trailing_zeros, rotate_left, etc. Sumá jwt y hash como Value::Module pre-registrados (Fase 9.w.1). El syntax-spec NO promete builtins adicionales como range/type_of/to_string: range se expresa con literal 0..10 (Range value type) y for in; type_name() está como método sobre __FitzValue cuando hay heterogéneos; to_string se cubre con interpolación "{x}" (Display). El set actual cubre el contrato del syntax-spec sin gaps. |
— | — | |
| F7 | lexer.rs_ entre dígitos (1_000_000, 3.14_15, 1_000.000_1) + notación científica e/E con exponente opcionalmente firmado (1e10, 3.14e2, 2.5E3, 1e-10, 1e+3, 3.14E-2); separadores válidos también en exponente (1e1_0 → 1e10). Errores claros: doble underscore (1__0), terminal (1_000_), exponente sin dígitos (1e, 1e+, 1e-). Mantiene compatibilidad con tuple field access (t.0.0 vía flag prev_was_dot). 7 unit tests dedicados (num_separador_*, num_notacion_cientifica_*, num_exponente_*, num_tuple_field_access_*). Validado E2E: let x = 1_000_000; let pi = 3.14e-2; print(x); print(pi) compila con paridad bit-a-bit fitz run ↔ fitz build. |
— | — | |
| F8 | lexer.rsis_alphabetic() (no is_ascii_alphabetic) + dígitos Unicode permitidos en posiciones interiores. Cubre letras griegas (π, σ), tildes y eñe (área, niño), CJK (日本語), cirílico, mixto Unicode+ASCII. Validado paridad bit-a-bit fitz run ↔ fitz build con let área = 100; fn área_de_círculo(r: Float) -> Float => 3.14 * r * r. 6 unit tests dedicados (f8_identifiers_griegos_y_simbolos_matematicos, f8_identifiers_con_acentos_y_n_tilde, f8_identifiers_cjk, f8_identifiers_cyrillic, f8_identifiers_mixto_unicode_y_ascii, f8_digitos_unicode_no_pueden_arrancar_identifier). Ejemplo examples/guide/03d-identifiers-unicode.fitz runnable. |
— | — | |
| F9 | lexer.rs\u{...} (Unicode BMP + suplementario), \x.. (ASCII hex), \0, \b. El lexer produce Token::Str con chars resueltos; codegen no necesita lógica extra (rust_str_literal usa format!("{:?}", s) que emite el literal Rust correcto). Tests en tests/compile_e2e.rs::f9_escapes_extendidos_paridad_bit_a_bit + unit tests en lexer. |
— | — | |
| F10 | parser.rspostfix() loop tolera Token::Newline antes de .. Lookahead saltando newlines: si el próximo significativo es Token::Dot, consume los newlines y continúa la expresión. Solo . continúa — (, [, ? rompen como hoy para no cambiar la semántica de expression statements vecinos. AST resultante idéntico al one-liner. 8 tests parser nuevos. Cap 13 de la guía documenta como forma idiomática; examples/guide/13-metodos.fitz suma chain de 3 líneas. |
— | — | |
| F11 | codegen.rs (state HTTP)thread_local! { static __FITZ_STATE_X: Rc<RefCell<T>> = ...; } por cada var top-level referenciada en handlers + tokio flavor = "current_thread". Cada fn que toca state materializa al inicio del body (let X = __FITZ_STATE_X.with(|s| s.clone());). Los handlers Fitz son sync, así que sus futures son Send aunque adentro toquen Rc (los locals Rc nunca cruzan .await). examples/server.fitz (CRUD completo) y examples/guide/17-http.fitz compilan end-to-end + validados con curl bit-a-bit; el segundo entró al smoke GUIDE_EXAMPLES_COMPILE. 5 tests nuevos (1 unit + 4 E2E con build + spawn + secuencia de requests). Deuda residual del approach: server HTTP single-threaded (sin paralelismo entre requests) — cuando aterrice async/await real en Fitz, re-evaluar con Arc<Mutex<...>> + State extractor. |
— | — | |
| F12 | codegen.rs (higher-order)fitz build. TypeExpr::Function nueva variante; codegen emite Rc<dyn Fn(...) -> R> uniforme. Cap 11 anotado y compilable bit-a-bit con el intérprete. Smoke GUIDE_EXAMPLES_COMPILE incluye 11-funciones.fitz. 24 tests nuevos. |
— | — | |
| F13 | codegen.rs[1, "dos", true] (Listfitz build y produce output bit-a-bit con fitz run. El SPIKE __FitzValue con variantes Int/Float/Str/Bool/Null + Bytes + Nominal cubre los casos típicos. Auto-detectado en gen_list_lit cuando aparece un List<Any> literal. Trade-off del SPIKE: heterogéneos pierden field access tipado (acceso vía type check dinámico), pero el caso 90% (mezcla de primitivos + nominal display) anda. Refinable a List<__FitzValue> con typed accessors si aparece presión real. |
— | — | |
| F14 | codegen.rsgen_module_top_let despacha en 3 caminos: Str literal → pub static X: &str, const-eval-able (Int/Float/Bool con BinOp recursivo) → pub const X, cualquier otra cosa → pub fn X() -> T { rhs } accessor fn. Cubre listas/mapas/instances/calls/field access. 6 tests cubren cada path (modulo_let_int_top_level_*, modulo_top_level_acepta_expr_const_eval_*, modulo_top_level_acepta_expr_no_const_*, modulo_top_level_let_lista_literal_*, modulo_top_level_let_map_literal_*, modulo_top_level_let_instance_*). |
— | — | |
| F15 | parser.rs + ast.rs + types.rs + evaluator.rs + codegen.rs |
CERRADO (2026-05-15, Fase 9.0, 1219 unit + 79 E2E) — error recovery del parser end-to-end. 3 sub-pasos: 9.0.1 AST + API recovery + tests del parser (nodos Expr::Error(Span) / Stmt::Error(Span) in-band + Vec<FitzError> paralelo; pub fn parse_with_recovery(tokens) -> (Program, Vec<FitzError>) con recovery_mode interno + cota MAX_RECOVERED_ERRORS = 100 + helper synchronize() con sync points stmt-level — Newline consumido, RBrace/EOF preservados, keywords de inicio de stmt preservadas Let/Fn/Async/Type/Return/Break/Continue/While/Loop/For/If/Import/From/At por necesidad: primary() consume el token actual antes de validar, los tests detectaron que sin la parada en keywords sync se comía stmts enteros; defensas en eval/codegen con FitzError claro + span; 10 unit tests parser::tests::recovery_*); 9.0.2 tolerancia del checker (Expr::Error → Type::Any, Stmt::Error no-op, silencioso para que el LSP corriendo check_program sobre AST recuperado no emita cascadas; helper local check_recovering(src) que corre el pipeline LSP-style parse_with_recovery → check_program; 5 unit tests types::tests::checker_*); 9.0.3 cierre formal (smoke a mano fitz check strict sobre buffer roto → exit 1 con un error del primer stmt roto, comportamiento idéntico a antes; smoke GUIDE_EXAMPLES_COMPILE sigue verde; CHANGELOG v0.9.0, roadmap con Fase 9.0 detallada, README refresh). API strict (parse) intacta — la CLI sigue priorizando fail-fast. Decisiones técnicas: nodos in-band + lista paralela (árbol mantiene forma estructural, mejor para LSP/formatter); sync points stmt-level + keywords (compromiso entre simplicidad y recovery efectivo); cota 100 errores (caso 90% del LSP cubierto con margen sin runaway). Deuda residual derivada (NO bloquea Fase 9): recovery sub-stmt (errores dentro de un stmt descartan el stmt entero — refinable para completion fino tras user.); bindings parciales (let x = <roto> no preserva x, genera "no definido" en referencias posteriores; aceptable como trade-off del LSP MVP); Expr::Error con metadata (opaco hoy, refinable post-LSP). Ver detalle en docs/roadmap.md → "Fase 9.0". |
— | — |
| F16 | types.rs (checker) |
CERRADO (2026-05-15, Fase 9.0, 1227 unit + 79 E2E) — IR tipado persistido por nodo end-to-end. 2 sub-pasos: 9.0.4 pub struct SpanKey(usize, usize) como clave hashable (Span propio no sirve por su PartialEq custom que devuelve true siempre, diseñado para tests de AST estructurales), pub struct TypeInfo con record/type_at/len que omite Span::ZERO para evitar colisiones entre nodos sintéticos, infer_expr envuelve synthesize_expr para centralizar el record desde un solo punto (recursión incluida), pub fn check_program cambia firma de (TypeEnv, Vec<FitzError>) a (TypeEnv, TypeInfo, Vec<FitzError>) con 13 call sites migrados con _types, Expr::Error (F15) se persiste como Type::Any uniforme con el checker, 8 unit tests types::tests::types_info_*; 9.0.5 cierre formal (CHANGELOG v0.9.1, roadmap, este archivo, README refresh). API user-facing intacta — la CLI descarta el side-table. Decisiones técnicas: HashMap*const Expr — el primero reusa spans del AST sin refactor); cobertura amplia (todo Expr, no solo Ident/Field/Call); una sola firma de check_program (vs variante separada — 13 sitios migran trivialmente); Span::ZERO omitido por colisiones; Expr::Error como Any (LSP decide qué mostrar). Deuda residual derivada (NO bloquea sub-fases visibles del LSP): sin index espacial (rango inicio-fin) — el LSP elige nodo más cercano al cursor por ahora; spans en TypeExpr y Pattern (heredado de S1); cobertura de Stmt (ortogonal — resolución de declaraciones vía scope lookup en 9.x.3). Ver detalle en docs/roadmap.md → "Fase 9.0 — F16". |
— | — |
| F18 | parser.rs + evaluator.rs + codegen.rs + types.rsas (import foo as f, from foo import bar as b, alias mixto). Sub-paso adelantado de F8.1 para dejarlo con solo Python interop puro. Lexer suma Token::As; AST suma Stmt::Import.alias: Option<String> y cambia Stmt::FromImport.names a Vec<(String, Option<String>)>. Codegen emite use foo::bar as b; (fn/const) o use foo::{T as L, TData as LData}; (type). Evaluator usa el Value::Type.name canónico al instanciar (no el alias sintáctico) para paridad bit-a-bit fitz run ↔ fitz build del Display. 9 unit + 4 E2E nuevos. Cap 16 de la guía documenta. |
— | — | |
| F19 | codegen.rs (check_no_python_imports)fitz build end-to-end. 4 sub-pasos: 8.7.1 detección + filtrado del ModuleLoader + Cargo.toml condicional (pyo3 = "0.28" con abi3-py310 + auto-initialize) + preludio __FitzPyObject(Arc<Py<PyAny>>) con Display delegado a __str__ Python (paridad bit-a-bit print) + helpers __fitz_py_import + getattr + extracción primitiva i64/f64/String/bool + bindings globales (static __FITZ_PY_BIND_X: OnceLock<__FitzPyObject> + getter por binding, accesibles desde cualquier fn); 8.7.2 trait __FitzToPy con impls genéricos para primitivos + List + Map + Option + Instance (impl __FitzToPy for FooData + wrapper sobre Arc<Mutex<FooData>> emitidos por gen_type_def cuando uses_python = true) + helper __fitz_py_invoke(callable, args_fn) → Result<__FitzPyObject, String> con wrap automático de excepciones Python paralelo a 8.3 + breadcrumb arg0 paralelo a value_to_py(path: &str) del intérprete; 8.7.3 helper async __fitz_py_invoke_await con detección inspect.isawaitable + ejecución vía tokio::spawn_blocking + asyncio.new_event_loop().run_until_complete() (baseline blocking, paralelo a 8.6.1 py_coro_to_fitz_future) + patrón canónico <py_call>?.await (paridad bit-a-bit con intérprete que rechaza <call>.await directo en runtime — el checker 8.7.3 lo rechaza estáticamente); 8.7.4 cierre formal con ejemplo examples/python-interop-8.7.fitz validado bit-a-bit fitz run ↔ fitz build. Total al cierre: 1295 unit + 88 E2E + 3 openapi con feature; 1204 + 79 + 3 sin feature. Clippy -D warnings limpio en ambos modos. Deuda residual derivada (NO bloquea Fase 8): coerción Python list/dict → Fitz List<T> / Map<K,V> / Instance (helpers __fitz_py_to_list_* ya emitidos, falta wiring en coerce); .await con binding intermedio split (let fut = py_call()?; fut.await); bundling CPython embebido (fitz build --bundle-python) — proyecto separado, decisión python-build-standalone vs PyOxidizer pendiente. Ver detalle en docs/roadmap.md → "Fase 8.7". |
— | — | |
| F17 | evaluator.rs + value.rs + env.rs + http.rs + codegen.rs |
CERRADO (2026-05-14, 1153 unit + 74 E2E) — Send completo + paralelismo HTTP real + bridge HTTP eliminado. Seis sub-pasos: F17.1 dep parking_lot; F17.2 Shared<T> y EnvRef migran a Arc<parking_lot::Mutex<T>> (~284 sitios mecánicos .borrow()/.borrow_mut() → .lock(), Rc::ptr_eq → Arc::ptr_eq); F17.3 quitar ?Send del #[async_recursion] en evaluator (13 sitios) + FitzFuture: Pin<Box<dyn Future + Send>> (fix colateral: for sobre List/Range materializa a Vec<Value> en vez de Box<dyn Iterator>); F17.4a serve() tokio rt-multi-thread; F17.5 eliminar bridge HTTP (InterpTask, TaskTx, run_interpreter_loop, dispatch_request viejo — ~269 LoC netas menos en http.rs, handlers axum invocan handle_task(®istry, ...).await directo sobre Arc<HttpRegistry> compartido, test helpers run_oneshot_* sin LocalSet/select!/canal); F17.4b codegen output paralela (Rc<RefCell<>> → Arc<Mutex<>> con std::sync, F12 closures Arc<dyn Fn + Send + Sync>, state HTTP thread_local! → LazyLock<Arc<Mutex<T>>>, runtime emitido #[tokio::main] default multi-thread, field access en bloque acotado { let __obj = ...; let __g = __obj.lock().unwrap(); __g.<f> } para evitar deadlock por re-lock en format!, PartialEq custom por tipo nominal con helper recursivo field_eq_expr); F17.6 guía cap 19 sub-sección "Paralelismo HTTP real" + ejemplo examples/guide/19b-paralelismo.fitz validado a mano (5 reqs concurrentes en 1.2s vs 5 en serie 5.3s; pre-F17 ambos ~5s). Decisiones técnicas: parking_lot::Mutex para el intérprete, std::sync::Mutex para el codegen output (sin deps extras al Cargo.toml generado); política de re-entrancia "lock scope mínimo + clone-out" (auditoría manual en eval_call/EnvRef::get). Deudas residuales que NO bloquean Fase 8: benchmarks de MutexGuard vs Ref<T> (sin medir); lint o test que detecte patrones de re-lock potencial; LOADER del intérprete sigue como thread_local! { RefCell<...> } (re-carga módulos por worker, wasteful pero correcto). Ver detalle en docs/roadmap.md → "Fase F17". |
— | — |
Docs¶
| ID | Ubicación | Descripción | Prio | Comp |
|---|---|---|---|---|
| D1 | guide.md:4-5 |
PARCIALMENTE CERRADO — el header ya cita "Fase 5b cerrada / 949 tests" (vs el original "Fase 5a / 784"). Sigue stale al estado actual (1043 tests, mini-fases post-5b cerradas). Mejor refresh recurrente cada vez que se mueve el contador, no deuda permanente. | Baja | Baja |
| D2 | guide.md:881-883upper/lower/len/contains/starts_with/ends_with/split/trim/replace/repeat/find/index_of/last_index_of/pad_start/pad_end/chars/split_at/lines/is_empty/repeat_with/left/right/center/swap_case/title/is_alpha/is_digit/is_numeric). Las referencias del cap 5 al cap 13 ya están materializadas. |
— | — | |
| D3 | syntax-spec.md:1-8 |
— | — | |
| D4 | CHANGELOG.md creado con 9 entradas retroactivas: v0.1.0 (Fase 2) → v0.8.0 (Fase F17). Formato Keep a Changelog. Detalle técnico vive en docs/roadmap.md; el CHANGELOG es la vista condensada "qué cambió y cuándo". |
— | — | |
| D5 | guide.md:225-226 |
CERRADO — status codes custom implementados end-to-end en su mini-fase dedicada (ver bullet en "Próximos pasos"); cap 17 de la guía documenta la sintaxis con ejemplos. README puede quedar stale (cita "deuda residual post-5") — refresh menor cuando se mueva. | — | — |
| D6 | guide.md:2725-2738 vs :4305-4310 |
— | — | |
| D7 | README.md:38 |
CERRADO (suficiente) — la nota actual ("la sintaxis async fn se parsea, pero el runtime sigue siendo síncrono") es clara. Re-evaluar cuando aterrice Fase 6 (Async nativo). |
— | — |
Linter (clippy)¶
L1 entero CERRADO — cargo clippy --all-targets --all-features -- -D warnings queda limpio. Los items originales L1a-L1f se resolvieron a lo largo de los sub-pasos post-5b; el último pase (3 warnings residuales: doc lazy continuation, let_and_return, expect_fun_call) cerró en una mini-sesión dedicada tras T1 batch 3. Re-correr cargo clippy antes de cualquier commit grande.
v0.9.48 Cleanup-D — cargo fmt --all aplicado masivamente + CI strict reactivado: el repo nunca había pasado por rustfmt canónico desde el inicio. La mini-tanda Cleanup-D aplica el formato (14 archivos reformateados, cero cambios funcionales), reactiva cargo fmt --check en ci.yml (estaba comentado), y promueve cargo clippy --lib → cargo clippy --all-targets (la deuda original de "11 errores en tests" ya había cerrado en mini-tandas previas — verificado con audit). Esto sacó el último ítem del bundle D del inventario y deja el repo en estado profesional para colaboradores.
v0.9.49 audit completo del inventario (2026-05-24): después de descubrir 2 sesiones consecutivas con inventario stale (v0.9.47 LSP — 3 deudas ya cerradas; v0.9.48 Cleanup-D — los 11 errores de clippy ya cerrados), dedicamos una sesión a auditar el resto. Resultado: 4 deudas más resultaron ya cerradas:
- F13 — heterogéneos en codegen: ✅ Cerrado vía SPIKE
__FitzValue. Verificado con smoke[1, "dos", true]produce[1, "dos", true]bit-a-bit confitz run. - 8.7-await-binding-split: ✅ Cerrado con test
py_await_split_emite_fitz_py_await_obj+ dispatch al helper__fitz_py_await_objcuandoinner_ty == PyAny. - multi-arch-docker: ✅ Implementado en
release.ymlJob 3docker-imagecon buildxlinux/amd64,linux/arm64. - fitz-python-image: ✅ Implementado en
release.ymlJob 3b con tag:latest-python.
Deudas reales restantes (auditadas como NO cerradas):
| ID | Categoría | Esfuerzo |
|---|---|---|
| ✓ CERRADO v0.9.53 | gen_return propaga expected type adentro de Ok(...)/Err(...); coerce inner directo al T/E del Result<T, E> esperado |
|
| ✓ CERRADO v0.9.54 | 4 helpers __fitz_py_to_map_string_<v> para v primitivo (Str/Int/Float/Bool) + wiring en coerce. V compuesto (Nominal/List/Map) sigue gradual como deuda menor — casos raros, workaround manual con iteración del PyDict |
|
| ✓ CERRADO v0.13.2 | v0.9.51 intentó declarar positionEncoding: utf-8 pero vscode-languageclient@9.0.1 hard-codea general.positionEncodings = ['utf-16'] (client.js:1370) y rechaza cualquier encoding distinto en client.js:835 — la extensión 0.13.1 era inservible en VSCode fresh (bug reportado durante curso M1.C1, 2026-06-04). v0.13.2 implementa el fix completo: server omite position_encoding (default UTF-16), position_to_offset/offset_to_position migrados a contar UTF-16 code units vía ch.len_utf16(), helper nuevo utf16_to_unicode_char(text, line, char_utf16) -> u32 (pub) traduce char_utf16 del cliente a chars Unicode 1-based del lexer para lookup en TypeInfo/DefinitionInfo, handlers del backend hover/goto_definition aplican la traducción antes del lookup, detect_completion_context traduce recv_col interno antes de armar CompletionContext::AfterDot. Soporta SMP (emoji, símbolos matemáticos avanzados) sin off-by-one en hover/definition/completion. Deuda residual cosmética (NO afecta navegación funcional): make_definition_location y ident_range_from_def retornan Range LSP con char en chars Unicode (no UTF-16), pero como las líneas de def son siempre ASCII en la parte ANTES del ident (keywords + identifiers son ASCII por reglas del lexer), char_unicode == char_utf16 en práctica |
|
| ✓ CERRADO v0.9.51 | parse_postfix preserva Expr::Field { field: "" } en lugar de descartar el stmt entero |
|
| ✓ RECLASIFICADO v0.9.56 | Verificado empíricamente 2026-05-24 que NO es cerrable: libpython3.so (13 KB) en python:3.X-slim exporta solo 4 símbolos glibc (no exporta API Python). En Linux NO existe equivalente al python3.dll shim de Windows. Movido a constraint arquitectural permanente; ver docs/deudas_lenguaje.md |
|
| ✓ CERRADO v0.9.57 | src/pyi_loader.rs nuevo con auto-pickup en 2 pases: pase 1 carga classes adyacentes al .fitz raíz, pase 2 procesa fns/vars del stub como fields tipados de un nominal sintético __pyi_module_<binding>. infer_method_call para Nominal busca primero en fields-as-callable (Function type). Binding from python import foo tipa como Type::Nominal(synth_id) si hay stub, sino fallback a PyAny. 14 unit tests + smoke E2E + cap 21.8b reescrito + ejemplo examples/guide/21c-pyi-autopickup/. Inventario activo queda vacío después de este cierre. |
|
| ✓ CERRADO v0.9.50 | smoke end-to-end con Postgres VERDE, imagen 136 MB | |
| ✓ CERRADO v0.9.52 | smoke end-to-end con Postgres + nginx + CORS preflight VERDE, imagen 136 MB |
Lección aprendida (tercera vez en 3 sesiones consecutivas): los inventarios escritos hace varias mini-tandas tienden a desactualizarse rápido. Convención nueva: al iniciar cualquier bundle, hacer audit rápido (10-15 min) de las deudas listadas antes de prometer trabajo. Ejemplos de comandos del audit:
- LSP: grep -nE "fn make_hover_with_range|fn resolve_cross_module|collect_local_bindings_at" src/lsp.rs
- Clippy: cargo clippy --all-targets --all-features -- -D warnings
- Codegen Python: reproducir el caso con un .fitz mínimo + fitz build (lo más confiable).
Qué NO entró en la auditoría¶
- Fase 6/⅞/9 (Async, DX HTTP, Interop Python, Ecosistema): decisión de roadmap, no auditoría.
- Features del syntax-spec NO implementadas todavía (async/await real, middleware, headers, TLS, streaming): documentadas como dirección, no contrato. La auditoría solo señala donde docs/código discrepan sobre el estado actual. Nota post-5b: status codes custom y query params se cerraron en mini-fases dedicadas y salieron de esta lista.
- Verificación bit-a-bit profunda de cada feature: el smoke test E2E ya cubre los ejemplos compilables; no re-verifiqué cada uno.
- Benchmarks de performance: las menciones P1-P5 son observaciones sobre el código, no medidas. Si alguna duele, hace falta benchmark dedicado.
Próximos pasos sugeridos¶
Quick wins cerrados (L1 clippy, R1 fn main + decorators no-@server,
D1 header guía parcial, D5 status codes spec). El cleanup chico que
queda en pie son L2 (helper with_temp_output — ~13 sitios) y
D3 (syntax-spec header desactualizado).
S1 (span en AST) está cerrado en sus tres frentes: B.1 (Stmt),
S1.2 (Expr en checker + evaluator), y S1.codegen (52 sitios
del codegen con err_at + 17 internos con err() documentados
como defensivos). Mensajes de error pasan de 0:0 a línea/
columna precisas en cualquier camino del compilador (checker,
runtime, codegen). Pendiente residual menor: Pattern y
TypeExpr sin span — deuda explícita, baja prioridad porque
los errores de patrones suelen estar en sitios donde el match
contenedor ya provee un span razonable. T1 cerrado entero
(ver ítem siguiente).
Las deudas funcionales son sub-pasos formales que mejor se
abren como mini-fases dedicadas, cada una con plan corto + tests
+ cierre. Estado actual:
- F2 (field assignment chequeo) ✅ — cerrada en C-F2.
- F12 (higher-order completo) ✅ — cerrada con TypeExpr::Function
+ codegen a Rc<dyn Fn(...) -> R>. Cap 11 ahora compila.
- F11 (state HTTP compartido) ✅ — cerrada vía thread_local!
+ tokio current_thread. examples/server.fitz y
examples/guide/17-http.fitz compilan + corren end-to-end.
Trade-off documentado: server single-threaded hasta que aterrice
async/await real (entonces se pivota a Arc/Mutex + State
extractor).
- S1.2 (span en Expr + checker + evaluator) ✅ — los 3
sub-pasos cerrados. Errores expr-level del checker y de
runtime citan posición exacta del nodo problemático (operador,
paréntesis, corchete, argumento concreto, valor del campo,
etc.). 19 tests dedicados de span entre parser/checker/
evaluator. Deuda residual menor: codegen call sites siguen con
err() (helper err_at listo en CodegenCtx).
- T1 (tests frágiles del codegen) ✅ — cerrado entero.
Infra ast_test (módulo adentro de mod tests) parsea el Rust
generado con syn::parse_file y expone ~30+ helpers para buscar
items, lets, signatures, derives, macro calls, method calls, loops,
matches, casts, attrs, visibilidad, routes axum, etc. con
stringificación normalizada via quote::ToTokens. ~115 tests
migrados en tres batches:
- Batch 1 (primer pase): expresiones, literales, primitivas.
- Batch 2 (28 tests): Listas/Mapas/Indexing/Métodos built-in +
F12 closures (FnExpr suelta, fn como valor/param/retorno,
captura no-Copy, FnExpr inline como arg).
- Batch 3 (50 tests en 4 sub-commits, 3a HTTP / 3b Result/match
/ 3c módulos / 3d sobrantes): HTTP wrappers async, Router, path
params, status codes custom, query params, body POST, server
decorator, state thread_local, type impls JSON, Result/Ok/Err/?/
match con bindings/range guards, módulos (pub items, static/const
top-level), type-def Display, struct-lit con defaults/nullables,
igualdad estructural, pasar instance, if-as-expr, str-interp.
Beneficio acumulado: cambios cosméticos del codegen (espacios,
agrupación de paréntesis, sufijos numéricos alternativos, orden de
attributes, formato de macros) no rompen estos tests — solo cambios
estructurales reales (renaming de tipos generados, eliminación de
bindings, cambio de semántica) los rompen. Removidos helpers
dead-code assert_contains y assert_http_contains tras la
migración. Residual aceptado: 10 code.contains siguen vivos
intencionalmente — 4 sobre ast_test::ts(&file) (tokens AST
normalizados, ya AST-based), 1 contrato de mensaje de error
user-visible (assert_err_contains-style), 1 negative check sobre
output completo, 4 sobre Cargo.toml (TOML, no Rust). Pipeline para
futuros tests: usar ast_test desde el arranque en cualquier test
nuevo del codegen.
- HTTP status codes custom — cerrado en mini-fase dedicada.
Sintaxis del spec
return <Int> <body>implementada end-to-end: - AST: nueva variante
Stmt::ReturnStatus { status, body, span }. - Parser: después de
return <Int>con{siguiente, parsea el body como Expr y emiteStmt::ReturnStatus. Sin{sigue como Return normal (preserva sintaxisreturn 42). - Checker: rechaza
ReturnStatusfuera de handlers HTTP (@get/@post/@put/@delete). Stackin_http_handlerparalelo alreturn_stack. No chequea body contra return type formal del handler (polimorfismo del spec). - Intérprete: nueva
Value::HttpResponse { status, body }opaca fuera de context HTTP.value_to_outcomela intercepta y emite elHandlerOutcomecon el status pedido. - Codegen: scan recursivo sobre body de cada fn HTTP; si hay
ReturnStatus, su return type Rust se cambia a__FitzResponse(struct nueva en preludio HTTP) y todos los returns (normales y custom) se envuelven uniforme. El handler wrapper destructura__FitzResponsey emite(StatusCode::from_u16(...), Json(body)). Flagresponse_modese resetea al entrar a FnExpr (callback inline + fn suelta) — el body del closure no hereda el modo del handler contenedor. - Polimorfismo del spec: handler
-> Strpuede mezclarreturn "ok"(200) conreturn 404 { ... }. El return type declarado se ignora en este path. - Cap 17 de la guía actualizado con sección "Status codes custom"
- 3 ejemplos.
examples/guide/17-http.fitzsumó endpoints/protected(401) y/users/{id}/profile(200 ó 404). Validado bit-a-bitfitz runvsfitz build.
- 3 ejemplos.
- 16 tests dedicados (parser 3, checker 4, http 3, codegen 4, E2E 2).
-
Deuda explícita que queda:
return 204sin body (parser exige body explícito; workaroundreturn 204 {}); responses como expresión libre (let r = 200 { ... }); status codes desde una var (return code { ... }concodeno literal). -
HTTP query params — cerrado en mini-fase dedicada (segunda mitad de la mini-fase HTTP combinada con status codes). Sintaxis del spec
@get("/items?limit={limit}&offset={offset}")implementada end-to-end: parse_path_template(http.rs): separa el path real del query template por el primer?y devuelvequery_params: Vec<String>adicional. Validaciones: la key del query debe coincidir con el nombre del param Fitz; template malformado (?limit,?=v,?{x}) emite error específico; duplicados entre path y query también.RouteSpec/RouteMeta: sumaronquery_params: Vec<String>yhas_query_params: bool.InterpTaskllevaquery_params: HashMap<String, String>con los raw values del request.build_method_router: 8 combinaciones de(has_path × has_query × expects_body)con axum extractors apropiados (AxumPath,Query<HashMap>,Bytes).handle_task(intérprete): para cada param Fitz, decide si es path/query/body. Query nullable (Int?) faltante →Value::Null; obligatorio faltante → 400 con mensaje. Coerción al tipo declarado víacoerce_path_param(Int/Float/Str/Bool).- Evaluator registro de
@get/@post: valida que cada?key={name}del template tenga un param Fitz correspondiente. Mismatch → error claro.param_typesahora carga tambiénis_nullable: boolpara que el dispatch HTTP decida si Null o 400. - Codegen:
parse_http_pathdelega aparse_path_templatepara devolver(path_axum, query_params). El wrapper HTTP categoriza cada param en path/query/body; para los query emiteQuery<HashMap<String, String>>+ binding tipado con coerción (limit: i64 = match __qmap.get("limit") { ... }). Nullable →Option<T>. Tipos no soportados (Lists, custom, Result) → error de codegen claro. - Bug fix colateral del codegen:
BinOp Eq/NotEqentreNullable<T>yNullahora emite.is_none()/.is_some()en vez del literal== ()(que Rust rechaza por mismatched types sobreOption<T>). Habilita patrones tipoif (limit == null) { ... }adentro del handler. - Cap 17 de la guía actualizado con sección "Query params" + 3
ejemplos.
examples/guide/17-http.fitzsumó endpoint/search?name={name}&limit={limit}conStr/Int?. Validado bit-a-bitfitz runvsfitz buildcon curl. - 17 tests dedicados (http path 5, codegen 7, E2E 5).
- Deuda explícita que queda: tipos no-primitivos en query
params (List, instancias); aliases de key (
?l={limit}, rechazado hoy); query params via vector (?ids=1&ids=2); query params como una struct ad-hoc (Mapimplícito).
Deuda residual de Fase 10.b (atacar antes del release v0.10.1)¶
Política del autor (2026-05-26): "Fitz tiene que tener todo lo mejor; anotar toda la deuda residual para atacarla antes del release al terminar todo". Todo lo que está abajo tiene que cerrarse ANTES del release v0.10.1 (cierre formal de Fase 10.b entera).
De 10.b.6 (Agregados scalares ORM)¶
- ✅ GROUP BY + aggregate (sum/avg/min/max) + count — CERRADO
2026-05-26 (10.b.14). Refactor con
Type::Aggregated<Row>nuevo:.group_by(...)muta deQueryBuilder<Row>aAggregated<Row>, y sobre Aggregated los aggregates devuelvenFuture<Result<List<Map<Str, Any>>>>(path GROUP BY) en vez deFloat/Int(path scalar). Helper de preludio db nuevoaggregate_groups(conn, agg_expr, agg_name)emite el SELECT con GROUP BY y materializa cada row comoVec<(__FitzValue, __FitzValue)>..all/.first/.update/.deletese rechazan sobre Aggregated (no tiene sentido sobre GROUP BY) con error claro del checker.program_uses_fitz_valueextendido para detectar.group_by(...)y forzar emisión del enum__FitzValue+ helpers. Test paridad real:orm_group_by_aggregate_paridad_codegen_e2evalida bit-a-bit count + sum agrupados por region (3 grupos PAT/BUE/CBA). Cambios: types.rs (variant nuevo + infer_aggregated_method), codegen.rs (gen_orm_qb_method conis_aggregatedflag + helper preludio db). Paridad estricta evaluator ↔ codegen restaurada.
De 10.b.7 (Navigation methods)¶
- ✅
#[allow(clippy::only_used_in_recursion)]enorm_field_coerce_block— CERRADO 2026-05-26 (10.b.10.1). Elenvse removió del signature; el cleanup quedó porque el caller (gen_orm_navigation→orm_lookup_meta_and_fields) ya hace las validaciones nominales que originalmente habían motivado mantener el param. Signature más chica + sin#[allow]. - ✅ Args extras a navigation (chain) — CERRADO 2026-05-26
(10.b.13).
instance.posts()(sin args) ahora devuelveQueryBuilder<Post>para encadenar.where(...).order_by(...). limit(N).all(db).await?igual queType.where(...). Backward compat:instance.posts(db)(con db) sigue siendo terminal directo (.allpara HasMany,.firstpara BelongsTo/HasOne). Checker, evaluator y codegen actualizados en paridad. Test paridad real:orm_navigation_chain_paridad_codegen_e2evalida bit-a-bit chain de 4 ops sobre nav + path legacy en el mismo programa. Kwargs (instance.posts(limit=10)) NO en MVP — usar el chain explícito. Más expressivo y consistente. - ✅ Eager loading (preload) — CERRADO 2026-05-26 (10.b.15).
User.where(...).preload("posts").all(db).await?evita N+1 ejecutando 1 query batch al target type conWHERE fk IN (parent_pks)y poblando los fields virtuales de cada parent. Implementación: state nuevopreloads: Vec<String>en el__FitzQueryBuilder<TData>+ métodowith_preload(name). El codegen de.all/.firstenvuelve el query base con un loop que itera los preloads y, por cada uno, hace match estático contra las HasMany relations conocidas del row type en compile-time (cero overhead cuando no se usa). Helperemit_preload_dispatch(meta)genera el bloque inline con el SQL batch, deserialize aVec<Arc<Mutex<TargetData>>>, particionado por FK, y mutación del field virtual del parent. Branch.preload(name)valida en codegen que name corresponda a una relation @has_many declarada.User.preload(...)directo +User.where(...).preload(...)chain ambos soportados. Test paridad real:orm_preload_has_many_paridad_codegen_e2evalida bit-a-bitu0=ada:3 u1=alan:1 u2=grace:0con 1 query para users + 1 batch para posts (en vez de N+1). MVP solo HasMany — BelongsTo y HasOne quedan como deuda menor abierta para v0.11 si entra demanda (sus casos típicos los cubren navigation methods directos sin riesgo de N+1). - ✅ Cross-type navigation con
@column(name=...)en el FK source field — CERRADO 2026-05-26 (10.b.10.2). Test paridad realorm_navigation_con_column_override_en_fk_source_paridad_codegen_e2econ esquema donde el SQL column del FK se llamaauthor_uid(≠ field Fitzuser_id). Validado bit-a-bit: el SELECT del Post usa el override, la navigation a User funciona correcto.
De 10.b.8.a (Arrays Postgres)¶
- ✅
.update(db, {"tags": [1,2]})con List literal — CERRADO 2026-05-26 (10.b.11.a).gen_qb_update_set_argsahora detecta fieldList<scalar>+ valueExpr::Listliteral y emite__FitzPgValue::Array { elem_oid, values: vec![...] }directo (sin pasar por el genérico__IntoPgValue::into_pg). Helper nuevofitz_scalar_lit_to_pg_value_codewrappea cada item al variant esperado. Test paridad real:orm_update_con_list_y_map_literal_paridad_codegen_e2evalida round-trip insert + update + select con tagsint8[]y metajsonb. - ⚠️ Arrays anidados (
List<List<T>>): Postgres soporta arrays multidimensionales nativamente, pero el driver Fitz solo parsea arrays planos (parse_array_textensrc/db.rs~1397). Cerrar esto requiere refactor del driver (parse + encode + tipos del wire) con beneficio marginal — los usuarios reales tienden a modelar data 2D como JSONB o como@has_many. No bloquea v0.10.1; queda como deuda menor abierta para v0.11+ si aparece demanda. Workaround: usarMap<Str, Any>y guardar el array anidado como JSON. - ⚠️
List<Nominal>(e.g.tags: List<Tag>con Tag tipo custom): Postgres NO tiene "array of struct" nativo. Las dos alternativas reales son (a) JSONB array (no esList<T>real, solo similar shape) y (b) tabla relacionada con@has_many(que YA está implementado en 10.b.7). No bloquea v0.10.1; el patrón canónico para esto es@has_many, no array. La deuda queda CERRADA con workaround documentado. - ✅ NULL adentro de arrays (
{1, NULL, 3}→List<Int?>) — CERRADO 2026-05-26 (10.b.12.a).orm_list_scalar_info_with_nulldetectaList<Int?>/etc. y propagainner_nullableflag. Coerce emiteVec<Option<T>>conmatches!(__item, Null) → None / Some(...). Marshal emitematch __it { Some(__v) => PgValue::T(*__v), None => PgValue::Null }. Test paridad real:orm_list_nullable_inner_paridad_codegen_e2evalida bit-a-bitlen1=5 len2=3con NULLs en arrays Postgres.
De 10.b.8.b (JSONB libre)¶
- ✅
.update(db, {"meta": {...}})con Map literal — CERRADO 2026-05-26 (10.b.11.b).gen_qb_update_set_argsdetecta fieldMap<Str, Any>+ valueExpr::Mapliteral y emite__FitzPgValue::Text(__fitz_fitz_value_to_jsonb(&__FitzValue:: Map(...)).expect(...)). Helper nuevofitz_lit_to_fitz_value_codewrappea recursivamente cualquier Fitz literal puro (Int/Float/Str/Bool/Null + List/Map anidados) a__FitzValue. Test paridad real valida JSONB anidado. - ✅
Map<Str, Str>,Map<Str, Int>(Map concretos no-Any) — CERRADO 2026-05-26 (10.b.12.b).orm_map_str_concrete_infodetectaMap<Str, Int|Float|Str|Bool>con T concreto y emite deserialize viaserde_json::from_str+ iter +as_i64/f64/str/ bool()validando shape. Marshal serializa directo aserde_json::Value::Number/String/Boolsin __FitzValue (más eficiente). El cast SQL sigue siendo::jsonb. Bonus: el helperprogram_uses_fitz_valueahora también activa serde_json cuando hay Map en types @table (aunque no haya Any). Test paridad real:orm_map_str_concreto_paridad_codegen_e2evalida bit-a-bit insert + select conMap<Str, Int>yMap<Str, Str>. Otros Map (Map<Int, T>, etc.) siguen rechazados — JSON objects solo aceptan keys string. - ✅ Validación shape JSONB — CERRADO 2026-05-26 (10.b.13.b)
por DECISIÓN DE DISEÑO.
Map<Str, Any>significa "cualquier shape JSON válido"; validación de shape específico (timestamps ISO, emails, UUIDs) es responsabilidad del user viamatch/is_in([...])/parsing manual. Para schemas conocidos a priori, el patrón recomendado esMap<Str, T>concreto (10.b.12.b) con T = Int/Float/Str/Bool, que valida el shape automáticamente. Schema annotations (@shape({"created_at": "iso8601"})) quedan como deuda menor abierta para v0.11+ si aparece demanda real — diseño grande (decorator + parser + validation engine) sin beneficio claro vs. validación manual en handlers.
Deudas viejas que siguen abiertas (impactan 10.b)¶
- ✅ Test paridad real
db_real_postgresno corre en CI default — CERRADO 2026-05-26 (10.b.16). Job nuevodb-postgresen.github/workflows/ci.ymlque levantapostgres:16como service container, exportaFITZ_TEST_PG_URL=postgres://postgres:postgres@ localhost:5432/fitz_test, y correcargo test --test db_real_postgres -- --ignored --test-threads=1. Solo Linux (Docker service containers más estables en GHA Linux runners; los tests no dependen de plataforma — el binario standalone es x86_64-linux). Los 14 E2E paridad real (belongs_to + has_many + arrays + JSONB - where combinatorio + between/mod/var_ext + array ops + nav
chain + group_by aggregate + Map
concreto + List - preload + CRUD lifecycle + order_by/limit + basics + col
override en FK source + .update con List/Map literal + agg
scalar) ahora corren en cada push a main.
#[ignore]se mantiene para quecargo testdefault sin env var siga rápido. - ✅ Smoke GUIDE_EXAMPLES_COMPILE no incluye ejemplos ORM —
CERRADO 2026-05-26 (10.b.17). Nuevo
examples/guide/32-orm.fitzpedagógico (~100 LoC) que muestra el shape canónico del ORM end-to-end:@tablecon@primary+@column+@belongs_to @has_many, insert, where + first, chainorder_by/limit/offset, operadoresstarts_with/is_in/between, aggregates scalarescount/avg, GROUP BY conAggregated<Row>, navigationbelongs_to/has_many, eager loading conpreload, yupdate/deletecon guard.where(...)obligatorio. Sumado al smokeGUIDE_EXAMPLES_COMPILE—fitz buildproduce binario que NO requiere Postgres real al compilar; elconnectruntime falla conErrclara cuando la URL inválida, así el ejemplo es ejecutable como guía aunque no haya Postgres local. Cierra la última deuda residual de Fase 10.b antes del release v0.10.1.
Mini-fase W17 (2026-05-27) — Virtual fields skip en impls cross-module¶
Descubierta durante el primer intento de implementar el boilerplate
api-orm-full (showcase del ORM + stack web first-class
multi-archivo). Cierra el último gap conocido del codegen cross-
module ORM. Ningún cambio user-facing: sin sintaxis nueva, sin
keyword nueva, sin decorator nuevo — solo el codegen ahora emite
impls __ToFitzJson/__FromFitzJson correctos para @table types
con relations virtuales declarados en módulos.
- ✅ W17 —
@tabletype con relations virtuales (@has_many/@has_one/BelongsToCompanion) declarado en módulo A + handler que lo retorna en módulo B. Antes del fix, el codegen al emitirimpl __FromFitzJson for UserDataen main.rs hacía remap de los fields virtuales (posts: List<Post>) →List<Any>(porque el target typePostno estaba en el env del importer) → emitíaVec<__FitzValue>. Pero__FitzValueno se activaba por el programa (sinMap<Str, Any>niList<Any>legítimo en el source Fitz), entonces rustc rompía concannot find type __FitzValue in this scopey el binario fallaba al linkear. Fix: skipear los virtual fields (HasMany/HasOne/BelongsToCompanion viaTableMetadata.is_virtual_field) en los impls__ToFitzJson/__FromFitzJson. Esos fields no van a la DB ni deben aparecer en JSON I/O — el cliente no debe poder enviarlos como body, y la response no los serializa. En el struct literal del__from_fitz_json, los virtuales se inicializan inline conDefault::default()para evitar nombrar el tipo remap-degradado. Cambios: nueva variantegen_type_http_impls_for_sig_with_meta(name, sig, meta: Option<&TableMetadata>)que filtra virtuales; ambos call sites (uno local engen_type_http_impls, otro cross-module enemit_helpers_for_imported_types) actualizados para pasar el meta. Test E2E nuevocross_module_orm_virtual_fields_skip_w17candea el caso con 3 archivos (models.fitz + posts.fitz + main.fitz). SmokeGUIDE_EXAMPLES_COMPILEverde — el ejemplo31-orm.fitzsigue compilando bit-a-bit; otros 6 tests cross-module (W8/W10/W11/W12/W15/W16) sin regresiones. Validado runtime:GET /usersdevuelve[{"id":7,"name":"ada"}]SIN incluir el virtualposts(skip correcto).
Deuda derivada de la sesión W17¶
-
⚠️ Inferencia del checker post-match Result con early-return Err → Option
. Caso: let x = match Result { Ok(v) => v, Err(_) => return Err("..."), }. El checker infierexcomoOption<String>cuando debería serString(elErrbranch termina enreturn, no produce valor). El codegen emitelet mut x: Option<String> = (match ...)que rustc rompe con "expected Option, found String". Workaround: anotar el tipo explícitamente — let x: Str = match .... Detectado al implementarauth.fitzdel boilerplateapi-orm-full. No bloquea ningún ejemplo de la guía (los patterns con Result + match exhaustivo NO usan bindings de la fork de Err en el caller). Refinement del checker queda como deuda menor abierta — no es urgente porque el workaround es trivial y descubrible. -
⚠️ Cross-module ORM 3 archivos — patrón
<table types en módulo + handlers en otro módulo + main solo imports>(probado por W17 fix). Aunque W17 cierra el bug del trait bound__ToFitzJson, hay deudas residuales menores derivadas de esa exploración: - Forward refs en
@has_many("Target")con Target declarado después en el mismo módulo: rompen el codegen ORM cuando el codegen emite navigation method al procesar el type. El ejemplo31-orm.fitzevita el caso porque no invoca navigation directamente — solo declara las relations. Caso confirmado en mi exploración:type User { ... @has_many Post ... } type Post { ... }falla con "type Post no registrado en TypeEnv" si el codegen intenta resolver el target. Workaround: declarar Target ANTES de User, y los companion fields (user: User?) backward-ref a User. -
Importar TODOS los
@tabletypes al módulo que usa cualquier uno: el codegen valida ALL los targets de relations de un type al procesarlo. Si User declara@has_many("Post", ...)pero el módulo solo hacefrom models import User, el codegen falla con "type Post no registrado". Workaround:from models import User, Post, ...(todos los referenciados). Refinement futuro: el codegen podría auto-resolver los target types desde el loader sin requerirlos en elfrom import. -
⚠️
Map<Str, Any>en HTTP response de handlers cross-module. El handler que retornaMap<Str, Any>(caso típico GROUP BY + db.query crudo) funciona OK en single-file. En cross-module, cuando el handler vive en módulo B y elMap<Str, Any>arrastra Vec<__FitzValue> al codegen del módulo, los impls__ToFitzJson/__FromFitzJsonnecesarios se buscan en main.rs. W17 no toca este caso (es un workaround del cap 31 sec 28 ya documentado para single-file). Refinement futuro: replicar la Decisión W17 (skip lookup local, usar Default::default) para Vec<__FitzValue> en módulos. No bloquea casos actuales.
Mini-fase W18+ (2026-05-28) — Gaps cerrados durante api-orm-full multi-archivo¶
Bloque cerrado al construir el boilerplate
api-orm-full (8va plantilla,
showcase del stack web first-class entero multi-archivo). Cada gap
descubierto durante la escritura del boilerplate se cerró en bloque
ANTES de declarar el boilerplate completo. 5 fixes del codegen
en una sesión:
-
✅ R.1.3 —
Map<Str, Any>con indexing assignment dinámico (m["k"] = v). El storage Rust deMap<_, Any>esVec<(__FitzValue, __FitzValue)>. El codegen del indexing assignment SIEMPRE emitía__g.push((__k, __v))con tipos crudos (String/T), generando "expected __FitzValue, found String". Fix engen_index_assign: detectarstorage_is_heterogeneous(k o v es Any) y envolver key/value conwrap_as_fitz_value_with_env. Caso canónico: partial updates en APIs REST. Test E2Emap_str_any_indexing_assign_compilado. -
✅ R.1.3-bis —
.has(var)sobreMap<Str, Any>. Paralelo al anterior:gen_map_hasno envolvía el arg como __FitzValue cuando el storage es heterogéneo. Fix: nuevo paramvalue_ty+ checkstorage_is_heterogeneouscon wrap igual. -
✅ W18 —
has_opaque_fieldignora virtuales del ORM en cross-module. El check previo agen_type_http_impls_for_sig_with_metamiraba TODOS los fields delremapped_sig, incluso los virtuales (@has_many/@has_one/BelongsToCompanion). Cuando un virtual apuntaba a un target no importado al main, el remap lo degradaba aNullable(Any)oList(Any)y el filtro skipeaba TODO el impl. Sin impl__ToFitzJson, rustc rompía con "trait bound not satisfied". Fix: filtrar virtuales antes del check usando elTableMetadataya disponible. Caso canónico: cross-module ORM 4-archivos (models+auth+posts+main) donde main soloimport postssin traer Post al scope local. Test E2Ecross_module_table_virtual_w18_remap_any. -
✅ Bug del format string en jsonb dynamic update. En el dispatch
Dynamicde.update(db, map_var)para fields jsonb, la string delErr(e)arm tenía{{}}(escaped braces) donde debería tener{}(placeholder de format). Como la string se produce vía.replace("{f}", ...)y NO viaformat!, las llaves quedan literales en el código Rust generado. Resultado: rustc rejecta con "argument never used" porque elejamás se interpola. Fix trivial: cambiar{{}}→{}. Cubierto por el boilerplate. -
✅
.has(var)sobre arrays Postgres (text[]/int8[]/etc.). El codegen rechazaba con "el value debe ser literal del tipo del array". Fix: delegar atranslate_closure_to_sqlcuando no hay match con literal Fitz; reusa la máquina de W3 (.like(var)) y W6 (body.field) que bindean via__IntoPgValue::into_pg(...). Caso canónico: filtros por tag en endpoints listables. Test E2Eorm_array_has_acepta_var_externa.
Tests al cierre del bloque W18+: smoke GUIDE_EXAMPLES_COMPILE
292 ejemplos verde con los 5 fixes integrados. 3 tests E2E nuevos
en tests/compile_e2e.rs.
Gaps descubiertos en la sesión y NO cerrados (NO bloquean el boilerplate, documentados para fases futuras):
-
⚠️ Narrowing flow-sensitive de
Nullable<T>→Tpost-if (x != null). El checker no refinaStr?aStrdespués del check.let s: Str = xfalla. Workaround idiomático: match arm conPattern::Ident(W2 ya cubre el refinement adentro de match). Refinement flow-sensitive enifes propio del checker y queda como deuda residual. -
⚠️ Broadcast HTTP → WS cross-handler.
conn.broadcast(msg)solo funciona DESDE un handler@ws. No hay primitiva para "handler HTTP triggerea broadcast a clientes WS conectados". Caso canónico SaaS (comment nuevo → notification realtime). Requiere API global tipows_broadcast(endpoint, msg: T)o unWsBroadcastercapturable en el scope del handler HTTP. Scope grande, queda como deuda visible. El boilerplate api-orm-full modela/feedcomo broadcast simétrico entre clientes WS para showcasear el WS sin pelear este gap.
Mini-fase post-release v0.10.7 — Gaps descubiertos en smoke real Docker¶
Bloque de gaps descubiertos al hacer el smoke end-to-end del
boilerplate api-orm-full con Postgres real adentro de Docker
(tag v0.10.7). El binario compila local + fitz check verde +
smoke 292 verde NO los detectaba — solo aparecen cuando el binario
levanta el server contra una DB real y se le pegan requests HTTP.
3 gaps cross-module nuevos abiertos:
-
⚠️ OpenAPI 3.1 schema vacío cuando los handlers HTTP viven cross-module.
GET /openapi.jsondevuelve{"paths": []}cuando los handlers@get/@post/@put/@deleteestán en módulos importados (caso canónico de cualquier boilerplate multi-archivo serio). El codegen del schema (openapi.rs) solo miraprogram.http_fnsdel main local, no recolecta los handlers cross-module via el loader. Resultado: el W16 (rutas cross-module se enchufan al Router) NO está coordinado con el OpenAPI auto-generación. El Router responde a los endpoints, pero/openapi.jsony/docssalen vacíos visualmente. Fix futuro v0.10.8: el generador de schema debe iterar tambiénloader.modules[*].http_fn_stmts(W16 ya los captura). -
⚠️ AsyncAPI 3.0 endpoint no se registra cuando los
@wsviven cross-module + los handlers WS mismos NO se enchufan al Router axum.GET /asyncapi.json→ 404 yWS /feed→ 404 en handshake. Más grave de lo que parecía: no es solo schema vacío como el OpenAPI cross-module — los WS handlers cross-module no se registran como rutas en el axum::Router del main. W16 (v0.10.7) cubrió solo@get/@post/@put/@delete, no incluyó@ws. Fix futuro v0.10.8: extender W16 para que itere tambiénloader.modules[*].ws_fn_stmtsy emita las rutas WS qualified (.route_service("/feed", crate::realtime::__ws_handler_feed)o equivalente), más el AsyncAPI schema con los@wsde módulos. Detectado en smoke real con cliente Nodews: handshake al endpoint cross-module devuelve 404 antes del upgrade HTTP→WS. -
⚠️ ORM no skipea fields
Str = ""del INSERT cuando hay DEFAULT en el schema. W4 cubre soloid: Int = 0para bigserial PK. Para timestamps conDEFAULT NOW()o cualquier field con DEFAULT del lado Postgres, el INSERT siempre incluye el field con el value de Fitz (típicamente""para timestamps), que Postgres rechaza con"invalid input syntax for type timestamp with time zone: \"\"". Workaround en el boilerplate api-orm-full: cambiar el schema detimestamptz NOT NULL DEFAULT NOW()atext NOT NULL DEFAULT ''(pierde el tipo nativo, gana smoke OK). Fix futuro v0.10.8: agregar sentinel general para Str/Nullable ("si value es el default literal, skipear field del INSERT") o exponer una API tipodb.now()built-in que emita el ISO 8601 actual desde Fitz al insertar. -
⚠️ W17 skipea virtuales del JSON aunque vengan poblados por
.preload(...). Caso canónico:Post.where(...).preload("author") .preload("comments").first(db)carga los virtuales en memoria, peroimpl __ToFitzJson for PostData(emitido con W17) los skipea al serializar — el response JSON nunca muestra el eager-loaded data. El feature.preload(...)queda parcialmente roto: ejecuta las queries adicionales pero el cliente no ve los resultados. Workaround actual: ninguno limpio — el cliente puede hacer un segundo request aGET /posts/{id}/commentsyGET /users/{author_id}para obtener los datos. Pierde el beneficio principal del eager loading (1 round-trip vs N+1). Fix futuro v0.10.8: W17 debe distinguir entre "skip virtuales en__FromFitzJson" (correcto — no van como body input) y "skip virtuales en__ToFitzJson" (incorrecto cuando están poblados — sí van en el response). Posible diseño: flag runtime "is_loaded" sobre el field virtual que el serializer chequea, o emitir el field en JSON solo cuando no es default (nullpara HasOne/BelongsToCompanion,[]para HasMany). -
⚠️ HTTP wrapper no desempaca
Result<T>tail sinOk(...)explícito. Cuando un handlerasync fn handler(...) -> Result<T>termina conreturn <expr_que_devuelve_Result<T>>(típicamente el.awaitde un chain ORM comoPost.where(...).first(conn).await), el HTTP wrapper serializa elResultentero como{"Ok": {...}}en lugar de extraer elTy devolverlo directamente. Pero si el handler termina conreturn Ok(x)explícito (típicamente traslet x = <chain>.await?; return Ok(x)), el wrapper SÍ desempaca y devuelveTpuro. Caso canónico que rompe:return <ChainQB>.first(db).await. Workaround en api-orm-full: reescribir los handlers afectados (get_post,update_post,delete_post,stats_posts_per_user) conlet x = ...?; return Ok(x)en vez dereturn ...await. Fix futuro v0.10.8: el HTTP wrapper debe detectar el tipo del expr final y siempre desempacarResult<T>sea explícito o no. Detectado al smoke real del boilerplate api-orm-full cuando todo el resto del stack ya funcionaba.
Parches temporales aplicados al boilerplate api-orm-full (REVERTIR cuando los gaps de v0.10.8 cierren):
Estos workarounds son temporales — el boilerplate debería volver a la sintaxis canónica (showcase del stack completo) cuando v0.10.8 cierre los gaps subyacentes. Lista para revertir post-v0.10.8:
schema.fitz— revertirtext NOT NULL DEFAULT ''→timestamptz NOT NULL DEFAULT NOW()en los fieldscreated_at(users/posts/comments) ypublished_at(posts). Pre-requisito: cerrar gap "ORM no skipea Str sentinel del INSERT". Sin esto, el INSERT sigue mandando''y rompe.
1.b. posts.fitz — revertir los handlers get_post,
update_post, delete_post, stats_posts_per_user a su
forma idiomática return <chain>.await (sin let x = ...?;
return Ok(x) boilerplate). Pre-requisito: cerrar gap
"HTTP wrapper no desempaca Result tail sin Ok() explícito".
Sin esto, los responses siguen viniendo como {"Ok": ...}.
-
docs/deudas-post-5b.md— borrar este bloque entero de "Mini-fase post-release v0.10.7 — Gaps descubiertos en smoke real Docker" cuando los 3 gaps queden cerrados. -
schema.fitz— opcional, si llegadb.now()o built-in time: el handler podría setearcreated_at: db.now()en lugar de depender del DEFAULT del schema. Decisión pedagógica abierta. -
README del boilerplate (
boilerplates/api-orm-full/README.md) — actualizar la sección "Notas de diseño" con la sintaxis canónica una vez los gaps cierren (sacar las menciones de workarounds que ya no apliquen). Verificar también las referencias av0.10.7y bump av0.10.8enFITZ_TAG. -
Documentación cap 31 de la guía /
docs/db-orm.md— si se documentan los gaps actuales como deudas del ORM, borrarlos de ahí también una vez que cierren.
🎯 Cierre formal v0.10.8 (2026-05-28) — TODOS los gaps cerrados¶
Mini-fase de cierre ejecutada en 4 rondas (10.8.1 → 10.8.8) en una sesión. Los 8 gaps descubiertos durante el smoke real Docker del boilerplate api-orm-full v0.10.7 quedaron cerrados con fix + test E2E + revert del workaround respectivo:
| # | Gap | Estado |
|---|---|---|
| #1 | Narrowing flow-sensitive Nullable<T> post-if (x != null) |
✅ CERRADO 10.8.4 |
| #2 | Broadcast HTTP → WS — built-in ws_broadcast(endpoint, msg) |
✅ CERRADO 10.8.7 |
| #3 | OpenAPI 3.1 cross-module paths (paths vacío) | ✅ CERRADO 10.8.5 |
| #4 | WS Router cross-module + AsyncAPI cross-module (404) | ✅ CERRADO 10.8.6 |
| #5 | ORM no skipea Str sentinel del INSERT con DEFAULT del schema | ✅ CERRADO 10.8.2 (decorator @db_default) |
| #6 | HTTP wrapper no desempaca Result<T> tail sin Ok() explícito |
✅ CERRADO 10.8.1 |
| #7 | W17 skipea virtuales del JSON aunque .preload(...) los pobló |
✅ CERRADO 10.8.3 (conditional emit) |
| #8 | Revert parches temporales del boilerplate | ✅ CERRADO 10.8.8 |
Tests al cierre: 8 E2E nuevos en tests/compile_e2e.rs +
smoke 292 verde + cargo fmt --all -- --check limpio + cargo
clippy --all-targets --release -- -D warnings limpio.
Boilerplate api-orm-full ahora en forma canónica:
- schema.fitz: timestamptz NOT NULL DEFAULT NOW().
- models.fitz: @db_default created_at: Str = "".
- posts.fitz: handlers con return <chain>.await directo +
narrowing if (status != null) en vez de match arm.
- comments.fitz: broadcast WS real con ws_broadcast("/feed",
resp) después del insert (notification realtime al feed).
Extensión VSCode v0.10.8: grammar TextMate suma
ws_broadcast, LSP completion lo lista en scope_level_completions.
Deudas detectadas en el primer bench (2026-05-29) — URGENTES¶
Descubiertas al correr el benchmark MVP de
benchmarks/orm-vs-sqlalchemy/ (Fitz ORM nativo vs SQLAlchemy
interop Python). Los headline numbers (cold start 5.5x, GET lista
9-11x faster en Fitz) ratifican la dirección, pero apareció una
anomalía y un set de mejoras al bench mismo.
✅ B-1 CERRADO (v0.10.13, commit 67efabd) — Overhead constante del driver Postgres en extended query¶
Resolución: el root cause no fue la falta de prepared
statement cache (hipótesis original); fue que los 5 mensajes del
Extended Query Protocol (Parse + Bind + Describe + Execute + Sync)
se enviaban como 5 llamadas separadas a self.write(...), cada
una con su socket.write() syscall. Aun con TCP_NODELAY activo,
las llamadas separadas sumaban latencia por await + scheduling de
tokio en cada round.
Fix (src/db.rs:1951-2005): los 5 mensajes
se serializan en un único Vec<u8> y se mandan al socket con un
solo write_all_bytes(&batch). Postgres NO responde hasta el
Sync — los 5 mensajes son "pipelined" en el sentido protocolar,
no es un cambio semántico; solo eliminamos overhead client-side.
TCP_NODELAY se activó en el mismo commit para evitar Nagle.
Números post-fix (corrida publicable v0.10.13, hardware Intel Core Ultra 7 155H + 64GB + Docker Desktop WSL2):
| Endpoint | Pre-fix Fitz p50 | Post-fix Fitz p50 | Python SQLAlchemy p50 |
|---|---|---|---|
GET /users/{id} |
43.70 ms | 3.60 ms | 31.87 ms |
GET /users |
4.92 ms | 4.88 ms | 37.85 ms |
Fitz pasó de "30% más lento que Python en single-by-PK" a 8.85x
más rápido. Bench publicable en
benchmarks/orm-vs-sqlalchemy/README.md → "Última corrida
publicable".
Las hipótesis alternativas (POOL_CACHE contention, allocations en
__FromFitzDbRow, prepared statement caching) NO se exploraron
porque el fix de batching ya tiró la latencia al piso. Quedan como
optimizaciones futuras solo si aparece presión real para sub-1ms
single reads.
Histórico — análisis original de B-1 (mantenido por valor pedagógico)¶
Cerrado en v0.10.13 con un fix distinto al planteado. El análisis original demostró el síntoma correctamente pero apuntó a la hipótesis equivocada (statement cache); el batching de wire messages fue el verdadero culpable.
Síntoma: GET /users/{id} (que usa WHERE id = $1, extended
query protocol) tarda 43.70 ms p50 en Fitz vs 31.09 ms p50 en
Python+SQLAlchemy. Python es ~30% más rápido en este endpoint
específico.
Comparación que destapa el bug: GET /users (sin params, simple
query) en el MISMO server Fitz tarda solo 4.92 ms p50 — un 89×
más rápido que Python (46 ms). Y GET /users/{id} en PG con índice
PRIMARY KEY es instantáneo (microsegundos). Entonces el overhead
extra de Fitz en /users/{id} viene del protocolo extended query
mismo, no de la query.
Sospecha: en src/db.rs::Connection::extended_query, el flow
hace 5 round-trips al server (Parse → Bind → Describe → Execute
→ Sync), cada uno con su read. Si cada round-trip suma ~8-10 ms de
overhead (cualquier source: locks del Connection, alloc en wire
buffer, await scheduling), eso suma los 40 ms observados.
Hipótesis alternativas (a descartar antes de optimizar):
- ¿db.connect(url) adentro del handler tiene overhead async que
multiplica por request? (POOL_CACHE singleton v0.10.9 debería
matar eso, pero capaz hay contención en el Mutex del cache).
- ¿__FromFitzDbRow para un struct con 4 fields tiene
allocations excesivas?
- ¿El extended query NO está reusando un statement preparado (Parse
cada vez)? SQLAlchemy probablemente sí cachea prepared statements
por SQL string.
Plan de investigación:
1. Reproducir con un test E2E aislado (1 handler, GET por id, 1000
requests serial, medir wall-time por request).
2. Agregar tracing con spans alrededor de cada round-trip wire en
extended_query y medir.
3. Si confirmado: cachear prepared statements por SQL en el pool.
Cambio interno al driver, sin tocar API pública.
Impacto si se cierra: read single-by-PK con WHERE params pasa de ~40 ms a probablemente <2 ms (mismo orden que simple query), convirtiendo Fitz en 15x faster que Python en ese caso también, no 30% más lento. Es el caso CRUD más común (GET resource by ID), así que cerrarlo desbloquea el headline "Fitz gana en TODO" en lugar de "gana en mucho pero no single-read".
B-2 — Mejoras del bench MVP (no bloqueantes)¶
Descubiertas al hacer dogfood del bench:
- Image size pesca imagen errónea cuando hay otros boilerplates
cacheados. El
grep -E "^${name}-api:"actual matchea bien al bench actual, pero el grep original ("api-orm") era too loose. Fix aplicado en run.sh v0.10.12 — anchor exacto al<dirname>-api:latest. - Memory peak
?cuandocontainer_namedel docker-compose difiere del que asume el run.sh. Eldocker statsfallaba silenc ioso, el sampler nunca escribíamem.log. Fix aplicado en run.sh v0.10.12 — container names correctos. - POST x 500 sequential en Git Bash Windows toma ~10 min por el
overhead del subshell (~1s por iter del for loop). Fix aplicado
en run.sh — bajado a x 100. Posible mejora futura: usar
ohacon body fijo para POST (ganamos throughput real ~100x, perdemos email único por request — fair si POST hace UPSERT o si solo medimos latencia bind/exec). - Hardware info NO se auto-detecta en el summary.md. Hay
placeholders
TODO. Posible mejora: el script intenta detectar CPU/RAM/OS automáticamente (cmdwmicWindows /lscpuLinux /sysctl -amacOS) y los pinea al final. - Bench unidimensional: 3 endpoints aislados. Faltan escenarios "extendidos" del roadmap original (mixed workload realista, bulk inserts, escritura concurrente saturada). Quedan como mini-fase futura cuando aparezca demanda real para publicar comparativa más amplia.
Mini-fase v0.10.10 (2026-05-28) — Fix deadlock __to_fitz_json has_many virtual¶
Cierre del preload hang dejado como deuda residual de v0.10.9.
Bug aislado con 3 ciclos de eprintln strategic (revertidos en el
mismo commit final). Root cause NO era el read loop del driver
(como asumí en v0.10.9): era el codegen del impl __ToFitzJson
del field has_many virtual.
- ✅ gen_type_http_impls_for_sig_with_meta (src/codegen.rs):
el conditional emit del field has_many virtual (introducido en
v0.10.8.3 para activar
.preload(...)end-to-end en el JSON response) hacía{ let __g = self.x.lock(); if !__g.is_empty() { ...self.x.__to_fitz_json() } }. El__to_fitz_jsondel impl genéricoArc<Mutex<T>>re-lockea el MISMO Mutex. Comostd::sync::MutexNO es reentrante, deadlock instantáneo.
Fix: liberar el guard ANTES del re-lock. Chequeo is_empty
en scope acotado:
{ let __is_empty = { let __g = self.x.lock().unwrap();
__g.is_empty() };
if !__is_empty { __obj.insert(..., self.x.__to_fitz_json()); }
}
Smoke real Docker validado: GET /posts/1 con preload responde
200 en ~140ms con author + comments preloaded embebidos en
el JSON.
Deuda residual del boilerplate descubierta durante el smoke:
el response expone password_hash del author porque
Post.author: User? incluye ese field. No es bug del lenguaje
— el boilerplate debería mapear a un PostPublic/UserPublic
que omita el field sensible. Fix para una próxima iteración del
boilerplate.
Mini-fase v0.10.9 (2026-05-28) — Pool singleton per URL¶
Sub-paso post-v0.10.8: smoke real Docker descubrió connection
pool leak crítico. Cada db.connect(url) desde Fitz creaba un
POOL NUEVO con 10 permits + TCP conns. Tras N requests al
boilerplate api-orm-full, Postgres se quedaba sin slots
(max_connections=100 default) y acquire() colgaba
indefinidamente, manifestándose como "preload hang" visible en
GETs con .preload(...).
-
✅ 10.9.2 (#2 nuevo) —
connect_urlsingleton per URL.fitz::db::connect_url(url)cachea elArc<DbConnHandle>en mapa global thread-safe (OnceLock<Mutex<HashMap>>). Calls subsiguientes con misma URL devuelven clone(Arc) — TODAS las conns TCP comparten via el pool único. Cambio coordinado: retornaArc<DbConnHandle>directo, call sites (evaluator + codegen runtime) actualizados. -
✅ 10.9.1 (#1 nuevo) — "Preload runtime hang" CERRADO en v0.10.10. La hipótesis inicial (bug del read loop del driver) era incorrecta — el driver recibe todos los
ReadyForQuerydel preload limpio. Aislado con eprintln en 3 ciclos: el hang estaba en el codegen delimpl __ToFitzJsondel field has_many virtual (introducido en v0.10.8.3 para activar.preload(...)end-to-end en el JSON response). El conditional emit hacía:El{ let __g = self.x.lock().unwrap(); if !__g.is_empty() { __obj.insert(..., self.x.__to_fitz_json()); } }__to_fitz_jsondel impl genéricoArc<Mutex<T>>re-lockea el MISMO Mutex que__gretiene.std::sync::MutexNO es reentrante → deadlock. Fix engen_type_http_impls_for_sig_with_meta: chequearis_emptyen un scope acotado que dropea el guard ANTES del re-lock. Validación smoke real Docker:GET /posts/1con preload responde 200 con author + comments embebidos en ~140ms.
Smoke real Docker bloqueado por bug ambiente Windows:
Docker Desktop Windows tiene un bug intermitente con SCRAM-SHA-256
sobre el bridge TCP que cuelga Connection::connect aún con
código pristine pre-v0.10.9. NO bloquea el release porque el fix
es localizado al pool singleton y el ambiente Linux real no tiene
ese issue. Validación smoke real queda como tarea CI Linux (job
db-postgres ya integrado en .github/workflows/ci.yml).
Deuda residual del ORM/DB post-v0.10.29¶
Sección creada al cerrar v0.10.29 (2026-05-31) — "cierre masivo del ORM" con 12 features residuales cerradas en bloque (JSON path operators,
@@text search,@uniquecomposite,@check_constraint, cross-schema FK, diff completo de indexes,fitz db inspect --all-schemas, redaction de secrets enFITZ_DB_LOG, DB errors enriquecidos con SQLSTATE+SQL+params,FITZ_DB_MAX_CONNS, skip deliberado de JSON||merge, docs masivos). Detalle exacto enCHANGELOG.mdentry v0.10.29.Esta sección lista lo que QUEDA pendiente al cierre. 29 ítems verificados con grep exhaustivo en
src/(confirmando que el código NO los implementa), agrupados en 5 tiers por scope. Mi recomendación: Tier A + B cierran el MVP fuerte del ORM en el sentido "sin fricciones residuales conocidas para los patrones canónicos".Tests al cierre de v0.10.29: 2739 unit + 292 smoke + 3 openapi + 81 cli_e2e + 52 db_real_postgres. fmt + clippy --all-targets + clippy --features lsp limpios.
Resumen de tiers¶
| Tier | Foco | Scope total | Items |
|---|---|---|---|
| A | Cierre MVP fuerte del ORM | ~30-40h | 10 |
| B | API completion Date/DateTime/Uuid | ~12-16h | 7 |
| C | Operadores SQL faltantes | ~12-20h | 3 |
| D | DX/LSP residual del ORM | ~5h | 2 |
| E | Visión a futuro (expansión lenguaje) | días-semanas | 7 |
Recomendación: si el norte es "ORM completo", arrancar por Tier A + B (~50h totales). Tier C complementa con operadores SQL avanzados. Tier D mejora DX sin tocar lenguaje. Tier E son features grandes que expanden el lenguaje (mini-fases dedicadas).
Tier A — Cierre MVP fuerte del ORM — 9/10 CERRADO 2026-06-01 (v0.10.31)¶
9 ítems cerrados en bloque (~12h reales vs ~30-40h estimadas).
Detalle por sub-paso en CHANGELOG.md v0.10.31. Solo A.10 queda
pendiente (FITZ_DB_* mid-run reload — refinable cuando aparezca
presión, hoy LazyLock cubre 99% del caso real).
| ID | Item | Estado |
|---|---|---|
| A.1 | fitz db diff --check-destructive con clasificación Safe/Risky/Destructive + abort sin --allow-destructive |
✅ |
| A.2 | ALTER COLUMN TYPE con USING col::T automático |
✅ |
| A.3 | db.connect(url, max_conns=N) kwarg del lenguaje |
✅ |
| A.4 | Savepoints / nested transactions vía tx_depth shared + SAVEPOINT/RELEASE/ROLLBACK TO |
✅ |
| A.5 | ALTER TABLE ADD/DROP CONSTRAINT para CHECKs via diff |
✅ |
| A.6 | FK targeting composite PK → error claro pre-DDL (en lugar de fallback silencioso a "id") | ✅ |
| A.7 | Drift check @check_constraint (introspect lee pg_constraint.contype='c' via pg_get_constraintdef) |
✅ |
| A.8 | Drift check cross-schema FK (introspect popula references_schema desde ccu.table_schema) |
✅ |
| A.9 | db.transaction(closure, isolation="...") con whitelist 4 ANSI levels + READ ONLY/WRITE |
✅ |
| A.10 | FITZ_DB_* mid-run reload (hoy LazyLock se fija al primer acceso) |
⏳ |
Deuda residual derivada (NO bloquea): (a) parse_check_def usa
trim+exact comparison para drift — cambios cosméticos en
espacios/case del expr pueden disparar DROP+ADD espurio (refinable
con SQL normalizer si entra presión); (b) @belongs_to(refs="col")
para FK single-col explícito a tabla con composite PK no
implementado — A.6 solo da error claro, sub-paso refs= futuro
permitiría declarar FK válida; © db.connect(..., max_conns=N)
implementado vía env var override antes del connect — si un
connect previo cacheó max_conns default, el override no aplica;
(d) transaction_with_isolation solo whitelistea 12 strings ANSI
+ modificadores — DEFERRABLE queda como deuda menor.
Tier B — API completion Date/DateTime/Uuid — CERRADO 2026-05-31 (v0.10.30)¶
Cerrado en bloque. 7 sub-pasos coordinados en 1 sesión (~6h reales
vs ~12-16h estimadas). Sin sintaxis nueva del lenguaje, paridad
bit-a-bit fitz run ↔ fitz build validada con 10 E2E nuevos. Sin
deps user-facing nuevas (chrono-tz + feature uuid/v7 ya internos al
binario). 0 breaking de los 292 ejemplos del smoke. Detalle por
sub-paso en CHANGELOG.md v0.10.30.
| ID | Item | Estado |
|---|---|---|
| B.1 | .add_days/months/years (Date) + .add_seconds/minutes/hours/days/months/years (DateTime) — n signed, overflow → error claro |
✅ |
| B.2 | .subtract_* symmetric — alias con negate runtime (checked_neg defensivo) |
✅ |
| B.3 | .diff_days(other) (Date) + .diff_seconds/minutes/hours/days(other) (DateTime) — signed Int, trunc hacia 0 para unidades > 1s |
✅ |
| B.4 | Comparison < > <= >= entre Date/DateTime — chrono::Ord nativo, sin coerción |
✅ |
| B.5 | Uuid.v7() time-ordered (RFC 9562) — feature uuid/v7 sumada al Cargo.toml |
✅ |
| B.6 | Shortcuts Date.tomorrow()/Date.yesterday()/DateTime.epoch() |
✅ |
| B.7 | DateTime.to_local() (sin dep) + DateTime.in_tz(iana) (chrono-tz, Result |
✅ |
Deuda residual derivada (NO bloquea): (a) el mensaje de overflow
de add_years(N) cita add_months(N*12) porque add_years se
implementa como scale + delegate (refinable pasando method name como
param); (b) to_local() formato fijo ISO 8601 con offset (no acepta
fmt custom — el user que necesita formato custom hace
dt.in_tz("system_tz")? + parse manual); © with_timezone(tz) que
rotara el instante (no solo display) queda explícitamente fuera
de scope — la semántica es ambigua y el user que necesita rotar puede
usar add_seconds(offset) manual.
Tier C — Operadores SQL faltantes — CERRADO 2026-06-01 (v0.10.32)¶
| ID | Item | Estado |
|---|---|---|
| C.1 | ts_rank full-text ranking en .order_by(fn(u) => -u.body.rank("query")) emite ORDER BY ts_rank("body", to_tsquery('query')) DESC; variante plainto_rank para plain queries |
✅ |
| C.2 | Expression indexes via @index(expression="lower(email)") kwarg dedicado. Drift check incompleto (introspect no parsea pg_index.indexprs) — refinar con name= explícito |
✅ |
| C.3 | JSON \|\| merge via qb.merge_jsonb(db, field, patch) separado de .update. Emite UPDATE tbl SET "field" = "field" \|\| $1::jsonb WHERE <where> |
✅ |
Deuda residual derivada de Tier C (NO bloquea): (a) C.1 acepta
solo Str literal en MVP (vars quedan como deuda menor — el path
order_by stream no tiene acceso al pg_args del where); (b) C.2
drift incompleto — la introspect no detecta cambio del expression
con mismo name (el user debe re-nombrar el index para forzar regen);
© C.3 NULL || anything = NULL por convención Postgres — el user
inicializa la col con {} al INSERT.
Tier D — DX/LSP residual del ORM — CERRADO 2026-06-01 (v0.10.32)¶
| ID | Item | Estado |
|---|---|---|
| D.1 | LSP completion ORM en .where() — métodos como is_in/like/matches/has_key/path_text/etc. aparecen en autocomplete sobre Str/Map/Int/Float/Date/DateTime con detail (ORM .where) distintivo |
✅ |
| D.2 | LSP hover sobre @table types muestra el CREATE TABLE SQL emitted vía migrations::schema_from_program + create_table_sql_for — útil para debuggear migrations sin abrir fitz db diff |
✅ |
Deuda residual derivada de Tier D (NO bloquea): (a) D.1 no
detecta scope context — los métodos ORM aparecen siempre, fuera del
.where llamarlos da error runtime (limitación documentada en el
detail string); (b) D.2 si schema_from_program falla (typo en
relations), el hover se devuelve sin el augment SQL — silente, no
hay feedback visual al user.
Tier E — Visión a futuro (expansión del lenguaje)¶
Estos NO cierran "el ORM" — lo expanden. Cada uno es mini-fase dedicada con decisiones de diseño previas. Scope grande (días-semanas). Documentados acá para que aparezcan en una sola tabla con el resto cuando se priorice.
| ID | Item | Evidencia código | Scope |
|---|---|---|---|
| E.1 | Decimal/Numeric type (precision arbitraria) — financial apps. Hoy Str o Float con precision loss |
Cero matches Type::Decimal/NUMERIC OID en driver. Requiere rust_decimal o similar + Type::Decimal + OID 1700 (NUMERIC) en wire protocol |
~1 semana |
| E.2 | Async query streaming cursor-based — Type.stream(db) -> Stream<Type> para resultsets gigantes sin cargar todo en memoria |
Cero matches DECLARE CURSOR/Stream<Row> en db.rs. Requiere wire del protocolo cursor + Stream API + integration con tokio Stream trait |
~3-5 días |
| E.3 | COPY FROM/TO — bulk loading de gigabytes. Hoy bulk_insert es batch INSERT (funciona pero no óptimo para > 100K rows) |
Cero matches COPY FROM/CopyFromStdin |
~3 días |
| E.4 | LISTEN/NOTIFY tipado — real-time pub/sub Postgres como capability del lenguaje (@listen("channel") fn handler(payload: ...) o similar) |
Cero matches LISTEN/NotificationResponse excepto un comment sobre pg_notify() builtin |
~3-5 días |
| E.5 | Window functions built-in (ROW_NUMBER/RANK/LAG/LEAD/OVER (PARTITION BY ...)) |
Cero matches en src/. Hoy escape hatch via db.query |
~1 semana |
| E.6 | CTE / WITH clauses built-in (Type.with("subquery", ...) o método similar) |
Cero matches with_clause/CteSpec. Es lo más complejo del bloque (requiere modelar dependencias entre query trees) |
~2 semanas |
| E.7 | UNION/INTERSECT/EXCEPT entre QueryBuilder<T> |
Cero matches en src/ |
~3-5 días |
Verificación¶
Cada ítem de esta sección fue verificado con grep exhaustivo en
src/ el 2026-05-31 (post-v0.10.29). 3 ítems del inventario
preliminar resultaron estar ya implementados y NO entran a esta
tabla:
- GROUP BY en HTTP returns (serialización
List<Map<Str, Any>>automática) → cerrado en v0.10.22 + v0.10.4 víaDB_HTTP_INTEGRATION_PRELUDE+impl __MapKey for __FitzValue. - Date/Time/Timestamp/UUID como tipos nativos del lenguaje → cerrado en v0.10.24 (intérprete) + v0.10.26 (codegen) con paridad bit-a-bit. Lo que falta es API completion (Tier B arriba).
- TLS strict (
verify-ca/verify-full) → cerrado en v0.10.23 (db.rs:372-373conSslMode::VerifyCa/VerifyFull).
Deuda CI fmt — CERRADO 2026-06-01 (v0.11.1)¶
Síntoma observado: el job cargo fmt --all -- --check del CI
Ubuntu falló durante v0.11.0 con un diff sobre src/cli.rs:419 que
el cargo fmt --all local (Windows) NO marcaba como problema. La
diferencia: rustfmt en Linux colapsaba el chain
p.type_.as_ref().map(|t| t.head_name()).unwrap_or("Str") en una
sola línea; el rustfmt local lo dejaba multi-línea.
Causa: el repo no tenía rustfmt.toml committed. Sin config
explícito, cada versión de rustfmt aplica sus defaults, y esos
defaults pueden divergir sutilmente entre minor versions de Rust
(visto entre 1.83 y 1.85 según observación local).
Fix: rustfmt.toml committed al repo root con configuración
mínima explícita (edition = "2021", max_width = 100,
use_small_heuristics = "Default"). Esto fija el formato canonical
para todos los devs + CI sin importar qué versión de rustfmt traigan
los runners. Verificado que el cargo fmt --check local + el del CI
ahora coinciden.
Deuda residual menor (NO bloquea): el rustfmt.toml actual es
minimalista. Si en algún momento queremos opciones más opinionadas
(import grouping, comment width, etc.), las sumamos ahí. Documentado
para visibilidad — todos los devs futuros saben que ESE es el lugar
donde fijar reglas de fmt.
9.w iteración 2 — Tier 1 deudas pre-M5 — CERRADO 2026-06-02 (v0.11.2)¶
Las tres deudas que bloqueaban escribir M5.C26 del curso "Fitz de
0 a experto" (acordadas el 2026-06-01 en docs/curso-plan.md →
"Tiers pre-M5") cerradas en bloque coordinado:
- Persistencia de jobs sobre DB nativa ✅ —
@cron("...", store=db)creafitz_cron_jobs+fitz_cron_runscon CREATE TABLE IF NOT EXISTS al boot del scheduler. Cada attempt va afitz_cron_runsconstatus running|ok|failed|retrying. Visibility manual conpsql(UI dedicada queda como sub-paso futuro si aparece demanda). - Retry con backoff exponencial para
@cron(+tz/retrytambién en@background) ✅ —retry={max: N, backoff: "exponential"|"linear"|"constant", initial_secs: I, max_secs: M}con delay capeado pormax_secs. Cada attempt registrado con número enattemptcolumn. - Cron timezone configurable ✅ —
tz="IANA/Name"viachrono-tz(ya transitive desde Fase 10).Schedule::upcoming (tz)tz-aware con conversión a UTC para el sleep.
Bonus cerrado en el mismo bloque (no estaba en T1 original):
catch_up=true|false— al boot, si hubo missed runs entrelast_run_atynow, ejecuta UN run inmediato (no N — evita spam). Defaultfalse= skip.
Paridad bit-a-bit fitz run ↔ fitz build validada contra
Postgres 15 local del autor. El codegen activa uses_db=true
automáticamente cuando detecta @cron(..., store=<Ident>)
(función program_has_persistent_cron walka AST). Cargo.toml
generado suma chrono-tz cuando uses_jobs && !uses_date_or_uuid.
Preludio dividido en 4 constantes (simple vs persistent) para
evitar referencias a __FitzDbConn cuando el programa no usa el
driver. Trait polimórfico __FitzCronStoreFrom acepta
__FitzDbConn directo Y Result<__FitzDbConn, String> —
destraba el patrón canónico let db = db.connect(...).await
top-level sin ?.
Deudas residuales derivadas de 9.w.3.iter2 (NO bloquean el arranque de M5, todas documentadas en cap 30 sub-sección "Qué no está en el MVP"):
@backgroundcon persistencia + retry sobrespawn(...)— diferido a iter3. Los args del spawn requieren serialización JSON estable + tablafitz_bg_jobsseparada.@backgroundaceptatz/retryen memoria pero nostore/catch_up.fitz runcron-only (programa con@cron(..., store=db)sin@serverni handlers HTTP) tiene bug heredado del runtime tokiocurrent_threaddel intérprete: la conn DB queda atada al runtime del evaluator que cierra al pasar amulti_threadpara el scheduler. Workarounds:fitz build(binario nativo arma su propio runtime multi-thread limpio) o sumar un handler HTTP trivial. Cierre del bug requiere refactor del flow de runtimes del intérprete (no trivial, deuda separada).- UI dedicada de visibility (panel admin tipo Sidekiq Web). Hoy los datos están en las dos tablas, cualquier dashboard externo (Grafana, Metabase) los lee.
- Coordinación multi-instancia con locks distribuidos para que
un
@cronsolo corra en un nodo. Hoy cada réplica corre todos sus jobs.
Detalle técnico completo: docs/roadmap.md → "9.w iteración
2" → sub-sección "9.w.3.iter2" expandida con los 6 sub-pasos
a/b/c/d/e/f y CHANGELOG v0.11.2.
Fase 12.3 — Observability minimal con OpenTelemetry — CERRADO 2026-06-03¶
Cierre formal de la fase entera con 11 commits a lo largo de 3
bloques (12.3.a + 12.3.b + 12.3.c). Total al cierre: 2894 unit
+ 81 cli_e2e + 3 openapi_e2e + 4 compile_e2e del logging, clippy
--all-targets -- -D warnings limpio, cargo fmt --all --check
limpio.
Lo que cierra (referencia):
- 12.3.a — Structured logging built-in:
log.info/warn/error/ debug(msg, kwargs)con kwargs heterogéneos (Int/Float/Str/ Bool/Null/Secret/List/Map). Output JSON flat a stderr contimestamp+level+msg+kwargs; pretty mode con ANSI bold colors cuando TTY o overrideFITZ_LOG_FORMAT=pretty. Filter viaRUST_LOG(defaultinfo). Secret redactado automático recursivo en List/Map. Paridad bit-a-bit intérprete↔binario contracing+tracing-subscriber+chrono+serde_jsoncomo infraestructura. - 12.3.b — Spans HTTP + métricas + correlación trace_id:
cada request HTTP abre un SpanContext root con IDs OTel-
compatibles (
trace_id32 hex /span_id16 hex, generados conuuid::Uuid::new_v4()). Logs adentro del handler heredan trace_id/span_id automático. Al final del request, access loglog.info("http.access", ...)conhttp.method/http.target(template del route, OTel-standard)/http.status_code/duration_ms+ Counterhttp_requests_total{method, path, status}+ Histogramhttp_request_duration_seconds{method, path, status}(paridad cross-metric con mismos labels). Opt- out total con@server(observability=false)— bypass del wrapper de instrumentación, cero overhead bare-metal. - 12.3.c — OTLP exporter para spans HTTP: cuando
OTEL_EXPORTER_OTLP_ENDPOINTestá seteada, conexión a backend OTel real (Jaeger/Tempo/Honeycomb/Datadog) conopentelemetry- otlp = "0.32"featurehttp-proto. SamplerTraceIdRatioBasedconOTEL_TRACES_SAMPLER_ARGclamp[0.0, 1.0]. Service name desdeOTEL_SERVICE_NAME(default"fitz-app"). Sin la env var, no-op silencioso — zero overhead, zero conexiones de red. Paridad bit-a-bit intérprete↔binario.
Decisiones técnicas clave confirmadas durante implementación:
- Sintaxis kwargs de Fitz usa
name: value(noname=value). Los=están reservados para kwargs en decoradores. Confirmación al implementar ellog.info(msg, k: v)— consistente condb.connect(url, max_conns: 5). - Approach híbrido tracing: instalamos
tracing-subscriberpara quetracing::enabled!respeteRUST_LOG, pero el JSON output lo emitimos manual conserde_json. Razón: kwargs heterogéneos runtime no se modelan limpios con las macrosevent!que esperan field names en compile-time. - Storage propio con
tokio::task_local!sobre tracing nativoSpan::extensions: simplicidad + control total del shape OTel-compatible + atraviesa thread boundaries del runtime tokio multi-thread (handlers HTTP saltean workers entre.awaitpoints). http.target= path template (no path resuelto): convención OTel para agrupar requests por endpoint en herramientas downstream (Datadog/Tempo). Evita cardinality explosion en métricas.- HTTP/proto transport sobre gRPC para OTLP: simplicidad (no requiere tonic), compatibilidad con proxies HTTP corporativos, recomendación Datadog/Honeycomb/New Relic.
OnceLock<bool>paraOTEL_ENABLED: evita lookup de env var en cada request. Se determina UNA vez al boot.
Deudas residuales derivadas de Fase 12.3 (NO bloquean Fase
12.4, todas documentadas en docs/roadmap.md → "Fase 12.3" →
sección "Deudas residuales derivadas"):
- Bridge métricas OTel — INTENTADO en Fase 12.3.iter2.Tier2
(2026-06-03), BLOQUEADO por version conflict, ABIERTO.
metrics::counter!/histogram!que ya emiten Counterhttp_requests_totaly Histogramhttp_request_duration_secondsdespachan a recorder global vacío hoy (excepto cuando Prometheus está activado por Tier3 — ahí van a la exposition format). El cratemetrics-exporter-opentelemetry = "0.2.1"(último release en crates.io, 2025-11-15) pineaopentelemetry_sdk = "0.31", pero nosotros estamos en0.32para traces (12.3.c) + logs (iter2.b). El árbol de deps no unifica —MetricExporter,Resource,SdkMeterProviderson tipos DISTINTOS aunque tengan el mismo nombre (E0277: trait bound not satisfied,E0308: mismatched types ... different SdkMeterProvider). El master del crate ya está en 0.32 (verificado en https://github.com/Noelware/metrics-exporter-opentelemetry/blob/master/Cargo.toml) pero no hay release nuevo aún. Cierre esperado: cuando Noelware publique la próxima versión del crate (probable v0.3.x), bumpear la dep y reintentar. Implementación ya diseñada (no toca codegen) en branch local descartado: enserve()llamarinit_otel_metrics()DESPUÉS deinit_prometheus()para que Prometheus tenga precedencia cuando ambos activos (solo UN recorder global demetricspermitido), instalarSdkMeterProviderconMetricExporterOTLP sobre/v1/metrics+ reader periódico, instalarmetrics_exporter_opentelemetry::Recordercomo global. Workaround mientras tanto: Tier3 (Prometheus) cubre el caso 90% — el user activa@server(prometheus=true), OTel collector hace scrape del endpoint/metrics. Pierde el beneficio "single sink" de OTLP push pero funciona end-to-end. Bridge logs OTelCERRADO en Fase 12.3.iter2.b (2026-06-03): cuandois_otel_enabled()estrueY elLogExporterse instaló correctamente,emit_log_recordemite el LogRecord en paralelo al backend OTel via OTLP HTTP/proto (endpoint/v1/logs). Stderr logs siguen intactos (emit ADITIVO, no reemplazo). Trace context derivado delSpanContextactivo — adentro de un request HTTP los logs heredan automático el mismotrace_id/span_idque el span OTel (cierre iter2.a), habilita correlación logs↔spans en el backend. ValoresSecretse redactan a"***"consistente con el output stderr. Paridad bit-a-bitfitz run↔fitz build(codegen emite__fitz_emit_log_to_otelreal adentro deOTEL_PRELUDE+ stub no-op enLOGGING_OTEL_NOOP_STUBcuando OTel no aplica). Cargo.toml emitido suma featurelogsa los 3 crates OTel. Decisión arquitectónica: usamos la APIopentelemetry::logsSDK directamente (noopentelemetry-appender-tracingcomo sugería el plan original); el appender requiere emit viatracing::event!pero nuestroemit_log_recordescribe directo a stderr con formatter custom (JSON/pretty de 12.3.a). Costo del refactor custom formatter no se justifica para el caso. 4 unit tests nuevos:logging::iter2b_value_to_any_value_{primitivos, secret_se_redacta,list_y_map_son_recursivos}+codegen::iter2b_codegen_{cli_log_emite_stub_no_op, http_log_emite_logger_provider_real}.Correlación trace_id Fitz↔OTelCERRADO en Fase 12.3.iter2.a (2026-06-03): cuandois_otel_enabled()estrue,dispatch_requestabre el span OTel PRIMERO y deriva elSpanContextpropio desdespan.span_context().trace_id().span_id()via el nuevo constructorSpanContext::with_ids(trace_id, span_id). Eltrace_idque aparece en los logs stderr/JSON es EL MISMO que el del span OTel en Jaeger/Tempo/Datadog/Honeycomb — habilita queries cross-pipeline. Sin OTel,SpanContext::new_root()sigue generando IDs propios via uuid. Paridad bit-a-bitfitz run↔fitz build(codegen emite el mismo patrónif let Some(span) = __otel_span.as_ref() { ... } else { new_root() }). 2 unit tests nuevos:logging::iter2a_span_context_with_ids_*codegen::iter2a_codegen_http_emite_with_ids_branch_*.EndpointCERRADO en Fase 12.3.iter2.Tier3 (2026-06-03):/metricsPrometheus opcionalmetrics-exporter- prometheus = "0.18"condefault-features = false(skipeahttp-listener+push-gatewayque no necesitamos). Dual gate:@server(prometheus=true)compile-time + env varFITZ_PROMETHEUS=1/true/yesruntime override (útil en producción sin recompilar). Cuando activo,serve()instalaPrometheusBuildercomo recorder global del cratemetrics(los Counter/Histogram que ya emitedispatch_requestempiezan a popular el recorder automático), ybuild_routerauto-mountaGET /metricsque renderea la exposition format en cada scrape — mismo puerto + transporte que el resto de la app (NO un puerto separado). Paridad bit-a-bitfitz run↔fitz build(codegen emitePROMETHEUS_PRELUDEcon__FITZ_PROMETHEUS_HANDLEstatic +__fitz_init_prometheus+__fitz_prometheus_routeparalelos a la SDK del intérprete).ServerConfig(runtime) +ServerConfigArgs(codegen) gananprometheus_enabled: bool(defaultfalse). Si el user declaró su propio@get("/metrics"), gana — mismo patrón que/openapi.json//healthz. 5 unit tests nuevos:evaluator::tier3_server_decorator_{acepta_kwarg_prometheus_true, default_prometheus_es_false,prometheus_no_bool_es_error}+codegen::tier3_codegen_http_{emite_prometheus_prelude_y_init_ call_falso_por_default,con_prometheus_true_emite_init_call_true}.
Detalle técnico completo: docs/roadmap.md → "Fase 12.3 —
Observability minimal con OpenTelemetry" expandida con los 11
sub-pasos (a.1-3, b.1-5, c.1-3).
Smoke compile_e2e — gating de deps emitidas — CERRADO 2026-06-04 (v0.13.1)¶
Heredado de v0.12.1 (Tier3 Prometheus). El smoke
GUIDE_EXAMPLES_COMPILE compila ~360 ejemplos en serie, cada uno
con su propio target/fitz-build/<stem>/ separado. Sin cache
compartido, cada ejemplo paga el cold-compile de toda dep que
emita el cargo_toml_for cuando has_http=true (incluso si no
las usa). Tras sumar metrics-exporter-prometheus = "0.18" en
Tier3, el smoke en Linux fresh runner cruzó los 15 min y rompió
CI (commit ci: bumpear timeout 15→25 min, 2026-06-03).
Cierre v0.13.1 (2026-06-04): gating refinado del Cargo.toml
emitido por cargo_toml_for. Concretamente:
metrics-exporter-prometheussolo cuando hay@server(prometheus=true)literal. Detector nuevoprogram_uses_prometheus_export(program)walka decorators top-level buscandokwargs["prometheus"] == Expr::Bool(true, _). Propagado aCodegenCtx.uses_prometheus_export+cargo_toml_for(param nuevo, último positional). 20 call sites de tests actualizados con un single-pass PowerShell.- **
emit_prometheus_prelude+__fitz_init_prometheus(...)call .merge(__fitz_prometheus_route())gateados por el mismo flag** (paralelo bit-a-bit). Programas sin opt-in no emiten ni el static__FITZ_PROMETHEUS_HANDLE, ni el helper, ni la ruta.- Breaking behavior aceptado: el path env var
FITZ_PROMETHEUS=1ya no funciona como override de runtime — exige@server(prometheus=true)literal. Documentado endocs/guide.mdcap 33.4. Trade-off: el opt-in compile-time cubre el 95% del caso real (production deployments declaran Prometheus en código); el env var override era nice-to-have.
Timing observado local (Windows 11 + Ryzen + NVMe + Cargo
cache fresh): baseline pre-fix 522.13s ≈ 8.7 min sobre 360
ejemplos. Post-fix se mide separado abajo. El CI Linux fresh
runner debería ver mayor mejora absoluta porque el cold-compile
de metrics-exporter-prometheus + sus transitivos (indexmap,
prometheus crate, protobuf) cae completo en programas no-
Prometheus (~95% de los ejemplos).
Decisión de scope confirmada al arrancar: las deps OTel
(opentelemetry, opentelemetry_sdk, opentelemetry-otlp)
quedan emitidas con has_http (sin cambio). Razón: el wrapper
HTTP del codegen emite __fitz_with_span_context(...) +
__fitz_log_info("http.access", ...) + branches sobre
__fitz_otel_is_enabled() sin opt-in del user — la línea
uses_logging = has_http || ... fuerza el preludio entero
cuando hay HTTP. Removerlas requiere también gatear el access
log auto del wrapper, lo cual cambia comportamiento user-visible
(programas HTTP simples como los ejemplos pedagógicos de la
guía pierden auto access logs + spans). Queda como deuda
residual abierta (ver abajo).
Tests al cierre: 3 unit tests nuevos en codegen::tests
(tier3_codegen_http_sin_prometheus_no_emite_prelude_ni_dep,
tier3_codegen_http_con_prometheus_true_emite_prelude_y_dep,
v0_13_1_program_uses_prometheus_export_detecta_kwarg_true,
v0_13_1_program_uses_prometheus_export_no_dispara_sin_kwarg).
Smoke compile_e2e — gating de OTel deps + access log auto — ABIERTO 2026-06-04¶
Deuda residual derivada del cierre parcial de la deuda anterior
(v0.13.1 solo cerró Prometheus). Las 3 deps OTel
(opentelemetry, opentelemetry_sdk, opentelemetry-otlp)
siguen emitidas con cualquier has_http=true porque
uses_logging se fuerza a true cuando has_http=true (línea
213-215 de src/codegen.rs) — el wrapper HTTP emite
__fitz_with_span_context(...) + __fitz_log_info("http.access",
...) + branches sobre opentelemetry::trace::* sin opt-in del
user.
Fix futuro: gatear las 3 deps + el access log auto del
wrapper + el OTel branch del wrapper por
uses_logging_explicit (es decir, program_uses_logging sin el
forzado || has_http). Programas HTTP que NO usen log.X(...)
explícito pierden el access log auto + spans OTel — caso típico:
ejemplos pedagógicos de la guía y CLIs HTTP triviales.
Trade-off del fix: pierdes la auto-observability "free para todo handler HTTP" que se vendió en Fase 12.3.b.4. Quedan opciones:
- (a) Default opt-in: si el user quiere access logs auto, declara
log.info("startup")en algún lugar del programa. - (b) Nuevo kwarg
@server(observability=true)explicit (rechazado por el prompt de v0.13.1, pero podría revisitarse si la presión real aparece). - © Hacer noop el access log auto cuando no hay subscriber
instalado (probable —
tracing::info!no allocates cuando no hay subscriber). El span sigue emitiendo "in-process" sin exportar, costo bajo.
Estimación del win: la dep opentelemetry-otlp con feature
reqwest-blocking-client pulla reqwest que pulla tokio +
hyper + chains varios. Es de las deps más pesadas del codegen.
Probablemente otro ~3-5 min CI menos sobre Linux fresh si se
gatea cleanly.
Bloqueante para arrancar: decidir entre las 3 opciones de arriba o un sub-paso dedicado con su propio mini-roadmap.
(HISTÓRICO) Fix temporal pre-v0.13.1 — timeout CI 15→25 min¶
Bumpear timeout a 25 min en .github/workflows/ci.yml. Compró
tiempo pero no escaló — cada feature nueva (12.4 sumará deps de
Docker compose codegen, 12.5+ podría sumar más) empujaba el
smoke arriba. Cerrado por v0.13.1 que ataca la causa raíz
del lado Prometheus. El timeout 25 min queda — sirve de
margen para el ojo del huracán cuando entre Fase 13+. La
deuda OTel arriba puede recortar otros ~3-5 min cuando se
cierre.
Fase 12.4.a — fitz docker init (Dockerfile autogenerado) — CERRADO 2026-06-03¶
Cierre parcial de Fase 12.4 (queda 12.4.b para smart detection rica +
fitz docker build wrapper). Sub-comando nuevo fitz docker init
[--force] que genera Dockerfile multi-stage (builder
ghcr.io/thegreekman76/fitz + runtime gcr.io/distroless/cc-debian12)
+ .dockerignore + docker-compose.yml con smart detection AST-only
del entry point declarado en [bin].main. Política skip-por-default;
--force sobrescribe. Validación smoke real verde contra dos
boilerplates (HTTP puro + HTTP+DB con compose smart sumando
postgres:16-alpine con healthcheck).
Decisiones técnicas del MVP: (a) sub-comando con sub-enum
Commands::Docker(DockerCmd::Init) para abrir paso a fitz docker
build de 12.4.b sin breaking; (b) AST-only del entry point (fast
~50ms, no cross-module); © runtime distroless siempre en 12.4.a
(Python interop diferido a 12.4.b); (d) ports: en compose siempre
que haya @server; (e) compose con DB sin restart: policies
(diferido a 12.4.b según @cron).
Deudas residuales derivadas (NO bloquean 12.4.b):
- Cross-module detection —
@server/db.connectadentro de un módulo importado no dispara el shape. Workaround: declarar@serveren el archivo principal (caso típico). Fix futuro: recursar a través del loader del módulo (similar al codegen). - Falso positivo
uses_dbcon variable local llamadadb— paralelo al codegen, trade-off aceptado del MVP. El user borradb:del compose a mano. Fix futuro: distinguir receptordbglobal vs binding local. Detección Python interop diferida a 12.4.bCERRADO en Fase 12.4.b (2026-06-03, v0.12.3):uses_pythondetectafrom python import X/import python.Xy el Dockerfile cae apython:3.12-slim-bookwormautomático.Healthchecks HTTP +CERRADO en Fase 12.4.b (2026-06-03, v0.12.3):restart:policies diferidos a 12.4.buses_cron→restart: unless-stopped; healthcheck HTTP contra/healthzcuando hay@serverY runtime con wget (uses_python).CERRADO en Fase 12.4.b (2026-06-03, v0.12.3): sub-comandofitz docker build [--tag X]wrapper diferido a 12.4.bfitz docker build [--tag X]thin wrapper sobredocker build -t <pkg>:latest .con override y propagación de exit code.
Tests al cierre: 2924 unit (+18 del módulo docker) + 87
cli_e2e (+6 del sub-comando) + 3 openapi_e2e. Clippy
--lib --tests --bins -- -D warnings limpio, fmt clean.
Detalle técnico completo: docs/roadmap.md → "Fase 12.4 —
Dockerfile autogenerado + fitz docker" → sub-paso 12.4.a expandido
con todas las decisiones.
Fase 12.4.b — Smart detection rica + fitz docker build — CERRADO 2026-06-03¶
Cierra Fase 12.4 entera. Suma detección AST de interop Python y
@cron, ajusta runtime + compose según el shape del programa, y agrega
el sub-comando fitz docker build [--tag X] que tag-ea y delega a
docker build.
Smart detection rica:
uses_python(from python import Xoimport python.X) → runtime stage cae apython:3.12-slim-bookworm(~55 MB con libpython3.12 + wget) en vez de distroless (~22 MB sin Python).uses_cron(cualquier@crondecorator) → compose sumarestart: unless-stoppedal service principal.- Healthcheck HTTP en compose solo cuando
server_port = SomeYuses_python(wget disponible). Con distroless emite comentario explicativo con receta para agregarlo a mano.
Sub-comando fitz docker build [--tag X]:
- Thin wrapper sobre
docker build -t <tag> .enmanifest_dir. - Default
--tag=<package.name>:latest. - Aborta con sugerencia si falta
Dockerfile(recomiendafitz docker init) ofitz.toml. - Propaga exit code de
docker buildpara CI.
Decisiones técnicas: (a) runtime swap atómico al detectar interop
Python (alternativa rechazada: distroless + libpython bundleada, deuda
mayor); (b) healthcheck con wget --spider solo en slim-bookworm —
distroless sin shell no permite CMD-SHELL (alternativas rechazadas:
mini-probe binario embebido, healthcheck TCP); © thin wrapper fitz
docker build sin --push/--platform/--no-cache — para flags
advanced, docker build directo.
Deudas residuales derivadas (NO bloquean Fase 12.5):
- Detección DB indirecta vía interop Python —
uses_dbsolo detectadb.X(...)nativo Fitz. Programas que usan SQLAlchemy a través defrom python import sqlalchemyno disparan el servicedb:en compose. Workaround: usar--forcey editar a mano, o usar el driver Postgres nativo de Fitz (cap 31). Fix futuro: detectarfrom python import sqlalchemy/psycopg2/asyncpgcon flag separado, o sumar--with-postgresalinit. - Healthcheck HTTP sin distroless — el bloque solo sale cuando hay
wget (
uses_python). Para programas no-Python con@server, el user puede agregarlo a mano siguiendo el comentario o cambiar el runtime. Fix futuro: bundlear mini binario HTTP probe en distroless, o usar healthcheck TCP (sin validar endpoint exacto). fitz docker buildno expone--push/--platform/--no-cache— thin de propósito. Refinable si aparece demanda real (CI multi-platform).- Cross-module detection sigue siendo deuda heredada de 12.4.a:
from python import X/@cron/@serveradentro de módulo importado no dispara el shape. Workaround: declarar todo en el archivo principal.
Tests al cierre: 2937 unit (+13 del módulo docker) + 93 cli_e2e
(+6 del sub-comando) + 3 openapi_e2e. Clippy --lib --tests --bins
-- -D warnings limpio, fmt clean.
Smoke real verde validado contra boilerplates/api-postgres-python
(interop SQLAlchemy → runtime python:3.12-slim-bookworm automático +
healthcheck HTTP en compose; ausencia de service db: documentada como
limitación conocida de interop indirecto).
Detalle técnico completo: docs/roadmap.md → "Fase 12.4 —
Dockerfile autogenerado + fitz docker" → sub-paso 12.4.b expandido
con sub-pasos 12.4.b.1 + 12.4.b.2.
Fase 9.w.1.iter2.a — @requires("role") (RBAC custom) — CERRADO 2026-06-03¶
Cierre parcial de Fase 9.w.1.iter2 (queda 9.w.1.iter2.b para token
blacklist + refresh). Decorator nuevo @requires("role") apilable
sobre handlers HTTP/@ws para roles más allá de @authenticated/
@admin. El runtime ejecuta el provider, inyecta el user, y valida
que user.role matchee al menos uno de los roles requeridos. Si no,
403 con role actual + requeridos en el mensaje.
Sintaxis:
@requires("editor")
@post("/articles")
fn create(body: Article, user: User) -> Article { ... }
@requires("editor")
@requires("publisher")
@put("/articles/{id}")
fn publish(id: Int, user: User) -> Article { ... } // OR
Decisiones técnicas del MVP: (a) @requires implica auth (corre
el provider igual que @authenticated/@admin); (b) multi-decorator
= OR (un user tiene UN role, pedir A AND B sería incoherente); ©
exige role: Str no nullable en User type (paralelo a @admin);
(d) mensaje de 403 enriquecido con role actual + lista de requeridos;
(e) MVP singular user.role: Str, multi-role user.roles: List<Str>
queda como deuda; (f) paridad bit-a-bit fitz run ↔ fitz build.
Implementación:
src/types.rs::check_auth_decoratorsaceptarequirescomo kind, valida shape sintáctico, rechaza role duplicado en decorators apilados. 9 unit tests (requires_*).src/http.rs::RouteSpecganarequired_roles: Vec<String>. El wrapperdispatch_requesty el WS path disparan el provider cuandoauth != None || !required_roles.is_empty(). Después del admin check, valida queuser.roleesté enrequired_roles. 5 E2E nuevos en oneshot router.src/codegen.rs::HandlerSigganarequired_roles,emit_auth_checkemite el role check después del admin check (paralelo en el WS wrapper).partition_program_stmtsaceptarequirescomo decorator válido.auth_user_param_namelookup dispara también con@requires.src/evaluator.rs: helper nuevocollect_required_rolesparalelo acollect_route_auth. Pipelineprocess_decorator → register_http_route/register_ws_routepropaga el slice.src/lsp.rs::decorator_completionssuma entradarequirescon snippetrequires("editor").
Tests al cierre: 2951 unit (+14) + 93 cli_e2e + 3 openapi_e2e.
Clippy --lib --tests --bins -- -D warnings limpio, fmt clean.
Deudas residuales derivadas (sub-iter futuro 9.w.1.iter2.b):
- Token blacklist + revocación server-side: builtins
auth.blacklist(db, jti, expires_at) -> Result<Null>yauth.is_blacklisted(db, jti) -> Result<Bool>con tablafitz_token_blacklist(jti TEXT PRIMARY KEY, expires_at BIGINT NOT NULL)auto-creada al primer call (paralelo a Fase 9.w.3.iter2 cron persistente). Patrón canónico: endpoints/auth/logouty/auth/refreshse escriben a mano (~10 LoC cada uno) con los builtins. Auto-mount fuera del MVP. Requiere DB obligatoria. - Multi-role:
user.roles: List<Str>con@requires(roles=[...]). MVP se cubre apilando@requiresdecorators (OR) o con check manualif user.roles.contains("editor") { ... }. - Role hierarchy ("admin implies editor implies viewer") no se modela. Aceptable para el MVP — el user lo arma a mano si quiere.
- Mensajes 403 i18n: el formato actual está en español (consistente con el resto del runtime). Multi-lenguaje queda como deuda separada si entra demanda.
Detalle técnico completo: docs/roadmap.md → sección "Tiers
pre-M5 del curso" → "T2 — 9.w.1.iter2" expandido con los dos
sub-pasos.
Fase 12.5 — Cap 35 + curso M7 + cierre formal Fase 12 entera — CERRADO 2026-06-03¶
Cierra Fase 12 entera (12.1 + 12.2 + 12.3 + 12.4 + 12.5). Sub-paso 100% docs/curso, sin cambios de código.
Sub-pasos:
- 12.5.a — Cap 35 nuevo "Deployment ciudadano primera clase" en
docs/guide.mdcon 8 sub-secciones integradoras y ejemplo runnableexamples/guide/35-deploy.fitz(<100 LoC end-to-end con todo el stack). Sumado al smokeGUIDE_EXAMPLES_COMPILE. Renumeración caps 36/37. - 12.5.b — Caps del curso M7 (C1-C4) completos en
docs/curso/m7-produccion-deploy/: - C1: Distribución avanzada (binarios + cross-compile + bundle).
- C2: Observability en producción (logs + spans + métricas + OTel).
- C3: Secrets management (
Secret<T>opaco + patterns K8s/fly/etc). - C4: Deploy avanzado (Docker autogenerado + healthz + K8s + 12-factor).
- Cierre del curso entero con 10 diferenciales resumidos.
- 12.5.c — Cierre formal: CHANGELOG v0.12.5 detallado, roadmap con sub-pasos expandidos, esta nota en deudas, CLAUDE.md, README/index.md actualizados, mkdocs.yml suma nav M7. Smoke GUIDE_EXAMPLES_COMPILE verde con +1 ejemplo (357 total).
Tests al cierre: 2951 unit + 93 cli_e2e + 3 openapi_e2e + 357 compile_e2e (+1 vs v0.12.4). Sin cambios de código — release 100% docs. fmt + clippy heredados de v0.12.4 limpios.
Verificación pre-bump completa (memoria
feedback_pre_release_verification): roadmap ✓, guide.md cap 35 ✓,
deudas (esta nota) ✓, CLAUDE.md ✓, CHANGELOG ✓, README ✓,
docs/index.md ✓, docs/curso/index.md M7 ✓, mkdocs.yml M7 nav ✓,
extensión VSCode (sin cambios — 12.5 es 100% docs) ✓, examples
ejemplo 35-deploy.fitz ✓, boilerplates (sin cambios) ✓, smoke +
lints ✓.
Cierre formal de Fase 12 entera (deployment ciudadano primera clase). Plan original cumplido al 100%:
- 12.1 healthz/readyz + SIGTERM drain (cerrado v0.12.0).
- 12.2 Secret
+ secret()/config()/load_env() (cerrado v0.12.0). - 12.3 Observability OTel (cerrado v0.12.0-12.1 con iter2 de Tier3 Prometheus + bridge logs + correlación trace_id).
- 12.4 Dockerfile autogenerado + fitz docker init/build (cerrado v0.12.2-12.3 con detección AST smart).
- 12.5 Cap 35 + curso M7 + cierre formal (cerrado v0.12.5).
Deudas residuales NO bloqueantes ya documentadas más arriba en
este archivo: bridge métricas OTel (Tier 2 BLOQUEADO esperando
release del crate metrics-exporter-opentelemetry), gating de
deps emitidas en smoke (ABIERTO).
Próximos nortes opcionales (sin demanda real, diferidos):
- 9.w.1.iter2.b — Token blacklist + refresh con builtins
auth.blacklist/auth.is_blacklisted+ tablafitz_token_blacklistauto-creada (CERRADO v0.12.6). - Fase 12.6 —
fitz deployorchestrator — CERRADO v0.13.0 con targetsdocker/compose. Targetsfly/railway/k8scon plugin architecture diferidos a Fase 13+ por demanda real. - Fase 12.7 —
@trace/@metricdecoradores explícitos sobre fns business logic — CERRADO v0.13.0. Paridad bit-a-bitfitz run(no-op honesto) ↔fitz build(instrumentación real contracing+metrics). Cap 33.5 nuevo en guía. - Fase 12.8 — Feature flags built-in — CERRADO v0.13.0.
@flag("name")+flag(name) -> Bool+ móduloflags. Manifest[flags]+ env var override. Cap 33.11 nuevo en guía.
Detalle técnico completo: docs/roadmap.md → "Fase 12.5" con
sub-pasos detallados, y docs/guide.md → cap 35 para la vista
integradora.
Fase 12 Tier 2 — fitz deploy + @trace/@metric + @flag — CERRADO 2026-06-04 (v0.13.0)¶
Cierra el Tier 2 entero de Fase 12: los tres sub-pasos diferidos en v0.12.5 (deploy + observability decoradores + feature flags) cierran en bloque coordinado al detectar suficiente demanda interna para arrancar Fase 13+ post-Tier2 sin deudas pendientes.
Lo que entra al release v0.13.0:
-
Fase 12.6 —
fitz deploy <target>(módulo nuevosrc/deploy.rs~430 LoC). Thin wrappers sobredocker build/compose up. Sólodockerycomposeen MVP (fly/ railway/k8s diferidos). Opt-outs:--no-push/--no-detach/--no-build. Aborta con sugerencia clara si falta el archivo esperado (recomiendafitz docker init). Propaga exit code para CI. 7 unit + 5 cli_e2e tests. -
Fase 12.7 —
@trace(name="X")+@metric(name="X")sobre fns user. Apilables (un@trace+ un@metricsobre la misma fn) pero NO sobre HTTP/WS handlers (auto-instrumentation Fase 12.3 ya los cubre; el checker rechaza estáticamente con mensaje claro). Kwargname=opcional, fallback al nombre de la fn. Decisión técnica clave: emit con__FitzMetricGuardRAII (registra histogram + counter al Drop) en lugar de wrap-down después del body — funciona correctamente conreturn Xexplícito sin código muerto. Paridad bit-a-bitfitz run(no-op honesto) ↔fitz build(instrumentación real contracing+metricscrates). Cap 33.5 nuevo endocs/guide.md+ ejemploexamples/guide/34-trace-metric.fitz. -
Fase 12.8 —
@flag("name")+flag(name) -> Bool+ móduloflags. Tres APIs paralelas: (a) decorator sobre HTTP/WS handlers que retorna 404 si la flag está off — gate hot path ANTES de middlewares/auth/coerciones (orden idéntico al runtimedispatch_request); (b) builtin globalflag(name) -> Boolpara branches programáticos dentro del código; © móduloflagsconis_enabled(name)(alias) ylist()(enumera flags conocidos en orden BTreeSet — manifest + env vars). Dos fuentes: sección[flags]enfitz.toml(defaults compile-time, baked-in al binario via__fitz_flag_init(...)al boot) + env varsFITZ_FLAG_<UPPERCASE>(override runtime sin recompilar). Defaultfalse(fail-safe — features nuevas opt-in). Paridad bit-a-bit con registry estáticoOnceLock+ cache lookup. Cap 33.11 nuevo endocs/guide.md+ ejemploexamples/guide/34b-feature-flags.fitz.
Tests al cierre v0.13.0: 3001 unit (+44 nuevos: 8 evaluator
flag + 9 checker flag + 4 trace/metric codegen + 3 manifest
flags + 4 codegen Cargo.toml flag/trace_metric + 14 LSP + 2
E2E compile flag) + 112 LSP + 360 compile_e2e (+2 ejemplos
nuevos: 34-trace-metric.fitz + 34b-feature-flags.fitz + smoke
verde) + 3 openapi. cargo fmt --all --check + cargo clippy
--lib --tests --bins -- -D warnings limpios.
Extensión VSCode bumpeada a 0.13.0: LSP completions
sumadas para @trace/@metric/@flag decorators (snippets
con kwarg name=/arg posicional), flag() global builtin,
flags.X after-dot. Grammar TextMate sin cambios (decorators
matchean @<ident> genérico).
Deudas residuales derivadas (NO bloquean Fase 13+):
- Deploy targets
fly/railway/k8s— plugin architecture pendiente, MVP cubre los dos targets de demanda real (docker/compose). Cuando entre demanda, los nuevos targets son extensión del enumDeployTarget+ handler dedicado. - Spans hijos ad-hoc dentro de una fn —
@traceenvuelve la fn entera. Para gate-ar solo unas líneas con un span dedicado, workaround: extraer la sección a una fn dedicada con@tracearriba. Sub-paso futuro de la spec: bloquespan("name"): { ... }si entra demanda. @flagsobre fns regulares (no HTTP/WS) — el shape se acepta pero la semántica MVP es no-op (el body se ejecuta igual). Patrón canónico: usarif flag(name)dentro del body para gating manual. Refinable a "early return Null" si entra demanda concreta.- Flags scoped por user/request o por % de tráfico — el flag es global por proceso. Para A/B testing por % de tráfico o segmentación por user, integración con LaunchDarkly/Unleash/Flagsmith via call HTTP desde el handler (workaround documentado en cap 33.11).
- Hot-reload de flags sin restart — flags son inmutables durante el lifetime del proceso. Cambio de env var requiere reinicio. TTL-wrapped lookups quedan diferidos.
- Bridge métricas OTel (Tier 2 de Fase 12.3, no del Tier 2
general) — sigue BLOQUEADO esperando release del crate
metrics-exporter-opentelemetryconopentelemetry_sdk 0.32.
Fase 9.w.1.iter2.b — Token blacklist (auth nativa cerrada) — CERRADO 2026-06-03¶
Cierra Fase 9.w.1.iter2 entera (.a RBAC custom + .b token
blacklist). Módulo built-in auth con 3 builtins async sobre
Postgres + paridad bit-a-bit fitz run ↔ fitz build + cap 28 con
patrón canónico de /auth/logout//auth/refresh.
API:
auth.blacklist(db, jti, expires_at) -> Future<Result<Null>>
auth.is_blacklisted(db, jti) -> Future<Result<Bool>>
auth.cleanup_expired(db) -> Future<Result<Int>>
Tabla fitz_token_blacklist(jti TEXT PRIMARY KEY, expires_at BIGINT
NOT NULL) auto-creada con CREATE TABLE IF NOT EXISTS al primer call
(paralelo a Fase 9.w.3.iter2 cron persistente).
Decisiones técnicas del MVP: (a) expires_at Unix epoch (BIGINT)
para matchear JWT exp claim sin conversiones; (b) auto-filtro
expires_at > now() en is_blacklisted (tokens vencidos no necesitan
seguir bloqueando, jwt.decode los rechaza primero); © ON CONFLICT
DO UPDATE en blacklist (re-blacklisteo del mismo jti actualiza sin
fallar); (d) server-clock manda (now() en SQL, no en Rust — evita
drift); (e) tabla auto-creada idempotente (Postgres serializa con
LOCK interno); (f) paridad bit-a-bit fitz run ↔ fitz build.
Implementación:
- b.1 Intérprete: 4 helpers
pubensrc/evaluator.rs(4 constantes SQL +ensure_token_blacklist_table), 3 fnsbuiltin_auth_blacklist/is_blacklisted/cleanup_expiredcon validación de args + signatures asyncFuture<Result<...>>, registro del móduloauthparalelo a jwt/hash/log enregister_builtins, checker conauthen scope base. 6 unit tests (aridad, primer arg DbConn, pre-registro del módulo) + 6 E2E reales contra Postgres entests/auth_blacklist_real_postgres.rscon#[ignore]. - b.2 Codegen:
expr_uses_authextendido detectaauth.X.emit_auth_preludecuandouses_auth && uses_dbemite 4 constantes SQL +__fitz_ensure_token_blacklist_table+ los 3 helpers__fitz_auth_*async retornandoResult<T, String>.gen_calldespachaauth.X(...)a fns helper paralelas agen_auth_jwt_encode/decode. Importación cross-module. 1 E2E compile test entests/compile_e2e.rs. - b.3 Docs/LSP/cierre: cap 28 de
docs/guide.mdsuma sub-secciónauthcon API + decisiones + patrón canónico completo de/auth/logout+/auth/refresh+ provider con check +@croncleanup en <60 LoC. LSPlsp.rs: sumadoauthascope_level_completions+ after-dot con signatures completas. CHANGELOG v0.12.6 + roadmap + esta nota + CLAUDE.md.
Tests al cierre: 2957 unit (+6) + 93 cli_e2e + 3 openapi_e2e +
358 compile_e2e (+1) + 6 E2E real Postgres #[ignore]. Clippy
--lib --tests --bins -- -D warnings limpio, fmt clean.
Verificación pre-bump completa (memoria
feedback_pre_release_verification): roadmap ✓, guide.md cap 28
sub-sec auth ✓, deudas (esta nota) ✓, CLAUDE ✓, CHANGELOG ✓, README
sin cambios (cap 28 ya cita auth nativa), index.md sin cambios,
extensión VSCode grammar sin cambios + LSP auth module
completions ✓, examples sin runnable nuevo (sería overkill — el
patrón vive en el cap 28), boilerplates sin cambios.
Deudas residuales derivadas (NO bloquean Fase 13+):
- Auto-mount de
/auth/logouty/auth/refresh: el flow exacto varía por proyecto. Mantenerlo manual da más control. Si entra demanda, sub-paso futuro con@server(auto_auth_endpoints=true)como opt-in. - In-memory blacklist (sin DB): para apps sin Postgres que
quieren revocation rápida, un
Map<Str, Int>global + check manual. Trade-off: no persiste entre restarts. Sub-paso futuro conauth.blacklist_local(jti, exp)+ flag opt-in. - Refresh tokens dedicados (OAuth2 clásico con dual-token): el MVP usa un solo token largo. Refresh tokens dedicados queda como pattern futuro si entra demanda.
jwt.encodeconjtiautomático: el user pone"jti": uuid.v4()a mano. Refinamiento futuro: kwargjti=trueque auto-genera y devuelve(token, jti).- Logging del blacklist hit: el flow actual no loguea por
default cuando un token se rechaza por blacklist. El user puede
agregar
log.warn("token revocado", jti: jti)adentro del provider.
Cierre formal de Fase 9.w.1.iter2 entera (auth nativa completa: RBAC custom + token blacklist). Plan original cumplido al 100%.
Detalle técnico completo: docs/roadmap.md → sección "Tiers
pre-M5 del curso" → "T2 — 9.w.1.iter2" → sub-paso 9.w.1.iter2.b
expandido, y docs/guide.md → cap 28 sub-sec auth para el patrón
canónico runnable.
Curso Fitz de 0 a experto — M7 nuevo (Interop Python) + M8 ampliado — CERRADO 2026-06-03¶
Cierre del curso entero (8 módulos / 41 capítulos). Plan original
tenía 7 módulos (M7 = Producción y deployment) con C32b opcional
sobre interop SQLAlchemy. Al cierre detectamos que el material
disponible de Fase 8 (15 sub-secciones del cap 21 + 9 ejemplos
runnable + bundling completo --bundle-python/--bundle-pip)
justificaba un módulo dedicado en lugar de UN cap opcional.
Decisión (2026-06-03): renumerar M7 anterior (Producción y deployment) a M8 y crear M7 nuevo dedicado a Interop Python con 3 caps. M8 además recibió un cap nuevo (M8.C5) sobre deploy real de apps con interop — específicamente para apps que salgan de M7 y necesiten distribución sin Python instalado en destino.
Sub-pasos:
- Renumeración M7 → M8:
git mvdel directorio, sed sobre los 4 caps internos cambiando "M7.C" → "M8.C", actualizando pre-req del primer cap (M8.C1 ahora apunta a M7.C3 o M6.C6 si saltás M7), "Validación final del módulo M7" → "M8", final summary del curso movido a M8.C5. - 3 caps nuevos M7 Interop Python (
docs/curso/m7-python- interop/): - C1 Setup venv +
from python import+ casos simples — venv estándar Python sin magia Fitz,cargo build --features python, primer programa con math/json/datetime. Auto-coerción primitiva (Fase 8.1.3), introducción al PyObject opaco. - C2 numpy + pandas reales — handler HTTP que sirve análisis
de clima con pandas + numpy. Coerción a
typenominal con anotación destino (Fase 8.4). Excepciones Python → Result automático (Fase 8.3). Benchmarks de marshaling. - C3 SQLAlchemy interop + bridge async + cuándo NO usarlo —
matriz de decisión honesta vs ORM nativo Fitz. Patrón
canónico
<py_call>?.await(Fase 8.6 bridge tokio↔asyncio).fitz py-typespara auto-generar types Fitz desde modelos SQLAlchemy. - 1 cap nuevo M8.C5 Deploy real con interop Python
(
docs/curso/m8-produccion-deploy/c5-bundle-python-pip- deploy.md) —fitz build --bundle-python(CPython 3.14.5 embebido via PBS de Astral) +--bundle-pip(paquetes pip empaquetados via tarball secundario). Comparativa Path A (Dockerfile default + venv en runtime, ~250 MB) vs Path B (bundling completo + distroless, ~200 MB). Trade-offs honestos (cuándo NO usar bundling). - Ejemplos runnable en
examples/curso/m7-python-interop/: c1-setup, c2-weather, c3-sqlalchemy — cada uno con README, app.fitz, archivos Python helper, y comandos exactos de smoke manual. - Actualización del index y nav:
docs/curso/index.mdcon tabla de 8 módulos / 41 caps + sección nueva M7 + M8 ampliada;mkdocs.ymlcon nav M7 (3 caps) + M8 (5 caps);docs/curso-plan.mdcon header de "Actualización 2026-06-03" expandido a 5 ajustes sobre el plan original, mapping curso→guide.md refrescado.
Decisiones técnicas: (a) renumeración M7→M8 vía git mv
preservando history; (b) bar editorial idéntico a M1-M6 (header
+ mermaid + tabla diferencial + 7-9 pasos + validación +
troubleshooting + lo que sigue); © cierre del curso entero en
M8.C5 (era M8.C4 antes); (d) M8.C5 marca opcional para apps
puramente Fitz nativas (M8.C4 deja link directo al cierre).
Tests al cierre: sin cambios — release 100% docs/curso. 2957 unit + 93 cli_e2e + 3 openapi_e2e + 358 compile_e2e + 6 E2E real Postgres. fmt + clippy heredados de v0.12.6 limpios.
Verificación pre-bump completa (memoria
feedback_pre_release_verification): roadmap actualizado ✓,
curso-plan revisado con nota nueva ✓, deudas (esta entrada) ✓,
CLAUDE entrada nueva ✓, CHANGELOG v0.12.7 ✓, docs/curso/index.md
✓, mkdocs.yml ✓, extensión VSCode sin cambios (release 100%
docs), examples/curso/m7-python-interop/ con READMEs + código ✓,
README raíz sin cambios.
Cierre formal del curso Fitz de 0 a experto entero: 8
módulos / 41 capítulos. Plan original cumplido + ampliado para
cubrir la interop Python como ciudadano pedagógico de primera
clase y el deploy real de esas apps en producción.
Deudas residuales derivadas (NO bloquean — refinamientos opcionales):
- Smoke automatizado del curso M7 en CI: hoy los ejemplos
runnable se validan a mano. Sumar al CI un job que corre
cargo build --features python+fitz-python check examples/curso/m7-python-interop/c*/app.fitzdaría no-drift guarantee. Costo: +5-10 min de CI por release. Recomendado si los caps M7 entran a Marketing/landing. - Smoke real Docker de M8.C5: el cap incluye Dockerfiles
bundleados completos. Validación manual al cierre; smoke real
contra
python:3.X-slim-bookwormcon SQLAlchemy + asyncpg queda como deuda si entra demanda. - Translation a inglés: el curso entero está en español (consistente con guide.md). Traducción a inglés queda como sub-paso futuro si el material gana tracción.
Detalle técnico completo: docs/curso/index.md,
docs/curso-plan.md con la nota de actualización 2026-06-03, y
los 3+5 caps en docs/curso/m7-python-interop/ +
docs/curso/m8-produccion-deploy/.
Fixes pendientes de la extensión VSCode / LSP — ABIERTO 2026-06-05¶
Backlog vivo de bugs y features faltantes descubiertos en la
experiencia de editing real (VSCode + extensión Fitz). El autor los va
a ir sumando a medida que los encuentre durante el curso Fitz de 0 a
experto y trabajo cotidiano. Cada entry trae síntoma/qué + causa/cómo
+ fix propuesto + impacto/costo.
V1 — Spans incorrectos dentro de string interpolation — CERRADO 2026-06-05¶
Fix aplicado: walker recursivo shift_expr_spans (src/parser.rs:3328)
ajusta los Span de cada nodo del Expr resultante del sub-parser de
StrInterp para que apunten al source original (no al sub-texto aislado).
Reusa nuevo helper Expr::span_mut() (src/ast.rs:318)
paralelo al span() existente. 4 unit tests en parser::tests::v1_*
cubren Ident/BinOp/Call/Field dentro de StrInterp.
Validación end-to-end con el bug reportado por el alumno:
{altitud_m + altitud_a} con Int + Str ahora reporta error en
"línea 5:31" (donde está el +) en vez de "línea 1:1".
Deuda residual menor (NO bloquea uso real): el walker NO recursa
en Stmt adentro de FnExpr.body/Loop.body/If.then/etc. ni en
Pattern/TypeExpr. En la práctica, FnExpr inline adentro de un
StrInterp es extremadamente raro. Si entra demanda, sumamos walker
para Stmt en otro sub-paso.
(Descripción original — para referencia)¶
Síntoma 1 — hover devuelve Str en vez del tipo real del ident:
Hover sobre altitud_m dentro del print(...) muestra Str en lugar
de Int. Funcionalmente todo anda — el evaluator y el checker tipan
altitud_m como Int (validable con {altitud_m + altitud_a} que
da suma numérica). Solo el LSP miente.
Síntoma 2 — error de checker se reporta en la línea/col equivocada:
let lugar = "Patagonia" # ← squiggle rojo aparece acá
let altitud_m = 350
let altitud_a = "350"
print(" altitud: {altitud_m + altitud_a} m") # ← el error real está acá
El mensaje del checker es correcto (el operador \+` no acepta `Int` y `Str`)
pero el squiggle rojo cae en la línea 1 col 1 en vez de en el+` real.
Causa raíz común: el parser, al ver {expr} dentro de un string
interpolado, arranca un sub-parser sobre el texto aislado ("altitud_m + altitud_a").
Ese sub-parser tokeniza desde line=1, col=1 y solo ajusta los line/column
de los errores del sub-parse (vía sub_col_base). Los Span de los
Expr exitosos que produce el sub-parser quedan con line=1 y col
relativa al sub-texto, no a la fuente original.
Por eso:
- En
TypeInfoelIdent("altitud_m")interno se registra conSpanKey(1, 1)en vez de(5, 21). La heurística "max col ≤ cursor en la misma línea" dehover_for_positionno lo encuentra y devuelve el tipo delStrInterpentero (Str). - En diagnostics, el
e.span()delBinOpinterno apunta a(1, 1), así que el squiggle rojo cae en línea 1.
Archivo afectado: src/parser.rs:3219-3229
(función que parsea el StrInterp; el sub_parser.expression() no
post-procesa los spans).
Fix propuesto (~30 LoC, una sola sesión):
- Walker recursivo
shift_expr_spans(expr: &mut Expr, line: usize, col_base: usize)que reescribee.set_span(Span { line, column: col_base + col.saturating_sub(1) })para cada nodo del Expr. - Llamarlo justo después de
let expr = sub_parser.expression()?;conline = line(la línea del source original) ycol_base = sub_col_base. - Test E2E que valida hover sobre
altitud_mdentro de StrInterp devuelveInt, y otro que valida queInt + Stradentro de StrInterp se reporta en la línea/col correcta.
Impacto: alto en DX. Cualquier ident usado dentro de un print("{x}")
queda invisible al hover y los errores de tipos dentro de interpolaciones
se reportan mal ubicados. Patrón muy común en código real.
Side benefit del fix: go-to-definition desde dentro de un StrInterp también queda bien (hoy seguramente apunta al primer token del archivo o falla silenciosamente).
V2 — Hover sobre el nombre de variable en let X = ... — CERRADO 2026-06-05¶
Fix aplicado: AssignTarget::Ident ahora lleva un Span propio
del token Ident del LHS (src/ast.rs:519). El parser
captura el span via nuevo helper expect_ident_with_span
(src/parser.rs:185). El checker registra el
tipo del binding bajo el span del LHS en TypeInfo
(src/types.rs:8252) — para anotaciones explícitas
usa el tipo declarado, no el inferido. 4 unit tests en
types::tests::v2_* cubren los 4 casos del cap M1.C5 del curso
(nombre/edad/activa/latitud + nullable explícito).
Hover sobre let edad = 200 ahora muestra Int sobre edad
(antes solo aparecía sobre 200).
Cierra parcialmente la deuda S1: el paralelo en Param /
For.var / MatchArm.pattern sigue pendiente — mismo patrón
arquitectural, sub-paso futuro independiente. Update 2026-06-05:
S1 completa cerrada en v0.14.2 — Param.name_span, Pattern::Ident(name, span),
Pattern::OkBinding(name, span), Pattern::ErrBinding(name, span)
todos con span propio + checker registra tipos en TypeInfo bajo
esos spans. Ver entry v0.14.2 del CHANGELOG y nueva sección S1
en este backlog.
S1 (deuda histórica) — Spans propios para Param / Pattern bindings — CERRADO 2026-06-05 (v0.14.2)¶
Hallazgo histórico: la deuda S1 era el paralelo del V2 que cerró
AssignTarget::Ident. Faltaban spans propios en Param.name (params
de fn), Pattern::Ident (var de for + binding genérico de match)
y Pattern::OkBinding/ErrBinding (bindings del unwrap de Result).
Sin esos spans, el checker no podía registrar los tipos en
TypeInfo bajo el lugar correcto y el LSP no mostraba nada al hover
sobre el nombre del param/binding.
Casos cubiertos en v0.14.2:
- Hover sobre
nenfn double(n: Int) => n * 2→Int. - Hover sobre
xenfn f(x) => x + 1→Any(sin anotación). - Hover sobre
ienfor i in 0..10→Int. - Hover sobre
nenmatch x { Ok(n) => n + 1 }→ tipo inner del Result. - Hover sobre
amounten métodos custom (type T { fn m(amount: Int) }) →Int.
Implementación:
- AST:
Paramsumaname_span: Span.Pattern::Ident(String)→Pattern::Ident(String, Span)(idemOkBinding,ErrBinding). - Parser: captura el span via
expect_ident_with_span(helper V2 reusado). - Checker: en
bind_pattern,bind_for_pattern_in_checker, handlers deFnDef/FnExpr/Method, usaname_span/ident_spancomodef_span(con fallback al span del nodo contenedor cuando el span es ZERO en nodos sintéticos de tests) Y registra el tipo enTypeInfobajo ese span.
Tests (5 unit en types::tests::s1_*): param anotado/sin anotar,
var del for sobre range, binding Ok(n), param de método custom.
Side effect: el sed bulk para arreglar los call sites de patrones literales en tests tocó ~40 sites (Pattern::Ident, OkBinding, ErrBinding) + ~22 Param constructors. Cambios mecánicos sin lógica nueva. Total ~1700 LoC de diff, mayoría tests.
Cierra la deuda S1 entera del proyecto — paralelo natural del V2, mismo patrón arquitectural.
(Descripción original — para referencia)¶
Síntoma: en
let nombre = "Patagonia"
let edad = 200
let activa = true
let latitud = -49.32
let datos: Int? = null
el curso dice "pasá el mouse sobre cada variable → ves su tipo inferido". En la práctica:
- Hover sobre
"Patagonia"(el literal) →Str✓. - Hover sobre
200→Int✓. - Hover sobre
nombre/edad/activa/latitud/datos(el nombre de la variable, LHS dellet) → no muestra nada.
El usuario espera que hover sobre cualquier variable muestre su tipo inferido (lo que TypeScript / rust-analyzer hacen). Hoy solo aparece el tipo si pasás el mouse sobre el valor del RHS.
Causa: AssignTarget::Ident(String) en
src/ast.rs:466-468 no tiene Span propio — es
solo el nombre. El checker en Stmt::Assign infiere el tipo del
value (RHS) y lo registra en TypeInfo con el span del valor, pero
no registra el ident del LHS. Resultado: hover_for_position no
encuentra nada cuando el cursor está sobre nombre/edad/etc.
Esta es una manifestación visible de la deuda S1 ya identificada en
el proyecto (AssignTarget::Ident/Param/For.var/MatchArm.pattern
sin span propio, hoy usan el span del stmt contenedor o nada).
Fix propuesto:
- AST:
AssignTarget::Ident(String, Span). Migrar parser para que pase elSpandel token del ident. - Checker: en
Stmt::Assigncontarget = AssignTarget::Ident(name, span), después delet ty = infer_expr(ctx, value), agregarctx.type_info.record(span, ty.clone())para que el LHS también aparezca en TypeInfo. - Mismos cambios paralelos en
Param,For.var,MatchArm.pattern(cierra S1 entera). - Tests E2E sobre hover en LHS de
letpara los 5 casos del curso (nombre/edad/activa/latitud/datos con anotación explícita).
Impacto: alto en DX y especialmente en el curso. La primera
sección del cap ⅔ del curso (Inferencia de tipos via hover)
asume este comportamiento. Hoy hay un drift entre lo que enseña el
curso y lo que el LSP hace.
Side benefit del fix: completion contextual scope-level también puede usar los spans para emitir Range exacto del symbol, y go-to-definition desde otros usos del binding apunta al ident, no al stmt entero.
V3 — Formatting on save (textDocument/formatting) — CERRADO 2026-06-05¶
Fix aplicado: capability document_formatting_provider: true
anunciada en initialize + handler formatting en
src/bin/fitz-lsp.rs que delega a
fitz::fmt::format_source y emite UN TextEdit con el documento
entero reformateado. Sobre doc con error de parser, devuelve null
silencioso — no aborta el save. Helper end_position_utf16 calcula
el range del final del doc en UTF-16 (default LSP).
2 E2E tests nuevos en tests/lsp_e2e.rs::v3_formatting_* validan:
(a) capability anunciada + doc no-formateado emite TextEdit con
código formateado (tabs → 4 espacios), (b) doc roto retorna null
sin error.
VSCode con "editor.formatOnSave": true ahora dispara fitz fmt
automático al guardar — sin necesidad de configurar formatter
externo.
(Descripción original — para referencia)¶
Qué falta: hoy fitz fmt funciona como CLI (Fase 9.z.1) y como
formatter externo si el usuario configura editor.formatOnSave = true
+ "[fitz]": { "editor.defaultFormatter": "..." } apuntando al binario.
Pero el LSP no implementa textDocument/formatting ni
textDocument/rangeFormatting, así que VSCode no lo detecta como
formatter nativo de la extensión.
Síntoma: el usuario instala la extensión Fitz, activa "format on
save" en VSCode, y nada pasa al guardar un .fitz — tiene que correr
fitz fmt a mano en la terminal o configurar el binario externo.
Cómo: el módulo fitz::fmt ya expone una API pura
format_source(source: &str) -> Result<String, FmtError>. Falta:
- Capability
formatting_provider: Some(OneOf::Left(true))eninitializeresponse del bin LSP. - Handler
formatting(&self, params: DocumentFormattingParams)que leestate.textdel documento, llamafitz::fmt::format_source(&state.text), y devuelveVec<TextEdit>con UN solo edit que reemplaza el doc entero (rango(0,0)..(last_line, last_col)→ nuevo texto). Es el patrón estándar para formatters non-incremental. - Manejo de errores: si
fmtfalla (código con error de parser), devolverOk(vec![])silencioso — no abortar el save.
Costo: 1 día. Plumbing puro. 2-3 unit tests del round-trip
(format_source ya tiene su propia suite).
Impacto: alto en DX cotidiana. "format on save" es lo primero que el dev configura al adoptar un lenguaje nuevo. Hoy hay drift entre lo que el ecosistema espera y lo que la extensión ofrece.
V4 — Signature help — MVP CERRADO 2026-06-05 + EXPANDIDO 2026-06-05 (v0.15.0)¶
v0.15.0 — V4 expandido: el MVP cubría solo fns user-defined del programa. v0.15.0 suma:
- Builtins globales: catálogo
BUILTIN_SIGScon 11 builtins comunes (print,len,sleep,env,env_or,load_env,flag,spawn,config,secret,bytes). Tipearlen(abre popup confn len(x: Any) -> Int. - Method calls sobre
List<T>/Map<K,V>/Str: catálogos paralelos (LIST_METHOD_SIGS,MAP_METHOD_SIGS,STR_METHOD_SIGS). Tipearxs.map(conxs = [1, 2, 3]muestra la firma del método. CallContextenum nuevo (FunctionvsMethod) reemplaza el(String, u32)previo. Walkback identifica.antes del(.- Heurística
infer_builtin_receiver_kind: walka elProgrammatcheando el value asignado al receiver por shape estructural.
3 E2E tests nuevos en tests/lsp_e2e.rs::v4_* validan builtins
+ method calls sobre List + Str.
Deuda residual (NO bloquea):
- Receivers no-Ident (
xs[0].method,f().method) no se identifican. - Métodos custom de
type Fooquedan pendientes.
(Descripción original — para referencia)¶
Fix aplicado: capability signature_help_provider con trigger
chars ( y , anunciada en initialize. Handler signature_help
en src/bin/fitz-lsp.rs delega a
signature_help_at_position (src/lsp.rs) que
combina dos helpers nuevos:
find_call_context(text, line, char): walkback heurístico contando(/)y,para encontrar elCallenclosing + index del param actual.signature_help_for_call(program, name, active_param): busca laStmt::FnDeftop-level por nombre y construyeSignatureInformationcon labelfn nombre(p1: T1, p2: T2) -> R+ParameterInformationcon offsets a cada param.
2 E2E tests nuevos en tests/lsp_e2e.rs::v4_* validan:
(a) cursor en add(| con fn add(a: Int, b: Int) -> Int → label
correcta + activeParameter = 0, (b) cursor en add(5, | →
activeParameter = 1.
Limitaciones del MVP (deuda menor):
- Solo cubre fns user-defined del programa. Builtins (
print,len, módulosjwt/hash/etc.) y method calls (xs.map() no muestran signature — los builtins tipan comoType::Anygradual y las signatures de métodos viven eninfer_*_methodpor tipo del receptor. - Walkback no respeta strings ni comments. Caso raro:
f("texto, con coma|")puede contar mal,.
(Descripción original — para referencia)¶
Qué falta: cuando el usuario tipea f( o f(a, el LSP debería
mostrar un popup con la firma de f y resaltar el param actual. Hoy
no hay implementación — el usuario tiene que recordar la firma o
hacer hover sobre la fn (que muestra el tipo pero no la firma posicional).
Síntoma: especialmente molesto en handlers HTTP con varios params
y kwargs (@get("/users/{id}") fn show(id: Int, ...)) y en builtins
con firmas complejas (hash.password(plaintext: Str) -> Str,
jwt.encode(payload: Map<Str, Str>, secret: Str, alg: Str?) -> Str).
El alumno del curso M5 (auth) lo va a sentir.
Cómo:
- Capability
signature_help_provider: Some(SignatureHelpOptions { trigger_characters: Some(vec!["(".into(), ",".into()]), ... }). - Handler
signature_help(&self, params: SignatureHelpParams): - Walkear hacia atrás desde el cursor para encontrar el
Callenclosing — heurística sobrestate.text(contar(no balanceados). - Lookup del callee por nombre en
TypeEnv(fns top-level / builtins). Si es method call (xs.map(), resolver el método sobre el tipo del receptor — elTypeInfoya tiene el tipo. - Construir
SignatureInformationconlabel = "fn nombre(p1: T1, p2: T2) -> R",parameters = [ParameterInformation per param],active_parameter= count de,entre el(y el cursor. - Reusar el helper
Type::displaypara renderear cada tipo.
Costo: 2 días. Lo más complejo es el walkback heurístico (parser
parcial es overkill para MVP). El catálogo de signatures ya está
todo en TypeEnv + builtins pre-registrados.
Impacto: alto en código que llama a fns con muchos params (auth, DB queries con kwargs, OpenAPI handlers). Bonus pedagógico — el alumno descubre la API de los builtins sin abrir la guía.
V5 — Autocomplete tras from X import — YA ESTABA CERRADO en v0.9.47 (2026-06-05 audit)¶
Hallazgo del audit pre-implementación (2026-06-05): al arrancar el Bloque 1 de fixes, descubrimos que esta feature ya estaba implementada desde v0.9.47. El backlog estaba con drift — esta entrada quedó como pendiente por error, sin auditar el código actual.
Verificación: src/lsp.rs ya tiene:
CompletionContext::FromImportList { mod_path }(src/lsp.rs:652)detect_from_import_list_context(src/lsp.rs:863)from_import_completions(doc_uri, mod_path)(src/lsp.rs:540)
Y el handler completion del bin (src/bin/fitz-lsp.rs:312-336) ya
llama a completion_at_position_with_uri pasando el doc_uri, lo que
permite resolver el archivo del módulo target y enumerar sus exports.
Acción: marcar como CERRADO. La entrada se mantiene para registro histórico del audit.
(Descripción original — para referencia)¶
Qué falta: al tipear from mod import | (cursor después de import),
el LSP debería sugerir los símbolos pub exportados por mod. Hoy
no hay nada — la lista está vacía y el usuario tiene que abrir el
archivo del módulo para saber qué exportar.
Síntoma: gap visible en los caps de módulos del curso (M3+) y en
boilerplates multi-archivo. El usuario tipea from models import |
y necesita memoria fotográfica de qué types/fns/consts hay en models.fitz.
Cómo:
- Detección del contexto en
detect_completion_context: walk hacia atrás desde el cursor sobre la línea actual, matchear patternfrom <ident> import [<ident>(,)]*<cursor>. Nueva variante del enumCompletionContext::AfterFromImport { module: String }. - Resolver el módulo: reusar
ModuleLoader::load(module, base_dir)que ya carga + cachea. Si el módulo falla parsing parcial OK (resolución best-effort). - Enumerar exports: walker sobre el
Programdel módulo cargado, coleccionarStmt::FnDef/Stmt::TypeDef/Stmt::Assigntop-level con visibilitypub(que en Fitz es implícito — todo top-level es exportable). Filtrar los ya importados de la lista actual delfrom ... import a, b, |para no sugerir duplicados. - Emitir
CompletionItemcon kind apropiado (Function,Classpara types,Constantparalettop-level con RHS literal) + detail con la firma corta.
Costo: 1 semana. Lo más caro: parser parcial robusto del from X import a, b, | mientras está en construcción (línea sintácticamente
inválida). Alternativa pragmática: regex sobre la línea para extraer
module_name + lista de imports ya escritos. Cubre 95% del caso real.
Impacto: cierra el gap más visible del autocomplete. Hoy after-dot
funciona, scope-level funciona, pero from X import | no sugiere
nada — inconsistencia que el alumno nota inmediatamente.
Deuda residual derivada: cuando llegue cross-module go-to-def completo, este completion puede reusar la misma infra de resolución.
Fixes pendientes del lenguaje (descubiertos en el curso) — ABIERTO 2026-06-05¶
Backlog hermano del de LSP. Acá van bugs y features del lenguaje
(lexer / parser / evaluator / codegen) que aparecen mientras el autor
sigue el curso Fitz de 0 a experto. Distinción con el backlog del
LSP: estos requieren cambio del compilador, no solo de la extensión.
V6 — Debugging interactivo en VSCode (Debug Adapter Protocol) — ABIERTO 2026-06-05¶
Qué falta: Fitz hoy tiene LSP (diagnostics + hover + goto-def + completion + signature help + format on save) pero no tiene debugging interactivo. El alumno no puede:
- Clickear el gutter para poner breakpoints.
- Pausar la ejecución y inspeccionar variables.
- Step in / over / out.
- Watch expressions.
- Ver el call stack en el panel de Debug.
Workarounds disponibles hoy:
| Técnica | Cómo |
|---|---|
print con interpolación |
print("x = {x}, n = {n}") |
| REPL interactivo | fitz repl + :load src/main.fitz + llamar fns con valores reales |
:type <expr> |
Inspeccionar tipos sin ejecutar |
:env |
Ver todos los bindings del scope actual |
| Diagnostics LSP | El checker estático captura type mismatches antes de correr |
Fix propuesto (~2 semanas):
- Bin
fitz-dapnuevo (paralelo afitz-lsp, feature gateddap). Implementa el protocolo DAP de Microsoft (JSON-RPC similar al LSP pero con shape distinto — request/response/event). - Evaluator instrumentado con:
- Tabla de breakpoints por archivo + línea.
- Hook al entrar a cada Stmt: check breakpoint, si está activo
pausar (mecanismo via
tokio::sync::Notify+ state machine). - Step in/over/out: contar profundidad de fn calls + comparar.
- Inspect: serializar
Valueactual de cada var del scope a JSON. - Extensión VSCode suma:
debuggersentry enpackage.json(lenguaje fitz, programafitz, tipofitz).- Template
launch.jsoncon configuración base (type: fitz,program: ${workspaceFolder}/src/main.fitz). LiveBuildpara spawnearfitz-dapal arrancar debug session.- Tests: E2E que simula un cliente DAP, pone breakpoint, ejecuta programa, valida que paramos en el breakpoint + variables expuestas correctas. Similar a los E2E del LSP pero sobre DAP.
- Cap nuevo del curso M7 (Producción): "Debugging en VSCode con breakpoints + watch expressions". Cierra el gap pedagógico vs Python/JS donde debugging es ciudadano primera.
Decisiones técnicas pendientes:
- (a) ¿
fitz buildtambién soporta debug info? Sería un breaking change del codegen (cargo build --debug→ simbolos), o un flag opt-in (fitz build --debug). Sin esto, debugging solo funciona confitz run(intérprete). - (b) ¿Hot reload integrado con debugging? VSCode soporta restart de
debug session — combinar con
fitz devsería poderoso pero invasivo. - © ¿Conditional breakpoints? El alumno los va a esperar
(
break if x > 10). Requiere evaluar expresiones Fitz en el contexto del breakpoint — interpretable pero adds complejidad.
Impacto: alto en DX. Es el gap más grande vs Python/JS para proyectos no triviales. Pedagógicamente es el cierre natural del módulo de producción del curso.
Riesgo: el evaluator hoy es #[async_recursion] sobre eval_expr
/ eval_stmt. Instrumentar con breakpoints sin meter overhead en el
hot path (cuando NO hay debug session activa) requiere thoughtful
design — atomic bool global + branchless skip, o feature flag
condicional.
L1 — ; como separador de stmts — CERRADO 2026-06-05¶
Fix aplicado: el lexer ahora reconoce ; como token y lo emite
como Token::Newline (src/lexer.rs:1188) —
cero cambios al parser/AST. El parser ya tolera Newlines repetidos
como separator único, así que ;\n no duplica stmts. 5 unit tests
en lexer::tests::l1_* cubren el caso solo, dos exprs con ;,
;\n real, ; adentro de strings (preservado literal), ;
adentro de comentarios (consumido como parte del comment).
Validación end-to-end:
fitz runsobre archivo conlet x = 5; let y = 10funciona.- REPL:
1 + 1; 2 + 2→= 4(solo el último valor imprime — exactamente el comportamiento que prometía el cap M1.C5 del curso).
Side effect del fix por diseño: el ; no se preserva en el AST.
El formatter fitz fmt reescribe 1 + 1; 2 + 2 como dos líneas
separadas, lo que es exactamente la convención canónica de Fitz.
Acción derivada: el cap M1.C5 del curso restauró la sección
"Múltiples expresiones por línea" (que se había sacado como fix
temporal). La guía también revirtió el call-out "Fitz no tiene punto
y coma" — ahora dice "el ; es separator opcional entre stmts —
newline lo cubre en casi todos los casos", alineado con la decisión
de diseño #5 ("punto y coma opcional, como en Go").
(Descripción original — para referencia)¶
Qué falta: hoy el lexer NO reconoce ; como token válido. Cualquier
intento de escribir 1 + 1; 2 + 2 aborta con Carácter inesperado: ';'.
La decisión de diseño #5 del proyecto (CLAUDE.md) dice "Punto y coma
opcional — como en Go, el parser maneja ambigüedades", pero la
implementación real es: solo newlines separan stmts, no hay soporte
de ; para nada.
Síntoma: especialmente molesto en el REPL donde el alumno quiere
encadenar varias expresiones cortas en una sola línea (let x = 5; x * 2,
1 + 1; 2 + 2). En .fitz files también es útil para one-liners
densos (debugging, scripts cortos).
Caso reportado por el alumno del curso (2026-06-05):
Fix mientras tanto (aplicado 2026-06-05): el cap M1.C5 del curso
y la guía se corrigieron para no mencionar ;. El alumno ya no choca
con drift entre docs y realidad.
Fix propuesto (~2-3 horas):
- Lexer: agregar branch para
';'que emitaToken::Semicolon(variante nueva del enumToken). El span lleva línea/col del;. - Parser: en los puntos donde hoy se acepta
Token::Newlinecomo terminador de stmt (loop principal del programa, body de fn, body de bloques), aceptar tambiénToken::Semicoloncon misma semántica. Sin cambios al AST — el;es solo terminator, no se preserva. - Tests: unit del lexer (
;produce token, span correcto), unit del parser (let x = 5; let y = 10produce 2 Stmt::Assign,1 + 1; 2 + 2en bloque produce 2 Stmt::Expr), E2E en REPL (1 + 1; 2 + 2imprime solo= 4), E2E compile (fitz runde archivo con;corre OK). - Decisión semántica del REPL: si la línea termina con
;después de la última expresión (1 + 1;), ¿imprime el valor o no? Recomendación: sí imprimir — el;es solo separator opcional, no marca "descartar". - Restaurar la sección "Múltiples expresiones por línea" en
docs/curso/m1-setup/c5-repl.mdcuando esto cierre. - Refinar la guía (
docs/guide.mdlínea ~3531): hoy dice "Fitz no tiene punto y coma". Cuando cierre, ajustar a "El;es separator opcional entre stmts — newline lo cubre en casi todos los casos".
Costo: 2-3 horas. Cambio chico pero invasivo en el parser (afecta
todos los call sites de expect_newline_or_eof). Tests E2E son
decisivos para validar que no se rompa nada existente.
Impacto: medio. La mayoría del código Fitz nunca usa ; (newlines
alcanzan), pero destrabar el REPL para chains de expresiones cortas
es alto valor pedagógico y ergonómico. Cierra el drift histórico con
la decisión de diseño #5.
Side benefit: alinea la realidad con la frase "como en Go" del documento de decisiones. Hoy esa frase es aspiracional, no descriptiva.
L2 — Inferencia bidireccional — MVP CERRADO 2026-06-05 + EXPANDIDO 2026-06-05 (v0.15.0)¶
v0.15.0 — L2 expandido: el MVP solo cubría callbacks de métodos
built-in (List<T>.map/filter/find/...). v0.15.0 suma:
- Fn user-defined con param
Fn(...) -> ...:
fn apply(f: Fn(Int) -> Int, x: Int) -> Int { return f(x) }
apply(fn(n) => n * 2, 5) // n: Int sin anotar
Implementado en Expr::Call cuando callee_ty es
Type::Function { params, .. } — propaga los params como hint
al arg correspondiente si es FnExpr.
let f: Fn(...) -> ... = fn(...) => ...:
Implementado en Stmt::Assign cuando hay anotación + RHS es FnExpr.
Resuelve la anotación a Type, si es Function extrae los params
y los empuja al hint stack ANTES de sintetizar el RHS.
3 unit tests nuevos en types::tests::l2x_*.
Reusa el mismo mecanismo ctx.fn_expr_param_hints introducido en
v0.14.1 — sin cambios al AST.
Deuda residual (NO bloquea): inferencia bidireccional para método custom con param Fn no se dispara (caso raro hasta que tipos custom expongan métodos higher-order canónicamente).
(Descripción original — para referencia)¶
Fix aplicado (alcance acotado al caso 90%): el checker propaga
el T del receptor a los params SIN anotación del callback cuando el
método es uno de los built-in con template paramétrico conocido sobre
List<T> (.map/.filter/.find/.any/.all/.count/.find_index/
.flat_map).
Implementación:
- Nuevo helper
expected_callback_param_for_builtin_method(obj_ty, method) -> Option<Vec<Type>>(src/types.rs). ParaList<T>+ cualquiera de los métodos cubiertos, devuelveSome(vec![T]). Otros casos (Map higher-order, Str, custom Nominal) →None(no rompe la lógica existente). CheckCtxgana stackfn_expr_param_hints: Vec<Option<Vec<Type>>>para soportar nested callbacks sin contaminación. El call site del método empuja el hint ANTES de sintetizar el arg si es un FnExpr directo.- Handler de
Expr::FnExpr(src/types.rs) consume el top del stack al entrar. Para cada param: si tiene anotación explícita, la anotación gana; si no, usa el hint en vez deType::Any.
Tests (6 unit en types::tests::l2_*):
[1, 2, 3].map(fn(x) => x * 10)→List<Int>✓ (caso pedagógico).[1, 2, 3].filter(fn(x) => x > 0)→List<Int>conBoolvalidado.["a", "b"].map(fn(s) => s.upper())→List<Str>.- Param con anotación explícita (
fn(x: Float) => x * 2.0sobreList<Int>) →List<Float>(anotación gana). .find(fn(x) => x == 2)→Result<Int>.- Nested callbacks (
xs.map(fn(x) => [x].map(fn(y) => y * 2))) → cada uno recibe su propio hint sin contaminación.
Acción derivada: el cap M1.C5 del curso restauró el ejemplo
original :type [1, 2, 3].map(fn(x) => x * 10) → :: List<Int> y
removió la nota sobre la limitación (que ya no aplica). El cap suma
una explicación corta sobre la inferencia bidireccional y cuándo
gana la anotación explícita.
Deuda residual derivada de L2 (NO bloquea uso real):
- Map
higher-order : hoyinfer_map_methodno expone callbacks (solo get/has/keys/values/len). Cuando llegue, agregar el caso al helper devolviendoSome(vec![K, V]). - Inferencia bidireccional GENERAL: el alcance del fix es
callbacks de métodos built-in conocidos. Casos no cubiertos —
ej. fn user-defined con param
Function, FnExpr asignada a var con anotación de Function — siguen sintetizando params sin anotación comoAny. Refactor invasivo del checker (1-2 semanas) si entra demanda real. flat_mapcon ret type del callback:flat_mapexigefn(T) -> List<U>y luego sintetiza U del ret. El hint actual propaga solo T (el param). Validar si el alumno escribe.flat_map(fn(x) => [x, x+1])y necesita inferencia del U también. En la práctica el U se sintetiza desde el body sin hint adicional.
(Descripción original — para referencia)¶
Qué falta: hoy el checker hace inferencia solo bottom-up (synthesis). Cuando el alumno escribe:
el FnExpr fn(x) => x * 10 se sintetiza primero sin contexto del
receptor: x queda como Any (sin anotación, no hay propagación),
x * 10 con BinOp(Any, Int) tipa como Any, ret = Any. Después
.map ve el callback Function { params: [Any], ret: Any } e instancia
U = Any. Resultado: List<Any>.
Síntoma reportado por el alumno (2026-06-05):
El curso M1.C5 prometía :: List<Int> (drift entre lo enseñado y
lo real). El runtime SÍ calcula x * 10 correctamente (la evaluación
es dinámica), pero el checker estático no puede saber que x es Int
sin que el alumno lo anote.
Fix mientras tanto (aplicado 2026-06-05): el cap M1.C5 del curso
se corrigió para usar fn(x: Int) => x * 10 con anotación explícita.
Una nota corta debajo del ejemplo cita esta deuda. Idem en el segundo
ejemplo del cap (línea 650+).
Fix propuesto (~1-2 semanas):
- Two-pass para method calls con callback: cuando
Expr::Calltiene calleeExpr::Fieldy el método resuelto pertenece a la tabla built-in con signature paramétrica (List<T>.map(fn(T) -> U)), PRIMERO resolver T del receptor, DESPUÉS propagar T a los params sin anotación del FnExpr arg. - Modificar
infer_list_method/infer_map_methodentypes.rs: en vez de sintetizar el callback con su contexto vacío, recibir unexpected_param_types: Vec<Type>derivado de la signature del método y pasarlo al wrapper desynthesize_fn_expr. - Wrapper de
Expr::FnExpr: aceptar opcionalexpected_typesy, para cada param sin anotación, usar el expected como tipo del binding en el scope del body. Ellubdel ret también puede beneficiarse pero NO es necesario para el MVP. - Tests:
xs.map(fn(x) => x * 10)sobreList<Int>tipaList<Int>; sobreList<Str>tipaList<Str>cuando el body es válido;xs.filter(fn(x) => x > 0)sobreList<Int>tipaList<Int>conret = Boolvalidado; param CON anotación incompatible con T del receptor sigue siendo error (no se sobreescribe silenciosamente). - Restaurar el ejemplo del curso (
fn(x) => x * 10sin anotación) y quitar la nota cuando esto cierre.
Costo: 1-2 semanas. No es trivial — toca el orden de visita del checker (synthesis vs checking modes) y abre la pregunta de si querés extender bidireccional a otros casos (anonymous fn como var, fn como param de fn user, etc.). Recomendable acotar el MVP a callbacks de métodos built-in con templates conocidos.
Impacto: medio-alto pedagógico. Cierra el case más común donde
el alumno espera "Fitz infiera como TS/Rust hacen" y se choca con
Any. Cubre patrón canónico .map/.filter/.find que aparece en
todos los caps de Listas/Loops/Higher-order.
Riesgo: cambio invasivo del checker. Tests E2E sobre todos los ejemplos de la guía + cursos antes/después son decisivos.
L3 — :load con paths absolutos estilo Unix no funciona en Windows (2026-06-05)¶
Qué pasa: hoy en el REPL, :load /tmp/helpers.fitz resuelve a
D:/tmp/helpers.fitz (o C:/tmp/... según el drive del cwd) en
Windows. El path absoluto estilo Unix se reinterpreta contra el drive
actual y casi nunca existe.
Caso reportado por el alumno (2026-06-05):
fitz> :load /tmp/helpers.fitz
✗ no se pudo leer `D:/tmp/helpers.fitz`: ... (os error 2)
fitz> :load /src/helpers.fitz
✗ no se pudo leer `D:/src/helpers.fitz`: ... (os error 3)
fitz> :load src/helpers.fitz
✓ cargado D:\CURSO_FITTZ\micosa\src/helpers.fitz
Causa: std::fs::read_to_string en Windows interpreta /tmp/...
como path relativo al drive del cwd. Es comportamiento estándar de
la API de Rust + Windows, no un bug propio de Fitz. Pero la UX del
curso asume Unix.
Fix mientras tanto (aplicado 2026-06-05): el cap M1.C5 Paso 8 se
cambió para usar :load src/helpers.fitz (relativo al cwd del REPL)
que funciona idéntico en Linux/macOS/Windows. Se sumó un "Tip cross-OS"
explicando por qué evitar paths absolutos Unix en los ejemplos.
Opciones de fix permanente:
- No hacer nada (mantener el fix de docs): el
:loadcon paths relativos es la convención portable y ya está documentada. Paths absolutos siguen siendo válidos para usuarios power que saben qué están haciendo. Recomendado. - Detectar
/path/...en Windows y emitir warning: si el primer char es/y estamos en Windows, sugerirPara paths absolutos en Windows usá D:/...; para portabilidad usá paths relativos. Costo muy chico (5 LoC en el handler de:load). - Mapear
/tmp/→%TEMP%en Windows como conveniencia: NO recomendado, demasiado magic, rompe expectativas en otros paths Unix-style.
Recomendación: opción 1 (cerrar como "by design + docs corregidas"). Si llega demanda real, opción 2.
Impacto: bajo desde que las docs están corregidas. Pre-fix, el alumno Windows se chocaba inmediatamente en el Paso 8 del cap M1.C5.
L4 — Strings con delimitador '...' no existen (curso prometía) (2026-06-05)¶
Qué pasa: el cap M2.C1 del curso documentaba que Fitz soporta
strings con dos delimitadores ("..." y '...'), con escapes
paralelos (\" para uno y \' para el otro). En la realidad, solo
"..." es string en Fitz. El ' se reserva para labels en
break/continue de loops anidados ('outer: loop { break 'outer }).
Caso reportado por el alumno (2026-06-05):
El lexer (src/lexer.rs:1229-1252) ve '
y arranca un Token::Label(name) — necesita un ident detrás, lo que
falla con cualquier escape o char no-ident.
Fix mientras tanto (aplicado 2026-06-05): cap M2.C1 corregido —
eliminada la fila de la tabla de escapes (\'), eliminada la línea
del demo (print('\'comillas\'')) y la línea del output ('comillas'),
y agregado un call-out explícito "Fitz usa solo "..." como
delimitador de strings. El char ' se reserva para labels de
break/continue en loops anidados". La guía y syntax-spec.md ya
estaban consistentes — el drift estaba solo en el cap del curso.
Opciones de fix permanente:
- No hacer nada (cerrar como by design): mantener
"..."como único delim,'reservado para labels. Convención clara, sin ambigüedades. Curso ya está corregido. Recomendado. - Soportar
'...'como string alternativo con desambiguación: el lexer al ver'mira el char siguiente — si es alfa válido para ident, es label; si no, es string. Problema:'a'sería ambiguo (label'avs string'a'). Costo medio + UX confusa. No recomendado. '...'como char literal (Rust-style):'a'sería un char de 1 byte. No encaja con el modelo de Fitz (no hay tipoCharseparado deStr). Requiere agregar el tipo entero. Gran feature, fuera de scope.
Recomendación: opción 1. Cerrar como "by design". El curso corregido refleja la realidad. El alumno que viene de Python/JS necesita el call-out explícito para no asumir simetría.
Impacto: bajo desde que el cap está corregido. Pre-fix, el alumno del curso M2.C1 se chocaba al copiar el demo de escapes.
Side note — bug colateral del cap: el demo del cap M2.C1 incluía
print("emoji: 🏔 🌍") y print("CJK: 名前は何ですか?") antes de
los strings con '. En la captura del alumno, el fitz run solo
imprime las primeras dos líneas válidas (cirílico y matemático)
y aborta en la línea 10 sin llegar a los emojis ni CJK. El runtime
SÍ soporta UTF-8 multibyte; lo que aborta es el parser por el '.
Una vez aplicado el fix, todos los print(...) van a correr OK.