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.
🟢 Phase 11.7 R3.5 + Frente 2 — CERRADAS 2026-07-19 (v0.22.0) + deuda residual nueva¶
Cierre: Phase 11.7 entera cerró en v0.22.0 para el target
client-WASM de .fitzv. R3.5 (a.1 lowerer de listas + a.2 fns
importadas transpiladas + b.1 click payload + b.2 form payload + c
kanban SPA) destrabó el port del kanban a una SPA WebAssembly
standalone. Frente 2 (11.7.c event bubbling child→parent + 11.7.d
<slot> con fallback) completó la composición <Child />. Las
K-debts previas relevantes al WASM (K-1 event bubbling, interpolated
props, imported-fn support) quedan cubiertas por este bloque para el
target WASM (el SSR ya las tenía o las rechaza con puntero claro).
Deuda residual nueva derivada (NO bloquea uso real; abre items para slices posteriores según demanda):
- Payload bubbling — 🟢 CERRADO 2026-07-19 (v0.23.0). El bubble
de 11.7.c ahora lleva payload. El callback slot pasó de
Box<dyn Fn()>aBox<dyn Fn(&HashMap<String, String>)>; un event que burbujea reenvía hacia arriba el payload que recibió (el mismodata-flv-value-*de los click/form handlers de R3.5b), y el handler del parent lo lee conpayload["k"]/payload.has("k"). Diseño: sin sintaxis nueva — el child elige qué exponer con sus atributosdata-flv-value-*; un handler bubbleado siempre toma el parampayload(para poder reenviarlo) aunque su body no lo lea; el parent recibe el payload solo si su handler lo consume. Cambio contenido ensrc/view/codegen_wasm.rs(WASM-only; el SSR ya rechaza@eventen child). Componentes no-bubbled emiten byte-a-byte idéntico. Ejemploexamples/view/event-bubblingactualizado (tres<Item @choose="on_pick" />que burbujean sulabel). Deuda residual derivada: el payload esMap<Str, Str>(los números/bools llegan como strings, se parsean del lado del parent); un payload tipado o el state completo del child sería un slice posterior si aparece demanda. - Named slots — 🟢 CERRADO 2026-07-19 (v0.24.0).
<slot name="X" />+ múltiples slots por child en el target WASM. El child gana un field__slot_<name>por cada<slot name="X" />(el__slotdefault queda idéntico → byte-a-byte para componentes default-only); el parent llena cada región tagueando un elemento top-level de<Child>... </Child>conslot="<name>"(convención nativa de Web Components), y el contenido sinslot=va al slot default. El emitter particiona el slot-content por atributoslot=, sintetiza un__render_slot_<n>por bucket (renderizado en scope del PARENT → reactivo) y cablea el field correspondiente; el atributoslot=se strippea del DOM emitido. Validación en emit:slot="X"sin<slot name="X" />en el child, o contenido unslotted sin slot default, o colisión de field (side-barvsside_bar→ ambos__slot_side_bar) → error con puntero claro. Cambio contenido ensrc/view/codegen_wasm.rs(WASM-only; el SSR sigue rechazando TODOS los slots). Ejemploexamples/view/named-slots(childCardcon title/default/actions + fallbacks; compila a WASM real 35.2 KB). Deuda residual derivada:<Child />anidado en slot content (misma que 11.7.d abajo); named slots dinámicos dentro de un{#for}no se contemplan (los slots son sitios estáticos). <Child />anidado en slot content — el método__render_slot_<n>del parent no tiene child-instance cache para un componente anidado; rechaza con mensaje claro. Requiere threading de child-site indices al render de slot content.- Helper fns importadas con constructos no soportados — R3.5a.2
emite TODAS las
fnde un módulo sibling importado; una que usematch/loops/Result/?rechaza en emit aunque no se use. Fix: emitir solo las alcanzables desde los nombres importados (closure transitiva del call graph). - Interactive slot content re-listeners — el contenido de slot se re-renderiza en cada render (listeners frescos), no se clona; OK para el modelo naive-render pero un refinamiento a signals evitaría el re-work.
- Cross-file
<Child />composition — 🟢 CERRADO 2026-07-19 (v0.25.0). Un<Child />puede vivir en un.fitzvSEPARADO, importado confrom Card import Card. Nuevoload_imported_components(src/view/wasm_build.rs) lee el sibling.fitzv(parse → expand) y registra sus components en unImportedComponentRegistry(src/view/codegen_wasm.rs, paralelo aNominalRegistry/ImportedFnRegistry). El emitter mergea la clausura reachable de components importados adelante de los locales en UN synthetic file (merge_imported_components) → cada pass existente (bubbled events, emit per-component, resolución de child) los trata como same-file, así el child importado se inlinea entero (struct +new+ handlers + render <style scoped>) y toda la superficie cruza el borde de archivo (props ↓, event bubbling ↑, slots default + named con fallback). El checker (check_with_imported_components,src/view/check.rs) valida la composición contra la surface real del child importado. CLI cablea enbuild_wasm_client_cmd(src/main.rs). Registry vacío → merge es un clone estructural → same-file byte-a-byte (los 8 ejemplos view previos regeneran sin cambios). Ejemploexamples/view/cross-file-child(App.fitzvimportaCard.fitzvcon prop +@likebubble + named/ default slots; compila a WASM real 36.2 KB). Deudas residuales derivadas — 🟢 las tres CERRADAS 2026-07-20 (v0.26.0):- Transitividad — 🟢 CERRADO (v0.26.0). Nuevo
collect_transitive_view_imports(src/view/wasm_build.rs) recorre el grafo de imports.fitzv(cycle-safe, un archivo por paso) y arma la unión transitiva que los 3 loaders consumen — un component / nominal / helperfnque vive en un archivo que el entry no importa directo se descubre igual. El CLI (build_wasm_client_cmd) computa la unión antes de los loaders. - Aliasing de components — 🟢 CERRADO (v0.26.0).
load_imported_componentsregistra un clon renombrado bajo el alias (from Card import Card as Row→<Row />resuelve), manteniendo el nombre original para composición interna de siblings. Solo se emiten los components reachable desde el parent → el original sin usar no se duplica al lado del alias. - LSP cross-file — 🟢 CERRADO (v0.26.0). Nuevo
lsp::check_view_source_with_base_dir: el bin deriva elbase_dirdel URI del documento y carga los sibling components importados (sobre la unión transitiva, honrando aliases) antes decheck_with_imported_components— paralelo estructural al pre-scan cross-module del path clásico.fitz(v0.19.3). Sin file context, fallback a single-file. Ejemplo end-to-endexamples/view/cross-file-transitive(alias + transitivo, WASM real ~35 KB). - Residual restante: local gana ante importado del mismo nombre; sin manejo de colisión de aliasing (importar el MISMO component dos veces con dos aliases aplica solo el primero); resolución contra un único dir plano.
🟡 Framework support gaps — surface durante fitz-liveviews kanban migration (Phase 8.5) — ABIERTAS 2026-07-16 (Phase 11.7+ scope)¶
Trigger: migración de kanban a .fitzv SFC (Phase 8.5 en
fitz-liveviews). Post-cierre de las 6 view pipeline gaps (§9.cc +
§9.dd + §9.ee), la migración pudo extraer el card_editor
@live_component a CardEditor.fitzv y los shared types a
card.fitz. Full Board.fitzv migration DIFERIDA porque necesita
framework features que hoy no existen. Documentado aquí para
priorización futura — cada uno DESBLOQUEA nuevos idioms + patterns
en fitz-liveviews.
K-1 — Event bubbling entre componentes¶
Síntoma: cuando CardEditor.save fires con payload["text"],
el Board component debería recibir el evento también (para
actualizar board.cards[i].title). Hoy dispatch_component_events
(frame) routes al UNIQUE component matching el component_name
del frame — no hay propagación upward al parent component.
Workaround actual (kanban main.fitz): post-dispatch manual
en la parent WS handler:
let handled = dispatch_component_events(frame)
if (handled && frame.event == "save") {
if (frame.payload.has("instance_id")) { ... update board.cards ... }
}
let board
top-level, NO como state de Board.fitzv SFC. Rompe la
composability.
Diseño futuro (candidate):
- (a) Explicit dispatch_to(parent_component_name, event,
payload) API — user code calls it inside sub-component's
event handler. Explicit, no magic. ~50 LoC framework.
- (b) Convention: @bubbles("event_name") attr on sub-component's
event decorator — framework auto-dispatches to parent when
event fires. React-style. ~100 LoC framework + @bubbles
decorator support in Fitz core.
- © Full DOM-style event bubbling model — every event
propagates through the component tree unless stopPropagation()
called. Vue-style. ~200 LoC + potentially conflicts with the
current dispatch model. Overkill for MVP.
Recomendación: (a) primero (explicit dispatch_to), (b) si
demand aparece.
K-2 — Cross-component state read/write API¶
Síntoma: parent handler quiere leer Board.cards[i] (Board
componenet's state) para propagar CardEditor's save. Hoy component
state es opaque — el framework maneja el store internamente, no
hay API para read/write from outside.
Workaround actual: board-level state vive en let board top-
level (Arc
Diseño futuro (candidate):
- component_state("name", "id") -> T — read component's
current state (returns snapshot).
- set_component_state("name", "id", new_state: T) — write
new state (framework triggers re-render).
Ambas APIs son necesarias para hydration / test fixtures / imperative updates. ~50 LoC each framework.
Recomendación: shipping juntas como PAIR (component_state
+ set_component_state) para simetría API.
🟢 K-3 — Component props para compound / nominal / interpolated types en <Child /> — CERRADO 2026-07-16 (SSR path)¶
Síntoma original: <Board initial-cards="{seedCards}" /> no
funcionaba end-to-end. Fitz core Phase 11.5.d shipped <Child
prop="v" /> composition SOLO con primitivos (Str/Int/Float/
Bool/Nullable<T> de primitivo). §9.dd extendió STATE
annotations para aceptar cross-file nominals via from X import Y.
Pero el path del child PROP coercion sólo aceptaba primitivos, y
<Card prop="{expr}" /> (interpolación dinámica) abortaba en
expand time con "dynamic prop — deferred to Phase 11.6+".
Cierre K-3 (post-v0.21.0, 2026-07-16) — dos sub-releases coordinados que juntos cierran el gap enteramente para el SSR path:
- K-3 List
(~130 LoC + 21 tests) — comma-separated static props. <Child tags="a,b,c" />contags: List<Str>coerciona avec!["a".to_string(), ...]en Rust literal (checker -
WASM emitter) y a
["a", "b", "c"]en Fitz literal (SSR emitter). Empty string →vec![]/[]. Whitespace around commas trimmed. Nested primitives (List<Nullable<Int>>, etc.) recurse. Both emitter paths share acceptance semantics viacheck::coerce_child_prop_raw_value(Rust) + parallelcodegen_ssr::coerce_child_prop_raw_value_to_fitz_literal(Fitz). WASM state fields conList<T>gainVec<Rust>type +vec![...]default viacodegen_wasm::type_expr_to_rust+default_expr_to_rust. -
K-3 Interpolated props (SSR) (~120 LoC + 5 tests) —
<Child prop="{expr}" />inlina la expresión parseada (viaparse_expr_aten expand time) en el struct literal de la composición, aplicando el mismo state-field rewriting rule del SSR emitter para templates (bare_identreferring to parent state field →state.<field>).ChildComponentPropgainsexpr: Option<fast::Expr>field;is_interpolated()helper discriminates.format_child_compositiondispatches on it — static path uses coerce helper, interpolated path usesformat_fitz_expr_scopedcon elstate_field_names+local_scopedel PARENT (closure-parameter locals de{#for x in xs}alrededor del<Child />shadow correctly). El WASM path rechaza interpolación con mensaje claro citando Phase 11.7+ (reactive propagation from parent state to mounted child needs child-lifecycle hooks). Expresiones ricas funcionan naturalmente vía la scoping helper ({n + 1},{title}, method calls, etc.). Nominal types funcionan via interpolation —<Card user="{seed}" />conseed: Useren parent state emiteuser: state.seeden el struct literal.
Con esto Board.fitzv migration queda destrabada para el SSR target (fitz-liveviews). El pattern canónico:
component App {
state { cards: List<Card> = [] }
<template>
<Board initial-cards="{cards}" />
</template>
}
Deudas residuales derivadas (NO bloquean Board):
- Client-WASM path para interpolated props — el WASM emitter
hoy rechaza con mensaje claro pointing at Phase 11.7+. La
razón real es plumbing de reactivity: cuando el parent muta
state.cards, el<Board />mounted no se re-renderiza sin child-lifecycle hooks + reactive-prop watchers. Refinable cuando aparezca demanda cliente-side. Map<K, V>static props — sintaxis<Child meta="k=v,x=y" />o similar; requiere decisión sobre key/value separator syntax. Workaround: interpolación (meta="{someMap}") cubre el caso.- Nominal-type STATIC props — hoy solo interpolados. Si el
user quiere un literal inline (
<Card user='User { id: 1 }' />) necesita interpolación o extraer alet x = User { ... }+<Card user="{x}" />. El pattern con interpolación es más natural y cubre el 100% del caso. - Checker type-check del expr interpolado vs field type —
actualmente skippeado (trust runtime). Si false negatives
aparecen en la práctica, refinable extendiendo el checker
para type-check el
Exprcontrafield.type_expr.
🟢 K-4 — SSR emitter acepta imported top-level fn refs en templates + event bodies — CERRADO 2026-07-16¶
Síntoma: al arrancar la Board.fitzv migration post-K-3, el
SSR emitter rechazaba {cards_in(cards, "todo").len()} dentro
del template Y cards.map(fn(c) => move_one(target, c)) dentro
del event body — el walker format_fitz_expr_scoped sólo
aceptaba idents que fueran state fields o closure params, y
rechazaba cualquier free-var con "free-var references need the
module loader's scope resolution — deferred to Phase 11.7+".
Esto forzaba a mover helper fns adentro del .fitzv (el parser
no las acepta ahí — top-level fns sólo viven en .fitz
classic), o a inlinear la lógica repetidamente en cada event
body / template site (verbose, difícil de mantener). Bloqueaba
un patrón MUY natural del framework: helpers en .fitz
importados con from helpers import move_one, cards_in y
llamados desde el SFC.
Cierre K-4 (post-K-3, 2026-07-16, ~200 LoC + 4 tests):
format_fitz_expr_scoped gana un nuevo param imported_names:
&[&str]. El resolution order para bare Idents queda:
local_scope— closure params ({#for x in xs}inside the template,fn(c) => ...inside event bodies) shadow todo. Emit verbatim.state_field_names— bare state field ref. Rewrite astate.<name>.imported_names— top-level fn / type / const brought into scope viafrom X import Yat the top del.fitzvfile (§9.dd). Emit verbatim; el classic checker running sobre el emitted module valida the reference contra su import table.- Otherwise — hard error con mensaje que menciona la imports table como fix hint. Real free-var (module-loader resolution) remains Phase 11.7+.
Threading: ExpandedViewFile.imports (ya poblado por §9.dd)
se aplana en emit_module_ssr a un Vec<&str> de nombres, y
pasa a través de la cadena entera del emitter — emit_component_
ssr_into → emit_render_fn + emit_event_fn (trivial +
widened) → emit_template_node_to_pieces + lower_event_body_
stmts + format_child_composition + emit_attr_to_pieces +
format_event_rhs + format_if_arm_value + el wrapper público
format_fitz_expr. ~30 call sites tocados con un imported_
names extra positional.
Patterns unlocked post-K-4:
from helpers import cards_in, move_one
component Board {
state { cards: List<Card> = [] }
event move_right() {
let target_id = payload["card_id"]
cards = cards.map(fn(c) => move_one(target_id, "right", c))
}
<template>
{#for c in cards_in(cards, "todo")}
<li>{c.title}</li>
{/for}
</template>
}
En vez del workaround verboso (inline logic O top-level fns
adentro del .fitzv que el parser rechaza), el user separa la
lógica pura en un .fitz classic y la importa naturalmente al
SFC. Los tres módulos (.fitz helpers, .fitzv component,
.fitz main) mantienen sus responsabilidades limpias.
Sin cambio breaking: el path viejo con state fields + closure params sigue funcionando idéntico. Solo cambia el rechazo inmediato del ident desconocido — ahora consulta la imports table antes de errorear.
Cambio funcional visible en la firma pública:
ChildComponentProp (K-3 remainder) + format_child_composition
(K-3 remainder) + el resto de la cadena SSR ahora incluyen
imported_names en su plumbing. Interno del emitter —
consumers externos (emit_module_ssr como entry point único
en producción) no ven el cambio.
Deudas residuales derivadas (NO bloquean Board):
- Type-check estático del expr importado contra la fn signature — hoy trust runtime (classic checker corre sobre emitted module). Refinable si false negatives aparecen (typo en el nombre importado no descubierto hasta parse del emitted).
- Alias en imports (
from X import Y as Z) — el emitter hoy usaimp.namesdirecto (nombre original solamente). El view parser de §9.dd tampoco soporta alias (verificado). Refinable si demand aparece.
Impacto acumulado + Phase 11.7+ scoping¶
Post-K-4 (2026-07-16): las 4 K-debts (K-1 event bubbling, K-2 component state R/W, K-3 compound / interpolated props, K-4 imported fn refs en SFC templates + event bodies) están CERRADAS para el SSR path (fitz-liveviews target).
- K-1 shipped en fitz-liveviews v0.5.0 con
dispatch_to()explicit event API. - K-2 shipped en fitz-liveviews v0.5.0 con
component_state() set_component_state().- K-3 shipped en Fitz core post-v0.21.0 con dos slices:
List
static + interpolated props (SSR).
Full Board.fitzv migration está desbloqueada para SSR. Kanban SPA port + client-side dynamic capabilities siguen para Phase 11.7+ (WASM path para interpolated props + reactivity plumbing entre parent state y mounted children).
Remaining gaps para Phase 11.7+ (post-Session A v0.21.2 refresh):
- 🟢
Map<Str, Str>static props — CERRADO S.2 (2026-07-17) víak=v,k=vconvention.Map<Str, Int>y richer key/value shapes siguen deferidos; workaround interpolación cubre esos casos. - 🟢 Checker type-check del expr interpolado vs field type —
CERRADO S.3 (2026-07-17) vía
light_check_interpolated_proppara el caso safest (bare Ident matching parent state field type). Richer expr shapes (BinOp, Call, Field access) siguen skipping; classic checker downstream catch mismatches profundos. - 🟢 Alias en imports SFC (
from X import Y as Z) — CERRADO S.1 (2026-07-17) vía Token::As + parser + tuple(String, Option<String>)mirror de PreF8.4. - 🟢 WASM interpolated props (caso simple) — CERRADO
Phase 11.7.a (v0.21.5). El WASM emitter (
codegen_wasm.rsemit_child_component+lower_child_prop_value+is_wasm_prop_simple_target) ya acepta<Child prop="{expr}" />cuando el prop es un bare parent state field ({title}) o aritmética sobre state numérico ({n + 1}) hacia un field PRIMITIVO del child. Propagación reactiva vía el modelo dirty-flag: el parent re-renderiza en cada state change → recomputa el prop → re-mount del child con el valor fresco. 5 unit testsphase_11_7_a_wasm_*+ ejemplo runnableexamples/view/reactive-props/(parentApp→ childBadge, compila a WASM 32.2 KB, smoketests/view_reactive_props_wasm_smoke.rs). Deuda residual derivada (para slices posteriores de 11.7): - Targets no-primitivos (nullable / nominal / list) +
shapes ricos (method calls, Str concat, field access,
imported names) rechazan con pointer a un slice posterior /
el SSR target. Requieren
__FitzValue-style marshaling + coerción tipo-consciente. - 🟢 Persistent child state — CERRADO Phase 11.7.e
(v0.21.6) para sites estáticos. El parent cachea cada
<Child />en un slot tipado__child_slot_<n>+ get-or-create; el child se reusa entre re-renders → su state local sobrevive. El ejemploreactive-propsahora lo demuestra (Badgecon contadortapspropio que aguanta el bump del parent). - 🟢
{#if}/{#for}en WASM — CERRADO Phase 11.7.b (v0.21.6). Estaban deferidos desde 11.4.c y nunca se habían implementado en el emitter WASM.{#if}(comparaciones +&&/||/!+{#else}),{#for}sobreList<primitive>(snapshot + loop-local scope).emit_if/emit_for/lower_cond_expr. Ejemploexamples/view/control-flow/(WASM 26 KB). Residual:{#for}sobreList<nominal>(ejList<Card>, lo que el kanban necesita) — bloqueado por la falta de soporte de tipos nominales en el target WASM.type_expr_to_rustrechaza nominales; hace falta emitir/importar el struct Rust del tipo classic. Prereq del kanban (R3).{#for}con iterable no-ident (method calls, imported fns) — necesita expr lowering más rico o el SSR target.- List/Str mutation en event bodies (
labels.push(...)) — deuda pre-existente de 11.4.c (event bodies solo hacenstate = numeric).
- 🟢
<Child />composition dentro de{#for}(keyed dynamic children) — CERRADO R2b (v0.21.7). El atributokey="{expr}"fluye cross-module (view parser → expand → check), special-caseado enexpand_child_componentpara NO tratarse como prop (se guarda enChildComponent.key; statickey="literal"rechaza en expand). El WASM emitter clasifica sites STATIC vs DYNAMIC (collect_child_site_typesdesciende Element/If/For como el render walk) y emite__child_map_<n>: RefCell<HashMap<String, Rc<Child>>>por sitio dinámico. El child se reconcilia conentry(key).or_insert_with(|| Child:: new())(reusa la instancia → state local sobrevive) + un seen set per-render +retainpost-loop (evicta keys huérfanas). Elkeylowerea víaformat!("{}", <expr>), típicamente la loop var. Ejemploexamples/view/keyed-composition/(WASM 40 KB). Residual:List<nominal>cerrado por R3 (abajo); la mutación live de la lista también entra con R3 (.push/.clear). - 🟢 Tipos nominales en WASM (
List<nominal>, field access, struct literals, mutación live) — CERRADO R3 (v0.21.8). El target client-WASM ya trata untypeclassic importado como ciudadano de primera clase:List<Card>state,{#for c in cards},{c.title}field access,Card { ... }construcción,<state_list>.push(...)/.clear()(mutación live → la reconciliation de R2b corre de verdad), y keyed<Child />con props primitivos desde campos nominales. El SSR difiere toda resolución nominal al loader classic en un 2do pass; el WASM no tiene 2do pass, así que R3 carga eltype Carddel.fitzsibling (load_imported_nominals: lexer + parser →Stmt::TypeDef) y sintetiza el struct Rust inline (emit_nominal_structs).NominalRegistry+emit_module_with_nominals+type_expr_to_rust/emit_for/lower_expr(armsStr/Field/StructLit)/lower_stmt(push/clear) extendidos. 13 unit tests + ejemplo runnableexamples/view/nominal-list/(WASM real end-to-end, cero warnings). Residual hacia el kanban completo (próximo slice — imported-fn support en WASM): (a){#for}sobre el resultado de un fn call ({#for c in cards_in(cards, "todo")}); (b).map/.filter+ closures en event bodies (los eventos move/delete del kanban reasignan la lista); © imported classic helper fns (cards_in,move_one,keep_if_not,make_card) transpiladas al crate WASM. El SSR target ya soporta los tres. - Event bubbling (11.7.c) +
<slot />fallback (11.7.d) — R2b+/posterior. - Nominal-type STATIC props — workaround: interpolación cubre.
- Cross-file
<Child />composition (S.6) — 🟢 CERRADO 2026-07-19 (v0.25.0) para el target WASM víaload_imported_components+ImportedComponentRegistry+merge_imported_components+check_with_imported_components(ver la entrada dedicada arriba). El path elegido fue proper loader integration (leer el sibling.fitzvy mergear la clausura reachable), no convention-based leaky. - Cross-component event flows más ergonómicos — K-1 shipped
explicit
dispatch_to(); implicit bubbling /@parent.eventdecorator queda para futuro.
No hay urgencia sobre estos remaining gaps — el SSR target cubre el 100% del caso Board + el 95% del caso general post-Session A (Session A cerró 3 de 6 debts menores, 4th diferida por buenas razones).
🟢 Phase 11.8 — LSP inside .fitzv — CERRADO SESSION B (2026-07-18)¶
Síntoma pre-Session B: editar un .fitzv en VSCode era
esencialmente texto plano — la extensión bundleada solo daba
sintaxis coloring (TextMate grammar) pero cero diagnostics,
completions, hover, go-to-def dentro del SFC. Los users tenían
que correr fitz check en la terminal para ver errores del
view lexer/parser/expand/check, lo cual rompía el flow de
editing típico de VSCode + rust-analyzer / pyright.
Cierre Phase 11.8 (v0.21.3, 2026-07-18, ~620 LoC + 20 tests):
4 sub-fases coordinadas cierran las 4 capabilities core del
LSP para .fitzv. Sin cambios breaking.
- 11.8.a Diagnostics — nueva
check_view_source(source) -> Vec<FitzError>routea via view pipeline + mapea 3 tipos de error (ViewParseError,ExpandError,CheckError) aFitzErrorshape. Nuevacheck_source_by_uri(uri, source)dispatch por extensión. Nuevauri_is_fitzv(uri)helper. LSP bincheck_and_publishroutea.fitzvtransparente. - 11.8.b Completions — nueva
completion_at_position_view(source, line, character)con 4 clases: (1) template directives ({#if}/{#for}/{#else}/{/if}/{/for}SNIPPETs tras{o{#); (2) event decorators (click/submittras@); (3) state field names del enclosing component; (4) event handler names. Heuristic scan robust to partial parses (unterminated{, mid-typing). LSP bincompletiondispatch. - 11.8.c Hover — nueva
hover_at_position_view(source, line, character) -> Option<Hover>con markdown code fence + label. Keyword filter evita false positives. LSP binhoverdispatch. - 11.8.d Go-to-def — nueva
definition_at_position_view( uri, source, line, character) -> Option<Location>salta a<name>: <type>line en state block Oevent <name>(...)line. Component boundary respect.
Deudas residuales derivadas (NO bloquean uso real):
- Fine-grained context routing en completion — MVP no distingue "cursor inside template" vs "inside state block". Suggestions siempre correctas pero pueden aparecer en contexts adjacentes. Refinable si false-positive noise aparece en práctica.
- Cross-module symbol lookup — hover/go-to-def sobre un
ident importado por
from X import Yno salta al target module hoy. Refinable con plumbing paralelo alresolve_cross_module_definitiondel path classic. - TypeInfo-based hover — hover MVP usa heuristic scan del source; una integración full con el classic checker corriendo sobre el emitted classic Fitz surface daría más precision (bare ident refs en event body con complex expr shapes). Sub-session B.2 si aparece demanda.
- Signature help / rename / references dentro de
.fitzv— NOT implementados por este MVP. Refinables si entra demanda concreta.
🟢 View pipeline gaps — surface durante fitz-liveviews chat migration (Phase 8.4) — CERRADAS ENTERAS 2026-07-16 (§9.cc + §9.dd + §9.ee)¶
Cierre resumen (post-v0.21.0, mismo día que la debt entry — el autor priorizó atacar las 6 gaps inmediatamente): las 6 blockers que la chat migration probe surface fueron cerradas en 3 sub-fases coordinated durante la misma sesión.
- §9.cc (2026-07-16) — V-4 (
payloadin view checker event-body scope, ~30 LoC insrc/view/check.rs::build_env_program, +5 tests) + V-6 (bare method calls on shadow-local state fields accepted by SSR emitter's §9.aa walker, ~40 LoC insrc/view/codegen_ssr.rs::lower_event_body_stmts, +6 tests). - §9.ee (2026-07-16) — V-1 (HTML5 comments
<!-- ... -->in templates, ~30 LoC insrc/view/parser.rs::parse_element+parse_html_commenthelper, +5 tests) + V-2 (bare boolean HTML attrs likerequired,disabled,checked,data-flv-clearaccepted, ~10 LoC insrc/view/parser.rs::parse_attribute, +4 tests). - §9.dd (2026-07-16) — V-3 (cross-file nominal type refs in
state annotations) + V-5 (cross-file nominal refs in struct
literals inside event bodies). Chosen approach:
from X import Ysyntax in.fitzv(Vue/Svelte convention), NOT loader-side env patching. ~500 LoC across 5 files (view lexer + parser + AST - expand + checker + SSR emitter). +19 tests. User-side: refactor
shared types to sibling
.fitzmodule (both parent and.fitzvimport from there — no cycle).
Empirical end-to-end validation: chat migration probe with
message.fitz sibling (declares type Message) + ChatRoom.fitzv
(from message import Message, state { messages: List<Message> =
[] }, event body with messages.push(Message { author, text }))
+ main.fitz (from ChatRoom import ...) — fitz run boots
clean, curl / → 200 with data-flv-component-name="ChatRoom" in
HTML. ALL 6 gaps closed, chat migration end-to-end unblocked.
Tests al cierre de todo el bloque: 3691/3691 lib tests verde
(+19 vs V-2 baseline 3672, +30 vs pre-§9.cc baseline 3661).
cargo fmt --all --check + cargo clippy --lib --tests -- -D
warnings limpios en cada sub-fase.
Deuda residual derivada NUEVA (menor, NO bloquea):
- .fitzv from X import Y as Z alias syntax NOT supported.
Rejected at parse time citando Phase 11.7+. Rare need.
- .fitzv import X (bare, no from) NOT supported. User must
use from X import Y explicit shape. Cleaner than import X
in a components-only context.
- Chat migration in fitz-liveviews still pending in ROADMAP —
§9.dd fixed the Fitz-core-side blockers; chat migration itself
needs a follow-up commit in fitz-liveviews that refactors
type Message to message.fitz sibling and migrates
main.fitz to consume the SFC. Now UNBLOCKED — no more Fitz
core work required.
(HISTÓRICO) Texto original de la deuda (preservado para referencia)¶
View pipeline gaps — surface durante fitz-liveviews chat migration (Phase 8.4) — ABIERTO 2026-07-16 (CERRADAS ver sección arriba)¶
Trigger: intento de migración del chat example de fitz-liveviews
a .fitzv SFC syntax (Phase 8.4 del ROADMAP de fitz-liveviews).
El chat es el 3er example atacado (post counter/dashboard) y el
primero que ejerce shared-state con List.push() bare expr stmt en shadow-local event body) sea siquiera
testeada. La dogfooding cumplió su rol: el pattern "chat = shared
List + push" no está soportado hoy end-to-end en .fitzv SFCs.
V-1 — HTML comments <!-- ... --> no soportados en template block¶
Síntoma (fitz run al cargar el .fitzv):
Repro:
component Foo {
state { count: Int = 0 }
<template>
<div>
<!-- This is a comment -->
<p>{count}</p>
</div>
</template>
}
Causa: el view template parser (src/view/parser.rs,
parse_template_element) ve < y espera un tag name; ! no
matchea el pattern y el parser aborta con "expected tag name".
HTML5 comments son standard y common en templates reales.
Severity: Low → Medium. Workaround: usar Fitz-style //
comments AFUERA del template block; NO usar HTML comments adentro
de <template>.
Fix sugerido: en parse_template_element, cuando el next char
después de < es !, matchear las 3 formas del HTML5 comment:
- <!-- ... --> (comment)
- <!DOCTYPE ...> (doctype — rare pero standard)
- <![CDATA[ ... ]]> (CDATA — rare)
Para MVP, solo cubrir <!-- ... --> (~30 LoC + 3 unit tests). Los
otros dos son opcionales.
V-2 — Bare boolean HTML attrs (required, data-flv-clear, disabled, checked, etc.) rechazados¶
Síntoma:
Error at line 43:63 — view parse error in `<path>.fitzv`:
attribute `required` requires a value in the POC —
bare boolean attrs land in Phase 11.2+
Repro:
Causa: el view template parser (documentado como Phase 11.2+
gap en el mensaje de error) todavía no acepta HTML5 boolean
attributes sin valor. HTML5 spec permite <input required> como
equivalente a <input required=""> o <input required="required">.
Severity: Medium. Casos afectados en HTML real:
- Forms: required, disabled, readonly, autofocus
- Media: controls, autoplay, loop, muted
- Semantics: checked, selected, hidden, open
- Fitz LiveViews conventions: data-flv-clear, data-flv-root
Workaround: forzar valor explícito (required="required",
data-flv-clear="true"). Funciona pero es non-idiomatic HTML.
Fix sugerido: extender parse_attribute en el view template
parser para aceptar el shape "attr name sin =". Emit como
attr_name="" (HTML5 semantics) o similar. ~40 LoC + tests.
V-3 — Cross-file nominal type refs en state { field: T } no resueltos¶
Síntoma:
Error at line 25:11 — view check errors in `<path>.fitzv` (1 error(s)):
- unknown type `Message` (component 'ChatRoom': state field 'messages')
Repro (ChatRoom.fitzv con Message definido en parent
main.fitz):
// ChatRoom.fitzv
component ChatRoom {
state { messages: List<Message> = [] }
...
}
// main.fitz
type Message { author: Str, text: Str }
from ChatRoom import ChatRoom
Causa: el view checker resuelve types del state { ... } block
contra el TypeEnv local del .fitzv module. Nominal types
definidos en otros archivos (parent main.fitz o siblings) no son
visibles. El view module también no soporta from imports (esa es
classic Fitz syntax, no view syntax).
Severity: High. Chat NO puede tipar List<Message> como state
sin este fix. Workaround: usar List<Any> (pierde tipado), o
declarar el Message inline en el .fitzv (si es soportado — probable
no).
Fix sugerido: 3 opciones:
- (a) Cross-file resolution via env inheritance: cuando el
loader transforma un
.fitzv, pasar al view checker un TypeEnv poblado con nominals del importer's env. Complejidad medio-alta porque hay que timing-coordinate la carga del importer y del imported. - (b)
from X import Ysupport en el view module syntax: permitir imports declarados en.fitzvmismo. Emerge la pregunta de si el view lexer soporta el token defrom(probable sí — es keyword reusado del classic Fitz). - © Type declarations inline en el
.fitzv: sumar syntax tipotype Message { ... }fuera delcomponentblock. Menos invasive pero cambia el shape del.fitzv(deja de ser 1 component per file).
Opción (b) es probablemente la más pragmática. ~150-200 LoC (view lexer + parser + checker).
V-4 — payload no está en scope del view checker en event bodies¶
Síntoma:
Error at line 27:3 — view check errors in `<path>.fitzv` (5 error(s)):
- unknown variable `payload` (component 'X': event handler 'send_message')
- unknown variable `payload` (component 'X': event handler 'send_message')
...
Repro:
component X {
state { text: Str = "" }
event send_message() {
if (payload.has("text")) {
text = payload["text"]
}
}
}
Causa: §9.z del plan Phase 11 fixed el SSR emitter para poblar
payload en el event-body local_scope (usa format_fitz_expr_scoped
con &["payload"]). Pero el view CHECKER (a different pipeline
stage, corre ANTES del emit) todavía no incluye payload en el
scope del event body. Resultado: código que EMITE correcto es
REJECTED por el checker.
Severity: High. Cualquier event body que necesita leer del
payload (99% de forms + user input events) está bloqueado por el
checker aunque el emit funcione. Sin workaround real — o el
checker acepta payload O el user no puede escribir event bodies
non-triviales.
Fix sugerido: en check_event_body o equivalente del view
checker, pre-poblar el local scope con payload: Map<Str, Str>
antes de walker el body. Paralelo bit-a-bit al fix de §9.z pero
en el checker. ~30 LoC.
V-5 — Cross-file nominal refs en struct literals dentro del event body también rechazados¶
Síntoma: mismo error que V-3 pero en un context distinto:
Repro:
component X {
state { messages: List<Any> = [] }
event send_message() {
messages.push(Message { author: "x", text: "y" })
// ^^^^^^^^^^^^^^^^^^^^^^^^ Message no está en scope
}
}
Causa: mismo que V-3 pero para struct literal (no state
annotation). El checker walka el event body y trata de resolver
Message { ... } contra el TypeEnv local del .fitzv.
Severity: High. Mismo blocker que V-3 desde otro angle — sin
cross-file type resolution NO se pueden crear instancias de types
del parent module en event bodies. Chat's Message { author: ...,
text: ... } es el ejemplo canónico.
Fix sugerido: mismo fix que V-3. Ambos (state annotation y event body struct literal) se resolverían juntos si el checker pobla el TypeEnv con cross-file nominals.
V-6 (PROBABLE, NO CONFIRMADO) — .push() bare expr stmt en shadow-local event body rechazado por §9.aa walker¶
Estado: no confirmado empíricamente porque V-3/V-4/V-5 nos
bloquearon antes. La chat migration probe usaba
messages.push(Message { ... }) en el event body, que el §9.aa
walker rechaza si la política "solo Stmt::Assign a Ident +
Stmt::Expr(Expr::If) accepted" está estricta.
Fix esperado: extender §9.aa walker para aceptar bare method
call stmts sobre shadow-local Lists (.push(x), .remove(idx)).
Mutation semantics preserved (ArcList<T>.appended(item) ->
List<T> immutable-return builtin (~80 LoC + tests + docs) —
permite messages = messages.appended(new_msg) que ES un
Stmt::Assign a Ident (accepted por §9.aa hoy).
Impacto acumulado¶
Chat migration BLOQUEADO — 5 blockers confirmados + 1
probable (.push()). La migración full-shape a .fitzv requiere
cerrar V-3/V-4/V-5 mínimo (para tipar state + eventos con
Message) más V-6 (para el append semantics). V-1 y V-2 son
polish menor.
Kanban migration LIKELY-BLOCKED también — kanban tiene el
mismo pattern (board.cards.push(...) + board.cards.map(...) +
board.cards.filter(...)). Solo el sub-componente card_editor
(que §9.aa validated) fits porque su event body es re-assign
puro (text = new_text) sin .push().
Dashboards con SFC + shared-state pattern también bloqueados — cualquier app real de fitz-liveviews que necesite append/remove to a shared List está en este bucket.
Contramedida: continuar Phase 8 skipping chat (documentar como "BLOCKED por §9.cc-9.gg del Fitz core"), atacar Phase 8.5 kanban en modo partial (solo card_editor SFC), o pivot a un NUEVO example (CRUD contra Postgres) que sidesteps el pattern porque el "state" son DB rows, no una List mutada in-memory.
Fix roadmap propuesto (§9.cc → §9.gg en Fitz core)¶
Sub-fases sugeridas para atacar en bloque coordinated (si el autor prioriza cerrar el gap):
- §9.cc — V-4 (payload in checker scope) + V-6 (
.push()en event body). Chicos, ~70 LoC combinado + tests. Cierra el event-body checker gap Y habilita append semantics. Highest return per LoC. - §9.dd — V-3 + V-5 (cross-file nominals en state + struct literals). Complejidad medio-alta (~150-200 LoC). Cierra el cross-file scope gap. Habilita chat migration end-to-end.
- §9.ee — V-1 (HTML comments) + V-2 (bare boolean attrs). Polish del template parser. ~70 LoC combinado.
Todo suma ~300-350 LoC coordinated para desbloquear TODO el chat + kanban shared-state migration. Realístico en 1-2 sesiones dedicadas del Fitz core.
🟢 Fase 11 — Native frontend .fitzv compilada a WASM + SSR emitter for fitz-liveviews — CERRADO por v0.21.0 (2026-07-16)¶
Release mayor aggregating Phase 11.1 → 11.5 + 11.6.a/b/c/d +
11.6.e §9.z/§9.aa/§9.bb en un solo bump. Cierra la fase más
ambiciosa del roadmap original ("frontend en .fitz") con dos
backends compilados: WASM para client-side interactivity + SSR
para server-rendered HTML targeting fitz-liveviews. Detalle
exhaustivo en docs/fase-11-plan.md §9.a–§9.bb
(~5400 líneas).
Sub-fases shipped:
- 11.1 — POC parser (
.fitzvextension, HTML sub-parser,src/view/module scaffold). - 11.2.a/b/c — Bridge classic AST + checker + template
directives
{#if}/{#for}/{#else}/<slot>+ view- lexer §7 (state annotations con generics). - 11.3.a/b/c — Scoped styles + CSS mini-parser +
apply_scopehelper wired enexpand. - 11.4.a/b/c/d — WASM emitter approach A2 (hand-rolled
wasm-bindgen+web-sysbajo feature opt-inclient-wasm). Bundle-size gate cerrado 2026-07-15 con 11.4 KB gzipped sobre 40 KB (28.6 KB headroom sobre el counter demo). Browser smoke manual Windows 11 / Chrome validado. - 11.5.a/b/c/d/e — CLI wiring
fitz build --bin <name> [--target <t>]+ manifest[[bin]]array-of-tables con legacy[bin]auto-migration (cierra debt 9.y.8+ multi- bin) + wasm-client emit + multi-component composition<Child prop="v" />con primitivos Fitz-literal + cierre formal. - 11.6.a/b/c/d — SSR emitter research + skeleton
src/view/codegen_ssr.rsconemit_module_ssr+ full expression grammarformat_fitz_expr_scopedcon state-field rewriting + closure-param local-scope tracking +<style scoped>/<style global>inline con CSS-brace escape - template directives
{#if}/{#for}con__fitz_view_str_ joinhelper + view lexer.fix + module loader integration para.fitzvtransparente (.fitzfirst,.fitzvfallback paralelo en 5 loader entry points) + same-file<Child />composición con primitive Fitz-literal coerce. - 11.6.e PARTIAL (§9.z + §9.aa + §9.bb).
- §9.z: SSR emitter
payloadscope en event bodies + enriched module-not-found hint forfitz_liveviews(targetedhint:block en bothevaluator::load_moduleycodegen::ModuleLoader::load_modulecon canonical git dep snippet). - §9.aa: Event-body widening.
emit_event_fndispatcheris_trivial_event_body→ trivial / widened path; wide path prime shadow localslet <field> = state.<field>+ walker recursivolower_event_body_stmtsaceptaStmt::AssignaIdent(new local o shadow mutation) +Stmt::Expr(Expr::If, _)guards con arm scope truncation. Walkerformat_fitz_ expr_scopedwidened paraExpr::If(single-expr arms viaformat_if_arm_value) +Expr::StructLit(walk fields verbatim). Unblocks kanban'scard_editor_save+ chat'ssend_message.fitzvmigrations. - §9.bb: Cross-module
@live_componentauto-inject paralelo bit-a-bit a W12 (pre_scan_imported_auth_provider) y B10 (pre_scan_imported_background_fns). Nuevopub struct ImportedLiveComponent { component_name, type_name, module_ name, render_fn, events }+ fieldTypeEnv.imported_live_ componentsconadd_imported_live_components/imported_live_components()accessors +pub fn extract_live_ components_from_program(program, module_name)walker (silent- drop-on-missing-render_for + deterministic alphabetical sort).inject_live_component_registrationsextended con imported loop: local-wins-over-imported silent skip, bare-Ident emission (matches local case shape), name-in-scope validation via nuevo private helpercollect_names_in_ scope(program)con actionable hintAdd \fromimport , _render, ...` . Nuevopre_scan_imported_live_componentsenmain.rswired encheck_program_with_pyi_stubs_and_deps. Removes manualflv_register(...)boilerplate para components declarados en imported.fitzv/.fitz` sibling modules.
Tests al cierre: 3651 unit (default) + 3787 unit (--features
lsp) + 115 cli_e2e + 3 openapi_e2e + 381 compile_e2e (4
pre-existentes: file-lock Windows races + orm_w17 #7 codegen
drift + http_coverage_metodos_headers routing 404 — todos
documentados pre-v0.21.0, cero regresiones imputables al diff
Phase 11 entero). cargo fmt --all --check + cargo clippy
--lib --tests --bins -- -D warnings limpios.
Deudas residuales derivadas (NO bloquean)¶
- Cross-file
<Child />composition (§9.y debt) — 🟢 CERRADO 2026-07-19 (v0.25.0) para el target WASM.load_imported_componentslee el sibling.fitzvy mergea la clausura reachable a través de checker + emitter. Ver la entrada dedicada al inicio del doc. fitz checkinject-time errors — cross-module auto- inject validation errors surface viafitz run/fitz buildsolo. El checker no corre inject (misma policy que v0.20.1 local case). UX gap refinable si LSP or CI-only workflows demand it.- Client-side dynamic capabilities (Phase 11.7) — dynamic
props (
prop={expr}), event bubbling, cross-file<Child />dynamic,<slot />fallback, persistent child state, drag-drop. Kanban SPA port pinned como acceptance criterion. Deferred hasta demanda real. - LSP support inside
.fitzv(Phase 11.8) — hover, autocomplete, template-attr completion. Deferred. - Pedagogic docs (Phase 11.9) — cap dedicado en
docs/guide.md+ módulo del curso M9 +docs/architecture.mdrefresh cubriendo el pipelinesrc/view/. Deferred. - Migration commits en
fitz-liveviewssibling repo — counter draft uncommitted desde §9.z; dashboard debería seguir el mismo shape (extractMetricTile.fitzv); chat + kanban ahora unblocked por §9.aa (event-body widening) + §9.bb (cross-module auto-inject). Land post-v0.21.0.
🟢 Multi-bin ([[bin]] array-of-tables) — CERRADO por Fase 11.5.b (2026-07-15)¶
Deuda 9.y.8+ arrastrada desde Fase 9.y.1 (2026-05-16). El
manifest MVP aceptaba solamente [bin] singular; multi-bin
quedaba explícito como "sub-paso futuro" hasta que apareciera
demanda real.
Disparador: Fase 11.5.a (research + decision para el CLI
routing de .fitzv) concluyó que la forma más limpia de
distinguir un binario nativo de un bundle WASM del browser en
un mismo proyecto es multi-bin ([[bin]] con target = "native"
vs target = "wasm-client"). Cerrar 9.y.8+ como side-effect
salió más barato que inventar una convención dedicada.
Fix (Fase 11.5.b, 2026-07-15):
src/manifest.rs:Manifest.bin: Option<Bin>→Manifest.bins: Vec<ManifestBin>con nuevos camposname/target/mount. Nueva enumTarget { Native, WasmClient, Ssr }con serde kebab-case yDefault = Native.- Custom
DeserializeviaRawManifest+ untaggedRawBinField { Single | Multiple }auto-migra legacy[bin]singular (rellenanamedesdepackage.namecuando se omite).[[bin]]array-of-tables exigenameexplícito por entry + unicidad. - Custom
Serializepreserva el shape visual[bin]singular para el caso scaffolded común (un bin,name == package.name, sintarget/mount) — cero cambio visible en los ~40 boilerplates + course examples existentes. - Cross-field validation eagerly rechaza
.fitzv+native(explícito o el default) ywasm-clientsinmount, con mensajes que citan el fix específico.target = "ssr"parsea OK y surfacea viaManifest::warnings()(consume el CLI al arrancarfitz build). src/main.rs:Commands::Buildgana--bin <name>y--target <t>(kebab-case). Nueva API públicaresolve_entry_with_bincon el selector + override; helperenforce_build_target_supportedrechazawasm-clientcitando 11.5.c yssrcitando 11.6+ ANTES de tocar disco.- Tests: 23 unit nuevos en
manifest::tests(legacy migration, multi-bin parse, target enum roundtrip, mount validation,.fitzv+ native rejection, SSR warning,select_bin) + 7 cli_e2e nuevos (phase_11_5_b_*).
Detalle completo en docs/fase-11-plan.md §9.q. Refleja en
docs/roadmap.md (Fase 9.y sub-pasos + Fase 11 sub-fase 11.5.b).
Deudas residuales derivadas (NO bloquean)¶
- Run/Check sobre multi-bin:
--binestá solo enCommands::Buildpor scope de 11.5.b (siguiendo la decisión de 11.5.a). Multi-bin projects que correnfitz run/fitz checksin selector recibenBinAmbiguous— el fix es pasar archivo explícito (fitz run src/main.fitz). Si aparece presión real, promover--bina Run/Check/Test/etc. en un follow-up. fitz test --bin: hoydiscover_test_sources_from_manifesttoma el primer bin. Multi-bin projects que quieran granularidad por bin abren refinamiento visible.--mountCLI flag: pendiente hasta 11.5.c (donde el emitter realmente consumemount). Single-file mode default:"#app"(documentado adonde aterrice el emitter).
🟢 LSP marca false positives sobre built-ins del lenguaje no registrados — smtp + Response — CERRADO 2026-06-28 (H1 confirmado)¶
CERRADO el mismo día del descubrimiento (sesión nocturna 2026-06-28 sobre repo del lenguaje, separada de la sesión fitzwatch que disparó la deuda). Investigación con 5 tests de regresión nuevos en
src/lsp.rs::testsinvalidó H2 (no hay divergencia entre LSP y CLI en código) y confirmó H1 (extensión VSCode instalada con.vsixbundleando unfitz-lsp.exepre-v0.18.0/v0.19.0). Acción del usuario: reinstalareditors/vscode/fitz-language-win32-x64-0.19.6.vsix(que ya bundlea LSP fresh con timestamp 2026-06-27 19:36, post-v0.18.0smtp+ post-v0.19.0Response). Sin bump de versión (el código del compilador estaba correcto desde el día del release). Detalle abajo preservado para referencia futura.
Resumen del cierre (2026-06-28)¶
H2 invalidada — 5 tests nuevos en src/lsp.rs::tests cubren el path
LSP completo y todos pasan contra el código actual de v0.19.6:
lsp_repro_smtp_module_no_unknown_variable— canónico del Caso 1.lsp_repro_response_built_in_no_field_not_found— canónico del Caso 2.lsp_repro_cross_module_branch_also_registers_smtp_and_response— cubre la rama_with_base_dir(el path heredado del fix v0.19.3 de cross-module@auth_provider/@background).lsp_audit_all_builtin_modules_visible_from_lsp_path— audit paralelo de los 8 módulos built-in (smtp/http/jwt/hash/log/db/auth/flags).lsp_audit_all_builtin_nominals_visible_from_lsp_path— audit de los 5 nominales (Request/Response/File/HttpClientResponse/SmtpResult) con su field canónico (method/status/name/status/delivered).
Razón por la que ambos paths del LSP funcionan correctamente:
- Single-file branch (
check_source_with_types_and_base_dir(src, None)): parse_with_recovery(tokens)→Program.check_program(&program)→ llama aresolve_program→ llama aresolve_program_with_env(program, TypeEnv::new(), Vec::new())→register_http_builtin_types(&mut env)al arrancar (línea 1532 desrc/types.rs) registraRequest/Response/File/HttpClientResponse/SmtpResultcomo nominales con sus fields.-
check_with_env(&program, env, errors)construyeCheckCtx::new(&env)que invocaregister_builtins()(línea 3167 desrc/types.rs) registrandosmtp/http/jwt/hash/log/db/auth/flagsconType::Anyen el scope 0. -
Cross-module branch (
check_source_with_types_and_base_dir(src, Some(bd))): - Idéntico al single-file pero con
resolve_program_with_env(program, TypeEnv::new(), Vec::new())explícito (también pasa porregister_http_builtin_types) + pre-scan de imports +check_with_env(también pasa porCheckCtx::new→register_builtins). - El registro de built-ins vive en
register_http_builtin_typesyregister_builtinsANTES del pre-scan de imports, por lo que la ausencia debase_diro módulos fallidos no afecta la registración.
H1 confirmada por exclusión — los 5 tests pasan en código → el bug
debe estar en el binario fitz-lsp.exe que VSCode está corriendo, no
en el path del checker. Si el .vsix instalado pre-data v0.18.0 (sin
smtp registrado) o v0.19.0 (sin fields del Response built-in), el
binario del LSP no conoce los símbolos y emite los false positives.
Acción del usuario (cierra el bug)¶
- Cerrar VSCode entero.
- (Opcional, recomendado)
Get-Process fitz-lsp | Stop-Processpor si quedó algún proceso huérfano. - Reinstalar el
.vsixexistente — desde la carpeta del repo: El--forceoverrides la instalación anterior. El bundle bundleófitz-lsp.execon timestamp 2026-06-27 19:36 (mismo día del release de v0.19.6). - Reabrir VSCode con el proyecto que usa
smtp/Response. El squiggle debería desaparecer.
Si después de reinstalar el squiggle persiste, escalar como H3 (cache
del cliente LSP) y forzar reload de la window (Ctrl+Shift+P →
"Developer: Reload Window").
Deuda residual derivada (NO bloquea uso real)¶
(1) Indicador de versión del LSP server visible en VSCode — hoy no
hay forma trivial de saber qué versión del fitz-lsp.exe está
corriendo dentro de la extensión. La extensión podría:
- Loguear la versión del server al status bar de VSCode (parallel
a rust-analyzer que muestra "rust-analyzer 0.4.2074" en el status
bar).
- Exponer un comando Fitz: Show LSP server version que ejecute
fitz-lsp --version (requiere agregar el flag al bin).
- Disparar warning popup si la versión del server < versión de la
extensión (caso de instalaciones con override de fitz.lspPath
apuntando a un binario stale en PATH).
Workaround actual: comparar timestamp del fitz-lsp.exe adentro del
.vsix (visible con unzip -l) contra fecha del release.
(2) Test de regresión sobre el binario empaquetado — los 5 tests
nuevos validan el código fuente, no el binario .exe empaquetado en
el .vsix. Refinamiento futuro: smoke test en CI que descomprima el
.vsix y verifique que el fitz-lsp.exe adentro corre --version y
matchea con el bumpeo del crate. Bajo riesgo hoy porque la pipeline de
release usa build:vsix que rebuildea el LSP fresh por design
(memoria feedback_vscode_extension_workflow).
Texto original de la deuda (preservado para referencia)¶
Severidad: amarillo (no urgente).
fitz checkreal acepta el código sin errores,fitz buildcompila y produce binario correcto. SOLO se manifiesta como squiggle rojo + error en la pestaña "Problems" del VSCode al editar archivos.fitzque usan los built-ins afectados. UX ruidoso para developers tocando código con esos built-ins, pero no bloquea ni el dev loop (build) ni el deploy.Actualizado 2026-06-28 noche: el patrón se confirmó simétrico — mismo bug afecta a OTRO built-in (
Response) además desmtp. La hipótesis 1 (extensión VSCode con LSP bundleado < v0.19.0 que no conoce los built-ins recientes) sube de prioridad y probablemente cierra ambos casos con un solo bump.
Casos confirmados del false positive¶
Caso 1 — smtp.send(...) (mini-tanda SMTP builtin v0.18.0):
LSP marca unknown variable smtp cuando se usa el módulo built-in
smtp (registrado en src/types.rs línea 3461)
con Type::Any (mismo pattern que http/jwt/hash/log/db/
auth/flags). Repro: ver
d:\fitzwatch\src\emails.fitz
línea 120.
Caso 2 — Response { status: ..., content_type: ..., body: ... }
(HTTP Response { ... } built-in v0.19.0): LSP marca type Response
does not have a field named X para los 5 fields del built-in
(status, content_type, headers, body, body_bytes).
fitz check real acepta perfectamente porque Response está
pre-registrado en el TypeEnv con los 5 fields (ver
CHANGELOG v0.19.0). Repro: ver
d:\fitzwatch\src\public.fitz
líneas 607, 620, 918, 1213, 1327 (~10 ocurrencias actuales del
patrón). Símbolos también afectados: ResponseData si se nombra
explícitamente.
Probables casos no validados (mismo pattern, asentar si aparecen):
- Request { method, path, headers } (built-in HTTP middleware MW.1
+ cross-module fix v0.19.5/v0.19.6).
- HttpClientResponse { status, body, headers, duration_ms }
(mini-tanda HTTP client v0.17.0).
- SmtpResult { delivered, message_id, duration_ms } (v0.18.0).
- File { path, name, size } (si fue agregado).
Repro mínima — combinado (cubre ambos casos)¶
Crear archivo repro_lsp_builtins.fitz en un proyecto con fitz.toml:
// Caso 1 — smtp module
async fn send_test(to: Str, body: Str) -> Result<Bool> {
let result = smtp.send({
"to": to,
"from": "noreply@example.com",
"subject": "Test",
"body_html": body,
}).await?
return Ok(true)
}
// Caso 2 — Response built-in
@get("/test")
async fn test_handler() -> Result<Response> {
return Ok(Response {
status: 200,
content_type: "text/html; charset=utf-8",
headers: { "Cache-Control": "no-cache" },
body: "<h1>Hola</h1>",
})
}
fitz check(CLI): ✓ pasa limpio sin errores en ambos casos.fitz build(CLI): ✓ binario producido OK con ambos.- LSP en VSCode:
- Caso 1:
unknown variable smtp(Error) en columna 22 sobresmtp. - Caso 2:
type Response does not have a field named status(Error) en cada uno de los 5 fields del struct literal.
Repro real validado en fitzwatch durante sesión 2026-06-28 noche:
- Caso 1: src/emails.fitz línea 120
(F.f.6 unsubscribe page brandeada).
- Caso 2: src/public.fitz líneas
607, 620, 918, 1213, 1327 (SSR OG tags por slug, Opción A
post-F.f.6 — los nuevos handlers slug_ssr_no_slash /
slug_ssr_with_slash y el helper slug_ssr_internal heredaron el
squiggle al usar Response { ... }).
Hipótesis (en orden de probabilidad)¶
Hipótesis 1 (más probable) — Extensión VSCode instalada con LSP
viejo pre-SMTP-builtin (v0.17.0 o anterior). La mini-tanda SMTP
builtin cerró en v0.18.0 (2026-06-19). Si el .vsix instalado en
VSCode trae un fitz-lsp.exe bundleado de versión < v0.18.0, el
binario del LSP no tiene smtp registrado en el checker:
- Verificar versión del binario LSP que VSCode usa:
- Si la extensión es self-managed: ver
editors/vscode/server/fitz-lsp.exey correrfitz-lsp --version(si soporta — si no, comparar md5 contra binarios de versiones recientes). - Si es bundle del
.vsix: ver versión delpackage.jsonde la extensión + verificar que el.vsixbundleó el LSP de esa misma versión (memoriafeedback_vscode_extension_workflowexige regenerar.vsixcon LSP fresh cada release).
Test del fix: bumpear extensión VSCode a v0.19.6+ (que bundlea
LSP v0.19.6+ ya con SMTP registrado) → reabrir VSCode → confirmar
que el squiggle desaparece sobre smtp.send(...).
Hipótesis 2 — Path del LSP que omite register_builtins cuando
hay imports cross-module. El compilador full-flow llama
CheckCtx::new que sí registra smtp
con Type::Any, pero quizás el LSP en el path
check_source_with_types_and_base_dir (heredado del fix v0.19.3
para cross-module @auth_provider/@background) crea un CheckCtx
custom que no incluye los builtins:
- Inspeccionar
src/lsp.rs::check_source_with_types_and_base_diry confirmar que elCheckCtxque construye llama a la inicialización que registrasmtp+http+jwt+hash+log+db+auth+flags. - Si el path omite alguno, sumar el registro paralelo a
register_imported_auth_provider_lspque ya hace el LSP-specific pre-scan.
Test del fix: dos unit tests en src/lsp.rs::tests paralelos a
smtp_module_is_pre_registered_as_any y al pre-registro de
Response en types.rs, pero corriendo sobre
check_source_with_types o check_source_with_types_and_base_dir:
#[test]
fn lsp_smtp_module_no_dispara_unknown_variable() {
let src = r#"
async fn send_test() -> Result<Bool> {
let r = smtp.send({"to": "x@y.com", "subject": "t"}).await?
return Ok(true)
}
"#;
let (_, _, _, errors) = check_source_with_types(src);
let smtp_errors: Vec<_> = errors.iter()
.filter(|e| e.message().contains("smtp"))
.collect();
assert!(smtp_errors.is_empty(),
"LSP no debe marcar smtp como unknown: {:?}", smtp_errors);
}
#[test]
fn lsp_response_builtin_no_dispara_field_not_found() {
let src = r#"
@get("/test")
async fn h() -> Result<Response> {
return Ok(Response {
status: 200,
content_type: "text/plain",
headers: { "X-Foo": "bar" },
body: "hi",
})
}
"#;
let (_, _, _, errors) = check_source_with_types(src);
let response_errors: Vec<_> = errors.iter()
.filter(|e| e.message().contains("Response")
&& e.message().contains("field"))
.collect();
assert!(response_errors.is_empty(),
"LSP no debe marcar fields del Response built-in: {:?}",
response_errors);
}
Idem para http/jwt/hash/log/db/auth/flags (módulos) +
Request/HttpClientResponse/SmtpResult/File (built-in types)
para evitar regresión paralela en otros built-ins.
Hipótesis 3 — Cache del cliente LSP en VSCode con env stale.
VSCode/tower-lsp client cachea el documents Map y el TypeEnv del
último open. Si el documento se abrió ANTES del bump de versión del
LSP, podría seguir corriendo el checker viejo:
- Workaround: cerrar VSCode entero, kill cualquier
fitz-lsp.exeque quede en background (Get-Process fitz-lsp | Stop-Process), reabrir VSCode. - Fix permanente: el LSP server debería invalidar cache en
did_changey forzar full re-parse cuando detecte que el TypeEnv root cambió de version.
Pasos para la sesión separada del compilador¶
- Confirmar Hipótesis 1 primero (más barato): verificar versión
del LSP que el VSCode del autor está usando. Si es < v0.18.0,
bumpear extensión + regenerar
.vsix(memoriafeedback_vscode_extension_workflow), pedirle al autor reinstalar.vsixdesde el release de GitHub. Si esto cierra el bug, no hay trabajo de compilador necesario. - Si H1 no cierra el bug, validar H2: leer
src/lsp.rs::check_source_with_types_and_base_diry comparar elCheckCtxque construye contraCheckCtx::new()(que sí incluye smtp). Si hay path divergente que omite builtins, sumar el registro en paralelo. - Agregar el unit test sugerido arriba (independiente de cuál
hipótesis cierre el bug) para protección contra regresión en los 8
built-ins módulo (
smtp,http,jwt,hash,log,db,auth,flags). - Audit paralelo del LSP: grep
register_builtins/Type::Any/ "smtp" ensrc/lsp.rsy confirmar que TODOS los built-ins están consistentes contypes.rs::CheckCtx::new(). - Validación post-fix: editar
d:\fitzwatch\src\emails.fitzlínea 120 en VSCode con la extensión bumpeada y confirmar que el squiggle desaparece sobresmtp.send(...). - Verification pre-bump (si toca código del compilador):
cargo fmt --all --check+cargo clippy --lib --tests --bins --features lsp -- -D warnings+cargo test --release --features lsp --lib lsp::tests+ smoke real reabriendo fitzwatch en VSCode. - Bump release coordinado si la fix toca código (v0.19.7 o
minor según scope) + extensión VSCode v0.19.7 con
.vsixregenerado + CHANGELOG/roadmap/CLAUDE/README updateado.
Por qué no es urgente¶
fitz checkreal funciona,fitz buildfunciona, deploy a prod funciona — el código que dispara el warning compila y corre correctamente.- El developer puede ignorar el squiggle al editar (paralelo a otros false positives del LSP que el ecosistema convive con — rust-analyzer también marca falsos positivos ocasionales).
- Workaround inmediato: confirmar que la versión del LSP instalado es la última (Hipótesis 1 cierra esto en 80% de los casos).
Workaround actual en fitzwatch¶
Ninguno — el código compila y corre. Solo es ruido visual en VSCode al editar archivos que usan los built-ins afectados:
smtp:src/emails.fitz(línea 120 directo),src/subscriptions.fitz+src/auth.fitzindirecto via import.Response { ... }:src/public.fitz(~10 ocurrencias incluyendo handlersslug_incidents_rss,slug_unsubscribe_get,slug_badge_svg,slug_badge_json,slug_embed_html,slug_ssr_no_slash,slug_ssr_with_slash+ helperslug_ssr_internal),src/subscriptions.fitz(handlerunsubscribe_getdespués de F.f.6).
🟢 Sub-caso v0.19.5 — wrapper HTTP en módulo IMPORTER del middleware: CERRADO v0.19.6 (2026-06-27)¶
CERRADO el mismo día del descubrimiento (2026-06-27 noche durante refactor real fitzwatch), patrón paralelo a las bugfix releases v0.19.2/v0.19.3/v0.19.4/v0.19.5 — coordinado con feedback real del uso del patrón canónico cross-module middleware. Fix en
src/codegen.rs(~50 LoC netas) + 1 E2E test nuevo. El módulo importer del middleware (donde vive el handler con@middleware(<imported_fn>)aplicado) ahora emite correctamenteuse crate::{Request, RequestData};aunque ninguna fn local del módulo declareRequesten su firma — el detector reconoce que el wrapper HTTP construirá__req: Request = Arc::new(... RequestData { ... })para pasarlo al middleware y dispara el import. Detalle abajo preservado para referencia futura.
Resumen del fix (v0.19.6)¶
Detector nuevo program_has_handler_with_middleware(program: &Program)
-> bool en src/codegen.rs, paralelo a program_uses_request_type
(v0.19.5) y program_uses_response_builtin (v0.19.1). Heurística (b)
de las 3 opciones evaluadas pre-fix: dispara cuando el módulo declara
al menos un handler HTTP (@get/@post/@put/@delete) con un
decorator @middleware(...) aplicado, sin importar si el ident del
middleware resuelve a una fn local del módulo o a una fn cross-module
importada. Costo del use extra en módulos benignos es despreciable
(rustc dead-code elimina si no se usa) y elimina la posibilidad de
regresión del bug.
El call site de generate_module_rs_with_bindings (~línea 5311 pre-fix,
ahora 5320+) que decide emitir use crate::{Request, RequestData} agrega
el nuevo predicado como tercer OR a los dos existentes
(module_uses_request_local + module_has_imported_middleware_fn).
E2E test nuevo (tests/compile_e2e.rs)¶
v019_6_cross_module_middleware_applied_in_importer_module_emits_request_imports— shape canónica de fitzwatch con 3 archivos:mw.fitz(async fn mw_strict(req: Request) { return null }),handlers.fitz(from mw import mw_strict+@middleware(mw_strict) @post("/protected") fn protected() -> Str => "ok"),main.fitz(from handlers import protected+@server(N) fn main() => 0). El test inspecciona elhandlers.rsemitido confirmando que contieneuse crate::{Request, RequestData}(o split forms). Paralelo bit-a-bit av019_5_cross_module_middleware_fn_con_request_arg_compilaque cubre el otro lado (módulo del middleware).
Detalle técnico del sub-caso (preservado para referencia)¶
Cómo apareció: refactor real fitzwatch 2026-06-27 noche. Después
del bump FITZ_TAG=v0.19.4 → v0.19.5 y aplicar el patrón canónico:
// rate_limit.fitz — módulo del middleware
async fn rate_limit_auth_strict(req: Request) {
let x_real_ip: Str? = match req.headers.get("x-real-ip") { ... }
...
match check_rate_limit_by_ip(ip, "auth_strict", 10).await {
Ok(_) => {},
Err(_) => return 429 { "error": t(loc, "rate_limit.exceeded") },
}
return null
}
// auth.fitz — módulo IMPORTER del middleware
from rate_limit import rate_limit_auth_strict, rate_limit_auth_slow
@middleware(rate_limit_auth_strict)
@header(name="X-Real-IP", into="x_real_ip")
@header(name="X-Forwarded-For", into="x_forwarded_for")
@header(name="Accept-Language", into="accept_lang")
@header(name="User-Agent", into="ua")
@post("/auth/login")
async fn login(x_real_ip: Str?, x_forwarded_for: Str?, accept_lang: Str?, ua: Str?, creds: Credentials) -> Result<LoginResponse> {
...
}
fitz check pasa limpio. fitz build aborta con 14 errores rustc
(en auth.rs y subscriptions.rs):
error[E0425]: cannot find type `Request` in this scope
--> src/auth.rs:1547:16
error[E0422]: cannot find struct, variant or union type `RequestData` in this scope
--> src/auth.rs:1547:68
... (7 ocurrencias = 7 handlers que aplican @middleware cross-module)
auth.rs emitido, líneas 1542-1551 (wrapper HTTP del handler login):
__fitz_span_ctx,
move || async move { let __req_headers_vec: std::sync::Arc<std::sync::Mutex<Vec<(String, String)>>> = std::sync::Arc::new(std::sync::Mutex::new(
__hmap.iter()
.filter_map(|(n, v)| v.to_str().ok().map(|s| (n.as_str().to_lowercase(), s.to_string())))
.collect()
));
let __req_path = String::from("/auth/login");
let __req: Request = std::sync::Arc::new(std::sync::Mutex::new(RequestData { // ← Request y RequestData usados acá
method: "POST".to_string(),
path: __req_path,
headers: __req_headers_vec,
}));
if let Some(__resp) = rate_limit_auth_strict(__req.clone()).await { // ← y pasado al middleware cross-module
return __apply_cors_and_respond(...);
}
auth.rs use crate::* imports emitidos (líneas 117-185):
use crate::audit::EVENT_LOGIN_LOCKED;
...
use crate::rate_limit::compute_client_ip;
use crate::rate_limit::rate_limit_auth_slow;
use crate::rate_limit::rate_limit_auth_strict;
...
use crate::__FitzResponse;
use crate::__apply_cors_and_respond;
...
// ← FALTA: `use crate::{Request, RequestData};`
rate_limit.rs use crate::* imports (línea 134) — emitido correctamente:
use crate::{Request, RequestData}; // ← v0.19.5 dispara para este módulo (declara fns con Request en su AST)
Conclusión: el detector program_uses_request_type cubre el caso
"el módulo DECLARA fns con Request en su AST", pero el codegen del
wrapper HTTP en auth.rs USA Request/RequestData aunque ninguna fn
del módulo los tenga en TypeExpr. Sub-caso del fix v0.19.5 no cubierto.
Workaround user-land aplicado (pre-v0.19.6, removible al bumpear)¶
// auth.fitz / subscriptions.fitz — al inicio del módulo, después de imports
// Workaround del codegen v0.19.5 — fuerza emisión de
// `use crate::{Request, RequestData};` en este módulo.
fn _codegen_request_anchor(req: Request) -> Bool => true
La fn dummy nunca se invoca, solo existe para que el detector
program_uses_request_type dispare. Costo: 1 LoC + 1 fn extra
en el .rs emitido (zero cost al runtime — rustc dead-code elimina).
Tras bumpear FITZ_TAG=v0.19.5 → v0.19.6 las 2 fns se quitan y el
patrón canónico cross-module middleware funciona sin workaround.
Fix aplicado en v0.19.6 (~50 LoC netas)¶
-
Detector nuevo
program_has_handler_with_middleware(program)ensrc/codegen.rs(opción (b) "trivial" de las 3 evaluadas pre-fix): walka el AST buscandoStmt::FnDefcon decorator@get/@post/@put/@delete+ al menos un decorator@middleware(...), sin importar si el ident del middleware resuelve local o cross-module. El predicado es OR-ed al call site existente que ya cubríamodule_uses_request_local(fn local declaraRequesten TypeExpr) ymodule_has_imported_middleware_fn(la fn local es target de un@middlewarecross-module). -
1 E2E test nuevo
v019_6_cross_module_middleware_applied_in_importer_module_emits_request_importsentests/compile_e2e.rscon shape canónica de fitzwatch (mw.fitz -
handlers.fitz + main.fitz), inspecciona el
handlers.rsemitido confirmandouse crate::{Request, RequestData}(o split forms). Paralelo bit-a-bit av019_5_cross_module_middleware_fn_con_request_arg_compila. -
Bump Cargo.toml
0.19.5→0.19.6+ bump extensión VSCode +.vsixregenerado + CHANGELOG entry detallado + esta sección reescrita a CERRADO con detalle del fix.
🟢 Cross-module @middleware(fn) + Request en codegen — CERRADO v0.19.5 (2026-06-27)¶
CERRADO un día después del descubrimiento, mismo patrón que las v0.19.2/v0.19.3/v0.19.4: bugfix coordinado con el feedback real de fitzwatch. Fix en
src/codegen.rsysrc/types.rs(~280 LoC netas) + 3 E2E tests cubriendo los 3 síntomas + async middleware cross-module también funciona como bonus. fitzwatch puede ahora refactorear el workaround inline (~42 LoC duplicadas en 7 endpoints) a@middleware(rate_limit_strict)cross-module limpio cuando bumpeeFITZ_TAGa v0.19.5+. Detalle abajo preservado para referencia futura; deudas residuales derivadas al final.
Resumen del fix (v0.19.5)¶
Tres cambios coordinados — paralelo a W12 (@auth_provider) + B10
(@background) cross-module pre-scan + W11/W16/W18
(use crate::{...} cross-module emission):
-
Pre-scan global de
@middleware(fn)referencias —pre_scan_imported_middleware_fns_for_loaderwalka main + todos los módulos importados (recursivo), parsea cada.fitz, extrae losIdentreferenciados en@middleware(name)decorators (helper público nuevocrate::types::extract_middleware_fn_names).ModuleLoader.main_imported_middleware_fnsguarda el set; cada módulo cargado consume la unión local + global. -
Propagación al checker del módulo —
TypeEnvsuma campoimported_middleware_fns: HashSet<String>paralelo aimported_background_fns. Setter públicoadd_imported_middleware_fns.collect_middleware_fn_namesdel checker mergea el set alctx.middleware_fn_namesantes del walk. Sin esto, el checker del loader sobre un módulo aislado (e.g.rate_limit.fitz) rechazareturn <status> { ... }porque el pre-scan local no ve la referencia externa. -
Propagación al codegen del módulo —
generate_module_rs_with_bindingsrecibe un parámetro nuevocross_module_middleware_fns: &[String]y pre-inserta los nombres enctx.middleware_fn_namesANTES depre_register_fns. La post-scan que clasifica por aridad (1=pre, 2=post) ve la unión y emite el Rust return typeOption<__FitzResponse>correcto + activain_middleware_fn=truepara losStmt::ReturnStatusyStmt::Returndel body.
Tres fixes adicionales colaterales:
-
@middleware(<imported_fn>)aceptado en main — el check build-time decollect_route_middlewares(línea ~28171) consultaba soloself.fn_sigs.contains_key(n). Ahora también busca viaself.module_bindings.get(n)paraResolvedBinding::Named { kind: NamedKind::Fn }y resuelve elFnSigdesdeloaded_modules[idx].fn_sigs. Paralelo ais_user_callable(v0.9.45). Helper nuevoresolve_fn_sig_anywhere. -
use crate::{Request, RequestData}en módulos — detector nuevoprogram_uses_request_typewalka el AST del módulo buscandoRequesten TypeExpr (fn params/return, type fields, let annotations). Cuando el módulo declara helpers conreq: Request(típico:fn get_client_ip(req: Request) -> Strenrate_limit.fitz), o cuando declara una fn que es referenciada como middleware desde otro módulo (todas las middleware fns tienenRequesten su primer param por spec), el codegen emiteuse crate::{Request, RequestData}al tope del.rs. Paralelo aprogram_uses_response_builtin(v0.19.1). -
use crate::{__FitzResponse, __ToFitzJson, ...}en módulos de middleware puro — cuando el módulo declara solo fns middleware (sin@get/@post/etc),module_has_httpesfalsey el codegen pre-fix no emitía los imports necesarios paraStmt::ReturnStatus. La condición se extiende amodule_has_http || module_has_imported_middleware_fnpara__FitzResponse+__apply_cors_and_respondy para__ToFitzJson/__FromFitzJson. -
Async middleware fn cross-module bonus —
emit_middleware_chaindetectaba el callsite comomw_name(__req.clone())siempre sync. Con cross-module +async fn mw_strict(req: Request), la fn devuelveFuture<Option<...>>y elif let Some(...) =mismatch. Helper nuevomiddleware_fn_is_async(name)consulta el FnSig (local o imported) y detectaType::Future(_)en el ret. El wrapper emite.awaitsuffix condicional para los 3 paths (pre-mw + post-mw response + post-mw result). Paralelo agen_callPhase 6.6. Habilita el patrón canónicoasync fn mw_strict(req: Request) { match check_rate_limit(req, ...).await { ... } }que fitzwatch necesita.
3 E2E tests cubren los 3 síntomas (tests/compile_e2e.rs)¶
v019_5_cross_module_middleware_fn_compila_a_binario_nativo— shape canónica:mw_simple(async gate-onlyreturn null) en módulomw.fitz+ main con@middleware(mw_simple)aplica un handler local. Validad: el binario compila + arranca.v019_5_cross_module_middleware_fn_con_request_arg_compila—mw_with_helperdelega aget_client_ip(req: Request) -> Strhelper local del módulo. Validad:mw.rsemitido contieneuse crate::{Request, RequestData};. Tests inspecciona el.rsemitido (paralelo av019_response_cross_module_emits_imports).v019_5_cross_module_middleware_fn_con_return_status_compila—mw_blockconreturn 429 { "error": "blocked" }. Validad: el checker del loader sobremw.fitzaislado YA NO rechaza elreturn <status>porque el set global tienemw_block.
Deudas residuales derivadas (NO bloquean)¶
-
Async middleware fns en intérprete (
fitz run) — descubierto durante el smoke de v0.19.5. El binario (codegen) ahora soporta async middleware fns cross-module; el intérprete sin embargo devuelve 500 al invocarasync fn mw_strict(req: Request)INCLUSO same-module. Bug pre-existente del evaluator (no es regresión de v0.19.5). Workaround: para validar middleware behavior en desarrollo, usarfitz build && ./binen vez defitz run. Fix futuro: extenderdispatch_requestdel evaluator para awaitear el Future cuando la fn middleware es async (paralelo aawait_if_futurede Fase 9.w.3.b). -
LSP cross-module pre-scan de
@middleware— paralelo a la deuda residual que v0.19.3 cerró para@auth_provider/@background. El LSP abriendo un módulo aislado de middleware (rate_limit.fitz) muestra falso positivo "only allowed inside HTTP handler". Refinable cuando aparezca presión real (el binario compila bien; el squiggle rojo es solo en el IDE). -
Wrap-style middleware (
Fn() -> Responsesecond param) — sigue siendo deuda pre-existente; rechazada en codegen con mensaje claro. No bloquea v0.19.5.
Detalle del bug original (preservado para referencia futura)¶
Patología en dos partes — la fn middleware está en módulo A, el
@middleware(fn)se aplica a un handler en módulo B. Ambas partes rompenfitz build. Bloquea el patrón canónico "módulo dedicado a rate limiting / auth / logging que provee middlewares reutilizables cross-module". Workaround user-land posible pero verboso (inline en cada handler) — encarece todo proyecto Fitz con HTTP de producción.
Síntoma 1 — Codegen rechaza fn middleware cross-module con return <status>¶
Cuando una fn vive en módulo rate_limit.fitz con shape:
async fn rate_limit_strict(req: Request) {
match check_rate_limit(req, "auth", 10).await {
Ok(_) => return null,
Err(_) => return 429 { "error": "rate limit exceeded" },
}
}
…y se importa + aplica desde otro módulo:
from rate_limit import rate_limit_strict
@middleware(rate_limit_strict)
@post("/auth/login")
async fn login(...) -> Result<...> { ... }
fitz build (NO fitz check) aborta con:
✗ codegen: Error — module `rate_limit` has type errors:
`return <status> { ... }` is only allowed inside an HTTP handler
(`@get`/`@post`/`@put`/`@delete`) or a fn applied as `@middleware(...)`
El checker pasa limpio porque ve la fn como middleware-aplicable; el
codegen aplica la regla "return status solo en handler/middleware del
mismo crate root", y al procesar rate_limit.fitz aislado, no ve el
@middleware(rate_limit_strict) que vive en auth.fitz.
Síntoma 2 — Codegen NO emite Request/RequestData en módulos importados¶
Una vez workaround-eado el síntoma 1 (mover la fn middleware al
mismo módulo del handler), si la fn middleware llama a otra fn del
módulo origen que recibe req: Request:
// rate_limit.fitz
fn get_client_ip(req: Request) -> Str { ... }
fn locale_from_req(req: Request) -> Str { ... }
fitz build aborta con docena de errores rustc:
error[E0425]: cannot find type `Request` in this scope
--> src/auth.rs:692:34
error[E0422]: cannot find struct, variant or union type `RequestData` in this scope
--> src/rate_limit.rs:XX:YY
El codegen emite la struct Request + alias en main.rs cuando ve
@middleware aplicado a algún handler, pero NO emite el use crate::
{Request, RequestData} en los .rs de módulos importados que
referencian el tipo. Tampoco emite la struct en esos módulos.
Repro mínima¶
a.fitz:
main.fitz:
from a import mw_strict
@server(3000)
fn main() => 0
@middleware(mw_strict)
@get("/")
fn h() -> Str => "hello"
fitz check → verde. fitz build → falla con uno de los dos
síntomas según orden de procesamiento.
Por qué bloquea fitzwatch / cualquier SaaS Fitz real¶
El patrón canónico de un SaaS HTTP es:
1. Un módulo rate_limit.fitz / cors.fitz / audit.fitz que
define los middlewares.
2. Handlers en varios módulos (auth.fitz, api.fitz, etc.) que
aplican @middleware(fn).
Hoy ninguna pieza de eso compila. fitzwatch tuvo que:
- Mover el shape fn(req: Request) -> ... al mismo módulo del
handler (duplicación de get_client_ip + locale_from_req en 2
módulos consumidores) — mitigaba síntoma 2.
- Pero el síntoma 1 seguía vivo: el codegen rechazó las fns con
return 429 {...} definidas en rate_limit.fitz aunque solo
iban a usarse como middleware cross-module.
Workaround final: abandonar @middleware cross-module y agregar
el check inline en cada handler usando @header(name="X-Real-IP") +
@header(name="X-Forwarded-For") + un helper que recibe Str?, Str?
(sin tipo Request). Cuesta ~6 líneas de boilerplate por endpoint
sensible × 7 endpoints = ~42 LoC duplicadas en fitzwatch.
Causa raíz (hipótesis del codegen)¶
Análoga al W18 (cross-module observability, ya cerrado): cuando
@middleware activa la emisión de built-ins (Request, RequestData,
__FitzResponse para los status returns), el codegen los emite en
main.rs y agrega use crate::{...} solo donde explícitamente sabe
que se usan. Los módulos importados que también los referencian
quedan sin import.
Paralela también al W12 / W17 / B10 — toda la familia "el codegen procesa cada módulo aisladamente y no propaga decoradores / built-ins asociados cross-module".
Fix propuesto del codegen¶
-
Pre-scan cross-module de
@middlewarereferenciado: cuando el codegen ve@middleware(foo)aplicado a un handler en módulo X, resolver el módulo de origen defooy marcar ese módulo comouses_middleware = true. La regla "return status solo en handler/middleware del mismo crate root" se relaja a "en handler o en fn que está marcada como middleware (sea local o referenciada cross-module)". -
Emisión condicional de
Request+use crate::{...}en módulos hijos (paralelo a W18 / W11 / W16): siuses_middleware = true, emitiruse crate::{Request, RequestData}al tope del.rsdel módulo + las helper fns siguen siendo accesibles. -
Tests E2E nuevos en
tests/compile_e2e.rs: cross_module_middleware_fn_compila_a_binario_nativocross_module_middleware_fn_con_request_arg_compilacross_module_middleware_fn_con_return_status_compila
Workaround user-land (FUNCIONA, pero verboso)¶
Lo que hicimos en fitzwatch:
1. rate_limit.fitz NO usa Request en ningún sig — solo Str /
Str? / Int. Helper compute_client_ip(x_real: Str?, xff: Str?)
-> Str recibe headers como params primitivos.
2. Cada handler con rate limit acepta @header(name="X-Real-IP",
into="x_real_ip") + @header(name="X-Forwarded-For", into=
"x_forwarded_for") + computa la IP inline.
3. Match sobre check_rate_limit_by_ip(...).await: Err → return
429 { "error": "..." }. El return 429 vive en el handler
directo, no en un middleware aparte.
Costo: 4 lineas extra de @header + 4 lineas de gate por handler.
Verboso pero ships hoy.
Impacto¶
- fitzwatch: 7 endpoints con boilerplate duplicado. Refactorable
a
@middlewarelimpio cuando se cierre la deuda. - Cualquier SaaS Fitz HTTP: el patrón "módulo dedicado a cross-cutting concerns" (rate limit, auth custom, audit log, etc.) está vetado. Encarece sustancialmente el costo de mantener un proyecto Fitz HTTP de producción.
- Sentido de fase: este es probablemente el patrón #1 que la guía sugiere implícitamente como "best practice" pero el codegen no soporta. Es un blocker de adopción Fitz para SaaS reales.
Estimación de fix¶
- Pre-scan
@middlewarecross-module + propagar marker: ~50 LoC ensrc/codegen.rsparalelo apre_scan_imported_auth_provider(W12) +extract_background_fn_names(B10). - Emisión condicional de
Request+use crate::{...}en módulos hijos: ~30 LoC engenerate_module_rs_with_bindingsparalelo a W11 / W16 / W18. - Tests E2E: ~3 tests nuevos × ~80 LoC c/u = ~240 LoC.
- Total estimado: 1 día de trabajo + smoke en fitzwatch.
Referencias del descubrimiento¶
- Fitzwatch sesión 2026-06-26 implementando "rate limiting endpoints públicos" (deuda residual de Fase G del ROADMAP fitzwatch).
- Repro: fitzwatch en commit
<próximo>que documenta el workaround inline enrate_limit.fitz+ comentarios enauth.fitz/subscriptions.fitz.
🟢 http.request({...headers: Map<Str, Str>, body: Map...}) desde async fn rompía Send via spawn(...) — CERRADO v0.19.4 (2026-06-23)¶
CERRADO el mismo día del descubrimiento. Fix mínimo en
src/codegen.rs::gen_http_request_opts(~10 LoC del format! snippet del fieldheaders) + 1 unit test + 4 E2E tests cubriendo los 4 casos sugeridos (headers Map + body Map + cross-module + regression v0.18.1). Sin cambio del contrato del builtin — programas existentes compilan bit-a-bit. fitzwatch deploy 2026-06-23 puede activar el refactorsmtp.send → http.requestResend REST y destrabar welcome email + incident notify outbound. Detalle abajo preservado para referencia futura.
Síntoma (3 errors rustc en fitz build)¶
error: future cannot be sent between threads safely
--> src/checks.rs:353:95
|
353 | = tokio::spawn(async move {
notify_subscribers_incident_opened(monitor.clone(), 0i64, ...).await
});
^^^^^^^^^^^^^ future created by async block is not `Send`
|
= help: within `{async block@src/checks.rs:353:...}`, the trait
`std::marker::Send` is not implemented for
`std::sync::MutexGuard<'_, Vec<(String, String)>>`
note: future is not `Send` as this value is used across an await
--> src/subscriptions.rs:384:982
|
384 | ... ((Arc::new(Mutex::new(vec![
(String::from("Authorization"), format!("Bearer {}", api_key.clone())),
(String::from("Content-Type"), String::from("application/json"))
]))).lock().unwrap().clone()), body_bytes: { ... }, ... }).await ...
| ----------------------------- has type
`MutexGuard<Vec<(String, String)>>` which is not `Send`
... ^^^^^ await occurs here, with
`(...).lock().unwrap()` maybe used later
3 errors derivados de la misma raíz:
src/checks.rs:353—spawn(notify_subscribers_incident_opened(...))rompe Send.src/incidents.rs:332—spawn(notify_subscribers_incident_closed(...))rompe Send.src/main.rs:6395(E0277) —__handler_subscribeno implementaaxum::Handlerporque su future (que indirectamente llama asend_email_to) deja de ser Send.
Repro mínima¶
// modulo: emails.fitz
async fn send_email(to: Str, api_key: Str) -> Result<Bool> {
let resp = http.request({
"url": "https://api.example.com/emails",
"method": "POST",
"headers": {
"Authorization": "Bearer {api_key}",
"Content-Type": "application/json"
},
"body": {
"to": to,
"subject": "test",
"html": "<p>hi</p>"
}
}).await?
return Ok(true)
}
// modulo: notify.fitz
from emails import send_email
@background
async fn notify(to: Str) -> Null {
let _ = send_email(to, "re_xxx").await
return null
}
// modulo: main.fitz
from notify import notify
@get("/test/{to}")
async fn trigger(to: Str) -> Result<Null> {
spawn(notify(to))
return Ok(null)
}
@server(3000)
fn main() => 0
fitz build → error: future cannot be sent between threads safely con el
mismo MutexGuard cross-await que en producción.
Causa raíz¶
El codegen, al ver un Map literal {"Authorization": "Bearer {key}", ...} en
el kwarg headers (o body Map) de http.request(opts), emite:
(Arc::new(Mutex::new(vec![
(String::from("Authorization"), format!("Bearer {}", api_key.clone())),
(String::from("Content-Type"), String::from("application/json"))
])))
.lock() // ← MutexGuard nace acá
.unwrap()
.clone() // ← clone OK pero el guard sigue vivo hasta `;` del stmt
El stmt envolvente es el field assignment del struct literal del request
(__FitzHttpRequestOpts { headers: <expr>, body: <expr>, ... }), y el
.await del request ocurre DENTRO del mismo statement. Resultado: el
MutexGuard atraviesa el await point → el future generado no es Send →
tokio::spawn(...) rechaza.
El bug no es de http.request per se — es del patrón general "Map
literal en kwarg de builtin async dentro de async fn que será spawneada".
La estructura del codegen para Maps siempre genera Arc<Mutex<Vec<...>>>,
y .lock().unwrap().clone() para extraer el valor mantiene el guard vivo.
Patología análoga (precedente cerrado)¶
Bug cerrado en v0.18.1 (cosecha post-fitzwatch sesión 2, 2026-06-20):
"for x in <List<Str>> con .await adentro de @cron rompe Send" —
mismo problema raíz (MutexGuard del lock de List<Str> cross-await en
context que después se spawnea). La solución fue extraer el guard a un
binding intermedio que se dropea antes del await:
// Antes (rompía Send):
for __x in (xs.clone()).lock().unwrap().iter() { ... .await ... }
// Después (Send OK):
let __snapshot: Vec<_> = (xs.clone()).lock().unwrap().clone();
for __x in __snapshot.iter() { ... .await ... }
Este bug nuevo es el caso análogo para Map literal en kwargs de builtin async.
Fix propuesto del codegen¶
Cuando el codegen emite código que extrae valor de un Arc<Mutex<...>>
para pasarlo como argumento a una expresión que contiene .await, debe
clonar a una variable local intermedia que termine ANTES del await:
// Patrón actual (rompe Send):
__fitz_http_request(__FitzHttpRequestOpts {
headers: (Arc::new(Mutex::new(...))).lock().unwrap().clone(),
body: ...
}).await
// Patrón correcto (Send OK):
let __headers_snap = {
let __h = Arc::new(Mutex::new(vec![...]));
let __g = __h.lock().unwrap();
__g.clone()
}; // ← guard se dropea acá
__fitz_http_request(__FitzHttpRequestOpts {
headers: __headers_snap,
body: ...
}).await
El bloque interior { let __g = ...; __g.clone() } garantiza que el
MutexGuard se dropea al cierre del bloque (antes de que la expresión
participe en el .await).
Aplicación: cualquier Map literal o List literal que se emita como
argumento de una llamada async (o como field de un struct literal cuyo
container es argumento de una llamada async). Probable que también aplique
a body Map (no solo headers).
Tests sugeridos al fixear¶
tests/compile_e2e.rs::http_request_with_headers_map_spawn_compila— repro mínima de arriba, validar que compila sin errors.tests/compile_e2e.rs::http_request_with_body_map_spawn_compila— mismo pero con body Map heterogéneo en lugar de headers.tests/compile_e2e.rs::http_request_anidado_en_async_fn_cross_module_spawn— variante consend_emaildeclarada en módulo importado (matchea fitzwatch real).- Regression sobre el case análogo de
for x in List<Str>con.awaiten@cron(v0.18.1) — asegurar no se rompe.
Workaround user-land (NO funciona, documentado para claridad)¶
Probados sin éxito:
- Bindear
let opts = {...}antes delhttp.request(opts)— el codegen inline-a igual y el guard sigue cross-await. - Bindear cada Map por separado (
let headers = {...}; let body = {...}) — mismo resultado. - Construir el body como Str JSON manual +
http.post(url, body_str)— evita el body Map pero NO permite headers custom (Resend exige Authorization Bearer header). - Eliminar el spawn del caller y volver a
.awaitdirecto — funciona el build pero pierde fire-and-forget (handler bloquea hasta que la llamada HTTP completa).
No hay forma de pasar Authorization header custom a Resend sin Map
literal en headers desde Fitz user-land. Cualquier alternativa
require que el codegen del lenguaje cierre este bug.
Impacto en producción¶
- fitzwatch (deploy 2026-06-23): subscribers públicos persisten en DB pero NO reciben welcome email. La UI dice "Listo! Revisá tu inbox" y el email nunca llega. Misma situación para notify de incidents — los webhooks funcionan, los emails no.
- Cualquier proyecto Fitz que necesite HTTP outbound con auth Bearer (la mayoría de APIs REST modernas) desde un context spawned. Cubre prácticamente todo: Stripe, Twilio, SendGrid, Mailgun, OpenAI, etc.
- Trigger del descubrimiento: bloqueo SMTP outbound de DigitalOcean
obligó migrar de
smtp.sendbuiltin (que NO usa Map literal pesado en spawn — su codegen ya cierra el guard inmediatamente) ahttp.request(opts)REST. Probable que muchos providers tengan bloqueo SMTP por default y este path sea el único viable.
Referencias del descubrimiento¶
- Sesión 2026-06-23: deploy de fitzwatch.com en VPS DigitalOcean (droplet shared con citai/syndesi).
- DigitalOcean bloquea outbound a smtp.resend.com:587/465/2587/2465
(confirmado con
nc -zv— timeout en todos los puertos SMTP, port 443 HTTPS OK). - Refactor de
subscriptions.fitz::send_email_topara usar Resend HTTP API REST (https://api.resend.com/emails) con Bearer auth →fitz checkpasa OK perofitz buildfalla con los 3 errors de arriba.
Cierre estimado (pre-fix)¶
Fix probable en src/codegen.rs — funciones que emiten args para
builtins async (gen_http_request_call, gen_http_post_call,
gen_smtp_send, gen_jwt_encode, posiblemente otros). Patrón uniforme:
wrappear .lock().unwrap().clone() en bloque que drop el guard. Estimado
~50-100 LoC + 4 tests E2E. Compatible con la solución de v0.18.1.
Fix aplicado (v0.19.4, 2026-06-23)¶
Auditoría del codegen reveló que el bug estaba acotado a UN solo sitio:
src/codegen.rs::gen_http_request_opts field headers línea 18569. Los
otros builtins async pasaron auditoría limpia:
gen_smtp_send_opts(smtp.send): el opts struct emitido lleva solo fieldsOption<String>(to/from/subject/body_text/body_html). Sin Map. Sin.lock().unwrap().clone()inline. ✓gen_http_body_marshal(body Map) :body_codese pasa comoArc<Mutex<Vec<...>>>al helper__fitz_http_body_from_map_str_strque toma ownership, lockea internamente y dropea el guard antes de retornar(Option<Vec<u8>>, Option<&'static str>). El guard NO cruza el await. ✓gen_http_body_marshal(Type::Bytes): representación esVec<u8>plano (no Arc). .clone()produce Vec sin guard. ✓- Resto del codegen del lenguaje: TODAS las apariciones de
.lock().unwrap().clone()(1 inline restante en comprehension chain - ~40 en
let __X = ... .clone();patterns) usan binding intermedio que dropea el guard en el;. ✓
Cambio mínimo del fix: format!("(({}).lock().unwrap().clone())", c)
→ format!("{{ let __headers_snap: Vec<(String, String)> = ({}).lock().unwrap().clone(); __headers_snap }}", c).
El let statement drop el MutexGuard temp en el ; antes de que el
block return el Vec, mismo patrón que let __for_snap = xs.lock().
unwrap().clone(); del fix de v0.18.1 para for x in List<Str> adentro
de @cron.
Tests nuevos:
src/codegen.rs::tests::v019_4_http_request_headers_map_emits_snapshot_binding_for_send— unit test sobre el snippet emitido en el callsite del struct literal__FitzHttpRequestOpts { ... }. Asegura presencia dellet __headers_snap: Vec<(String, String)>en el fieldheaders:del struct.tests/compile_e2e.rs::v019_4_http_request_with_headers_map_spawn_compila— repro mínima single-file:@background async fn+http.requestcon headers Map literal + handler que hacespawn(notify(...)). Pre-fix:fitz buildabortaba conMutexGuard<Vec<(String, String)>> not Send.tests/compile_e2e.rs::v019_4_http_request_with_body_map_spawn_compila— mismo case pero conbodyMap. Asegura que el path del body marshaling (que ya cerraba el guard internamente vía __fitz_http_body_from_map_str_str) no introdujo regresión bit-a-bit.tests/compile_e2e.rs::v019_4_http_request_cross_module_spawn_compila— variante consend_emaildeclarada en módulo importado (matchea el shape real de fitzwatch). Combinación del fix de v0.19.2 (cross- module spawn emite .await) + v0.19.4 (headers Map no rompe Send).tests/compile_e2e.rs::v019_4_regression_v018_1_for_list_str_await_in_cron_no_send_break— regression sobre el case análogo de v0.18.1 (for x in List<Str>con.awaiten@cron). Asegura que el nuevo fix no introduce regresión en el patrón paralelo del for loop.
Deudas residuales derivadas (NO bloquean uso real):
bodyMap heterogéneo (Map) : el path solo soportaMap<Str, Str>strict + Instance + Bytes. Heterogéneos requieren__FitzValueintegration (deuda mayor del codegen, paralelo ajwt.encode). Para Resend API, todo el payload (to/subject/html) es Str, así que NO afecta.http.requestconoptscomo variable (no Map literal): el MVP exige Map literal en el callsite para validación estática shape. Pasarlet opts = {...}; http.request(opts)→ error claro de codegen. Refinable si entra demanda real.
🟢 LSP false positive: @auth_provider cross-module no se resuelve — CERRADO 2026-06-23¶
CERRADO el mismo día del descubrimiento (sesión 2026-06-23, smoke real del fix v0.19.2 sobre fitzwatch). Fix ~280 LoC netos en
src/lsp.rs+src/bin/fitz-lsp.rs+ 5 unit tests nuevos enlsp::tests::cross_module_*. Sin cambio de comportamiento del lenguaje — el binario producido porfitz build/fitz check/fitz runya resolvían cross-module via W12 (auth_provider) y B10 (background fns); este fix replica esa misma pre-scan en el LSP path que corría aislado.
Síntoma original (pre-fix): cuando un módulo importa un
@auth_provider declarado en OTRO módulo (patrón W12 cross-module), el
LSP emite el diagnostic:
aunque el @auth_provider SÍ existe en el grafo de imports y el binario
compila + ejecuta sin issues. Ejemplo canónico en fitzwatch:
// auth.fitz
@auth_provider
async fn current_user(headers: Map<Str, Str>) -> Result<User> { ... }
// incidents.fitz
from auth import User
import auth
@authenticated // ← pre-fix: el LSP emitía falso positivo
@put("/api/incidents/{id}/status")
async fn update_incident_status(...) -> Result<IncidentUpdated> { ... }
Causa raíz (confirmada): src/lsp.rs::check_source_with_types
corría check_program(&program) sobre el módulo abierto en aislamiento,
sin cargar el grafo de imports. El checker local NO veía el
@auth_provider declarado en auth.fitz y emitía el diagnostic falso.
El codegen (src/codegen.rs::pre_scan_imported_auth_provider_for_loader)
y el CLI (src/main.rs::pre_scan_imported_auth_provider) sí cargaban
el grafo entero, por eso la build real andaba.
Casos cerrados con este fix:
@authenticatedcross-module (caso más común — handler en módulo por feature + provider enauth.fitz).@admincross-module (mismo flow, regla extra derole: Str).@requires("role")cross-module (Fase 9.w.1.iter2.a, mismo patrón).spawn(<imp_fn>(...))con@backgroundcross-module (B10) — el mismo path resolvía esto en el checker pero estaba inaccesible desde el LSP; ahora el LSP lo pre-scanea también.@ws(...)+@authenticatedapilados (mismo path del checker — cubierto por la misma fallback encollect_auth_provider).
Fix aplicado:
src/lsp.rs— wrapper nuevocheck_source_with_types_and_base_dir(source, base_dir: Option<&Path>)con la firma de 5-tupla idéntica acheck_source_with_types. Cuandobase_diresSome:- Corre
resolve_program_with_envpara pre-poblar nominales locales. - Helper privado
pre_scan_imported_auth_provider_lspwalksStmt::Import/Stmt::FromImport, resuelve cada módulo a un.fitzrelativo abase_dir, parsea, invocatypes::extract_auth_provider_signature(la misma API pública usada por W12 en main.rs/codegen.rs) y populaenv.set_imported_auth_provider. - Helper análogo
pre_scan_imported_background_fns_lspinvocatypes::extract_background_fn_names(B10) y populaenv.add_imported_background_fns. - Llama a
types::check_with_envcon el env enriquecido. - El
check_source_with_types(source)legacy queda como wrapper que pasaNone(compat con callers sin file context: REPL, unit tests internos). src/bin/fitz-lsp.rs::check_and_publish— derivabase_dirdel open document viauri.to_file_path().parent()y lo pasa al nuevo wrapper. Fallback transparente a single-file mode cuando la URI no esfile://.- Política de error: silent fallback sobre módulos que fallan
lectura/parse (paralelo a
pyi_loader::load_stubsy W12 en main.rs). El LSP enriquece el env; el codegen/runtime loader es quien reporta los errores reales. - Alcance del MVP: un nivel de profundidad (sin recursión sobre
transitive imports), paralelo a W12/B10. Cubre el 90% del caso
(handler-per-feature + provider en
auth.fitz). No consultadep_registry(el LSP no tiene acceso al manifest, paralelo limitación deresolve_cross_module_definitionyfrom_import_completions).
Tests nuevos (5, en lsp::tests::cross_module_*, feature lsp):
cross_module_auth_provider_resuelve_via_base_dir_sin_diagnostic_falso— canónico: SINbase_diraparece el falso positivo, CONbase_dirdesaparece.cross_module_admin_decorator_resuelve_via_base_dir— variante@adminconrole: Strextraído del módulo origen víahas_role_field.cross_module_requires_decorator_resuelve_via_base_dir— variante@requires("role")(Fase 9.w.1.iter2.a).cross_module_background_spawn_resuelve_via_base_dir— variante@background+spawn(<imp>(...))(B10), confirma que el mismo pipeline cierra ambos diagnostics.cross_module_pre_scan_silent_fallback_on_missing_module— robustez: si el módulo importado no existe en disco, el pre-scan skipea silencioso sin contaminar la lista de errores.
Verificación pre-bump: cargo fmt --all limpio; cargo clippy
--lib --tests --bins --features lsp -- -D warnings limpio; suite LSP
completa 131/131 verde; full lib suite 3331/3331 verde.
Deudas residuales derivadas (NO bloquean):
- Imports transitivos: si
posts.fitzimportalib.fitzque a su vez importa el provider deauth.fitz, el LSP no lo encuentra (recursión un nivel solo). Misma limitación que W12 en main.rs. Refinable si aparece demanda real. - Dep registry: imports vía
[dependencies]defitz.tomlno resuelven en el LSP (path deps con relative paths sí). Mismo gap quefrom_import_completions. Cuando se cierre, ataca los tres consumidores juntos. - Cache: cada keystroke re-parsea TODOS los módulos importados del grafo. Acceptable para programas chicos (<10 imports). Si presión real aparece sobre proyectos grandes, agregar cache con invalidación por modtime.
- B20 —
@cron(store=X)cross-module en LSP: tiene la misma patología (referencia a var declarada en otro módulo). Sale del scope del fix actual — ataque cuando se cierre B20 entero.
Referencias:
- Reportado en sesión 2026-06-23 durante smoke real del fix v0.19.2 sobre fitzwatch (apps multi-módulo).
src/lsp.rs::check_source_with_types_and_base_dir(entry nuevo).src/lsp.rs::pre_scan_imported_auth_provider_lsp/pre_scan_imported_background_fns_lsp(helpers nuevos).src/bin/fitz-lsp.rs::check_and_publish(consumer del nuevo entry).- Paridad con:
src/main.rs::pre_scan_imported_auth_provider(W12) /pre_scan_imported_background_fns(B10). - Cap 22 de
docs/guide.mdactualizado con el bullet "Diagnostics en vivo" mencionando la pre-scan cross-module.
🟢 spawn(<cross_module_@background_async_fn>(...)) silent drop — CERRADO v0.19.2 (2026-06-23)¶
CERRADO en el release v0.19.2 (mismo día del cierre técnico). Fix mínimo (~10 LoC) en
collect_module_sigs+ 1 E2E test nuevo que candea la regresión inspeccionando el Rust emitido. Detalle abajo preservado para referencia futura.
Síntoma original (pre-fix): cuando spawn(...) recibía un call a
una fn @background declarada en otro módulo (importada vía
from foo import bar), el runtime aceptaba la línea pero nunca
invocaba la fn. Sin error, sin panic, sin log — silent drop.
Repro mínima validada:
worker.fitz:
main.fitz:
from worker import do_work
@get("/trigger/{id}")
async fn trigger(id: Int) -> Result<Str> {
let _ = spawn(do_work(id)) // pre-fix: NO disparaba el log
return Ok("dispatched")
}
Causa raíz (descubierta al inspeccionar el Rust emitido en
target/fitz-build/<bin>/src/main.rs): el closure de tokio::spawn
se emitía SIN .await:
do_work es async → do_work(id) devuelve un Pin<Box<dyn Future>>
que el async move {} envuelve y dropea sin pollar. Silent drop.
Por qué solo cross-module: el path local en pre_register_fn_signatures
(Fase 6.6, src/codegen.rs:12423-12427) YA envolvía el ret en
Type::Future(...) cuando is_async = true. El path cross-module en
collect_module_sigs (src/codegen.rs:5561-5566) no replicaba ese
wrap. gen_spawn_call decide si emite .await con
matches!(target_ret, Type::Future(_)) — para cross-module veía
sig.ret = Type::Null (no Type::Future(Null)) y omitía el .await.
Fix (src/codegen.rs:5536-5570, ~10 LoC netas):
- Stmt::FnDef desestructura is_async además de los otros campos.
- Variable renombrada ret → inner_ret.
- Wrap aplicado cuando is_async = true:
pre_register_fn_signatures y
el modo en que el bug se manifiesta (silent drop).
Test E2E nuevo (tests/compile_e2e.rs::cross_module_spawn_async_background_emits_await_no_silent_drop):
build a 2 archivos + inspección directa del Rust emitido en
<workspace>/target/fitz-build/<stem>/src/main.rs. Confirma que
contiene do_work(id).await y NO tokio::spawn(async move { do_work(id) })
bare.
Smoke real validado bit-a-bit (curl al binario nativo): el log
estructurado {"msg":"worker.start id=42"} aparece en stderr
inmediatamente después del response 200 dispatched. Pre-fix el log
nunca aparecía.
Validación sin regresiones:
- cargo test --lib --release 3200/3200 verde.
- Smoke GUIDE_EXAMPLES_COMPILE ~370 ejemplos guía+curso+TaskHub
verde (~14 min).
- fitz check sobre los 10 boilerplates verde.
- cargo fmt --all --check limpio.
- cargo clippy --lib --tests --bins -- -D warnings limpio.
LSP: el false positive paralelo en el LSP (mensaje "spawn: fn X
is not declared with @background" en imports) ya estaba cerrado
por B10 (sub-paso 5 de la cosecha post-fitzwatch 2026-06-19,
extract_background_fn_names + TypeEnv::add_imported_background_fns).
Ese fix cerró el path del checker; v0.19.2 cierra el path del
runtime/codegen. Los dos paths reportaban síntomas distintos del
mismo gap: el checker bloqueaba fitz check/fitz build, el
codegen dropeaba silently. Ambos paths quedan paralelos y consistentes.
Hallado durante: integración real con un proyecto del autor que
usaba @background cross-module para bulk SMTP. La fn aparecía en
el código, compilaba limpio, pero los emails nunca se enviaban. El
workaround temporal era .await directo (perdía fire-and-forget,
bloqueaba el handler) hasta que cerró v0.19.2.
🟢 Response { ... } built-in: 3 bugs del Bloque 3.c detectados en fitzwatch (CERRADO v0.19.1, 2026-06-21)¶
CERRADO ENTERO en el release v0.19.1 (junio 2026, mismo día del descubrimiento). Bug 1 (cross-module imports) + Bug 2 (signature mismatch en
Result<Response>con?propagation) + Bug 3 (metrics::*not found, era incidental al Bug 1) los 3 fixeados + 3 E2E tests nuevos cubren cada caso. Detalle abajo preservado para referencia futura.
Resumen del cierre (2026-06-21):
- Bug 1 fix: nuevo walker
program_uses_response_builtin(program)ensrc/codegen.rs(~140 LoC, paralelo aprogram_uses_db/program_uses_http_client); engenerate_module_rs_with_bindingsse agrega bloque condicional que emiteuse crate::{Response, ResponseData};cuando el módulo los necesita (paralelo a W11 DB / W16__FitzResponse/ HTTP client). - Bug 2 fix:
gen_top_fn(~13436) ahora consultadetect_response_builtin_kind(&effective_ret, env)(helper de Bloque 3.b ya existente) antes de computarhas_return_status. Si el handler retorna Response built-in,body_has_tryNO activaresponse_mode— la user-fn conserva su signature natural (-> Result<Arc<Mutex<ResponseData>>, String>o-> Arc<Mutex<ResponseData>>) ygen_tryusa el?nativo Rust (válido porque el container ES Result). - Bug 3: cerrado automático con Bug 1 — era el mismo error rustc cross-module enmascarando los demás. El test E2E lo verifica.
- 3 E2E tests nuevos en
tests/compile_e2e.rs:v019_response_cross_module_emits_imports,v019_response_in_result_ok_signature_matches_wrapper,v019_response_with_auth_db_ws_observability+ helpersbuild_expect_okybuild_expect_ok_multi(validan build success sin invocar el binario, útil para HTTP servers). - Verificación pre-bump completa: 3200 lib tests + 98 cli_e2e
- 3 openapi_e2e + smoke 370+ ejemplos guía + 5 boilerplates representativos (api-orm-full-fullstack multi-file incluido) + fmt + clippy (default + lsp) — todo verde, sin regresiones.
Próximo norte: notificar al autor para retomar fitzwatch Fase F.d (30 min). Detalle completo en CHANGELOG v0.19.1.
Detalle histórico de los 3 bugs (preservado para referencia)¶
Contexto del descubrimiento (2026-06-21) (pre-cierre): Bloqueaba el caso integrador real (fitzwatch Fase F.d) que estresa el feature con auth + DB + WS + observability + cross-module. El ejemplo
examples/guide/17l-response-custom.fitzcompila OK porque es CLI HTTP simple standalone — los 3 bugs solo aparecían al usar Response built-in en proyectos reales.
Contexto del descubrimiento (2026-06-21): fitzwatch (producto del
autor, status page open-source — repo PRIVADO en
github.com/Thegreekman76/fitzwatch) tenía Fase F.d (RSS feed por
slug) bloqueada por la deuda HTTP HandlerOutcome.content_type. Esa
deuda se cerró con v0.19.0 (Response { ... } built-in). Al intentar
retomar F.d con la sintaxis nueva, detectamos 3 bugs del Bloque 3.c
del codegen que NO están cubiertos por los E2E del release. fitzwatch
es el primer stress test real del feature.
Estado en fitzwatch:
- Código F.d entero implementado (helpers escape_xml,
fetch_rss_items, build_rss_xml, type RssItem, handler
slug_incidents_rss con shape idiomatic -> Result<Response> + ?,
frontend RSS link + JS wire). Handler comentado en
d:\fitzwatch\src\public.fitz hasta que estos 3 bugs cierren.
- Container actual fitzwatch-app corre con FITZ_TAG=v0.18.2
(stable, F.d off — handler ausente).
- Cuando los 3 bugs cierren: bump FITZ_TAG en .env, descomentar
handler, docker compose up -d --build app, smoke con curl + RSS
reader. 30min de trabajo.
Bug 1 — Cross-module: Response/ResponseData no se importan en módulos hijos¶
Síntoma: handler con -> Response o -> Result<Response> declarado
en un módulo importado (public.fitz) genera src/public.rs
referenciando Response y ResponseData (líneas tipo
<Response as __ToFitzJson>::__to_fitz_json(&(Arc::new(Mutex::new(ResponseData { ... })))))
pero NO emite use crate::Response; use crate::ResponseData; en el
preludio del módulo.
Error rustc:
error[E0425]: cannot find type `Response` in this scope
--> src/public.rs:671:53
|
= help: consider importing this type alias: use crate::Response;
error[E0422]: cannot find struct, variant or union type `ResponseData` in this scope
--> src/public.rs:671:117
|
= help: consider importing this struct: use crate::ResponseData;
Causa probable: el preludio HTTP del codegen
(emit_helpers_for_imported_types/equivalente en src/codegen.rs)
emite use crate::{...} para tipos detectados como ORM virtual fields
(W17), __FitzResponse, __ToFitzJson, etc. pero no para Response
ni ResponseData cuando el módulo importado los usa.
Fix esperado: paralelo bit-a-bit al patrón W17 ORM cross-module.
Detectar si el módulo importado usa el Response built-in (
stmt_uses_response_builtin walker análogo a stmt_uses_python/
stmt_uses_cron que ya existen) y agregar
use crate::{Response, ResponseData}; al preludio del módulo.
Bug 2 — Signature mismatch en Result<Response> (path InResultOk del Bloque 3.c)¶
Síntoma: el wrapper axum del handler detecta el Response built-in
y emite el dispatch nuevo (Bloque 3.c) esperando que la user-fn devuelva
Result<Arc<Mutex<ResponseData>>, String> (mirror de Result<Response>
Fitz). Pero la user-fn se emite con shape LEGACY que devuelve
__FitzResponse directo. Inconsistencia interna del codegen.
Código emitido (capturado en d:\fitzwatch\target\fitz-build\main\src\main.rs:5665-5727):
// User-fn — shape LEGACY (E I R R Ó N E O)
async fn slug_incidents_rss(mut slug: String) -> __FitzResponse {
let mut user_id: i64 = (match ((slug_to_user_id(slug.clone())).await) {
Ok(__v) => __v,
Err(__e) => return __FitzResponse {
status: 500,
body: serde_json::json!({"error": __e}),
},
});
if (user_id == 0i64) {
return __FitzResponse {
status: 200, // ⚠️ EMITE 200 con body JSON del Response
body: <Response as __ToFitzJson>::__to_fitz_json(&(Arc::new(Mutex::new(ResponseData {
status: 404i64,
content_type: String::from("text/plain; charset=utf-8"),
headers: Arc::new(Mutex::new(Vec::new())),
body: String::from("slug not found"),
body_bytes: None
}))))
};
};
// ... etc, todos los returns siguen el shape legacy ...
}
// Wrapper axum — shape NUEVO (CORRECTO según Bloque 3.c, espera Result<Response>)
async fn __handler_slug_incidents_rss(...) -> axum::response::Response {
let __result = slug_incidents_rss(slug).catch_unwind().await;
let __built = match __result {
Ok(__resp_arc) => {
// extract status/ct/headers/body/body_bytes del Arc<Mutex<ResponseData>>
let (__status, __ct, __headers, __body, __bbytes) = {
let __g = __resp_arc.lock().unwrap();
(__g.status, __g.content_type.clone(), ...)
};
// axum builder con Content-Type custom + headers + body|body_bytes
...
},
Err(__e) => (INTERNAL_SERVER_ERROR, Json({"error": __e})).into_response(),
};
...
}
Error rustc:
error[E0308]: mismatched types
--> src/main.rs:5739:9
|
5738 | let __built = match __result {
| -------- this expression has type `__FitzResponse`
5739 | Ok(__resp_arc) => {
| ^^^^^^^^^^^^^^ expected `__FitzResponse`, found `Result<_, _>`
Causa probable: el codegen tiene DOS lugares que deciden el shape
del Response built-in:
1. Wrapper (emit_handler_dispatch_and_response con su helper
emit_response_builtin_dispatch) — funciona correcto.
2. User-fn (gen_top_fn / wherever the body of the user fn is
emitted) — NO consulta HandlerSig.response_builtin_kind y cae
a path legacy (emite -> __FitzResponse con __ToFitzJson).
Fix esperado: cuando HandlerSig.response_builtin_kind ==
ResponseBuiltinKind::InResultOk (o Direct), la user-fn debe
emitirse con signature -> Result<Arc<Mutex<ResponseData>>, String>
(o -> Arc<Mutex<ResponseData>> para Direct), y los returns deben
emitir Ok(Arc::new(Mutex::new(ResponseData { ... }))) /
Err(format!(...)) paralelo al evaluador del intérprete.
Bug 3 — metrics::counter!/histogram! not found con mezcla Response + auth + DB + WS + observability¶
Síntoma: handler -> Response direct (path Direct del Bloque 3.c)
en programa con auth + DB + WS + observability rompe con E0433 cannot
find counter in metrics sobre las líneas del access log del wrapper
HTTP que emite el codegen automático (Fase 12.3.b):
metrics::counter!("http_requests_total", &__labels).increment(1);
metrics::histogram!("http_request_duration_seconds", &__labels).record(__duration_secs);
Lo extraño:
- Cargo.toml emitido tiene metrics = "0.24".
- Verificado en ~/.cargo/registry/src/.../metrics-0.24.6/src/macros.rs:
counter! y histogram! están #[macro_export] (accesibles como
metrics::counter!).
- Ejemplo oficial v0.19.0 examples/guide/17l-response-custom.fitz
(CLI HTTP simple standalone con Response built-in) compila OK con
ese mismo Cargo.toml + macros.
- fitzwatch BASE (sin handler RSS) compila OK con v0.19.0.
- Aparece SOLO al sumar handler Response built-in al programa
con auth + DB + WS + observability. El programa entero queda con
E0433.
Causa por investigar: interacción de paths del codegen. Hipótesis
candidatas:
- El preludio HTTP se emite DUPLICADO cuando hay módulos importados +
Response built-in, y la segunda emisión shadowea algo del metrics.
- Algún use crate::* glob importa un símbolo metrics (variable
o módulo) que collisiona con la crate dep.
- Bug del macro resolver de rustc con tokio multi-thread + features
específicos.
Repro mínimo recomendado (sumar al test suite del lenguaje): handler
-> Response direct en programa con @auth_provider + db.connect +
@ws("/x") + @server(observability=true) (default). Build debe
emitir todas las macros metrics::* resolubles.
Plan de fix sugerido para los 3 bugs¶
- Reproducir aislado en
tests/compile_e2e.rslos 3 casos con programas mínimos: v019_response_cross_module_emits_imports: Response builtin en módulo importado confrom main import+ handler en módulo + el emittedsrc/<mod>.rsdebe teneruse crate::{Response, ResponseData};.v019_response_in_result_ok_signature_matches_wrapper: handler-> Result<Response>en single-file. La user-fn DEBE compilar contra el wrapper. (Caso integrador del Bloque 3 que no estaba.)v019_response_with_auth_db_ws_observability: handler-> Responsedirect mezclado con auth + DB + WS + observability. Reproduce Bug 3.- Fix Bug 1: walker
stmt_uses_response_builtinanálogo astmt_uses_python/stmt_uses_cron. Preludio cross-module emiteuse crate::{Response, ResponseData};cuando aplica. - Fix Bug 2: actualizar el codegen de la user-fn (signature +
return wrap) cuando
HandlerSig.response_builtin_kind != None. Paralelo bit-a-bit a lo que ya hace el wrapper en Bloque 3.c. - Investigar Bug 3: revisar el emitted Rust de fitzwatch para identificar la causa raíz. Si es interacción del codegen, probablemente fix incidental con Bug ½. Si no, gating o reorden del preludio.
- Release
v0.19.1con los 3 fixes + 3 E2E nuevos. Documentar en CHANGELOG + cierre acá. Bump CHANGELOG/roadmap/CLAUDE.md/extensión. - Notificar al autor para retomar fitzwatch Fase F.d (30min de
trabajo: bump
FITZ_TAGen.env, descomentar handler enpublic.fitz,docker compose up -d --build app, smoke con curl + RSS reader externo como Feedly).
Referencias¶
- Repo del producto bloqueado:
d:\fitzwatch\src\public.fitz(handler comentado al final del archivo) +d:\fitzwatch\ROADMAP.md(sección Fase F.d) +d:\fitzwatch\NEXT-SESSION.md. - Emitted Rust con los 3 bugs reproducidos:
d:\fitzwatch\target\fitz-build\main\src\main.rs:5665-5810(caso Direct sin cross-module — Bug 2 + Bug 3 visibles) +d:\fitzwatch\target\fitz-build\main\src\public.rs:662-689(caso cross-module conResult<Response>— Bug 1 + Bug 2 visibles, fix del Bug 1 deja Bug 2 expuesto). - Ejemplo oficial que NO triggea los bugs (confirma que el feature
funciona para casos simples):
examples/guide/17l-response-custom.fitz.
🟢 HTTP Response { ... } built-in (CERRADO v0.19.0, 2026-06-21)¶
Cerrado entero en el release v0.19.0 (junio 2026). El type built-in
Response con 5 fields (status / content_type / headers / body /
body_bytes) habilita respuestas non-JSON desde el handler:
@get("/feed.rss")
fn rss_feed() => Response {
content_type: "application/rss+xml; charset=utf-8",
body: "<?xml version=\"1.0\"?><rss/>",
}
5 bloques coordinados: (1) intérprete con HandlerOutcome extendido
+ helper response_instance_to_outcome con validación XOR + status
range; (2) opt-in binary path body_bytes: Bytes?; (3) codegen paridad
bit-a-bit con ResponseBuiltinKind detection en resolve_handler_signature
+ rama dedicada en emit_handler_dispatch_and_response + validación XOR
build-time; (4) integración OpenAPI 3.1 con ResponseContentTypeKind +
helper detect_response_content_type_kind walker AST + schema
200.content.<media_type> con format: binary cuando aplica; (5) docs
+ ejemplo runnable + LSP + extensión VSCode v0.19.0. Detalle en
CHANGELOG.md v0.19.0.
Tests al cierre: 11 unit http intérprete (Bloques 1+2) + 12 unit
codegen (Bloque 3) + 6 unit openapi (Bloque 4) + 2 unit LSP (Bloque 5)
+ 3 E2E tests/compile_e2e.rs::v019_block3d_* + smoke
GUIDE_EXAMPLES_COMPILE con examples/guide/17l-response-custom.fitz
sumado + validación bit-a-bit a mano con curl sobre los 4 casos
canónicos.
Deudas residuales derivadas (NO bloquean uso real):
- Multi-arm bodies (if/match retornando distintos
Response { ... }por arm) no se detectan en compile-time — el schema cae al path legacyapplication/jsonpara esos handlers. El runtime funciona correcto (peek dinámico). Refinable post-MVP. - Response built-in + post middleware (
@middleware(fn)con 2 args) no soportado — el post-mw recibe__FitzResponseJSON-wrapped que pierde content_type / body_bytes. Workaround documentado: usarreturn <status> { ... }o remover el post-mw. - Helpers Bytes adicionales si aparece demanda real:
bytes_from_b64,bytes_from_hex, etc.
Workarounds actuales (todos malos):
- Devolver el XML como Value::Str → axum lo serializa JSON-quoted: "<rss>..." (no parseable como RSS).
- Devolver {"xml": "<rss>..."} JSON wrapper → el cliente tendría que des-envolver con JS antes de pasar a RSS reader (no estándar).
- Servir el XML estático via nginx desde un archivo escrito por un @cron periódico (requiere arquitectura adicional, no responsivo a cambios live).
Propuesta de solución (alta nivel, sin implementar):
- Opción A: nuevo type built-in Response { status: Int, content_type: Str, body: Str } que el handler puede retornar directamente. El wrapper async detecta el Value::Response y arma HandlerOutcome con content_type propio.
- Opción B: extender la sintaxis del status response, return 200 "<rss>..." content_type="application/rss+xml" (más invasivo al parser).
- Opción C: builtin response.with_content_type(body, ct) que retorna un wrapper Value::Response. Más simple del lado parser, requiere builtin nuevo.
Decisión recomendada: A (type built-in Response { status, content_type, body }). Coherente con Request/Response/File ya built-in. Mantiene la paridad fitz run ↔ fitz build.
Impacto si se cierra: cualquier proyecto downstream que necesite RSS / Atom / Sitemap / CSV exports / SVG badges / etc, sin recurrir a workarounds. Caso bloqueado documentado: status page de un user de Fitz que necesitaba feed RSS por cada workspace y tuvo que diferir la feature.
Test E2E que validaría el cierre: handler que retorna Response { status: 200, content_type: "application/rss+xml", body: "<?xml..." } → curl -I ve el Content-Type correcto y el body es el XML sin JSON-quoting. Paralelo fitz run ↔ fitz build bit-a-bit.
🟡 B20 — @cron(store=X) cross-module no resuelve binding del módulo origen (deuda futura, 2026-06-20)¶
Detectada al destrabar fitzwatch después de cerrar B19 (cron cross-module no spawneaba). Caso edge no soportado por el codegen: @cron(..., store=X) declarado en un módulo importado (e.g., scheduler.fitz) referencia un binding X que debería resolver al let X = db.connect(...).await también declarado en el módulo. Hoy el spawn cross-module emite (&X).into_store() en fn main() del crate root, donde X no está en scope → error[E0425] cannot find value 'X' in this scope.
Workaround canónico (pattern TaskHub): declarar let X = db.connect(...).await + @cron(store=X) en el archivo main.fitz. El módulo importado solo exporta la fn async helper sin decorator, y el @cron del main la llama (mod.fn().await). Pattern usado en TaskHub + fitzwatch (post-refactor v0.18.2).
Por qué no cerramos en v0.18.2: implementar cross-module store=X resolution requiere ~200-300 LoC:
- Soportar let X = db.connect(...).await top-level en módulos (hoy pre_register_top_lets solo acepta consts literales).
- Emitir pub static __FITZ_STATE_X: OnceCell<__FitzDbConn> + init fn pub async fn __init_state_X() en el módulo.
- En emit_cron_job_spawns, cuando module_path.is_some() y store_var.is_some(), materializar let X = crate::<mod>::__FITZ_STATE_X.get().unwrap().clone() ANTES del spawn.
- Tocar el module emission entero — riesgo alto de break en TaskHub + curso + guía.
Plan de cierre cuando aparezca demanda real: el patrón canónico (declarar en main) cubre el 90% del caso de uso. Si aparece un boilerplate o user externo que requiere cross-module organization estricta del cron + store, abrir mini-fase dedicada con auditoría de los call sites afectados y tests E2E paralelos a TaskHub.
Tests verdes que cubren el pattern canónico actual: cron_in_imported_module_is_spawned_b19 (B19) + cron_with_persistent_store_in_imported_module_b19_derived (bug derivado del preludio); ambos en tests/compile_e2e.rs. Smoke real fitzwatch v0.18.2 valida persistencia + retry cross-module (refactor en main).
🟢 Mini-tanda traducción ES→EN del código — CERRADA v0.16.0 (2026-06-15)¶
Hito de internacionalización: 47 commits coordinados en 7 sub-fases (F1-F6 + F5.d) llevan el surface user-facing del compilador (CLI, LSP, errors), test assertions internas y comentarios del grammar TextMate de español a inglés. grep "esperaba|esperaban" src/ → 0 ocurrencias post-cierre.
Por sub-fase:
- F1: Comments en src/**/*.rs (17 batches).
- F2: Test function names (~2647 renames en 44 archivos).
- F3: Error messages + runtime emit strings (10 batches).
- F4: Output user-facing — CLI, LSP, examples, docs internacionales (syntax-spec, architecture).
- F5.a/b/c: Test assertions internas + barrida cross-archivo + F4 leftover + driver Postgres TLS.
- F5.d: Residual "esperaba"/"esperaban" — 253 strings en mod tests de 11 archivos + 1 user-facing en src/db.rs:535 que F5.c.2 había perdido (cierre final).
- F6: Comentarios del grammar TextMate (editors/vscode/syntaxes/fitz.tmLanguage.json).
NO cubierto por la mini-tanda (deuda explícita pendiente):
- docs/guide.md, docs/curso/, docs/taskhub/, README.md, docs/index.md — material pedagógico se mantiene en castellano por decisión de proyecto. Traducción a inglés queda como sub-paso futuro si el material gana tracción internacional.
- Fixtures .fitz adentro de tests (ej: "El Chaltén", "división por cero", "id inválido", passwords como "contraseña-secreta-del-usuario") — son fixtures, no surface del compilador. Quedan intactos por diseño.
Verificación pre-bump completa (toda verde): 3052 unit (sin feature) / 3170 (lsp) / 3143 (python) + 352 compile_e2e + 1 smoke gigante (290 ejemplos guía+curso+TaskHub) + 98 cli_e2e + 3 openapi + 3 builds release (default + lsp + python) + clippy strict en 3 modos + fmt limpio. 8 failures de compile_e2e son pre-existentes documentados acá mismo (Windows file lock paralelo + codegen cross-module + observability + routing 404 + codegen drift orm_w17 #7 + Postgres apagado para sslmode=require). Cero regresiones de F1-F6+F5.d.
Detalle por sub-paso: CHANGELOG.md → v0.16.0.
🟢 Mini-tanda HTTP client builtin — CERRADA ENTERA (2026-06-18, v0.17.0)¶
Hito: módulo built-in nuevo http con cliente HTTP outbound async ciudadano de primera clase del lenguaje. Cierra una de las dos brechas chicas que quedaban en el stack web (Fitz ya tenía HTTP server-side, WS, auth, OpenAPI, async, jobs, ORM nativos; faltaba el lado cliente). Detectada como deuda explícita el 2026-06-18 al pausar el desarrollo de fitzwatch (status page open-source en Fitz puro) — necesitaba http.head para chequear si las URLs monitoreadas responden 200.
API: 6 builtins async (http.get/http.head/http.post/http.put/http.delete/http.request) que devuelven Future<Result<HttpClientResponse>>. Body shapes Str / Map<Str, Any> (auto-JSON + Content-Type: application/json) / Bytes. Tipo built-in nuevo HttpClientResponse { status: Int, body: Str, headers: Map<Str, Str>, duration_ms: Int } paralelo a Request/Response del HTTP server-side. Backend reqwest = "0.12" con ["json", "rustls-tls"] no condicional (linkeado estático, sin openssl en el host).
Bloques cerrados (un commit por bloque):
- B1 evaluator (commit 3cefd2e) — Value::Module { name: "http" } registrado en register_builtins paralelo a jwt/hash/log/db/auth/flags, los 6 builtins async, pre-registro de HttpClientResponse como nominal en TypeEnv, helper privado body_to_reqwest_body(value) con dispatch por tipo del Value, medición duration_ms con Instant::now(), errores como Result::Err(Value::Str) con prefijo identificable.
- B2 checker (commit d1aaf70) — pre-registro de http y HttpClientResponse en CheckCtx::new, fields tipados (status: Int, body: Str, headers: Map<Str, Str>, duration_ms: Int), llamadas devuelven Result<Any> paralelo a 8.4 con regla de exhaustividad sobre Result + regla del operador ?.
- B3 codegen (commit 9e214e3) — detector program_uses_http_client(program) walka AST buscando calls http.X(...), cargo_toml_for suma reqwest = "0.12" condicional, preludio HTTP_CLIENT_PRELUDE con struct __FitzHttpClientResponse + impls + static __FITZ_HTTP_CLIENT: LazyLock<reqwest::Client> (timeout=30s, follow_redirects=true) + helpers async __fitz_http_get/etc + helper __fitz_http_body_apply paralelo bit-a-bit al intérprete, dispatch en gen_call, importación cross-module con use crate::{__fitz_http_*}.
- B4 LSP (commit 29bc041) — scope_level_completions suma http con descripción "module: get/post/put/delete/head/request (HTTP client async)", after_dot_completions (recv_name == "http") tira los 6 métodos con signatures completas + ejemplos en el hint, paralelo a dispatch por recv_name de jwt/hash/log/db/auth/flags.
- B5 guía + ejemplos runnable (commit 03cd71e) — sub-sección nueva en cap 17 de docs/guide.md "HTTP client outbound" entre "Middleware y CORS" y el cierre del cap, con panorama vecino (requests/axios/reqwest/OkHttp) + 5 diferenciales + API completo + body shapes con tabla + modelo de errores + integración + limitaciones MVP. 4 ejemplos runnable nuevos en examples/guide/: 17e-http-client-basico.fitz (los 5 métodos comunes), 17f-http-client-errores.fitz (timeout/DNS/4xx/5xx + helper + ?), 17g-http-client-webhook.fitz (dispatcher canónico con @background+spawn), 17h-http-client-health-checker.fitz (cron+http.head fitzwatch-style). Los 4 sumados al smoke GUIDE_EXAMPLES_COMPILE en tests/compile_e2e.rs. Smoke verde 363 ejemplos en 251.89s + fmt + clippy (default + lsp) limpios + validación bit-a-bit fitz run ↔ binario nativo sobre 17e contra httpbin.org real.
Detalle por bloque: docs/http-client-roadmap.md.
Bloques pendientes: ninguno — la mini-tanda cerró entera con v0.17.0.
- B6 barrida cross-docs (commit 931fd18): CLAUDE + README + index.md + deudas + roadmap actualizados; mkdocs verde sin warnings nuevos.
- B7 curso M5.C5 (b499c36 + chore 3c6f264): cap nuevo M5.C5 dedicado HTTP client outbound + ejemplo capstone integrador del módulo M5 entero (auth + jobs + client). Curso 42→43.
- B8 boilerplate api-orm-full (1468e0d + chore a01da2d): update chico sumando webhook outbound al publicar post; @background async fn notify_post_published(...) + spawn(...). Validación end-to-end vía smoke alternativo 17g-http-client-webhook.exe (202 en 207ms + webhook delivered status=200 duration_ms=724ms).
- W18 (commit 63b3d3f): fix codegen cross-module observability (cierra deuda crítica descubierta al validar B8 sobre api-orm-full multi-archivo — entry dedicada arriba).
- B9 cierre formal (v0.17.0, 2026-06-18): CHANGELOG + roadmap + deudas + CLAUDE refresh + bump Cargo.toml + extensión VSCode 0.17.0 + .vsix regenerado + blog drafts ES/EN.
Diferencial del feature: ningún lenguaje moderno del cuadro (Python requests, JS axios/fetch, Java OkHttp/HttpClient, Rust reqwest, Go net/http) provee HTTP client outbound como builtin del lenguaje con paridad bit-a-bit intérprete↔binario y zero deps externas para activarlo.
🟢 W18 (post-B8) — Codegen cross-module observability CERRADA (2026-06-18, v0.17.0, commit 63b3d3f)¶
Hito: cierra el gap más visible del codegen cross-module heredado de Fase 12.3.b y la dieta de los bloques W11/W16: cuando un módulo importado declaraba un handler HTTP (@get/@post/@put/@delete) y/o llamaba log.{info,warn,error,debug}(...), el __handler_<name> wrapper emitido en el módulo invocaba 6 símbolos del preludio LOGGING + SPAN_CONTEXT + OTEL pero faltaban los use crate::{...} correspondientes. Resultado: fitz build rompía con 12-134+ errores E0425/E0433 según el tamaño del proyecto. Bloqueaba la primera versión del boilerplate boilerplates/api-orm-full end-to-end (la deuda pre-existente que B8 dejó documentada).
Detectado: post-Bloque 8 de la mini-tanda HTTP client (2026-06-18), al validar boilerplates/api-orm-full con el webhook outbound recién agregado. Confirmado con git stash → HEAD pre-B8 reproduce los mismos errores (NO regresión de B8).
Repro mínima (4 líneas en main + 1 handler en módulo):
// repro/main.fitz
from handlers import ping
@server(port=3000)
fn main() => 0
// repro/handlers.fitz
@get("/ping")
fn ping() -> Str => "pong"
__fitz_otel_is_enabled, __fitz_otel_tracer, __FitzSpanContext, __fitz_with_span_context, __fitz_log_info, __FitzLogValue). Post-fix: compila a binario, arranca server, responde "pong" con log estructurado JSON y trace_id correlation.
Fix (4 sub-cambios coordinados en src/codegen.rs):
- Imports observability del wrapper: el bloque
module_has_httpengenerate_module_rs_with_bindingsya emitía W11/W16 (__FitzResponse/__ToFitzJson/__FromFitzJson/__apply_cors_and_respond/__panic_payload_msg/__parse_*); extendido para emitir 3 grupos nuevos paralelos cuandomodule_has_http && main_observability_enabled:__fitz_otel_is_enabled/__fitz_otel_tracer+__FitzSpanContext+__fitz_with_span_context. - Imports de los 4 log helpers (independientes del wrapper): nuevo flag
module_uses_logging = program_uses_logging(program)paralelo amodule_has_http. Cuando(module_has_http && main_observability_enabled) || module_uses_logging, emiteuse crate::{__fitz_log_info, __fitz_log_warn, __fitz_log_error, __fitz_log_debug, __FitzLogValue}. Cubre dos sources de llamadas: (a) access log auto-emitido porgen_http_handler_wrappercuando observability ON, (b)log.{info,warn,error,debug}(...)del user code en cualquier módulo (con o sin HTTP). - Propagación de
@server(observability=false)main → módulos: nuevo helperextract_main_observability_enabled(program: &Program) -> boolque walka decorators top-level buscando@server(...)con kwargobservability=Bool. Por defectotrue. Pre-scaneado engenerate_projectANTES del loader y threaded vía nuevo fieldModuleLoader.main_observability_enabled+ setterset_main_observability_enabled.generate_module_rs_with_bindingsrecibe el flag como nuevo argmain_observability_enabled: booly seteactx.observability_enableddel módulo. Resultado: cuando main opta por bare-metal, módulos también — elgen_http_handler_wrapperleeself.observability_enabledy skipea el bloque entero de instrumentación bit-a-bit paralelo. Cierra la inconsistencia semántica que existía desde Fase 12.3.b.5 (main bare-metal pero módulos instrumentados). - Gating refinado de los
use crate::{...}: las 3 líneas observability se gatean pormodule_has_http && main_observability_enabledpara NO emitir imports inútiles en bare-metal mode (semánticamente coherente + sin warningsunused_importsaunque tengamos el#[allow]).
Tests (9 unit nuevos en codegen::tests::w18_*):
- w18_extract_main_observability_default_is_true / _with_server_no_kwarg_is_true / _false / _true_explicit (helper extract).
- w18_module_with_http_handler_emits_use_crate_observability_helpers (caso happy path — module con @get).
- w18_module_with_log_call_emits_use_crate_log_helpers (módulo con log.warn sin HTTP).
- w18_module_without_http_or_logging_does_not_emit_observability_use_crate (regresión negativa).
- w18_module_with_http_handler_and_observability_false_does_not_emit_wrapper_imports (propagación observability=false).
- w18_module_with_http_observability_false_and_user_log_still_imports_log_helpers (gating refinado — log helpers separados del wrapper).
Validación end-to-end:
- Repro mínima compila a binario + arranca server + responde "pong" con log estructurado y trace_id.
- boilerplates/api-orm-full compila a target/release/fitz-api-orm-full.exe (~134 errores E0425/E0433 pre-fix → 0 post-fix). Binario arranca, mounta los 13+ endpoints listados en el banner, /healthz y /readyz responden {"status":"ok"}.
- Full lib suite: 3116 unit tests passing, 0 failed (post-fix: 9 W18 nuevos).
- cargo fmt --all --check limpio + cargo clippy --lib --tests --bins -- -D warnings limpio.
Impacto user-facing: sin breaking. Programas single-file y proyectos multi-archivo SIN handlers/log-calls en módulos importados compilan idénticos (test negativo lo anchora). Proyectos multi-archivo CON handlers/log-calls en módulos ahora compilan a binario nativo end-to-end. Cualquier proyecto con @server(observability=false) (que antes era inconsistente entre main y módulos) ahora va bare-metal en todo el árbol.
Deudas residuales derivadas (NO bloquean — refinamientos visibles post-W18):
- gen_http_handler_wrapper lee ctx.observability_enabled en módulos pero no considera que main pueda tener su propio @server con flag distinto al de un futuro @server en módulo. MVP: solo main puede declarar @server (parser/loader convention). Si se permite @server en módulos en algún momento, el threading necesita refinarse.
- El detector extract_main_observability_enabled solo mira top-level del main — no walkea módulos importados. Coherente con la convención actual (@server solo en main).
- Modules sin HTTP que importan tipos @table con campos Date/Time/Uuid siguen el patrón W11/uses_db (no afectados por W18).
Commit asociado: fix dedicado pre-B9 en commit 63b3d3f (2026-06-18) — fix(codegen): W18 — cross-module observability + log helpers + observability=false propagation. Cierre formal del Bloque 9 + bump v0.17.0 entra en el commit subsiguiente del release.
🟡 Hallazgos del codegen del Bloque 5 HTTP client — 3 deudas residuales NO bloqueantes (2026-06-18)¶
Descubiertas al validar bit-a-bit fitz run ↔ fitz build sobre los 4 ejemplos runnable 17e/f/g/h. Documentadas con workaround idiomático conocido para cada caso; el ejemplo respectivo aplica el workaround en línea con comentario explicativo. Ninguna bloquea la mini-tanda HTTP client.
1. ? top-level rechazado por el codegen (esperado por la regla del checker 5.3.3)¶
Comportamiento: let r = http.get("https://...").await? directo a nivel top-level es rechazado tanto por el checker como por el codegen, porque la regla 5.3.3 exige que ? viva adentro de una fn que retorna Result<T>.
Workaround idiomático (documentado en 17e/17f):
async fn run() -> Result<Null> {
let r = http.get("https://...").await?
print("status: {r.status}")
return Ok(null)
}
// Top-level
match run().await {
Ok(_) => print("listo"),
Err(e) => print("falló: {e}"),
}
Conclusión: NO es bug — la regla 5.3.3 es deliberada (el ? huérfano sería runtime error inevitable). El workaround es el patrón canónico para tooling HTTP client desde un script CLI. Documentado en el cap 17.X de la guía.
2. for x in <List<Str>> con .await adentro de @cron rompe Send — CERRADO 2026-06-20 (v0.18.1, cosecha post-fitzwatch sesión 2)¶
Síntoma: el codegen del binario nativo rompe con error de tipos del estilo "future is not Send" cuando un body de @cron itera una lista y hace .await por iteración:
@cron("*/30 * * * * *")
async fn check_all_endpoints() -> Result<Null> {
let urls = ["https://a.com", "https://b.com"]
for url in urls {
let r = http.head(url).await? // ← rompe Send acá
log.info("status", url: url, status: r.status)
}
return Ok(null)
}
Causa: el codegen del for in sobre List<T> tomaba MutexGuard del Arc<Mutex<Vec<T>>> y lo mantenía activo durante todo el body del loop. Cuando había .await adentro, el MutexGuard cross-await rompía el bound Send + 'static que axum/tokio exigen para tasks spawneadas.
Fix v0.18.1: el codegen emite ahora un bloque acotado con let __for_snap previo al for:
{
let __for_snap = (xs).lock().unwrap().clone();
for mut x in __for_snap.into_iter() {
// body — el MutexGuard YA no sobrevive cross-await
}
}
El let __for_snap = ...; libera el MutexGuard temporal al ;, dejando solo el Vec<T> owned para el loop. Aplicable a List_. Cambios en src/codegen.rs::gen_for_loop (los 4 sitios del lock chain). Test E2E nuevo for_over_list_with_await_in_body_does_not_break_send_b17 en tests/compile_e2e.rs.
Patrón canónico que estaba bloqueado (descubierto en fitzwatch v0.18.0):
async fn dispatch_all(channels: List<NotificationChannel>, payload: Map<Str, Str>) -> Null {
for ch in channels {
let _ = dispatch_channel(ch, payload).await // ← ANTES rompía
}
return null
}
Ahora compila bit-a-bit con fitz build. Cap 17h y otros ejemplos guía que tenían workarounds documentados pueden refactorearse al patrón canónico (deuda menor, cosmética).
3. Map literal heterogéneo (Bool + Str) en return <status> { ... } rompe el codegen¶
Síntoma: handler HTTP que retorna un status custom con Map literal mezclando Bool y Str rompe el codegen:
@post("/webhook")
async fn webhook() -> Result<Map<Str, Any>> {
if invalid_payload() {
return 400 { "ok": false, "error": "missing field" } // ← rompe codegen
}
return Ok({"ok": true, "id": "abc"})
}
Error del codegen: typo "could not find type __FitzValue in this scope" — el trait __FitzValue que el preludio HTTP usa para serializar Map<Str, Any> heterogéneo NO está activo porque ningún otro camino del programa lo dispara.
Causa probable (sub-case del fix de v0.10.4): v0.10.4 cerró MapDB_HTTP_INTEGRATION_PRELUDE + impl __MapKey for __FitzValue emitido "cuando __FitzValue activo". El caso del Bloque 5 es que UNA sola aparición de Map heterogéneo adentro de return <status> { ... } no activa __FitzValue porque el detector busca el tipo en field declarations y type sigs, no en literals de body de handlers con status custom.
Workaround idiomático (documentado en 17g):
Fix futuro (refinable post-MVP, paralelo a v0.13.1 que gateó metrics-exporter-prometheus): extender el detector de __FitzValue para que walke literals Map heterogéneos en posiciones de Stmt::ReturnStatus con shape Map. ~20 LoC en el detector.
🟢 Cosecha codegen post-fitzwatch — CERRADA ENTERA 2026-06-19¶
Acordado con el autor 2026-06-18, cerrado 2026-06-19: el próximo norte del lenguaje después de v0.17.0 era atacar esta cosecha. 6 sub-pasos en 2 días cierran B1-B12 + B14 + B15 (B13 NO reprodujo en v0.17.0, W18 lo había cerrado; B16 descubierto durante sub-paso 2, abierto como deuda separada porque el fix vive en el checker, no es mecánico). B15 fue el grande (sub-paso 6, ~115 LoC + 3 unit + 1 E2E) y al diagnosticarlo descubrimos que el trigger REAL era FK Nullable, no nullables-en-parent-type como decía el doc original. Probablemente fitzwatch no estaba pausado por B15 estricto (sus models declaran todos los FK como
Int = 0sentinel) sino por W18 (cerrado en v0.17.0); cuando el autor retome fitzwatch va a poder hacerfitz builddirecto.
Contexto¶
Durante el desarrollo de fitzwatch (proyecto en d:\fitzwatch\,
status page + uptime monitor open-source en Fitz puro, showcase
profesional del stack nativo + Vue 3 + Vuetify + Chart.js + SheetJS),
descubrimos 15 bugs del codegen Fitz v0.17.0 que bloquean
fitz build. fitz check y fitz run no están afectados —
todos los bugs son específicos del path codegen → cargo build.
fitzwatch quedó PAUSADO esperando cerrar B15 (el crítico). El backend (13 módulos Fitz), frontend (admin Vue 3 + Vuetify + Chart.js + SheetJS profesional con dashboard + grillas paginadas + export Excel + WS live + gestión de webhooks), Docker setup, nginx config, deploy README y Cloudflare prep ya están terminados.
Doc completo con repros: d:\fitzwatch\CODEGEN-BUGS.md.
B1 — .order_by("string") rechazado por codegen — CERRADO 2026-06-19 (sub-paso 5 cosecha)¶
Síntoma original (cargo build):
✗ codegen: Error en línea N:M — `.order_by(closure)` espera una closure literal (fn(<param>) => <expr>)
Repro original:
Severity: Medium. Fix aplicado: .order_by("col") y .order_by("-col") (DESC con - prefix) ahora aceptados tanto en evaluator (orm_qb_order_by en src/evaluator.rs) como en codegen (emit_qb_order_by_chain en src/codegen.rs). Validación de field existence consistente con el path closure (chequea state.fields/fields). Direction extraída del - prefix; el resto del string es el nombre del field. SQL emitido idéntico al path closure ("\"<sql_col>\" {ASC|DESC}" en evaluator, .with_order_by(col, desc) en codegen). 3 unit tests nuevos: codegen_orm_order_by_accepts_str_literal_asc_b1, codegen_orm_order_by_accepts_str_literal_desc_with_dash_b1, codegen_orm_order_by_str_literal_unknown_field_aborts_b1. ~70 LoC totales.
B2 — http.post body con type Instance — CERRADO 2026-06-19 (sub-paso 5 cosecha)¶
Síntoma original (cargo build):
Repro original:
type Payload { event: Str, count: Int }
http.post("https://api.com", Payload { event: "x", count: 1 }).await
Severity: Medium. Fix aplicado: gen_http_body_marshal (src/codegen.rs) suma rama para Type::Nominal(_) que emite __fitz_http_body_from_json(({body}).__to_fitz_json()). Helper nuevo __fitz_http_body_from_json(json: serde_json::Value) sumado al HTTP_CLIENT_HTTP_INTEGRATION_PRELUDE (que ya está gated por has_http, donde __ToFitzJson + serde_json viven). Auto-setea Content-Type: application/json. La blanket impl __ToFitzJson for Arc<Mutex<T>> ya existente delega a T::__to_fitz_json() automáticamente. Limitación documentada: requiere has_http=true (programa con al menos un @get/@post/etc.) porque sin HTTP server-side la trait + serde_json no están en scope. Caso 90% real (webhook dispatcher: handler HTTP propio + outbound a API externa) cubierto. Programas con solo HTTP client + Instance body reciben error claro citando workarounds (declarar stub handler o convertir manual a Mapcodegen_http_post_body_instance_emits_from_json_b2, codegen_http_post_body_instance_without_http_server_aborts_b2. ~45 LoC totales.
B3 — ws_broadcast desde módulo sin handler HTTP/WS rompe en cargo build — CERRADO 2026-06-19 (sub-paso 1 cosecha)¶
Síntoma: cargo build: cannot find trait__ToFitzJsonin this scope --> src/<module>.rs:N.
Repro: módulo X sin @get/@post/@put/@delete/@ws propios llama ws_broadcast("/path", msg). El preludio HTTP no se emite en ese módulo, falta __ToFitzJson.
Workaround user-side: helper en módulo CON handlers HTTP/WS (fn emit(evt: Map<Str,Str>) { ws_broadcast("/x", evt); return null }), módulos sin handlers delegan.
Severity: High. Fix aplicado: separado del bloque HTTP el import de use crate::{__ToFitzJson, __FromFitzJson}; en generate_module_rs_with_bindings (src/codegen.rs). Se emite cuando module_has_http || program_uses_ws_broadcast(program). ~20 LoC. Validado contra el repro fitzwatch (checks.fitz sin handlers HTTP llamando a ws_broadcast directo): los 3× error[E0405] cannot find trait __ToFitzJson desaparecieron.
B4 — .len() sobre List<X>? (Nullable) rechazado por codegen — CERRADO 2026-06-19 (sub-paso 2 cosecha)¶
Síntoma: ✗ codegen: method call.lensobreList: no soportado en codegen.
Repro:
let rows = match conn.query("...", []).await {
Ok(r) => r,
Err(_) => return ""
}
// rows tipa como List<DbRow>? en codegen
if (rows.len() == 0) { ... } // ✗
Workaround user-side: helper async fn x() -> Result<T> con .await? (el propagation refine bien). Match con return temprano confunde al codegen.
Severity: Medium (sub-case de B14). Fix aplicado (parte del meta B14): flow-sensitive refinement en gen_match (src/codegen.rs ~línea 24796). El método ahora trackea un Vec<bool> paralelo arm_divergent que marca cada arm cuyo body termina en return/break/continue. El cálculo del LUB filtra esos arms antes de unificar (! en Rust coerce a cualquier T; incluirlos widening-eaba spuriously al Nullable). Si todos divergen, el resultado queda Type::Null (comportamiento legacy preservado — rustc acepta ! → ()). 4 unit tests nuevos: match_with_divergent_err_arm_does_not_widen_to_nullable_b4_len / _b5_index / _b6_field + match_with_all_divergent_arms_types_as_null. ~80 LoC totales. Validado contra repro mínima en d:\tmp\sub2-divergent-repro\repro.fitz (canonical pattern con Ok(r) => r, Err(_) => return Err(...)).
B5 — Indexing [] sobre List<X>? rechazado por codegen — CERRADO 2026-06-19 (sub-paso 2 cosecha)¶
Síntoma: ✗ codegen: indexing[]overList: only supported on List<T> and Map<K, V>.
Repro: igual que B4, pero rows[0].
Severity: Medium (sub-case de B14). Fix aplicado: cerrado junto a B4 — el mismo cambio en gen_match filtra arms divergentes del LUB, así rows queda como List<X> directo y [] dispatch funciona normal. Test unit dedicado match_with_divergent_err_arm_does_not_widen_to_nullable_b5_index valida que rows[0] sobre List<Int> tipa como i64.
B6 — Field access .x sobre T? (Nullable) rechazado por codegen — CERRADO 2026-06-19 (sub-paso 2 cosecha)¶
Síntoma: ✗ codegen: field access.pausedoverT?: only supported on instances of custom types.
Repro:
let monitor = match refresh(id).await {
Ok(m) => m,
Err(_) => return null
}
if (monitor.paused) { ... } // ✗
Severity: Medium (sub-case de B14). Fix aplicado: cerrado junto a B4/B5 — el mismo cambio en gen_match filtra arms divergentes del LUB, así monitor queda como T (Nominal directo) y field access dispatch funciona normal. Test unit dedicado match_with_divergent_err_arm_does_not_widen_to_nullable_b6_field valida que who: User después del match y who.name tipa como String.
B7 — match { Ok(v) => v, Err(_) => null } no envuelve en Some() al asignar a Nullable — CERRADO 2026-06-19 (sub-paso 3)¶
Síntoma original (cargo build):
error[E0308]: mismatched types
| expected `Option<String>`, found `String`
help: try wrapping the expression in `Some`
Repro original:
Severity: Medium. Fix aplicado (sub-paso 3 de la cosecha post-fitzwatch): post-procesamiento en gen_match después de calcular result_ty. Cuando el LUB de los arms no-divergentes da Nullable(inner), recorremos cada arm no-divergente y aplicamos:
- body_ty == Null con body_code == "()" → emitimos None directo.
- body_ty == Null con side-effects en bloque → emitimos { <body>; None } para preservar los side-effects.
- body_ty ya Nullable(_) → dejamos sin tocar (idempotente — handles bindings ya tipados como T?).
- otherwise → wrap en Some(<body>).
Arms divergentes (return/break/continue, type !) coercen a cualquier T y no requieren rewrite. ~60 LoC en src/codegen.rs::gen_match (incluyendo refactor menor para guardar pat_prefix + body_code separados antes de ensamblar arm_pieces). Tests unit dedicados:
- match_with_nullable_lub_wraps_inner_arm_in_some_and_null_arm_in_none_b7 (Str?)
- match_with_nullable_lub_wraps_int_arm_in_some_b7 (Int?)
- match_with_nullable_binding_arm_does_not_double_wrap_b7 (idempotencia)
Repro en d:/tmp/sub3-nullable-wrap-repro/repro.fitz validado bit-a-bit fitz run ↔ fitz build ↔ binario nativo (cinco prints, salida hola\nnull\n42\nnull\nnull).
B8 — Option<T>: __IntoPgValue not satisfied (query params Nullable → driver PG) — CERRADO 2026-06-19 (sub-paso 4)¶
Síntoma original (cargo build):
error[E0277]: the trait bound `Option<i64>: __IntoPgValue` is not satisfied
error[E0277]: the trait bound `Option<String>: __IntoPgValue` is not satisfied
Repro original:
@get("/x?id={id}")
async fn h(id: Int?) -> Result<Null> {
let _ = conn.query("SELECT * FROM t WHERE id = $1", [id]).await?
return Ok(null)
}
Severity: Medium. Fix aplicado (sub-paso 4 de la cosecha post-fitzwatch): blanket impl impl<T: __IntoPgValue> __IntoPgValue for Option<T> sumado al preludio DB del codegen (gated por program_uses_db, no entra a programas sin db.*). None → __FitzPgValue::Null, Some(v) → v.into_pg() delegando al impl interno. ~12 LoC efectivas en src/codegen.rs::emit_db_prelude. Tests unit dedicados:
- db_prelude_emits_into_pg_value_for_option_t_b8 (impl + arms presentes en preludio)
- db_prelude_not_emitted_when_program_does_not_use_db_b8 (gating verificado)
- db_query_with_nullable_arg_emits_into_pg_value_call_b8 (call site emite <_ as __IntoPgValue>::into_pg(<expr>) y rustc resuelve la trait selection vía el blanket impl)
Repro en d:/tmp/sub4-option-pg-repro/repro.fitz validado: fitz check OK, fitz build OK (binario compilado), fitz run y binario nativo producen el mismo error de runtime ("I/O: ...") al intentar conectar al puerto inexistente — paridad bit-a-bit confirmada porque el código compila y solo falla en runtime DB, no en codegen ni en cargo build.
B9 — Str? == Str (Nullable vs concrete) — CERRADO 2026-06-19 (sub-paso 5 cosecha)¶
Síntoma original (cargo build):
Repro original:
Severity: Low. Fix aplicado: gen_binop (src/codegen.rs, Eq/NotEq arm) suma rama nueva entre la verificación de Nullable vs Null y los paths de Nominal/Str/numeric. Cuando un lado es Type::Nullable(inner) y el otro es exactamente el mismo primitivo (Str/Int/Float/Bool), emite <opt>.as_ref() == Some(&<conc>) (o !=) que rustc resuelve correctamente sobre PartialEq de Option<&T>. Aplica simétrico (LHS o RHS Nullable). Mixed primitives (Int↔Float Nullable vs concrete distinct primitivo) y comparaciones Nominal Nullable vs concrete quedan como deuda menor (caen al path existente). 4 unit tests nuevos: binop_eq_nullable_str_vs_str_emits_as_ref_some_b9, binop_neq_nullable_str_vs_str_emits_as_ref_some_b9, binop_eq_nullable_int_vs_int_emits_as_ref_some_b9, binop_eq_str_vs_nullable_str_emits_as_ref_some_swapped_b9. ~40 LoC totales.
B10 — spawn(fn) cross-module no detecta @background — CERRADO 2026-06-19 (sub-paso 5 cosecha)¶
Síntoma original:
✗ codegen: spawn: fn `run_check` is not declared with `@background`. Mark the fn with `@background\nfn run_check(...) { ... }`...
Repro original:
// checks.fitz
@background async fn run_check(id: Int) -> Null { ... }
// scheduler.fitz
from checks import run_check
@cron("*/10 * * * * *") async fn tick() -> Result<Null> {
spawn(run_check(42)) // ✗ no detecta @background cross-module
return Ok(null)
}
Severity: Medium. Fix aplicado (3 piezas coordinadas):
- Infrastructure del checker:
TypeEnvsumaimported_background_fns: HashSet<String>(paralelo aimported_auth_provider); métodos pubadd_imported_background_fns(I: IntoIterator<Item = String>)+imported_background_fns(); pub fn nueva ensrc/types.rs:extract_background_fn_names(program) -> Vec<String>walks top-level FnDefs y colecta los marcados con@background.collect_background_fnsextiende a merge de los imported names sobre el set local. - Pre-scans en main.rs y codegen.rs:
pre_scan_imported_background_fnsenmain.rs(pathfitz check/fitz run) +pre_scan_imported_background_fns_for_loaderencodegen.rs(pathfitz build, walk per-módulo).ModuleLoadersuma campomain_imported_background_fns: Vec<String>propagado a cada módulo enload_module(combinado con las imports propias del módulo). - Dispatch del codegen:
gen_spawn_callextiende el lookup del target — primeroself.fn_sigslocal, fallback amodule_bindingscuando el target esfrom <bg_mod> import bg. La sig viene deself.loaded_modules[idx].fn_sigs. Lause crate::<bg_mod>::bg;ya emitida poremit_module_use_declspermite emitir la call con el nombre unqualified (bg(args)en lugar debg_mod::bg(args)).
3 unit tests nuevos en types: extract_background_fn_names_collects_marked_top_level_fns_b10, extract_background_fn_names_returns_empty_when_no_background_fns_b10, spawn_cross_module_imported_background_fn_passes_with_pre_scan_b10. 1 E2E nuevo en compile_e2e: cross_module_spawn_background_b10 (3-archivos canonical: checks.fitz con @background, main importa + spawn(run_check(42))). ~150 LoC totales.
B11 — Response types con List<NominalType> anidados rompen codegen (impacto cross-cutting) — CERRADO 2026-06-19 (sub-paso 1 cosecha)¶
Síntoma (cargo build):
Trigger real (descubierto al reproducir desde fitzwatch): el caso dispara cross-module — public.fitz declara type PublicOverview { items: List<MonitorStatus> = [] } con handler @get("/x") fn(...) -> Result<PublicOverview>. main.fitz importa el handler con from public import handler pero NO los nested types (MonitorStatus). emit_helpers_for_imported_types en main.rs hace remap del field items: List<MonitorStatus> → List<Any> (porque MonitorStatus no está en el env del main) → emite impl __FromFitzJson for PublicOverviewData { let items: Vec<__FitzValue> = ... }. Pero __FitzValue no se activa por ningún detector (el walker program_uses_fitz_value mira ASTs, no el remap codegen-time), así que rustc rompe.
Workaround user-side: split en endpoints separados que devuelvan List<X> directo + endpoint de metadata aparte.
Severity: High. Fix aplicado (2 sub-cambios coordinados en src/codegen.rs):
1. Detector pre-pass cross_module_compound_degrades_to_fitz_value(program, loader) — walka loader.modules.type_sigs y por cada field con compound (List<Nominal> / Map<_, Nominal> / Map<Nominal, _> o Nullable wrappers) chequea si el inner Nominal es resoluble por main (local TypeDef + types importados via from X import T). Si no → activa uses_fitz_value = true antes del emit_prelude. Aplicado en generate_project (línea ~272) Y en generate_main_rs (línea ~5290 — recompute path heredado del split de Phase 5b).
2. Stub __FromFitzJson — nuevo enum FromFitzJsonMode { Real, Stub } + variante gen_type_http_impls_for_sig_with_meta_and_mode. Cuando emit_helpers_for_imported_types detecta compound degrade (helper nuevo field_type_compound_degraded que compara original vs remapped), emite __FromFitzJson con cuerpo Err("cross-module __FromFitzJson for X is a stub: the type has compound fields...; if you need to deserialize this body cross-module,from every nested type the body references"). El stub satisface el trait bound; __ToFitzJson se emite normal (solo invoca self.<field>.__to_fitz_json() — no referencia el tipo remapped). El stub se dispara en runtime solo si alguien intenta deserializar el type cross-module — el caso típico (response output-only) NUNCA llega ahí.
~150 LoC totales. Validado contra el repro fitzwatch (public.fitz con PublicOverview { monitors: List<MonitorStatus>, incidents_open: List<OpenIncident>, incidents_recent: List<RecentIncident> }): los 6× error[E0425] cannot find type __FitzValue desaparecieron.
B12 — Cross-module @auth_provider detection en codegen — CERRADO 2026-06-19 (sub-paso 5 cosecha)¶
Síntoma original:
✗ codegen: module `metrics` has type errors: @authenticated on fn ...: no `@auth_provider` registered in the program
Repro original: módulo con @authenticated que no hace import auth ni from auth import ... (aunque el provider esté en auth y se haya cargado por main).
Severity: Low. Fix aplicado: ModuleLoader suma campo main_imported_auth_provider: Option<ImportedAuthProvider> + setter set_main_imported_auth_provider. generate_project pre-scanea las imports de MAIN ANTES de collect_imports y popula el slot del loader (usa pre_scan_imported_auth_provider_for_loader existente). En load_module, el provider del módulo se calcula como pre_scan_imported_auth_provider_for_loader(módulo) or_else main_imported_auth_provider. Cierra el caso típico donde un módulo importer (ej. metrics.fitz) tiene handlers @authenticated pero solo importa el User type desde un módulo no-auth (ej. from types import User), y el @auth_provider real vive en auth.fitz (importado solo desde main). El módulo ahora ve el provider via fallback de main. 1 E2E nuevo en compile_e2e: cross_module_auth_provider_via_main_b12 (4-archivos canonical: types.fitz declara User, auth.fitz importa User + tiene @auth_provider, metrics.fitz importa User + tiene @authenticated sin importar auth, main.fitz importa auth + metrics). ~70 LoC totales.
B13 — log.X(...) con kwargs heterogéneos rompe cross-module — NO REPRODUCE en v0.17.0 (cerrado por W18, 2026-06-18)¶
Investigación 2026-06-19 (sub-paso 1 cosecha): el repro del doc (log.info("evt", count: 42, name: "x") cross-module) NO dispara en v0.17.0. Reverteamos el workaround en checks.fitz de fitzwatch (cambiando los string interp a kwargs heterogéneos) y compila limpio. Probablemente W18 (commit 63b3d3f, 2026-06-18, mismo día del cierre de fitzwatch) cerró este caso cuando atacó el preludio cross-module de logging para que módulos importados que llaman log.X reciban los imports correctos. El doc fitzwatch quedó con el bug listado porque se escribió contra una versión PRE-W18 (probable v0.16.x).
gen_log_call (src/codegen.rs ~línea 15855) emite cada kwarg como __FitzLogValue::Int(...) / __FitzLogValue::Str(...) / etc directos — nunca usa el enum __FitzValue (son tipos distintos: __FitzLogValue es el tagged union local del módulo logging). El walker program_uses_logging activa el preludio __FitzLogValue ya en módulos cross-module desde W18.
Severity: Medium NO APLICA. Acción: ninguna — el bug está cerrado por W18. Si reaparece en algún caso edge, abrir entrada nueva.
B14 — (meta) match con return temprano no refine tipos en codegen — CERRADO 2026-06-19 (sub-paso 2 cosecha)¶
Patrón meta de B4/B5/B6. Cierra junto a los 3 con un solo cambio en gen_match (~80 LoC + 4 unit tests). El fix es flow-sensitive refinement sobre los arms del match: cuando un arm body termina en return/break/continue, su body code emite ! (never) en Rust (vía strip_trailing_semi ya existente), pero el TIPO interno de Fitz se setteaba a Type::Null y entraba al LUB de los arms, widening-eando spuriously hacia Nullable(T). Ahora trackeamos divergencia con un Vec<bool> paralelo (arm_divergent) y filtramos esos arms del LUB. Si todos divergen, el resultado queda Null (rustc acepta ! → ()). El binding del let siguiente queda con el tipo correcto (List<X>, T Nominal, lo que sea del arm Ok), y .len() / [] / .field dispatchan normal en codegen. Ver B4 para detalle del fix aplicado.
B15 — .preload() sobre @belongs_to companion con FK Nullable + path HasMany simétrico — CERRADO 2026-06-19 (sub-paso 6 cosecha)¶
Síntoma original (cargo build):
error[E0308]: mismatched types
| expected `i64`, found `Option<i64>`
--> src/main.rs:N (en __FitzPgValue::Int(__g.<fk>))
error[E0277]: can't compare `i64` with `Option<i64>`
--> src/main.rs:N (en __tg2.<pk> == __fk)
Repro mínima REAL (descubierto al diagnosticar — el trigger es FK
nullable en el parent del @belongs_to, NO "nullables en el parent
type" como decía el doc original):
@table("users") type User {
@primary id: Int = 0
name: Str = ""
@has_many("Post", via="author_id") posts: List<Post> = []
}
@table("posts") type Post {
@primary id: Int = 0
@belongs_to("User") author_id: Int? = null // ← FK nullable
author: User?
title: Str = ""
}
@get("/posts")
async fn list_posts() -> Result<List<Post>> {
let conn = db.connect("...").await?
return Post.where(fn(p) => p.title == "x").preload("author").all(conn).await
}
Hallazgo importante al diagnosticar: el repro original del doc
(con FK Int = 0 no-nullable + nullables varios en el parent type)
NO disparaba el bug post-sub-pasos 1-5 (B7 + B8 lo habían cerrado
parcial). El trigger REAL del E0277/E0308 es el FK Nullable
(@belongs_to ... X_id: Int? = null). Los models de fitzwatch
declaran todos los FK como Int = 0 sentinel, así que probablemente
fitzwatch quedó pausado por otro bug (W18 cerrado en v0.17.0) y no por
B15 estricto. Pero B15 sigue siendo bug crítico porque cualquier
usuario que declare FK opcional (Int?) lo dispara, sin workaround
viable user-side.
Severity: 🔴 Critical — BLOQUEANTE. Fix aplicado: dos
cambios coordinados en src/codegen.rs:
emit_belongs_to_companion_preload_arm— nuevo paramparent_fields: &[TypeSigField]para detectar nullable del FK. CuandoType::Nullable(_):- IDs collection emite
filter_mapen lugar demap:__guard.iter().filter_map(|__p| { let __g = __p.lock().unwrap(); __g.<fk>.map(__FitzPgValue::Int) }).collect()— las rows conNoneFK skipean elIN (...)query entero. -
Lookup del
__matchedemitematch __fk { None => None, Some(__fk_v) => __targets.iter().find(|__t| { let __tg2 = __t.lock().unwrap(); __tg2.<pk> == __fk_v }).cloned(), }— comparación coni64directo en el armSome. -
emit_preload_dispatch(path HasMany) — sibling fix:target_fieldsbusca el FK del child. Si nullable, emite__cg2.<fk> == Some(__pid)en lugar de__cg2.<fk> == __pid. El__pidesi64(PK del parent) y el__cg2.<fk>ya esOption<i64>.
Política consistente: row con FK = None significa "no tiene parent
en el target" → el companion queda como None (no agarra ningún
match). Semántica idéntica al intérprete (que usa la representación
unificada Value).
Tests nuevos (3 unit + 1 E2E):
- codegen_orm_preload_companion_with_nullable_fk_emits_filter_map_b15
- codegen_orm_preload_companion_with_nonnull_fk_emits_simple_map_b15
(no-regression — FK no-nullable sigue emitiendo el path legacy)
- codegen_orm_preload_has_many_with_nullable_child_fk_emits_some_pid_b15
- tests/compile_e2e.rs::cross_module_orm_preload_nullable_fk_b15 —
end-to-end con BelongsToCompanion + HasMany ambos con FK nullable
en el mismo programa.
~115 LoC totales en src/codegen.rs (delta + tests inline).
B16 — match arms con tipos incompatibles (i64 vs ()) en posición no-Nullable¶
Descubierto durante el sub-paso 2 de la cosecha al reproducir fitzwatch. NO se cierra en el sub-paso 5 — el fix correcto vive en el checker (no es mecánico como B1/B9 etc.) y requiere discusión de UX (error proactivo del checker vs trade-off de over-trigger).
Síntoma (cargo build):
Repro:
let n = match result {
Ok(_) => 0,
Err(e) => log.error("msg", err: e), // log.error retorna Null (i.e. () en Rust)
}
log.error(...) retorna Null (no es divergent — no aborta), y el otro arm retorna i64. La unificación en el codegen NO promueve a Nullable(Int) (porque ninguno de los arms es null literal o tipa como Nullable), así que el match queda como expresión de tipo Int pero un arm produce ().
Workaround user-side (validado durante sub-paso 2):
let n = match result {
Ok(_) => 0,
Err(e) => { log.error("msg", err: e); 0 }, // terminar arm con sentinel `; 0`
}
Severity: Low. Diferencia con B7 (que sí cerramos en sub-paso 3): B7 cuando uno de los arms es null literal y el otro tiene un T concreto → LUB produce Nullable(T) y mi fix wrap-ea con Some()/None. B16 cuando uno de los arms es una call expression que devuelve Null y el otro un T concreto → LUB no promueve a Nullable (no hay null literal involucrado). El codegen NO tiene info suficiente para insertarle el sentinel automáticamente.
Fix sugerido futuro (sub-paso 6+): detectarlo en el checker como type error claro citando el workaround. Mensaje sugerido: "match arm N retorna Int, arm M retorna Null. Si querés ignorar el valor del log call, terminá el arm con ; <valor_default>". Alternativa más invasiva: el codegen detecta el caso y auto-emite el ; + sentinel del tipo del otro arm — me gusta menos por semánticamente oscuro.
Tabla resumen + plan de ataque sugerido¶
| # | Bug | Sev | Workaround user | Estimado fix (LoC) | Test E2E necesario |
|---|---|---|---|---|---|
| B1 | .order_by(str) |
Medium | ✅ closure | ✅ CERRADO 2026-06-19 ~70 LoC + 3 unit tests | sí |
| B2 | http.post body Instance |
Medium | ✅ Map |
✅ CERRADO 2026-06-19 ~45 LoC + 2 unit tests | sí |
| B3 | ws_broadcast cross-module |
High | ✅ helper | ✅ CERRADO 2026-06-19 ~20 LoC | sí |
| B4 | .len() sobre List<X>? |
Medium | ✅ helper Result | ✅ CERRADO 2026-06-19 (parte de B14) | unit |
| B5 | [] sobre List<X>? |
Medium | ✅ helper Result | ✅ CERRADO 2026-06-19 (parte de B14) | unit |
| B6 | .x sobre T? |
Medium | ✅ helper Result | ✅ CERRADO 2026-06-19 (parte de B14) | unit |
| B7 | match no envuelve Some() |
Medium | ✅ sentinels | ✅ CERRADO 2026-06-19 ~60 LoC + 3 unit tests | unit |
| B8 | Option<T>: __IntoPgValue |
Medium | ✅ sentinels | ✅ CERRADO 2026-06-19 ~12 LoC + 3 unit tests | unit |
| B9 | Str? == Str arms incomp |
Low | ✅ coerce local | ✅ CERRADO 2026-06-19 ~40 LoC + 4 unit tests | unit |
| B10 | spawn(fn) cross-module @bg |
Medium | ✅ wrapper local | ✅ CERRADO 2026-06-19 ~150 LoC + 3 unit + 1 E2E | sí |
| B11 | Response con List<Nominal> |
High | ✅ split endpoints | ✅ CERRADO 2026-06-19 ~150 LoC | sí |
| B12 | Cross-module @auth_provider codegen |
Low | ✅ import auth |
✅ CERRADO 2026-06-19 ~70 LoC + 1 E2E | sí |
| B13 | log.X kwargs heterogéneos |
(n/a) | ✅ NO REPRO en v0.17.0 (W18 lo cerró) | n/a | |
| B14 | (meta) match refine | — | (helpers Result) | ✅ CERRADO 2026-06-19 ~80 LoC + 4 unit tests | unit (cubre B4/B5/B6 + all-divergent) |
| B15 | .preload() + FK Nullable |
🔴 Critical | ❌ sin workaround | ✅ CERRADO 2026-06-19 ~115 LoC + 3 unit + 1 E2E | sí |
| B16 | match arms i64 vs () E0308 |
Low | ✅ ; <sentinel> |
abierta (sub-paso 7+) | sí |
Plan de ataque sugerido (orden por dependencias + impacto):
- Sub-paso "detectores unificados" — B3 + B11 + B13 — ✅ CERRADO 2026-06-19. B3 + B11 fixeados con ~150 LoC en
src/codegen.rs; B13 NO reprodujo en v0.17.0 (W18 lo había cerrado). Detalle: ver entries individuales arriba. Smoke:cargo test --release --lib3116/3116 verde post-fix. - Sub-paso "refine flow-sensitive en match" — B4 + B5 + B6 (todos del meta B14). ✅ CERRADO 2026-06-19. ~80 LoC + 4 unit tests. Filtro de arms divergent (
return/break/continue) en el LUB degen_match. Smokecargo test --release --lib match_with12/12 verde; smokecompile_e2e smoke_ejemplos_guia_compilables_compilan363/363 verde; repro mínima end-to-end (d:\tmp\sub2-divergent-repro\repro.fitz) check + run + build + binario ejecutado con paridad bit-a-bit. - Sub-paso "wrap automático Some()" — B7. ✅ CERRADO 2026-06-19. ~60 LoC + 3 unit tests. Post-procesamiento en
gen_matchdespués del LUB: cuandoresult_tyqueda comoNullable(inner), los arms no-divergentes conbody_ty == Nullse reescriben aNoney los arms conbody_tyconcreto compatible coninnerse envuelven enSome(...). Arms yaNullablequedan idempotentes; arms divergentes (!) no requieren rewrite. Smokecargo test --release --lib match_84/84 verde; repro mínima end-to-end (d:/tmp/sub3-nullable-wrap-repro/repro.fitz) validada bit-a-bitfitz run↔fitz build. - Sub-paso "Option
→ PgValue" — B8. ✅ CERRADO 2026-06-19. ~12 LoC + 3 unit tests. Blanket impl__IntoPgValue for Option<T>sumado al preludio DB del codegen (gated porprogram_uses_db).None → __FitzPgValue::Null,Some(v) → v.into_pg(). Libcargo test --release --lib b819/19 verde (incluye los 3 nuevos); lib completa 3126/3126 verde; smokecompile_e2e smoke_ejemplos_guia_compilables_compilan363/363 verde (~4 min); repro mínima end-to-end (d:/tmp/sub4-option-pg-repro/repro.fitz) —fitz checkOK,fitz buildOK, paridad bit-a-bitfitz run↔ binario nativo (mismo error de runtime DB esperado). Próximo norte de la cosecha: sub-paso 5. -
Sub-paso "fixes mecánicos" — B1 + B2 + B9 + B10 + B12. ✅ CERRADO 2026-06-19.
Nuevo E0308 match arms. ~375 LoC + 16 tests nuevos (unit + E2E). Detalle:i64vs()registrado como B16 (deuda separada — fix requiere refinement del checker, no es mecánico)- B1 (~70 LoC + 3 unit):
.order_by("col")y.order_by("-col")con Str literal aceptados por el evaluator (orm_qb_order_by) y por el codegen (emit_qb_order_by_chain); ASC por default,-prefix → DESC; validación de field existence consistente con el path closure; emite mismo.with_order_by(col, desc)en codegen. - B2 (~45 LoC + 2 unit): body con type Instance en
http.post/put/etc. ahora despacha a__fitz_http_body_from_json(__to_fitz_json())cuandohas_http=true(la trait__ToFitzJson+ serde_json viven en el preludio HTTP server-side). Helper nuevo sumado alHTTP_CLIENT_HTTP_INTEGRATION_PRELUDE. Sinhas_http, error claro citando el workaround (declarar handler stub o convertir a Map). - B9 (~40 LoC + 4 unit):
Nullable(T) == T/!= Tpara primitivos (Str/Int/Float/Bool) emite<opt>.as_ref() == Some(&<conc>)(paralelo a.is_none()/.is_some()ya existente para Nullable vs Null). Aplica simétrico (LHS o RHS). Mixed primitives (Int↔Float) y nominales quedan como deuda menor (cae al path existente que sigue rechazando). - B10 (~150 LoC + 3 unit + 1 E2E): cross-module
@backgrounddetection.TypeEnvsumaimported_background_fns: HashSet<String>+ métodosadd_imported_background_fns/imported_background_fns. Nueva pub fn en types:extract_background_fn_names(program). Pre-scans nuevos:pre_scan_imported_background_fnsenmain.rs(pathfitz check/fitz run) +pre_scan_imported_background_fns_for_loaderencodegen.rs(pathfitz buildper-módulo). ElModuleLoadersuma campomain_imported_background_fnspropagado a cada módulo.gen_spawn_callextiende su lookup para chequearmodule_bindingsademás delfn_sigslocal, así emite la llamada Rust con la sig importada cuando target =from <bg_mod> import bg. - B12 (~70 LoC + 1 E2E): cross-module
@auth_providerpara módulos. ElModuleLoadersuma campomain_imported_auth_providerpre-scanneado engenerate_projectANTES decollect_imports. Enload_module, el provider del módulo esor_else-combined: primero las propias imports del módulo (path histórico W12), luego como fallback el de main. Cierra el caso "módulo con@authenticatedsinimport authpropio porque main ya lo importó".
Smoke
cargo test --release --lib b1 b2 b9 b10 b1214/14 verde. Smokecargo test --release --test compile_e2e cross_module_spawn_background_b10 cross_module_auth_provider_via_main_b122/2 verde. Próximo norte de la cosecha: sub-paso 6 (B15 — el bloqueante). 6. Sub-paso BLOQUEANTE "ORM.preload()+ Nullable companion" — B15. ✅ CERRADO 2026-06-19. ~115 LoC + 3 unit + 1 E2E. Hallazgo importante al diagnosticar: el trigger REAL es FK Nullable (@belongs_to ... X_id: Int? = null) en el parent del companion, NO "nullables en el parent type" como decía el doc original (que post-sub-pasos 1-5 con B7+B8 cerrados ya NO disparaba el bug). Dos cambios coordinados ensrc/codegen.rs: (a)emit_belongs_to_companion_preload_armrecibeparent_fieldsy, cuando el FK es Nullable, emitefilter_map(|p| p.<fk>.map(__FitzPgValue::Int))para elIN (...)+match __fk { None => None, Some(v) => find(v) }para el lookup; (b)emit_preload_dispatch(path HasMany sibling) detecta el FK del child Nullable y emite__cg2.<fk> == Some(__pid)en lugar de bare__pid. Smokecargo test --release --lib b153/3 verde; smokecargo test --release --test compile_e2e cross_module_orm_preload_nullable_fk_b151/1 verde; smokecompile_e2e smoke_ejemplos_guia_compilables_compilan366/366 verde (~5 min); fmt + clippy--lib --tests --bins -- -D warningslimpios. Lib completa 3141/3141 verde (+3 vs sub-paso 5). Repro mínima validada end-to-end (d:/tmp/sub6-preload-repro/repro_fk_null.fitzcon FKInt? = null+ companion + handler —fitz buildproduce binario nativo). Importante para fitzwatch: los models declaran todos los FK comoInt = 0sentinel (no Nullable), así que probablemente el bug que pausó fitzwatch fue otro (W18 cerrado en v0.17.0), no B15 estricto. Pero B15 sigue siendo bug crítico que cualquier usuario con FKInt?dispara sin workaround viable user-side. Cosecha codegen post-fitzwatch CERRADA ENTERA: sub-pasos 1-6 cubren B1-B12 + B14 + B15 cerrados; B13 NO reproduce (W18); B16 abierto como deuda separada (fix vive en el checker, no es mecánico). - B1 (~70 LoC + 3 unit):
Validación al cerrar la cosecha:
- cd d:\fitzwatch && fitz build debería pasar limpio.
- Smoke: docker compose up -d --build en fitzwatch + curl tests del API + browser tests del admin Vue 3.
- Si todo OK → fitzwatch al deploy VPS (Cloudflare DNS + Origin Cert + nginx site enable).
- Bump versión: probable v0.17.1 (parche, todos son bugs sin cambios de API) o v0.18.0 (minor, si se aprovecha para algún feature).
🟡 ORM nativo — gaps detectados durante fitzwatch (2026-06-18)¶
Auditoría hermana de la cosecha codegen (arriba). Durante el desarrollo de fitzwatch (
d:\fitzwatch\, status page + uptime monitor en Fitz puro) auditamos el balance ORM:SQL-crudo del proyecto. Terminó 35:65 en queries totales — el ORM nativo cubre el CRUD básico, pero seis categorías nos forzaron a bajar aconn.query/conn.execcrudo. Esta sección las agrupa para que futuras tandas las evalúen en conjunto.Independiente de la cosecha codegen — pueden cerrarse en cualquier orden o paralelo. Los workarounds user-side (
conn.query/conn.exec) son el patrón canónico documentado endocs/db-orm.mdy replicado en los boilerplatesapi-orm-full/taskhub/api-multi-tenant.
O1 — .update({...}) no acepta expresiones SQL como NOW() / EXTRACT¶
Síntoma: el ORM .update(conn, {...}) solo acepta valores literales o variables Fitz. Para last_check_at = NOW() o duration_secs = EXTRACT(EPOCH FROM (NOW() - started_at))::int hay que bajar a conn.exec(SQL crudo, [args]).
Repro fitzwatch (checks.fitz → update_monitor_last_status y close_open_incidents):
let _ = conn.exec(
"UPDATE monitors SET last_check_at = NOW(), last_status = $1 WHERE id = $2",
[status, monitor_id],
).await?
Workaround user-side: conn.exec(...) crudo. Funciona pero pierde tipado del lenguaje sobre el field.
Severity: Medium. Fix sugerido: permitir expresiones whitelisted en el Map del .update({...}), tipo {"last_check_at": db.now(), "count": db.raw("count + 1")} o helpers similares. Otra opción más quirúrgica: aceptar strings prefijados con SQL! que el ORM trata como expresión cruda (similar a sqlalchemy.func.now()). ~80-120 LoC.
O2 — Sin migrations automáticas (fitz db diff/migrate)¶
Síntoma: cualquier proyecto serio que usa ORM nativo tiene que mantener el SQL crudo del CREATE TABLE en un módulo aparte (schema.fitz en fitzwatch) y llamarlo al boot del main con init_schema().await. El ORM declara los @table types pero no genera ni el DDL inicial ni los diffs cuando los types cambian.
Repro fitzwatch (schema.fitz → 5 CREATE TABLE IF NOT EXISTS).
Workaround user-side: db.exec("CREATE TABLE IF NOT EXISTS ...", []) al boot. Es lo que hacen también los boilerplates api-orm-full, api-multi-tenant, taskhub, etc. (patrón canónico).
Severity: High (impacto al ecosistema entero). Fix sugerido: deuda 10.6 ya conocida — fitz db diff + fitz db migrate. Mini-fase dedicada cuando la cosecha codegen cierre — paralelo a Diesel CLI / Alembic / Prisma migrate. NO bloquea la cosecha; queda como deuda visible en el roadmap.
O3 — Aritmética de fechas no soportada en .where(closure)¶
Síntoma: filtros tipo "monitores con last_check_at + interval_secs < NOW()" no se pueden expresar con el translator AST→SQL del .where(...). Sin SQL crudo, hay que cargar TODOS los monitores activos y filtrar en aplicación con date arithmetic JS-style — N queries ineficientes.
Repro fitzwatch (scheduler.fitz):
let rows = conn.query(
"SELECT id FROM monitors WHERE paused = false AND (last_check_at IS NULL OR last_check_at + make_interval(secs => interval_secs) < NOW())",
[],
).await?
Workaround user-side: conn.query crudo.
Severity: Medium. Fix sugerido: extender el translator .where(...) para soportar operadores de fecha (método tipo m.last_check_at.add_seconds(m.interval_secs) < db.now() con helpers db.now() / db.add_interval(...)). Probablemente paralelo a la deuda del tipo nativo DateTime mencionada en fitzwatch DEUDA.md. ~100-150 LoC (junto con O1 comparten translator).
O4 — Agregaciones complejas (COUNT + SUM(CASE WHEN) + JOINs + GROUP BY)¶
Síntoma: el .aggregate(...) del ORM cubre count, sum, avg, min, max simples por una sola columna, pero NO expresiones tipo SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) o COUNT(*) / NULLIF(COUNT(c.id), 0) con LEFT JOIN.
Repro fitzwatch (public.fitz → fetch_monitor_statuses, el snapshot del status page público):
let rows = conn.query("""
SELECT m.id, m.name, m.kind, m.last_status, m.last_check_at,
COALESCE(ROUND(100.0 * SUM(CASE WHEN c.status = 'up' THEN 1 ELSE 0 END)
/ NULLIF(COUNT(c.id), 0), 2), 0)::float8 AS uptime_24h_pct,
COUNT(c.id) AS checks_24h_count
FROM monitors m
LEFT JOIN checks c ON c.monitor_id = m.id
AND c.recorded_at > NOW() - INTERVAL '24 hours'
WHERE m.paused = false
GROUP BY m.id, m.name, m.kind, m.last_status, m.last_check_at
ORDER BY m.name
""", []).await?
Workaround user-side: conn.query crudo.
Severity: Low (el escape hatch es el patrón canónico). Fix sugerido: probable que NO se cierre nunca — los proyectos serios con SQL complejo siempre necesitan crudo (Diesel también recomienda esto). Lo relevante es documentar más fuerte en db-orm.md y la guía que es un trade-off intencional, no un gap. ~50 LoC docs.
O5 — CTEs anidadas + window functions + percentile_cont + date_trunc¶
Síntoma: el ORM no expresa CTEs (WITH ... AS), window functions (OVER (...)), funciones de Postgres específicas (percentile_cont(0.95) WITHIN GROUP ORDER BY ..., date_trunc('hour', ts)).
Repro fitzwatch (metrics.fitz → dashboard_overview con CTE anidada de 3 niveles + monitor_timeline con percentile_cont + date_trunc).
Workaround user-side: conn.query crudo.
Severity: Low (igual que O4 — el escape hatch es canónico). Fix sugerido: igual que O4 — documentar más fuerte que es un trade-off intencional. CTEs y window functions en un ORM serían un mini-DSL en sí mismas. ~50 LoC docs (junto con O4).
O6 — JOINs custom con SELECT alias (m.name AS monitor_name)¶
Síntoma: el ORM soporta JOINs implícitos via .preload(...) que populan el companion field del type parent. Pero no soporta SELECTs con alias custom tipo SELECT i.id, m.name AS monitor_name FROM incidents i JOIN monitors m ON ..., donde el resultado es un row con shape distinto al type ORM (mezcla campos de varios).
Repro fitzwatch (grids.fitz → grid_incidents con JOIN + filtros opcionales con sentinels ($2 = '' OR ...)).
Workaround user-side: conn.query crudo + tipo plano construido manualmente desde row.get_str(...) (que también sufrió bugs del codegen B7+B11 sobre nullable fields).
Severity: Low (sub-case de O4/O5 — escape hatch canónico). Fix sugerido: posible API tipo .select_custom("col1, col2, ...").join(...) que devuelva List<Map<Str, Any>> o similar. Pero probablemente no se justifique — el escape hatch ya está. ~80 LoC si entra demanda real.
Tabla resumen + plan sugerido¶
| # | Gap | Sev | Workaround user | Fix sugerido / decisión |
|---|---|---|---|---|
| O1 | .update() con NOW()/EXTRACT |
Medium | ✅ conn.exec crudo |
helpers db.now()/db.raw(...) o tipo SQL! strings |
| O2 | Sin migrations automáticas | High | ✅ CREATE TABLE IF NOT EXISTS al boot |
deuda 10.6 ya conocida — fitz db diff/migrate |
| O3 | Aritmética de fechas en .where |
Medium | ✅ conn.query crudo |
helpers db.now()/db.add_interval(...) (junto con O1) |
| O4 | Agregaciones complejas (SUM/CASE) | Low | ✅ conn.query crudo |
documentar como trade-off, no cerrar |
| O5 | CTEs + window functions + Postgres-specifics | Low | ✅ conn.query crudo |
documentar como trade-off, no cerrar |
| O6 | JOINs custom con SELECT alias |
Low | ✅ conn.query crudo + tipo manual |
sub-case de O4/O5 — escape hatch canónico |
Plan de ataque sugerido (después de cerrar la cosecha codegen — son independientes):
- O1 + O3 — helpers de expresiones SQL en ORM: nueva sub-fase del ORM que suma
db.now()/db.add_interval(...)/db.raw(...)para usar adentro de.update({...})y.where(closure). ~150-200 LoC. Cierra los dos en un solo bloque coordinado porque comparten el translator. - O2 — migrations automáticas: deuda 10.6, mini-fase dedicada (proyecto separado, paralelo a Diesel CLI / Alembic / Prisma migrate). ~500-1000 LoC. Cuando entre, beneficia al ecosistema entero. No bloquea la cosecha codegen ni fitzwatch.
- O4 + O5 + O6 — documentación reforzada: actualizar
docs/db-orm.mdydocs/guide.mdcap 31 con un párrafo explícito tipo "estos tres casos son intencionales —conn.querycrudo es el escape hatch canónico, paralelo a Diesel/SQLAlchemy". Sumar adocs/curso/m6/también. ~100 LoC docs.
Origen de la auditoría: balance ORM:SQL-crudo de fitzwatch terminó en aproximadamente 35:65 en términos de queries. La filosofía es la misma que en los boilerplates api-orm-full / taskhub / api-multi-tenant: ORM para CRUD básico, SQL crudo para todo lo demás. La tabla por módulo de fitzwatch está en d:\fitzwatch\NEXT-SESSION.md.
🟢 SMTP builtin — CERRADA v0.18.0 (mini-tanda 2026-06-19)¶
CERRADA: el módulo
smtpbuilt-in fue implementado en bloque (B1-B8) durante la mini-tanda iniciada el 2026-06-19, cerrando la deuda anotada el 2026-06-18 durante el desarrollo de fitzwatch. Fitz tiene ahorasmtp.send(opts)async ciudadano de primera clase, paralelo bit-a-bit al HTTP client de v0.17.0, con paridad intérprete↔binario, sin deps externas en el host. VerCHANGELOGv0.18.0, cap 17 dedocs/guide.md(sub-sección "SMTP outbound"), y los 3 ejemplos runnableexamples/guide/17{i,j,k}-smtp-{basico,errores,magic-link}.fitz. El plan original (deuda anotada abajo) fue cumplido al 100%.Detectada durante el desarrollo de fitzwatch al armar el módulo de notificaciones (
notifications.fitz). El proyecto necesita despachar notificaciones por email cuando se abre/cierra un incident. Fitz no tiene SMTP builtin — el workaround actual es delegar todo a webhooks outbound y que el user enganche un n8n/ifttt/zapier que traduzca webhook → email.
Contexto¶
El stack web nativo de Fitz cierra estos ciudadanos de primera clase:
HTTP server-side, HTTP client (v0.17.0), WebSockets tipados, auth (JWT
+ Argon2id), cron + background jobs, OpenAPI, AsyncAPI, ORM Postgres,
observability OTel + Prometheus, feature flags, deploy ciudadano.
SMTP outbound NO está — los proyectos que necesitan enviar mail
(notificaciones, password reset, magic links, alerts, marketing
transactional, etc.) tienen que rebotar por webhook + servicio
externo, o aplicar interop Python con smtplib.
API target sugerida (paralelo a http.X)¶
// Módulo `smtp` built-in, paralelo a `http`/`jwt`/`hash`/`log`.
// Send simple (texto plano)
let r = smtp.send({
"to": "user@example.com",
"from": "fitzwatch@status.prothos.com.ar",
"subject": "Incidente abierto: API Producción",
"body": "El monitor cayó hace 30s. Ver detalles: https://...",
}).await?
// r: SmtpResult { delivered: Bool, message_id: Str, duration_ms: Int }
// Send con HTML + texto plano (multipart/alternative)
let r = smtp.send({
"to": "user@example.com",
"from": "...",
"subject": "...",
"body_text": "Texto plano fallback",
"body_html": "<html>...</html>",
}).await?
// Send con attachments
let r = smtp.send({
"to": "...",
"from": "...",
"subject": "...",
"body": "...",
"attachments": [
{ "filename": "report.pdf", "bytes": pdf_bytes, "mime": "application/pdf" },
],
}).await?
// Tipo built-in nuevo paralelo a HttpClientResponse:
type SmtpResult {
delivered: Bool
message_id: Str
duration_ms: Int
}
Configuración (env vars + builtin)¶
El módulo lee config de env vars al boot (paralelo a cómo db.connect
toma URL):
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=fitzwatch@example.com
SMTP_PASSWORD=...
SMTP_FROM=fitzwatch@example.com # default From si el send no especifica
SMTP_TLS=starttls # starttls | implicit | none
O config explícita via builtin (sin env vars):
smtp.configure({
"host": "smtp.gmail.com",
"port": 587,
"user": "fitzwatch@example.com",
"password": secret("SMTP_PASSWORD"),
"tls": "starttls",
})
Modelo de errores¶
Paralelo a http.X y jwt.encode: Result<SmtpResult> con Err(Str)
para errores de transporte (DNS, conexión, auth, TLS handshake,
timeout, etc.). Status 5xx del SMTP server NO son Err (el user mira
r.delivered); solo errores de transporte van a Err.
Backend implementación¶
Probable crate base: lettre = "0.11" (la opción canónica del
ecosistema Rust, soporta async con tokio1 feature, mantenida
activamente, sin deps externas en el host). Linkeado estático sin
openssl (rustls backend).
Integración con el resto del stack¶
@cron/@background: dispatch async natural (smtp.send(...).await?).- Observability: cada send emite
log.info("smtp.delivered", ...)conduration_ms,to,message_id. Métricas Prometheussmtp_sends_total smtp_send_duration_seconds.- Templates: por ahora
bodycomo Str (conformat!-style interpolation del lenguaje). Templates dedicados (HBS / Tera-like) como deuda menor del módulo, refinables si entra demanda. - Tipos:
Map<Str, Any>como input requiere__FitzValueintegration (paralelo ajwt.encodey al body dehttp.post). Si esa deuda no cerró antes, el MVP del SMTP builtin acepta soloMap<Str, Str>strict para los kwargs delsend(...)y los attachments quedan como deuda menor del builtin.
5 diferenciales (paralelo a HTTP client)¶
- Built-in del lenguaje — no
pip install yagmail/npm install nodemailer/cargo add lettre. - Paridad bit-a-bit
fitz run↔fitz build— el binario standalone tiene el cliente SMTP linkeado. - Async ciudadano de primera — se integra con
@cron/@background/ handlers HTTP/spawn(...). Result<T>automático — errores como valores,?propaga.- Sin deps externas en el host —
rustlsbackend, sin openssl.
Plan de bloques sugerido (paralelo a HTTP client builtin)¶
- B1 evaluator:
Value::Module { name: "smtp" }registrado enregister_builtins+ builtinsmtp.send(...)async + pre-registro tipoSmtpResult+ helper privado dispatch input → message lettre. - B2 checker: pre-registro
smtp/SmtpResultenCheckCtx::new - signatures + regla
?heredada de Result. - B3 codegen: detector
program_uses_smtp(program)walka AST +cargo_toml_forsumalettrecondicional + preludioSMTP_PRELUDEconstatic __FITZ_SMTP_CLIENT: LazyLock<SmtpTransport>+ helpers async__fitz_smtp_sendparalelo bit-a-bit al intérprete + dispatch engen_call. - B4 LSP: completions de
smtp+SmtpResult. - B5 guía + ejemplos: sub-sección nueva en cap apropiado de
docs/guide.mdcon panorama vecino (smtplib/nodemailer/lettre) - ejemplos runnable (envío simple, HTML, attachments, error handling contra MailHog local).
- B6 docs cross-cutting: CLAUDE + README + index.md + roadmap + este doc actualizado.
- B7 boilerplate: sumar SMTP a uno de los boilerplates existentes
(probable
taskhubcon notificaciones de tasks asignadas, o ejemplo de magic-link auth en alguno de los api-*). - B8 cierre formal: CHANGELOG + roadmap + extensión VSCode bump
.vsixregenerado + blog drafts ES/EN.
Severity + prioridad¶
Severity: Medium. Prioridad: después de cerrar la cosecha codegen post-fitzwatch (próximo norte). Es deuda visible pero no bloqueante — los proyectos que necesitan email pueden usar webhook + servicio externo mientras tanto (es lo que hace fitzwatch por ahora).
Workaround actual (lo que aplica fitzwatch v0.x)¶
notifications.fitz despacha solo webhooks. Para kind="email":
if (channel.kind == "email") {
log.warn("notify.email.not_supported ch={ch.id} dest={ch.destination}")
return null
}
El user que quiere emails configura un canal webhook que apunta a n8n/ifttt/zapier/activepieces y traduce ahí webhook → SMTP. Funciona pero contradice el modelo "todo nativo en el core".
🟢 NO es deuda — trade-off documentado del ORM JSON serializer (analizado 2026-06-09, no requiere fix del lenguaje)¶
Contexto: durante el smoke E2E del TaskHub post-v0.15.14, el
frontend rompió con Cannot read properties of undefined (reading
'forEach') al abrir un project con 0 tasks. La causa: el endpoint
GET /projects/{id} con .preload("tasks") retorna JSON sin el
campo tasks cuando la lista preloaded está vacía.
Verificación del codegen (src/codegen.rs:25160-25203,
v0.10.8 fix #7):
// Tradeoff aceptado: una lista vacía legítima (post sin
// comments) NO se distingue de "no preloaded". El user que
// necesite distinguir esos casos puede usar un handler
// dedicado (`GET /posts/{id}/comments`) en lugar de eager
// loading.
if is_virtual(&f.name) {
match relation_kind {
Some(HasMany) => {
// Arc<Mutex<Vec<T>>> — emit si NO está vacía.
writeln!(...,
"if !__is_empty {{ __obj.insert(\"{name}\".to_string(), ...); }}"
)
}
Some(HasOne) | Some(BelongsToCompanion) => {
// Option<T> — emit si Some.
}
}
}
Decisión de diseño DELIBERADA: el codegen no tiene flag
was_preloaded per-instancia (sería ~struct overhead) y elige
"lista vacía = asumimos no preloaded" como heurística pragmática.
El comentario in-code lo cita explícitamente con el workaround
sugerido para casos que necesiten distinguir "preloaded con 0
rows" vs "no preloaded".
Conclusión sobre la "deuda":
- El lenguaje NO tiene bug — el codegen hace exactamente lo que documenta hacer.
- El cliente DEBE defensar —
project.tasks || []es el patrón correcto que el codegen asume del consumidor cuando el field puede o no estar presente. - El frontend del TaskHub TENÍA bug — asumía
project.taskssiempre presente sin defensa. Fixeado enboilerplates/taskhub/frontend/assets/app.jscon comentario in-line que cita esta decisión del codegen. - Mejora del cap C4 del curso TaskHub (deuda de docs, no
del lenguaje): debería citar el trade-off del codegen +
mostrar el patrón defensivo del cliente como pattern canónico
cuando se usa
.preload().
Caso archivado — el análisis quedó documentado para que futuras sesiones no vuelvan a marcar esto como "deuda del lenguaje".
🔴 DEUDAS URGENTES — Smoke E2E TaskHub Dockerizado (2026-06-08) — AMBAS CERRADAS v0.15.13¶
Dos bugs reales del lenguaje encontrados al hacer el smoke E2E con
docker compose up sobre boilerplates/taskhub. Ambos son
reproducibles fuera del TaskHub y afectan cualquier programa Fitz
que use el patrón correspondiente.
Estado al 2026-06-08: AMBAS CERRADAS en v0.15.13 con tests unit + E2E reales contra Postgres + smoke real del TaskHub.
✅ URGENTE-1 — init_storage de @cron con persistencia: race condition en CREATE TABLE (CERRADA v0.15.13 intérprete + v0.15.14 codegen)¶
⚠️ Lección aprendida — fix incompleto en v0.15.13: el fix
v0.15.13 cubrió SOLO el intérprete (src/cron_jobs.rs). El smoke
E2E real con docker compose up sobre el TaskHub (2026-06-09)
descubrió IN VIVO que el binario producido por fitz build
seguía rompiendo con el race original, porque el codegen
(src/codegen.rs::SQL_HELPERS_PRELUDE) emite su propio
__fitz_cron_init_storage paralelo que NO consultaba el OnceCell
del intérprete.
Cierre real en v0.15.14: paralelo bit-a-bit del fix v0.15.13
agregado al preludio del codegen. El binario producido ahora
también tiene static __FITZ_CRON_INIT_STORAGE_ONCE:
tokio::sync::OnceCell<Result<(), String>> global +
__fitz_cron_init_storage_inner (helper real) + wrapper
__fitz_cron_init_storage que invoca get_or_init. Verificado in
vivo con docker compose up sobre el TaskHub fixeado.
Por qué no se detectó en v0.15.13: los tests unit del codegen
verifican el shape del Rust generado (string contains), pero no
ejecutan el binario producido contra Postgres real. Los tests E2E
reales contra Postgres (tests/cron_jobs_real_postgres.rs) cubren
el path del intérprete (src/cron_jobs.rs), no el del binario
nativo. El smoke E2E con docker compose es lo que cierra esa
brecha — fue ese smoke (post-release v0.15.13) el que reveló el
bug residual.
Mejora futura del proceso: todo fix a la lógica del scheduler de cron debe sumar test E2E del codegen path — compilar un programa con N crons + ejecutar el binario producido contra Postgres real + verificar que no rompe. Es el nivel de cobertura que faltó.
Síntoma reproducible: dos o más @cron("...", store=db_result)
adentro del mismo programa. Al boot del binario, uno de los jobs
aborta con:
🕐 cron job 'X' no pudo inicializar storage, abortando task:
ERROR [23505]: duplicate key value violates unique constraint
"pg_type_typname_nsp_index"
[sql: CREATE TABLE IF NOT EXISTS fitz_cron_jobs (...)]
El otro job arranca normal. Resultado: el cron que aborta nunca corre — silent failure parcial del scheduler.
Reproducción mínima (cualquier programa con ≥2 @cron
persistentes):
let db_result = db.connect(env_or("DATABASE_URL", "")).await
@cron("0 0 * * * *", store=db_result)
async fn job_a() -> Result<Null> { return Ok(null) }
@cron("0 0 * * * *", store=db_result)
async fn job_b() -> Result<Null> { return Ok(null) }
@server(8080, "0.0.0.0")
fn main() => 0
Causa real (verificado en código, no hipótesis):
src/cron_jobs.rs:615-624 —
run_cron_job ejecuta init_storage(&conn).await por cada job
spawneado, sin coordinación. Los N jobs se lanzan en paralelo con
tokio::spawn(__fitz_run_cron_job(...)) desde
emit_cron_job_spawns del codegen, y los N corren
CREATE TABLE IF NOT EXISTS fitz_cron_jobs simultáneo. Postgres
tiene una race condition documentada en su catálogo de tipos
(pg_type) cuando dos sesiones intentan crear la misma tabla al
mismo tiempo — el IF NOT EXISTS checkea la existencia ANTES de
intentar el INSERT al catálogo, pero entre el check y el insert hay
una ventana. Postgres NO serializa los CREATE TABLE
internamente con un lock de catálogo a nivel de "esta tabla", solo
el SET de catálogo entero (race en pg_type específicamente).
Por qué el init_storage no es idempotente bajo concurrencia:
es idempotente bajo el modelo SQL (IF NOT EXISTS), pero el
chequeo CREATE TABLE de Postgres no es transaccional contra
operaciones paralelas del mismo objeto. Bug documentado upstream
en postgres-archives
desde ~2020. Patrón conocido y la solución estándar es serializar
el CREATE con un advisory lock.
Fix sugerido (~20 LoC, mini-fase dedicada):
Opción A — OnceCell<Result<(), String>> en cron_jobs.rs:
static INIT_STORAGE_ONCE: tokio::sync::OnceCell<Result<(), String>>
= tokio::sync::OnceCell::const_new();
async fn ensure_storage_initialized(
conn: &DbConnHandle,
) -> Result<(), String> {
INIT_STORAGE_ONCE
.get_or_init(|| async {
init_storage(conn).await
})
.await
.clone()
}
Reemplazar la llamada directa en run_cron_job:618 por
ensure_storage_initialized(&conn).await. El primer job que llega
ejecuta el init; los demás esperan al OnceCell y reusan el
resultado. Sin race, sin overhead extra después del primer init.
Opción B — pg_advisory_xact_lock(hash('fitz_cron_init')) antes
del CREATE TABLE. Permite múltiples instancias del binario en
paralelo (lock compartido a nivel DB, no a nivel proceso). Más
robusto que A si en algún momento se corren múltiples instancias.
Recomendación: Opción A para el MVP (cubre 99% del caso — binario único). Opción B si aparece deploy multi-instancia con crons coordinados.
Fix implementado en v0.15.13 (commit)
- Nuevo
tokio::sync::OnceCell<Result<(), String>>global ensrc/cron_jobs.rs::INIT_STORAGE_ONCE. - Nuevo wrapper
pub async fn ensure_storage_initialized(conn)que llamaINIT_STORAGE_ONCE.get_or_init(...)— solo el primer caller del proceso ejecutainit_storagereal, los demás reciben elResultclonado. run_cron_job:618ahora llama aensure_storage_initialized(en lugar delinit_storagedirecto). Comentario actualizado eninit_storageadvirtiendo que NO es seguro para uso paralelo directo.- Helper test-only
reset_init_storage_once_for_tests()con#[doc(hidden)](no#[cfg(test)]porque los integration tests necesitan llamarlo desde otro crate).
Tests E2E reales contra Postgres (tests/cron_jobs_real_postgres.rs,
opt-in con FITZ_TEST_PG_URL):
v0_15_13_ensure_storage_initialized_evita_race_con_10_paralelos: 10tokio::spawnconcurrentes invocanensure_storage_initialized. Sin el fix al menos uno rompía conpg_type_typname_nsp_index; con el fix los 10 completan OK.v0_15_13_ensure_storage_initialized_cachea_resultado_segundo_call_no_corre_create_table: tras el primer init, drop manual de las tablas + segundo call retorna OK (reusa cache, NO re-ejecuta SQL) — verificable consultando que la tabla NO fue re-creada.
Validado contra Postgres 15 local: 2/2 tests verdes con
FITZ_TEST_PG_URL.
Smoke real TaskHub: tras el fix + restart, ambos crons
(daily_due_reminders + cleanup_old_tasks) arrancan sin error.
Limitaciones pendientes (deuda menor abierta):
- Multi-proceso: el OnceCell es por proceso. Si dos binarios
fitz arrancan simultáneamente contra la misma DB, ambos pueden
golpear el race de Postgres. Fix futuro: sumar
pg_advisory_xact_lockadentro deinit_storageantes del CREATE TABLE. NO se hizo en v0.15.13 porque el caso típico despliega UN container por servicio. - Si el primer init falla (ej: DB down al boot), todos los jobs futuros reciben el mismo error sin reintento. Restart del proceso es lo que destraba. Patrón aceptado para el MVP.
✅ URGENTE-2 — @server(host=...) no acepta sintaxis kwarg (CERRADA v0.15.13)¶
Síntoma: el patrón canónico para Dockerizar exige bindear a
0.0.0.0 (no 127.0.0.1 que es el default). El user intenta:
El codegen aborta con:
✗ codegen: Error — @server: kwarg 'host' no reconocido. Soportados:
docs, api_version, ws_heartbeat_secs, shutdown_timeout_secs,
observability, prometheus.
Workaround actual — pasar host como 2do positional:
Funciona, pero es API inconsistente. Todos los otros parámetros del
decorator son kwargs nombrados (ws_heartbeat_secs,
shutdown_timeout_secs, etc.) — solo port y host son
positionals. El user que ya conoce el patrón kwarg del resto del
decorator espera lo mismo para host y se rompe.
Causa real (verificado en código):
src/evaluator.rs:1335-1401 —
register_server_config itera deco.args (positionals)
explícitamente buscando args[0] (port) y args[1] (host). El
loop posterior sobre deco.kwargs mapea solo a docs,
api_version, ws_heartbeat_secs, shutdown_timeout_secs,
observability, prometheus. host jamás aparece en el switch
de kwargs.
Por qué importa para el patrón Dockerizado:
- Default
127.0.0.1es decisión correcta por seguridad (igual que FastAPI exige--host 0.0.0.0explícito). - Pero los boilerplates Dockerizados (TaskHub + 8 más) siempre
explicitan
"0.0.0.0". - El user que viene de cap 28 / cap 33 de la guía aprende a usar
kwargs
prometheus=true,docs=false, etc. y asume quehostes kwarg también — falla silenciosamente. - Especialmente confuso porque el mensaje de error lista los
kwargs soportados y
hostNO está. El user descubre el workaround positional por leer otros boilerplates, no por la documentación.
Fix sugerido (~30 LoC + tests + doc update):
Aceptar host como kwarg además de positional. Adentro de
register_server_config, después del loop de positionals, agregar
una rama nueva al match de kwargs:
"host" => match value_expr {
Expr::Str(s, _) => {
// Validamos misma regla que el positional
if s.parse::<std::net::IpAddr>().is_err() {
return Err(err(format!(
"@server sobre fn '{}': host '{}' no es IP válida",
fn_name, s,
)));
}
// Si también vino como positional, error de conflicto
// (igual que Python: TypeError con doble especificación).
config.host = s.clone();
}
other => return Err(err(...)),
},
"port" => match value_expr {
Expr::Int(n, _) => { /* idem */ }
...
},
Y validación de conflicto: si vienen positionals + kwargs para el mismo parámetro, error claro. Patrón estándar de Python args parsing.
Paridad bit-a-bit codegen: mismo helper
parse_build_server_args en src/codegen.rs debe aceptar el
kwarg también.
Tests requeridos:
- @server(8080, host="0.0.0.0") compila y bindea OK
- @server(port=8080, host="0.0.0.0") (ambos kwargs) compila OK
- @server(8080, "127.0.0.1", host="0.0.0.0") rechaza con conflicto
- Tests del intérprete + codegen + cap 17 de la guía actualizado
Fix implementado en v0.15.13 (commit)
Cambios paralelos bit-a-bit en evaluator y codegen:
src/evaluator.rs::register_server_config (~50 LoC):
- Flags
port_set_via_positionalyhost_set_via_positionalpara detectar doble-especificación. - Match de kwargs gana ramas
"port"y"host"con error claro estilo Python ("port pasado dos veces") si ya vino positional. - Reuso de la validación existente (port en
[1, 65535], host como IP literal parseable). - Mensaje de error de kwarg desconocido actualizado para citar
port, host, docs, api_version, ws_heartbeat_secs, shutdown_timeout_secs, observability, prometheus.
src/codegen.rs::parse_server_decorator (~50 LoC paralelo):
Mismo cambio bit-a-bit en fitz build. El comportamiento del
binario nativo es idéntico al intérprete.
Tests:
- 9 unit tests evaluator (
v0_15_13_server_*): host kwarg solo, port kwarg solo, mixed con otros kwargs, port positional + host kwarg, doble port (positional + kwarg) error, doble host error, host kwarg no-str error, port kwarg fuera de rango error, host kwarg IP inválida error. - 5 unit tests codegen (
v0_15_13_server_*): host kwarg emite0.0.0.0:3000, port kwarg emite127.0.0.1:9090, mixed emite0.0.0.0:8080, conflictos rechazados. - Test viejo
server_kwarg_desconocido_lista_docs_y_api_versionactualizado al mensaje nuevo (incluyeportyhost).
Patrón canónico nuevo (recomendado, equivalente a positionals):
Más claro que mezclar positionals con kwargs. Los positionals siguen funcionando para backward-compat.
Smoke real TaskHub: tras el fix, cambiar
@server(8080, "0.0.0.0", ws_heartbeat_secs=30, prometheus=true)
por @server(port=8080, host="0.0.0.0", ws_heartbeat_secs=30,
prometheus=true) compila/corre bit-a-bit igual.
Codegen interop Python + ORM completo — TaskHub blockers (2026-06-08)¶
Mini-fase descubierta al smoke-testear boilerplates/taskhub
(showcase del stack completo). El boilerplate combina por primera
vez ORM con DateTime/Date/Uuid + interop Python +
@background/@cron + relations virtuales en un solo programa,
exponiendo gaps del codegen que no se manifiestan en stacks
individuales (los 10 boilerplates anteriores andan OK).
Fix aplicado (CERRADO 2026-06-08 en source local, pendiente bump)¶
5 mejoras al codegen src/codegen.rs (~115 LoC nuevas):
impl __FitzToPyparachrono::NaiveDate/DateTime<Utc>/uuid::Uuiden el preludio Python. Serializan a Pythonstrcanonical (ISO 8601YYYY-MM-DD/ RFC 3339 / UUID canonical). Antes los types FitzDate/DateTime/Uuidno tenían impl y rustc rompía al intentarself.created_at.__fitz_to_py(...).- Branches
Date/DateTime/Uuidenpy_field_extract_arms(Python → Fitz, no-nullable). Parsean Pythonstral type Rust nativo (NaiveDate::parse_from_str(s, "%Y-%m-%d")/DateTime::parse_from_rfc3339(s).with_timezone(&Utc)/Uuid::parse_str(s)). - Mismos branches en
py_inner_extract_for_nullable(cubreDate?/DateTime?/Uuid?nullable). Antes el_branch rechazaba con "fieldXde tipoY(nullable): inner type compuesto no soportado todavía". - Skip de virtual fields en
gen_fitz_py_to_instance_helper(W17 paralelo, v0.10.7 hizo lo mismo para__FromFitzJson/__ToFitzJson): companion fields BelongsTo /@has_many/@has_onese inicializan conDefault::default()en el struct literal en lugar de intentar extraerlos del PyDict (son sentinels del ORM, no datos que vengan de Python).gen_python_helpers_for_typeygen_fitz_py_to_instance_helperahora aceptanmeta: Option<&TableMetadata>análogamente agen_type_http_impls_for_sig_with_meta. - Branches
Date/DateTime/Uuiden.update(db, Map var)runtime match (~30 LoC en line 19387+). Acepta__FitzValue::Str(s)y emite__FitzPgValue::Text(s.clone())paralelo a cómoimpl __IntoPgValue for chrono::NaiveDateya hace.
Validación: cargo test --lib 3121 verdes, 0 failed. Cero regresiones sobre 10 boilerplates + 99 guide examples.
Bug derivado del cap C5 (TaskHub) — fixeado en boilerplate¶
El cap C5 del proyecto Construyendo TaskHub usaba db.query(sql, [])
(módulo) en el endpoint admin GET /api/jobs cuando debería ser
conn.query(sql, []) (la connection bindeada). El intérprete lo
permitía (dispatch implícito sobre conn global) pero el codegen
solo soporta db.connect. Inconsistencia intérprete vs codegen
documentada como deuda secundaria — no aplica al fix del codegen,
pero el cap C5 + ejemplo + boilerplate están corregidos a
conn.query(...).
Bug derivado del cap C4 — workaround documentado¶
El cap C4 (background fn) usaba match Task.where(...).first(conn).await { Ok(t) => t, ... }
sin anotación explícita del tipo de t. El codegen rechazaba el
field access task.assignee_id posterior con "field access sobre
T?: solo se soporta sobre instancias de tipos custom". Workaround:
agregar anotación explícita let task: Task = match ... { Ok(t) => t, ... }.
Deuda menor del codegen — el checker entiende el patrón pero el
codegen no infiere el tipo desde el match arm Ok(t) => t. Si
aparece presión, mini-fase futura puede inferir el tipo del Result
inner en el codegen.
Mini-fase post-2026-06-08 — LazyLock<.await> + spawn(fn(arg)) capture-by-move (CERRADA v0.15.12)¶
Los 2 blockers que quedaron al cierre de v0.15.11 (TaskHub baja
de 21 → 2 errores rustc) eran bugs pre-existentes del codegen que
aparecieron al destrabar los 19 anteriores (efecto cascade
unblocking). Ambos cerrados en v0.15.12 — TaskHub fitz build
exit 0, binario 9.7 MB, healthz/readyz/metrics validados end-to-end.
Blocker 1 — LazyLock<T> con .await en init (E0728) → OnceCell<T>
Trigger: let X = <expr>.await top-level + handlers HTTP/WS/cron
que consumen X. El codegen lo hoisteaba a static __FITZ_STATE_X:
LazyLock<T> = LazyLock::new(|| <expr>.await) cuyo closure es sync
— rustc emitía error[E0728]: await is only allowed inside async.
Fix v0.15.12: helper expr_contains_await(e) recursivo detecta
async init en resolve_state_var_types, llena
state_var_async: HashMap<String, bool> en CodegenCtx.
gen_http_main dispatch entre dos paths según el flag:
- Sync (caso típico
let users = []): mantiene path LazyLock bit-a-bit idéntico —static X: LazyLock<T> = LazyLock::new(|| init); - materialización
(*X).clone(). Zero cambio. - Async (caso
let db_result = db.connect(url).await): emitestatic X: tokio::sync::OnceCell<T> = OnceCell::const_new();+ init eager en el body delasync fn main()antes de spawn/serve via{ let __init: T = init; X.set(__init).expect("..."); }. Materialización engen_top_fn+emit_cron_job_spawnscambia aX.get().expect("...").clone().
Mismo costo runtime (Arc clone, no contenido).
Blocker 2 — spawn(fn(arg)) movía vars del outer (E0382) → shadow-clone preventivo
Trigger: let _ = spawn(send_due_reminder(new_task.id)) seguido por
uso de new_task después. El tokio::spawn(async move { ... })
capturaba new_task por valor moviéndolo al closure; el caller
después rompía con error[E0382]: use of moved value.
Fix v0.15.12: helper collect_idents_in_expr(e, &mut HashSet)
recursivo recolecta idents del scope outer en inner_args. gen_spawn_call
emite let <name> = <name>.clone(); para cada ident filtrado por
var_in_any_scope (descarta fns/builtins/types — no clonables, no
necesarios) ANTES del tokio::spawn. El async move captura los
shadow clones; el outer scope sigue accesible.
Output esperado para el caso TaskHub:
{
let new_task = new_task.clone(); // shadow
let __jh = tokio::spawn(async move {
send_due_reminder(new_task.lock().unwrap().id).await
});
Box::pin(async move { __jh.await.unwrap() })
}
Tests nuevos al cierre v0.15.12: 6 unit tests dedicados (v0_15_12_*)
+ 1 actualizado (v0_15_11_* shape OnceCell). Sin regresión en 3037
unit + 99 smoke guide + 360 compile_e2e + 3 openapi. fmt + clippy
limpios. Smoke real TaskHub verde end-to-end.
Deudas residuales heredadas de v0.15.11 (NO bloquean) — siguen abiertas como refinamientos menores del codegen/checker:
- Inferencia del Result inner en match con un arm que aborta
(
return Err(...)) — workaround: anotación explícitalet conn: DbConn = match db_result { ... }. - Checker no detecta
!Future<Bool>como olvido de.await— workaround:.awaitexplícito. - Coerción PyAny → primitivo adentro de match arm no se propaga
desde anotación del
letcontenedor — workaround: shadow var tipada adentro del arm.
Cada una con workaround trivial, ninguna bloquea uso real. Mini-fase futura si entra demanda concreta.
Mini-fase post-2026-06-08 — (&db_result).into_store() no resuelve en __main_inner (CERRADA v0.15.11)¶
Diagnóstico inicial (sesión anterior) decía "db_result no en
scope para @background/@cron". Investigación 2026-06-08
descubrió que el bug real es distinto: los handlers
@background/@cron SÍ ven db_result (vía la materialización
local que gen_top_fn hace para state vars referenciados,
líneas 11738-11764 de src/codegen.rs).
El bug real: el bloque de tokio::spawn(__fitz_run_cron_job(...))
emitido por emit_cron_job_spawns dentro de __main_inner (o
fn main en modo cron-only) emitía store: (&db_result).into_store()
usando el nombre de usuario. Pero db_result está hoisteado a
static __FITZ_STATE_DB_RESULT cuando algún handler también lo
consume (caso típico TaskHub: HTTP handlers + @cron(store=db_result)
combinan). En el cuerpo de __main_inner no existe el local
db_result, sólo el static — y rustc rompe con error[E0425]:
cannot find value 'db_result' in this scope.
Fix (v0.15.11): emit_cron_job_spawns materializa los state
vars referenciados como store_var ANTES del loop de spawns con
let db_result: T = (*__FITZ_STATE_DB_RESULT).clone(); (mismo
patrón que el body de gen_top_fn líneas 11738-11764). Lista
deduplicada (si N jobs usan el mismo store var, una sola
materialización). Sin overhead — Arc::clone y nada más. Test
unitario nuevo v0_15_11_cron_store_kwarg_con_state_var_materializa_local_antes_de_spawn
candea la regresión.
Sub-fix simultáneo en boilerplates/taskhub/src/main.fitz
para destrabar el smoke real del boilerplate:
-
17 sitios
let conn = match db_result { ... }→ anotación explícitalet conn: DbConn = match db_result { ... }. Sin la anotación el codegen tipaconncomoT?(Nullable) y declara la variable comoOption<__FitzDbConn>aunque ambos arms devuelven__FitzDbConnplano. Deuda residual del codegen (no nueva — ya documentada como "bug derivado del cap C4"): el codegen no infiere el inner del Result desde el match armOk(c) => ccuando el otro arm aborta conreturn. -
@healthz fn check_db_alive() -> Bool→@healthz async fn check_db_alive() -> Bool+.awaitsobrec.is_closed(). El métodois_closed()retornaFuture<Bool>(paridad intérprete + codegen, versrc/codegen.rslínea 16565). El checker NO detectó que el!Future<Bool>era inválido. Mismo fix para@readyz fn check_ready_for_traffic. Deuda menor del checker: aceptar!sobre tipoFuture<Bool>sin emitir error de "se olvidó.await". -
let suggested: Int = match priority.suggest_priority(...) { Ok(p) => p, ... }→ introducirlet v: Int = padentro del arm para forzar coerción PyAny → Int de Fase 8.4. Deuda menor del codegen: la coerción PyAny → primitivo adentro de match arms con anotación destino en elletcontenedor no se propaga al arm. El intérprete sí lo hace.
Las tres deudas del codegen/checker son refinamientos no-bloqueantes — los workarounds son triviales y el patrón canónico del boilerplate queda como ejemplo de "los 3 patches" para futuros usuarios.
Pre-existente — M7.C1 app.fitz (examples/curso/m7-python-interop/c1-setup/app.fitz)¶
4 errores del checker independientes del fix interop:
Error 24:24 — operador * espera operandos numéricos, recibió PyAny y Float
Error 25:29 — operador * espera operandos numéricos, recibió Float y PyAny
Error 37:29 — el tipo Result<Any> no tiene el método isoformat
Error 47:5 — return devuelve Result<Any> pero la función declara Str
Pre-existente (no relacionado con mi fix de codegen). Deuda
del cap M7 — el ejemplo del cap probablemente quedó stale tras
cambios al checker sobre PyAny arithmetic + isoformat method
discovery. Mini-fase separada del cap.
Verificación de tooling local¶
- Toolchain
:latest-pythonde GHCR existe y publica OK (verificado condocker manifest inspect+docker run --rm <img> fitz --versiondevuelve0.15.0). Falsa alarma del primer smoke — era imagen local cacheada vieja. Forzar re-pull condocker image rm <img> + docker pull <img>resuelve. - CI release.yml job
docker-image-python(líneas 363-445) corre OK en cada release tag, publica:vX.Y.Z-python+:latest-pythonparalinux/amd64. Verificado en run de v0.15.0 (4m 9s, completed).
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: usarCERRADO (2026-06-17) — el bench nuevoohacon body fijo para POSTbenchmarks/mixed-workload/usak6con body unique per VU per iter (templatevu${vu}-it${iter}-r${random}@...) sosteniendo 50 VUs concurrentes 1 min enwrites-only.js. Mide POST throughput real sin perder email único: Fitz 847 RPS vs Python 170 RPS sobre el mismo POST/users— 5x ganancia invisible en el bench v0.10.13. Detalle enbenchmarks/mixed-workload/README.md. - 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).CERRADO PARCIAL (2026-06-17) — verbenchmarks/mixed-workload/(bench nuevo conk6en lugar deohapara scripted scenarios). Cubre: mixed workload realista (60% reads + 40% writes intercalados), escritura concurrente saturada (50 VUs sostenidos writes-only — cierra "POST mide el cliente, no el server"), VU ramping para detectar saturation point, p99.9 + error rate sobre el peak, comparativa 3 stacks (Fitz vs Python+SQLAlchemy vs Node+Prisma) sobre dominiousers + postscon FK. Headline: Fitz 31x mejor p95 que Python + 5.5x mejor p95 que Node en writes-only sostenido; 45x mejor p95 que Python bajo mixed peak (Python satura a 503 ms p95, Fitz mantiene 11 ms). Pendiente: bulk inserts (1k+ rows en una transaction), queries con JOINs profundos sobreapi-orm-fullcomo base — quedan como mini-fase futura si aparece demanda.
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.
Fase 11.3 — CSS scoping para .fitzv components — CERRADO 2026-07-14¶
Tercera mini-fase completa de la Fase 11 (frontend nativo). Cierra
end-to-end el pipeline <style scoped> / <style global> desde
el source hasta el ExpandedComponent.style tipado.
Tres mini-commits (todos en el mismo día, uno por decisión de diseño):
- 11.3.a —
<style global>como sibling first-class de<style scoped>en el lexer + parser + AST del view module. Nuevoenum StyleKind { Scoped, Global }sobreStyle. RefactorToken::StyleScopedRaw(String)→Token::StyleRaw { kind, body }. Bare<style>(sin opt-in) rechazado con targeted error nombrando ambos forms aceptados (Vue defaults to global, Svelte to scoped, Fitz refuses to pick a silent default). +5 lexer tests + +5 parser tests. - 11.3.b — CSS mini-parser standalone en
src/view/css_parser.rs(~900 LoC + 45 unit tests + 1 doctest) conapply_scope(css_raw, scope) -> Result<String, CssParseError>. Estrategia class-suffix: cada.fooen el CSS se convierte a.foo-<scope>. Char-by-char, cero deps, cero regex. Recurse en@media/@supports/@containerbodies;@keyframes/@font-face/@import/@charset/@namespaceopacos. Handles strings + block comments + attribute selectors. Selector-arg pseudos (:not(.foo),:is(.a, .b),:has(.c)) scope el inner correctamente por el walk transparente sobre parens; non-selector-arg pseudos (:nth-child(2n+1)) no tienen.así que no se tocan. - 11.3.c — Wire scoping end-to-end en
expand: nuevoenum ExpandedStyle { Scoped { css_scoped, scope_class, loc }, Global { css, loc } }reemplaza el passthrough raw. Scope class synthesised via FNV-1a de<component>::<css_raw>truncado a 8 hex, forma<component-kebab>-c-<8hex>. Template rewrite recursivo baja a Element children + If then/else + For bodies, agrega variantes sufijadas de cada clase original preservando las originales (class="card"→class="card card-<scope>", así JS externo querying.cardsigue funcionando). Global styles son passthrough puro (sin transform, sin rewrite). Interpolatedclass="{expr}"queda intacto (limitación documentada). Malformed CSS surface aExpandErrorcon el contexto del componente. +21 unit tests. Closes 11.3 entire.
Detalle exhaustivo de cada mini-commit vive en las secciones
§9.i, §9.j y §9.k de docs/fase-11-plan.md,
incluyendo 8 decisiones nombradas + deudas residuales listadas
por mini-commit.
Verificación pre-cierre (baseline pre-11.3 = 3411 lib / 3547
con --features lsp):
cargo test --libverde: 3487 (+76 delta = +10 de 11.3.a- +45 de 11.3.b + +21 de 11.3.c).
cargo test --lib --features lspverde: 3623 (mismo delta +76 espejado).cargo test --doc view::verde (1 nuevo doctest enapply_scope).cargo fmt --all --checklimpio.cargo clippy --lib --tests --bins -- -D warningslimpio.cargo clippy --lib --tests --bins --features lsp -- -D warningslimpio.
Deudas residuales derivadas (NO bloquean 11.4/11.5, cada una tiene fix propuesto en la sección §9.i/j/k correspondiente):
:global(...)escape hatch dentro de<style scoped>— refinable enapply_scopesin ripple a expand ni al template rewrite.- Interpolated
class="{expr}"stays as-is — el rewrite actual sólo tocaAttr::Static. Refinable con un runtime helper que suffijee on-the-fly. - Type / ID / attribute selectors NOT scoped (trade-off del
MVP class-suffix strategy). Refinable con un
<style scoped=deep>opt-in que swap el transformer + inyectedata-c-XXXXen cada elemento. - Precise position mapping de CSS errors dentro del blob — mismo debt que los otros blob parsers del view module.
- "Un scoped + un global" side-by-side — MVP capa a uno
solo. Refinable ampliando
Component.style: Option<Style>aComponent.styles: Vec<Style>cuando aparezca demanda. @layerat-rule opaque — no recurse. Refinable agregando"layer"al allowlistat_rule_nests_selectors.
Sin CHANGELOG entry — la Fase 11 vive en fase-11-plan.md
hasta que suficiente user-facing surface justifique un release
bump (siguiendo la convención de 11.1/11.2). Los tres commits
del día se ven en git log como feat(view): Phase 11.3.<x>
mini-commit — ....