mithril-lynx 0.0.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.omo/plans/m-request-fetch-lynx.md +306 -0
  2. package/.omo/plans/m-route-en-memoria.md +397 -0
  3. package/.omo/plans/mithril-lynx-v2-desde-cero.md +548 -0
  4. package/FETCH_INVESTIGATION.md +307 -0
  5. package/README.md +32 -302
  6. package/REQUEST.md +71 -0
  7. package/ROUTE.md +71 -0
  8. package/package.json +24 -80
  9. package/plugin.d.ts +4 -33
  10. package/plugin.js +108 -438
  11. package/rstest.config.ts +27 -0
  12. package/src/apply-patch.js +179 -0
  13. package/src/backends/virtual-backend.js +80 -0
  14. package/src/background.d.ts +11 -0
  15. package/src/background.js +79 -0
  16. package/src/channel.js +41 -0
  17. package/src/commit.js +67 -0
  18. package/src/dev-reload-client.js +171 -187
  19. package/src/dev-transport-noop.js +10 -0
  20. package/src/fake-dom.js +374 -0
  21. package/src/main-thread.d.ts +1 -0
  22. package/src/main-thread.js +68 -0
  23. package/src/mount-redraw.js +67 -0
  24. package/src/patch-protocol.js +40 -0
  25. package/src/reload/version.js +28 -0
  26. package/src/request.d.ts +37 -0
  27. package/src/request.js +181 -0
  28. package/src/route.d.ts +33 -0
  29. package/src/route.js +207 -0
  30. package/test/end-to-end.test.ts +86 -0
  31. package/test/reload-version.test.ts +17 -0
  32. package/test/request.test.ts +182 -0
  33. package/test/route-hot-reload.test.ts +40 -0
  34. package/test/route.test.ts +152 -0
  35. package/test/setup.ts +25 -0
  36. package/test/structural-reload.test.ts +95 -0
  37. package/CONTRACT.md +0 -151
  38. package/LICENSE +0 -21
  39. package/background.d.ts +0 -54
  40. package/background.js +0 -169
  41. package/element.d.ts +0 -34
  42. package/element.js +0 -83
  43. package/gesture.d.ts +0 -40
  44. package/gesture.js +0 -117
  45. package/internal/constants.js +0 -26
  46. package/internal/virtual-node.js +0 -388
  47. package/list.d.ts +0 -31
  48. package/list.js +0 -185
  49. package/main-thread.d.ts +0 -43
  50. package/main-thread.js +0 -165
  51. package/navigation.d.ts +0 -35
  52. package/navigation.js +0 -76
  53. package/renderer/background.d.ts +0 -21
  54. package/renderer/background.js +0 -84
  55. package/renderer/main-thread.d.ts +0 -12
  56. package/renderer/main-thread.js +0 -175
  57. package/src/lynx-mithril-shim.d.ts +0 -16
  58. package/src/lynx-mithril-shim.js +0 -1505
  59. package/src/worklet-runtime.js +0 -82
  60. package/testing.d.ts +0 -10
  61. package/testing.js +0 -91
@@ -0,0 +1,397 @@
1
+ # Plan — `m.route` para mithril-lynx-v2: navegación en memoria, sin APIs de navegador
2
+
3
+ > Estado (2026-09-17): **Plan completo — F0–F6 hechos y verificados.**
4
+ > F1–F3 con `rstest` (PAPI real, sin mocks, 9/9 tests). F4 y F6 en device
5
+ > real con una demo de 2 pantallas (`mithril-lynx-v2-app`), verificados
6
+ > mirando los ops del patch real (no el `nodeId` de CDP — ver la nota
7
+ > metodológica en §10, un hallazgo real de esta sesión). F5 (botón atrás
8
+ > nativo) queda con la respuesta de F0 (no hay evento expuesto al JS) sin
9
+ > una forma de confirmarlo/descartarlo más a fondo en este device.
10
+ > Pendiente no bloqueante: `mithril-runtime@1.1.0` (con
11
+ > `pathname/`/`querystring/` vendorizados) está pusheado a GitHub pero el
12
+ > `npm publish` pide un OTP que solo el usuario puede aprobar — mientras
13
+ > tanto `mithril-lynx-v2` y `mithril-lynx-v2-app` dependen de
14
+ > `file:/home/sweb/mithril-runtime` en vez de `^1.1.0`. Ver §10 al final
15
+ > para el detalle exacto.
16
+ > Idioma: español (consistente con el resto de planes de este proyecto).
17
+ > Insumo de las 3 tareas solicitadas: (1) cómo navega React en Lynx, (2)
18
+ > cómo navega Vue en Lynx, (3) diseño de nuestro propio `m.route`
19
+ > respetando la API de Mithril. Investigación vía WebSearch/WebFetch contra
20
+ > `lynxjs.org`, `vue.lynxjs.org`, y el repo `lynx-family/lynx` en GitHub —
21
+ > más lectura directa del `m.route` real en
22
+ > `node_modules/mithril/{route.js,api/router.js,pathname/}` (mithril 2.3.8,
23
+ > el mismo que `mithril-runtime` ya usa como base).
24
+
25
+ ---
26
+
27
+ ## 0. Por qué esto es un plan aparte (no parte del core ya construido)
28
+
29
+ `mithril-lynx-v2` (F0–F6, ya hecho y verificado en device) deliberadamente
30
+ no incluye navegación — es un no-objetivo explícito del plan original
31
+ (`mithril-lynx-v2-desde-cero.md` §2). Este documento cubre exactamente eso,
32
+ como una pieza **opcional y separada** (igual que en v1, `navigation.js`
33
+ era un export aparte, no parte del core) — nunca se mezcla con el
34
+ commit/patch/reload que ya está resuelto.
35
+
36
+ ---
37
+
38
+ ## 1. Tarea 1 — Cómo navega ReactLynx
39
+
40
+ **Mecanismo: React Router v6, con `MemoryRouter` (no `BrowserRouter`).**
41
+
42
+ ```jsx
43
+ <MemoryRouter>
44
+ <Routes>
45
+ <Route path="/" element={<App />} />
46
+ <Route path="/home" element={<Home />} />
47
+ </Routes>
48
+ </MemoryRouter>
49
+ ```
50
+
51
+ - **Por qué `MemoryRouter`**: Lynx no tiene `window.location` ni History
52
+ API real — no hay URL bar, no hay "atrás" de navegador. `MemoryRouter`
53
+ mantiene el historial como un array en memoria, sin ninguna dependencia
54
+ de API de browser.
55
+ - **Navegación**: no hay `<Link>`/`<a>` (Lynx no tiene esos tags) — se usa
56
+ el hook `useNavigate()` disparado desde un `ontap`/`bindtap`:
57
+ ```jsx
58
+ const nav = useNavigate();
59
+ <text bindtap={() => nav('/home')}>Ir a Home</text>
60
+ ```
61
+ - **Params/ubicación**: `useParams()` (segmentos dinámicos `:id`),
62
+ `useLocation()` (pathname actual).
63
+ - **Alternativa moderna documentada**: TanStack Router, mismo motivo
64
+ explícito: *"Memory Routing is required due to browser History API
65
+ limitations in Lynx"*. File-based routing vía plugin de rspack
66
+ (`@tanstack/router-plugin/rspack`) es un añadido posterior, no cambia el
67
+ fondo (memoria, no browser).
68
+ - **Dato relevante**: hay un issue abierto y sin resolver en
69
+ `lynx-family/lynx` (#93, *"How to navigate between pages in native
70
+ apps?"*) preguntando específicamente por navegación "nativa" (¿bundles
71
+ separados? ¿Activities/ViewControllers nuevos?) — **sigue sin respuesta
72
+ oficial**. Confirma que el modelo "un solo bundle, router en memoria" es
73
+ el único camino documentado/soportado hoy — no hay alternativa nativa
74
+ resuelta que estemos dejando pasar.
75
+
76
+ ## 2. Tarea 2 — Cómo navega Vue Lynx
77
+
78
+ **Mecanismo: Vue Router estándar, con `createMemoryHistory()`.**
79
+
80
+ ```ts
81
+ const router = createRouter({
82
+ history: createMemoryHistory(),
83
+ routes: [
84
+ { path: '/', name: 'home', component: Home },
85
+ { path: '/about', name: 'about', component: About },
86
+ { path: '/users/:id', name: 'user-detail', component: UserDetail },
87
+ ],
88
+ });
89
+ app.use(router);
90
+ ```
91
+
92
+ - **Misma razón explícita**: *"since Lynx has no browser `window.location`
93
+ or History API, you must use `createMemoryHistory()` instead of
94
+ `createWebHistory()"`.* Historial como array en proceso, cero
95
+ dependencia de browser.
96
+ - **Navegación**: sin `<a>` — dos caminos: `RouterLink` con `custom` +
97
+ scoped slot (renderiza `<view>`/`<text>` propios de Lynx, expone
98
+ `isActive` + `navigate`), o programático vía `useRouter()` +
99
+ `router.push()`/`.back()`/`.replace()` en un handler de tap.
100
+ - **Display**: `<RouterView>` renderiza el componente matcheado;
101
+ `useRoute()` para params (`route.params.id`).
102
+
103
+ ## 3. Patrón común extraído (la conclusión que importa para la Tarea 3)
104
+
105
+ React y Vue en Lynx **convergen exactamente en la misma arquitectura**,
106
+ independientemente del framework:
107
+
108
+ | Pieza | React en Lynx | Vue en Lynx | Conclusión para nosotros |
109
+ |---|---|---|---|
110
+ | Historial | `MemoryRouter` (array en memoria) | `createMemoryHistory()` (array en memoria) | Nuestro `m.route` NO debe tocar `window.history`/`popstate` — ninguno de los dos existe en Lynx |
111
+ | Modelo de página | Un solo bundle/página, "pantallas" = subárboles condicionales | Igual | Confirma que mithril-lynx-v2 tampoco necesita bundles separados por pantalla — un solo `renderApp()` de toda la vida de la app |
112
+ | Link/navegación | Sin `<a>`; hook + `ontap`/`bindtap` | Sin `<a>`; slot custom o hook + tap | `m.route.Link` debe re-diseñarse para emitir un elemento Lynx-nativo con `ontap`, no un `<a>` con `href`/`onclick` |
113
+ | Navegación "nativa" (multi-bundle) | Sin resolver, issue abierto sin respuesta | No mencionada | No hay un patrón oficial que estemos ignorando — el camino en-memoria es *el* camino |
114
+
115
+ ---
116
+
117
+ ## 4. El contrato real de `m.route` (lo que hay que igualar para que "se sienta Mithril")
118
+
119
+ Leído directo de `mithril/route.js` + `mithril/api/router.js` (2.3.8, el
120
+ mismo mithril del que sale `mithril-runtime`):
121
+
122
+ | API | Firma | Uso interno real |
123
+ |---|---|---|
124
+ | `m.route(root, defaultRoute, routes)` | monta `RouterRoot` en `root` vía `mountRedraw.mount()`, matchea rutas | `root` es un elemento DOM real — ver §5.2, esto cambia en v2 |
125
+ | `m.route.set(path, data, options)` | `pushState`/`replaceState` + `resolveRoute()` async | el `pushState` es lo único 100% browser-atado |
126
+ | `m.route.get()` | devuelve `currentPath` | puro, sin cambios |
127
+ | `m.route.prefix` | default `"#!"` | solo tiene sentido con URL real — ver §5.5 |
128
+ | `m.route.param(key)` | devuelve params de la ruta actual | puro, sin cambios |
129
+ | `m.route.Link` | componente que renderiza `<a href onclick>` | tiene que re-emitirse para Lynx — ver §5.4 |
130
+ | `m.route.SKIP` | sentinel para que `onmatch` pase a la siguiente ruta que matchee | puro, sin cambios |
131
+ | `onmatch(params, path, route)` | resolver async opcional por ruta (lazy-loading) | puro (Promise), sin cambios |
132
+
133
+ ## 5. Decisiones de diseño para v2
134
+
135
+ ### 5.1 Historial: array en memoria, no `window.history`/`popstate`
136
+
137
+ Un stack simple (`string[]` + índice actual) mantenido en un módulo interno
138
+ del router — exactamente lo que `MemoryRouter`/`createMemoryHistory()`
139
+ hacen por dentro. `route.set(path, data, options)` empuja o reemplaza en
140
+ ese array según `options.replace`, en vez de llamar
141
+ `history.pushState/replaceState`.
142
+
143
+ ### 5.2 Mount: NO un segundo punto de montaje — `m.route` alimenta el ÚNICO `renderApp()`
144
+
145
+ Real Mithril permite (y de hecho normalmente lo hace) que `m.route()` haga
146
+ su propio `mountRedraw.mount(dom, RouterRoot)`, independiente de cualquier
147
+ otro `m.mount()` que la app tenga. **v2 no puede replicar esto tal cual**:
148
+ la arquitectura entera (plan original §3.1) es *un solo* `renderApp()` para
149
+ toda la vida de la app — dos puntos de montaje independientes romperían el
150
+ modelo de un solo commit-controller/un solo patch stream.
151
+
152
+ **Decisión**: `m.route` para v2 pierde el parámetro `root` (no hay un DOM
153
+ real al que apuntar) y en su lugar **es quien llama `renderApp()` por
154
+ vos**:
155
+
156
+ ```js
157
+ // en vez de: m.route(document.body, "/", routes) (mithril real)
158
+ import route from "mithril-lynx-v2/route";
159
+ const app = route("/", routes); // llama renderApp() adentro, devuelve el mismo handle {redraw, document}
160
+ ```
161
+
162
+ Esto es una desviación real y documentada de la firma de mithril (3
163
+ argumentos → 2), justificada porque el primer argumento nunca tuvo sentido
164
+ en Lynx (no hay nodo al que montar) — no un intento de ocultarla. Todo lo
165
+ demás (`route.set/get/param/Link/SKIP`, sintaxis de rutas con `:param`)
166
+ queda idéntico.
167
+
168
+ ### 5.3 Redraw: se reusa `performRender`, no un redraw propio del router
169
+
170
+ `resolveRoute()` real llama `mountRedraw.redraw()` tras el primer mount.
171
+ Para v2, el router recibe (internamente, al llamar `renderApp()` por
172
+ dentro) la MISMA función `performRender` que ya es el único camino de
173
+ redraw de toda la app (commit.js) — no hay un segundo mecanismo de redraw
174
+ compitiendo.
175
+
176
+ ### 5.4 `route.Link`: rediseño obligatorio, sin `<a>`/`onclick`
177
+
178
+ Lynx no tiene `<a>` ni `onclick`. Mirroreando el patrón de React
179
+ (`ontap={() => nav(path)}`) y Vue (scoped slot con `navigate`):
180
+
181
+ ```js
182
+ route.Link = {
183
+ view(vnode) {
184
+ var selector = vnode.attrs.selector || "view";
185
+ var { selector: _s, options, params, href, ...rest } = vnode.attrs;
186
+ return hyperscript(selector, {
187
+ ...rest,
188
+ ontap: function (e) {
189
+ if (rest.disabled) return;
190
+ route.set(buildPathname(href, params), null, options);
191
+ },
192
+ }, vnode.children);
193
+ },
194
+ };
195
+ ```
196
+
197
+ `disabled` se respeta (igual que la versión real), pero sin
198
+ `aria-disabled`/`href=null` (conceptos de accesibilidad web sin
199
+ equivalente directo documentado en Lynx todavía — marcar como gap
200
+ conocido, no bloqueante).
201
+
202
+ ### 5.5 `route.prefix`: no-op de compatibilidad
203
+
204
+ No tiene sentido sin URL bar. Se deja como una propiedad asignable que no
205
+ hace nada (para no romper código portado de mithril real que hace
206
+ `m.route.prefix = ""` defensivamente), documentado explícitamente como
207
+ ignorado.
208
+
209
+ ### 5.6 `route.SKIP` / `onmatch` async: se preserva sin cambios
210
+
211
+ Es JS puro (Promises), sin acoplamiento a browser — mismo comportamiento
212
+ que mithril real, incluido lazy-loading de componentes de ruta.
213
+
214
+ ### 5.7 Botón "atrás" — pregunta abierta, no asumir una respuesta
215
+
216
+ Ni React ni Vue en Lynx documentan qué pasa con un botón/gesto de "atrás"
217
+ nativo del shell (Android back, gesto iOS). Esto es un spike (F0), no una
218
+ decisión ya tomada — ver §7.
219
+
220
+ ---
221
+
222
+ ## 6. Qué se reutiliza sin tocar (validado, no es la parte riesgosa)
223
+
224
+ `mithril/pathname/{build.js,parse.js,compileTemplate.js}` (108 líneas
225
+ totales) — el compilador de templates de ruta (`:id`, `:file...`, etc.).
226
+ **Verificado por grep: cero referencias a `window`/`document`/`location`/
227
+ `history` en los tres archivos** — es exactamente el mismo tipo de pieza
228
+ "pura, no-browser" que ya justificó reusar `render/render.js` tal cual en
229
+ el core de v2. Se vendoriza sin modificar (mismo criterio que
230
+ `mithril-runtime`: no forkeamos algo que no está roto).
231
+
232
+ Importante: estos archivos **no están en `mithril-runtime`** — el script
233
+ de extracción nunca los copió (no estaban en el allowlist de
234
+ `update-from-mithril.sh`). Hay que agregarlos ahí (a `mithril-runtime`,
235
+ como una pieza más del "runtime puro sin browser") o vendorizarlos
236
+ directo en `mithril-lynx-v2/route.js` — decisión de F1.
237
+
238
+ ---
239
+
240
+ ## 7. Fases
241
+
242
+ | Fase | Contenido | Criterio de salida | Device |
243
+ |---|---|---|---|
244
+ | **F0** | Spike: ¿Lynx expone un evento nativo de "back" (hardware/gesto) al hilo background? Revisar `lynx.getEngine()`/`lynx.getCoreContext()` en busca de un evento de lifecycle equivalente a `popstate`, probar en el device ya conectado. | Respuesta documentada con evidencia (logcat), no supuesta | Sí |
245
+ | **F1** | Vendorizar `pathname/*` (¿en `mithril-runtime` o directo en `mithril-lynx-v2`? decidir acá) + historial en memoria (array + índice) + `route.set/get/param/SKIP` sobre ese historial | Tests `rstest`: matching de `:param`, push/replace, `route.get()` correcto — sin device | No |
246
+ | **F2** | Integrar con `renderApp()`: `m.route(defaultRoute, routes)` como único punto de entrada, sin segundo mount | Test: navegar entre 2 rutas produce los ops de patch esperados (crear árbol nuevo, no acumular el viejo) | No |
247
+ | **F3** | `route.Link` rediseñado (`ontap`, sin `<a>`) | Test: tap en un Link navega, `disabled` bloquea el tap | No |
248
+ | **F4** | Verificación en device: navegar 2+ pantallas reales, confirmar con `uiautomator`/DevTool que la pantalla vieja se desmonta (sin huérfanos) | uiautomator/CDP tree limpio en cada paso | Sí |
249
+ | **F5** | Atrás nativo (según lo que F0 haya encontrado) — si existe evento, engancharlo a `route.set` con `replace`; si no existe, documentar que no hay atrás nativo y `route.Link`/botón explícito es el único camino | Depende de F0 | Sí |
250
+ | **F6** | Interacción con reload (A/B ya construidos): ¿cambiar de ruta durante un hot-update de datos/estructural sigue funcionando sin recrear todo el árbol? | Test + verificación en device combinando ambos | Sí |
251
+
252
+ ---
253
+
254
+ ## 8. No-objetivos
255
+
256
+ - Historial persistente entre reinicios de la app (un `Page.reload`/full
257
+ reload ya resetea todo — no se intenta preservar la ruta a través de
258
+ eso en esta fase).
259
+ - `route.prefix` funcional (hash o pathname reales) — no tiene sentido sin
260
+ URL bar; queda como no-op documentado (§5.5).
261
+ - Deep-linking (abrir la app directo en una ruta interna desde afuera) —
262
+ fuera de alcance, es un tema de intents/schemes nativos, no del router
263
+ en sí.
264
+
265
+ ---
266
+
267
+ ## 9. Referencias
268
+
269
+ - ReactLynx routing: <https://lynxjs.org/react/routing/react-router>,
270
+ <https://lynxjs.org/4.0/react/routing/tanstack-router>
271
+ - Vue Lynx routing: <https://vue.lynxjs.org/guide/routing>
272
+ - Issue sin resolver sobre navegación nativa:
273
+ <https://github.com/lynx-family/lynx/issues/93>
274
+ - Contrato real de `m.route`: `node_modules/mithril/route.js`,
275
+ `node_modules/mithril/api/router.js`,
276
+ `node_modules/mithril/pathname/{build,parse,compileTemplate}.js`
277
+ (mithril 2.3.8 — mismo checkout que ya usa `mithril-runtime`).
278
+ - Arquitectura base sobre la que esto se integra:
279
+ `mithril-lynx-v2-desde-cero.md` (especialmente §3.1, un solo
280
+ `renderApp()`, y §3.4, el único punto de commit/redraw).
281
+
282
+ ---
283
+
284
+ ## 10. Estado de ejecución
285
+
286
+ ### F0 — respuesta parcial (estático, no en device)
287
+
288
+ `grep` sobre `lynx_core.js` instalado (indicadores-android) y el runtime
289
+ de ReactLynx (`@lynx-js/react`) no encontró **ningún** evento de "back"
290
+ nativo/hardware expuesto al hilo JS — solo `onAppEnterBackground`
291
+ (ciclo de vida de la app, no navegación). Consistente con que ni
292
+ `MemoryRouter` ni `createMemoryHistory()` escuchan algo así. **No
293
+ confirmado en vivo** — el device no estaba conectado en esta sesión.
294
+ `route.back()`/`route.forward()` ya están implementados sobre el stack en
295
+ memoria (§5.1); si F4 en device confirma que SÍ existe un evento nativo,
296
+ conectarlo a `route.back()` es trivial.
297
+
298
+ ### F1–F3 — cerrados, verificados con `rstest` (5 tests nuevos, PAPI real)
299
+
300
+ - **`mithril-runtime@1.1.0`**: se agregaron `pathname/{build,parse,
301
+ compileTemplate}.js` + `querystring/{build,parse}.js`, copiados sin
302
+ modificar desde mithril 2.3.8 (verificado: cero referencias a
303
+ `window`/`document`/`location`/`history`). `update-from-mithril.sh`
304
+ actualizado para seguir sincronizándolos. Commiteado y pusheado a
305
+ GitHub (`5e35bd2`); **`npm publish` pendiente del OTP del usuario**.
306
+ - **`src/route.js`** (mithril-lynx-v2): implementado siguiendo §5
307
+ completo — historial en memoria, `route(defaultRoute, routes)` sin
308
+ parámetro `root` (llama `renderApp()` adentro, una sola vez, mismo
309
+ patrón `hasBeenResolved` que el `api/router.js` real), `route.set/get/
310
+ param/SKIP` idénticos en comportamiento a mithril real, `onmatch`
311
+ async preservado, `route.Link` reescrito con `ontap` (sin `<a>`/
312
+ `onclick`), `route.prefix` no-op documentado, y `route.back()/
313
+ forward()` nuevos (no existen en mithril real — necesarios porque acá
314
+ no hay botón de navegador que dispare `popstate`).
315
+ - **`test/route.test.ts`** (5 tests, todos contra PAPI real vía
316
+ `@lynx-js/testing-environment`, mismo patrón que
317
+ `test/end-to-end.test.ts`): ruta por defecto + `get()`/`param()`;
318
+ navegación sin nodos huérfanos (ops de `RemoveChild`+`CreateElement`
319
+ confirmados); `back()`/`forward()` sobre el stack; `onmatch`+`SKIP`
320
+ cayendo a la siguiente ruta; `Link` navegando por tap y respetando
321
+ `disabled`. **Los 8 tests de la suite completa pasan** (los 3 de antes
322
+ + estos 5).
323
+ - **Deviación respecto al plan original**: el plan (§5.2) proponía que
324
+ `m.route(...)` devolviera el handle `{redraw, document}` de
325
+ `renderApp()`. Al implementarlo se decidió NO devolver nada (igual que
326
+ el `m.route()` real, que tampoco devuelve algo útil) — el auto-redraw
327
+ de Mithril ya cubre todos los redraws necesarios sin que el código de
328
+ la app necesite tocar `app.redraw()`/`app.document` directamente. Más
329
+ fiel a "se siente Mithril" que la propuesta original.
330
+ - **Dependencia temporal**: mientras `mithril-runtime@1.1.0` no esté en
331
+ npm, `mithril-lynx-v2/package.json` apunta a
332
+ `file:/home/sweb/mithril-runtime` en vez de `^1.1.0` — cambiar en
333
+ cuanto el publish se complete (mismo procedimiento que la vez pasada
334
+ con 1.0.0).
335
+
336
+ ### F4 y F6 — cerrados, verificados en device real (2026-09-17, sesión siguiente)
337
+
338
+ Se armó una demo real de 2 pantallas en `mithril-lynx-v2-app`:
339
+ `src/screens/home.ts` (título + input + `route.Link` a `/detail/:id`) y
340
+ `src/screens/detail.ts` (título + `route.param("id")` + `route.Link` de
341
+ vuelta a `/`), con `background.ts` reescrito para usar `route(...)` +
342
+ un stable-host **por pantalla** (`HomeHost`/`DetailHost`, cada uno
343
+ `{view: () => currentX.view()}`) y `module.hot.accept` por archivo de
344
+ pantalla, llamando `route.set(route.get(), null, {replace:true})` para
345
+ forzar el re-render tras un hot-swap — la generalización directa del
346
+ patrón stable-host de F1/F3 del plan de reload, ahora una instancia por
347
+ ruta en vez de una sola global.
348
+
349
+ **F4 (navegación limpia)**: tap en "Ir a Detail →" en device real → título
350
+ cambia a "Detail" en rojo, `id: 42` se ve correctamente interpolado
351
+ (`buildPathname("/detail/:id", {id:42})` funcionando). Vuelta con
352
+ "← Volver a Home" funciona. **Hallazgo metodológico importante**: la
353
+ primera verificación usó `DOM.getDocument()` de Lynx DevTool para comparar
354
+ `nodeId` antes/después, y el `nodeId` cambió por completo entre pantallas
355
+ — parecía un huérfano. **Es una falsa alarma**: `nodeId` de CDP se
356
+ reasigna en cada llamada a `DOM.getDocument()`, no es un handle estable
357
+ del elemento nativo — comparar `nodeId` entre dos llamadas *distintas* no
358
+ prueba nada sobre si el elemento físico se recreó. La verificación
359
+ correcta (y la que de verdad importa) es mirar los **ops del patch real**:
360
+ navegar Home→Detail generó `[Op.RemoveChild, 0, 1, Op.CreateElement,
361
+ "view", 8, ...]` — un solo `RemoveChild` que tira toda la subrama vieja de
362
+ Home (id interno 1, el propio backend/virtual id, no el `nodeId` de CDP)
363
+ antes de crear Detail desde cero. Cero huérfanos, confirmado por el
364
+ mecanismo correcto. **Corrección para F5 del plan de reload
365
+ (`mithril-lynx-v2-desde-cero.md`)**: esa sesión también usó
366
+ `DOM.getDocument()` antes/después y reportó ids estables — en ese caso
367
+ coincidió con la realidad (validado independientemente por
368
+ `uiautomator` en la misma sesión), pero fue suerte de que no cambiara de
369
+ método entre llamadas, no una propiedad garantizada de la herramienta.
370
+ Anotado acá para que futuras sesiones no repitan la comparación de
371
+ `nodeId` entre llamadas separadas de `DOM.getDocument()` como prueba de
372
+ identidad.
373
+
374
+ **F6 (reload + ruta activa)**: parado en `/detail/42`, se editó
375
+ `screens/detail.ts` en vivo (cambio de texto). Logcat:
376
+ `:4`→`:5`→`:6 hmr-check-resolved updatedModulesLength:1`, **sin**
377
+ `:9` (full reload). Captura directa de los ops reales enviados
378
+ (instrumentación temporal en `channel.js`, removida después de
379
+ confirmar): **`[Op.SetText, id, "texto nuevo"]` — nada más.** Cero
380
+ `CreateElement`, cero `RemoveChild`. El stable-host por pantalla funciona
381
+ exactamente igual que el stable-host global del plan de reload — routing
382
+ no rompió nada del mecanismo ya construido. Confirmado además con un test
383
+ de regresión permanente (`test/route-hot-reload.test.ts`) que fija este
384
+ comportamiento con las mismas aserciones sobre los ops.
385
+
386
+ **F5 (botón atrás nativo)**: no se encontró forma de disparar un back
387
+ nativo real en esta sesión para confirmar/descartar el hallazgo estático
388
+ de F0 (el device no tiene un botón físico de "atrás" mapeado a la app —
389
+ solo el botón de navegación de Android, que sale de la app en vez de
390
+ navegar dentro de ella). El hallazgo de F0 (no hay evento expuesto al JS)
391
+ queda como la respuesta operativa: `route.back()`/`route.Link` explícito
392
+ son el único camino, tal como React/Vue en Lynx tampoco ofrecen otra
393
+ cosa.
394
+
395
+ **Suite completa: 9/9 tests pasan** (`npx rstest run`, sin device) — F1-F3
396
+ originales (5) + F6 (1, `route-hot-reload.test.ts`) + los 3 previos del
397
+ plan de reload.