Move servers/dating → demos/dating/server and the dating route's co-located _components/_lib/_design assets → demos/dating/web. Single `npm run dating:dev` boots both server and web via concurrently. Replace the warm-paper / raspberry-pink Claude Design adaptation with a sober desktop-first system: - Tokens: slate neutrals + violet accent, Inter family, dark mode via prefers-color-scheme + explicit data-theme. - Inline SVG icon set (Icon.svelte, Feather-style) — no Unicode glyph placeholders. - Layout shell: 240px rail nav on desktop, bottom tab bar on mobile; auth + onboarding own their own shell. - Discover: square photo carousel (dots + chevrons), name/age/location overlay, action buttons floating off the photo edge, sticky context panel on desktop. - Matches: clean conversation list with unread dot + chevron-on-hover. - Chat: 3-column on desktop (threads + thread + match context), 2-col on tablet, single thread on mobile. Day separators, retry/dismiss on failed messages, autogrow composer with Enter-to-send. - Profile: sticky hero card + 3-section form, dirty/saved indicator. - Photos: dropzone + grid with hover-only cell toolbar (set primary, reorder, delete). - Onboarding: sidebar stepper (320px) + content panel. - Auth: split layout (form left, brand quote right) on ≥900px. Server boot: env loader's repo-root resolution corrected after the move (../../.. instead of ../..) so .env.local at the repo root is picked up again. Hoist BRAND.md and SECURITY.md into docs/, drop superseded audit notes that were already closed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>master
parent
a001bc3b2d
commit
3ca1945dd9
@ -1,549 +0,0 @@
|
||||
# Auditoria Codex - ecosistema Active + Orca
|
||||
|
||||
Fecha: 2026-05-04
|
||||
Workspace: `G:\dev\svelte\active`
|
||||
|
||||
## Alcance
|
||||
|
||||
Se audito el estado actual del ecosistema con foco en el nuevo modulo
|
||||
`src/arts/orca` y sus puntos de acoplamiento con:
|
||||
|
||||
- `src/arts/active-app`
|
||||
- `src/arts/bus`
|
||||
- `src/arts/session`
|
||||
- `src/arts/cache`
|
||||
- `src/arts/perm`
|
||||
- `src/arts/connection`
|
||||
- documentacion publica en `src/arts/*/README.md` y pagina activa generada.
|
||||
|
||||
No se modifico codigo existente. El arbol ya tenia cambios previos en:
|
||||
|
||||
- `src/arts/orca/consts.ts`
|
||||
- `src/arts/orca/result.ts`
|
||||
- `src/arts/orca/types.ts`
|
||||
|
||||
Esos cambios parecen corregir parte de la deriva de comentarios `v0.1+` respecto a
|
||||
timeout/fatal/gates/compensation, pero no cierran todos los problemas.
|
||||
|
||||
## Veredicto
|
||||
|
||||
`orca` esta bien orientado arquitectonicamente. La decision fuerte, y la que
|
||||
realmente eleva el ecosistema, es mover las reacciones inter-modulo a una capa de
|
||||
orquestacion declarativa situada en `active-app`, no dentro de `cache`, `perm`,
|
||||
`session` o `connection`. Eso reduce acoplamiento y permite testear flujos
|
||||
compuestos como objetos de ejecucion (`OrcaRunResult`).
|
||||
|
||||
El modulo, sin embargo, ya ha crecido mas rapido que su contrato. El motor actual
|
||||
implementa gates, timeouts de accion, compensacion, transacciones, payloads en
|
||||
tokens, fan-in y politicas de cola. La documentacion mezcla tres versiones a la
|
||||
vez: boceto conceptual, v0 inicial y estado real. Antes de seguir expandiendo
|
||||
`orca`, conviene congelar el contrato real y eliminar ambiguedades.
|
||||
|
||||
Estado de pruebas focalizadas:
|
||||
|
||||
```txt
|
||||
npx vitest run src/arts/orca/test src/arts/active-app/test/presets.test.ts
|
||||
|
||||
Test Files 4 passed
|
||||
Tests 169 passed
|
||||
```
|
||||
|
||||
## Hallazgos P1
|
||||
|
||||
### 1. Liberacion de trace no es segura con runs paralelos
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:928`
|
||||
- `src/arts/orca/engine-orca.ts:931`
|
||||
- `src/arts/orca/engine-orca.ts:935`
|
||||
|
||||
`maybeReleaseTrace()` borra `traceStates` si no quedan eventos en cola. El propio
|
||||
comentario reconoce que no sabe si hay runs en vuelo del mismo trace. Eso era
|
||||
defendible con ejecucion estrictamente secuencial, pero ya existe
|
||||
`ORCA_QUEUE_PARALLEL`. Si dos eventos derivados del mismo trace corren en
|
||||
paralelo, el primero que termine puede borrar el estado de trace mientras el otro
|
||||
sigue vivo.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Los contadores `maxDepth`, `maxEventsPerTrace`, `repeatedEventLimit` y
|
||||
`dedupeKey` pueden resetearse antes de tiempo.
|
||||
- Un loop por eventos derivados puede saltarse la proteccion si el trace se borra
|
||||
mientras quedan runs paralelos activos.
|
||||
- La trazabilidad de un flujo enterprise deja de ser confiable justo en el modo
|
||||
mas peligroso: paralelo.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Mantener `traceInFlightCount` por `traceId`.
|
||||
- Incrementarlo en `spawnRun()`.
|
||||
- Decrementarlo en el `finally` de `executeRun()`.
|
||||
- Liberar el trace solo cuando `queuedCount(traceId) === 0` e
|
||||
`inFlightCount(traceId) === 0`.
|
||||
- Anadir test: dos eventos derivados con `ORCA_QUEUE_PARALLEL` en el mismo trace;
|
||||
el primero termina, el segundo emite otro evento repetido y el guard sigue
|
||||
aplicando.
|
||||
|
||||
### 2. `provides` no limita `emits`, asi que la validacion puede mentir
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/types.ts:421`
|
||||
- `src/arts/orca/types.ts:424`
|
||||
- `src/arts/orca/types.ts:425`
|
||||
- `src/arts/orca/engine-orca.ts:1117`
|
||||
- `src/arts/orca/engine-orca.ts:1124`
|
||||
|
||||
El contrato dice que `provides` es lo que una accion puede producir, y
|
||||
`validate()` usa `provides` para detectar tokens imposibles. Pero el runtime
|
||||
acepta cualquier token que venga en `result.emits`, aunque no este declarado en
|
||||
`provides`.
|
||||
|
||||
Impacto:
|
||||
|
||||
- `validate()` no es una garantia fuerte; es una estimacion basada en intencion.
|
||||
- Una accion puede desbloquear otra con un token que el grafo no declaraba.
|
||||
- Un typo en `emits` puede cambiar el pipeline sin que `validate()` lo vea.
|
||||
- El futuro `setupOrca()` tipado pierde parte de su sentido si el runtime permite
|
||||
emisiones fuera de esquema.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- En modo dev, lanzar o registrar `CONFIGURATION_INVALID` cuando una accion emite
|
||||
un token no declarado en `provides`.
|
||||
- En modo prod, al menos registrar diagnostic `orca.configuration.invalid` o
|
||||
descartar el token no declarado segun opcion.
|
||||
- Documentar excepcion solo si se decide permitir tokens dinamicos, pero entonces
|
||||
`validate()` debe llamarse "best effort", no "static graph validation".
|
||||
|
||||
### 3. `ORCA_QUEUE_REPLACE` no es `replace-current`, solo reemplaza queued
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/consts.ts:73`
|
||||
- `src/arts/orca/engine-orca.ts:420`
|
||||
- `src/arts/orca/engine-orca.ts:421`
|
||||
- `src/arts/orca/engine-orca.ts:422`
|
||||
- `src/arts/orca/README.md:439`
|
||||
- `src/arts/orca/README.md:1444`
|
||||
|
||||
Hay dos semanticas distintas usando el mismo nombre mental:
|
||||
|
||||
- Una seccion del README describe `replace` como "aborta el run activo y empieza
|
||||
uno nuevo", estilo `takeLatest`.
|
||||
- El motor implementa "reemplaza solo entradas queued; no aborta in-flight".
|
||||
- Otra seccion posterior del README ya reconoce esta realidad.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Para formularios, busquedas, navegacion o cambio de identidad, un usuario puede
|
||||
esperar "ultima intencion gana", pero el run anterior seguira ejecutandose.
|
||||
- Si la accion vieja muta cache/conexiones despues de la nueva, puede pisar estado.
|
||||
- El nombre `replace` es ambiguo para usuarios que vienen de saga/Rx/RTK.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Renombrar la politica actual a algo explicito: `ORCA_QUEUE_REPLACE_QUEUED` o
|
||||
`ORCA_QUEUE_LATEST_QUEUED`.
|
||||
- Reservar `ORCA_QUEUE_REPLACE_CURRENT` / `ORCA_QUEUE_TAKE_LATEST` para la
|
||||
variante fuerte que aborta in-flight.
|
||||
- Si se mantiene el nombre actual, borrar del README cualquier promesa de abortar
|
||||
el run activo.
|
||||
|
||||
### 4. IDs de run/event/trace usan `Date.now()` y `Math.random()` fuera de `timr`
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:1513`
|
||||
- `src/arts/orca/engine-orca.ts:1514`
|
||||
- `src/arts/orca/engine-orca.ts:1517`
|
||||
- `src/arts/orca/engine-orca.ts:1518`
|
||||
- `src/arts/orca/engine-orca.ts:1521`
|
||||
- `src/arts/orca/engine-orca.ts:1522`
|
||||
- `src/arts/orca/README.md:841`
|
||||
|
||||
El README establece que `orca` no debe usar `Date.now()` ni `setTimeout()`
|
||||
directamente. Los timestamps del envelope ya pasan por `timers.clock.now()`, pero
|
||||
los IDs de run/event/trace no.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Tests y replay no son plenamente deterministas.
|
||||
- En SSR/hydration o tests con clocks controlados, los IDs no siguen el tiempo
|
||||
simulado.
|
||||
- La disciplina "todo tiempo via timr" queda rota justo en el modulo que quiere
|
||||
ser trazable.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Anadir `idFactory` a `EngineOrcaOptions` o tres factories separadas:
|
||||
`runIdFactory`, `eventIdFactory`, `traceIdFactory`.
|
||||
- Default: contador monotono con prefijo, alimentado por `timers.clock.now()` si
|
||||
se quiere incluir tiempo.
|
||||
- Tests: factory determinista para snapshots.
|
||||
|
||||
### 5. `setupOrca()` esta documentado como API recomendada pero no existe
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/README.md:235`
|
||||
- `src/arts/orca/README.md:241`
|
||||
- `src/arts/orca/README.md:271`
|
||||
- `src/arts/orca/README.md:284`
|
||||
- `src/arts/orca/README.md:1271`
|
||||
- `src/arts/orca/index.ts`
|
||||
|
||||
El README recomienda `setupOrca()` para apps grandes y muestra ejemplos, pero no
|
||||
hay export ni implementacion en `src/arts/orca`.
|
||||
|
||||
Impacto:
|
||||
|
||||
- La documentacion invita a usar una API inexistente.
|
||||
- El usuario no sabe si debe usar `App.Orca.onEvent()` directo, presets o un
|
||||
setup tipado futuro.
|
||||
- La promesa de validacion fuerte de tokens/eventos queda sin soporte.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- O implementar un `setupOrca()` minimo, aunque solo envuelva `onEvent` con
|
||||
declaracion de eventos/tokens/actions.
|
||||
- O mover esa seccion a "Futuro / v0.1" y dejar claro que v0 real es
|
||||
`createEngineOrca()` + `App.Orca.onEvent()`.
|
||||
|
||||
### 6. La integracion con `connection` no esta cerrada y la documentacion promete mas de lo que existe
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/active-app/presets/standard.ts:28`
|
||||
- `src/arts/active-app/presets/index.ts:5`
|
||||
- `src/arts/active-app/service-factories/connections.ts:9`
|
||||
- `src/arts/connection/README.md:83`
|
||||
- `src/arts/connection/README.md:93`
|
||||
- `src/arts/connection/README.md:100`
|
||||
- `src/arts/connection/session-wiring.ts:27`
|
||||
|
||||
`applyStandardOrca()` solo registra presets de cache y perm. No hay preset para
|
||||
`connections.reauthenticateAll()` ni para `connections.closeAll()` en cambios de
|
||||
identidad/revoke/expire. A la vez, la documentacion de `connection` habla de
|
||||
`autoReauthOn: 'standard'` y de escuchar `SESSION_EVENT_IDENTITY_CHANGED`.
|
||||
|
||||
Ademas, `defineActiveConnections()` ya no inyecta `bus` ni `session`; el comentario
|
||||
dice que identidad debe venir por orca preset o por una `session` manual, pero ese
|
||||
preset no existe.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Una app que siga la documentacion puede creer que las conexiones se reautentican
|
||||
al cambiar de usuario, pero con `applyStandardOrca(App)` no ocurre.
|
||||
- El caso critico que motivo `orca` ("chat conectado con credenciales del usuario
|
||||
anterior") sigue sin preset estandar.
|
||||
- `connection` conserva un mecanismo interno de `session-wiring` que compite
|
||||
conceptualmente con la nueva regla: los artefactos publican/reciben eventos; la
|
||||
app orquesta reacciones.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Crear preset en `active-app/presets`:
|
||||
`applyConnectionsReauthOnIdentityChange(App)`.
|
||||
- Definir si el estandar hace `reauthenticateAll()` o `closeAll()` cuando no hay
|
||||
credencial nueva.
|
||||
- Actualizar `applyStandardOrca()` para incluir conexiones cuando `App.connections`
|
||||
exista.
|
||||
- Decidir si `connection.session-wiring` queda como modo local/manual o se depreca
|
||||
a favor del preset de `orca`.
|
||||
|
||||
## Hallazgos P2
|
||||
|
||||
### 7. `runActionWithTimeout()` aborta de forma cooperativa, pero la accion puede seguir mutando estado
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:1042`
|
||||
- `src/arts/orca/engine-orca.ts:1052`
|
||||
- `src/arts/orca/engine-orca.ts:1056`
|
||||
- `src/arts/orca/engine-orca.ts:1058`
|
||||
- `src/arts/orca/engine-orca.ts:1070`
|
||||
|
||||
El timeout hace `actionController.abort()` y resuelve `orcaTimeout(timeoutMs)`, pero
|
||||
la promesa original de la accion sigue viva si la accion no respeta `ctx.signal`.
|
||||
Esto es normal en JS, pero debe tratarse como contrato de seguridad.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Una accion timeout puede mutar cache, conexiones o permisos despues de que el run
|
||||
ya haya tomado otra decision.
|
||||
- Los tests pueden pasar porque el resultado es `timeout`, pero el side-effect
|
||||
tardio queda fuera del trace.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Documentar que toda accion async debe comprobar `ctx.signal.aborted` antes y
|
||||
despues de awaits relevantes.
|
||||
- Anadir helper `ctx.throwIfAborted()` o `orcaAbortIfSignaled(ctx)`.
|
||||
- Anadir test: accion con timeout que intenta mutar despues; demostrar que el
|
||||
patron recomendado lo evita.
|
||||
|
||||
### 8. La cola es global para eventos no-parallel, aunque el contrato se lee como per-event
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:526`
|
||||
- `src/arts/orca/engine-orca.ts:527`
|
||||
- `src/arts/orca/engine-orca.ts:529`
|
||||
- `src/arts/orca/engine-orca.ts:531`
|
||||
|
||||
`canStartRun()` serializa globalmente todos los eventos no-parallel:
|
||||
`nonParallelInFlight === 0`. Eso significa que un evento lento de `cache` puede
|
||||
bloquear un evento no relacionado de `perm`, `connection` o cualquier feature.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Semantica mas conservadora y lenta de lo que sugiere "queue policy per event".
|
||||
- Posible cuello de botella en apps grandes con flujos independientes.
|
||||
- Si esta decision es intencional, falta nombrarla como "single lane".
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Decidir explicitamente:
|
||||
- Modelo A: serializacion global por defecto, documentada como garantia simple.
|
||||
- Modelo B: serializacion por evento, con concurrencia entre eventos distintos.
|
||||
- Si se mantiene A, renombrar/comentar como `globalSerialLane`.
|
||||
- Si se pasa a B, usar `inFlightByEvent` para bloquear solo el mismo evento.
|
||||
|
||||
### 9. `commit()` congela el grafo; eso choca con features lazy si no se documenta como modo prod
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/types.ts:711`
|
||||
- `src/arts/orca/types.ts:723`
|
||||
- `src/arts/orca/engine-orca.ts:1275`
|
||||
- `src/arts/orca/test/engine-orca.test.ts:4013`
|
||||
|
||||
El comportamiento actual esta claro en codigo y tests: despues de `commit()`,
|
||||
`onEvent()` y `configureEvent()` lanzan `OrcaFrozenError`. Esto no es un bug por
|
||||
si mismo, pero tensiona el objetivo de registrar acciones desde features
|
||||
lazy-loaded.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Si una app llama `App.Orca.commit()` durante bootstrap, una ruta lazy ya no puede
|
||||
registrar sus acciones al cargar.
|
||||
- HMR/devtools pueden quedar bloqueados si no se separa "validar" de "sellar".
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Documentar `commit()` como opcion production/seal, no como paso obligatorio.
|
||||
- Considerar `validate()` para dev y `commit()` solo para builds donde el grafo es
|
||||
completo al arrancar.
|
||||
- Si se quiere lazy + freeze, introducir versionado por evento: cada run usa un
|
||||
snapshot, pero el registro global puede crecer entre runs.
|
||||
|
||||
### 10. `ctx.emit()` es correcto, pero la atribucion via `bus.publish()` depende de AsyncLocalStorage
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:133`
|
||||
- `src/arts/orca/engine-orca.ts:204`
|
||||
- `src/arts/orca/engine-orca.ts:864`
|
||||
- `src/arts/orca/als.ts`
|
||||
- `src/arts/orca/README.md:606`
|
||||
- `src/arts/orca/README.md:621`
|
||||
- `src/arts/orca/README.md:1470`
|
||||
|
||||
El diseno correcto es claro: modulo -> `bus.publish()`, accion -> `ctx.emit()`.
|
||||
El motor intenta interceptar `bus.publish()` dentro de acciones mediante
|
||||
AsyncLocalStorage cuando esta disponible. En navegador puede no estar disponible,
|
||||
por lo que el mismo `bus.publish()` dentro de una accion puede ser hijo en Node y
|
||||
root event en browser.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Trazas distintas entre SSR/tests Node y browser.
|
||||
- Reentry guards no aplican igual si un evento derivado sale como root.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Mantener la regla dura: las acciones usan `ctx.emit()` siempre.
|
||||
- Tratar la interceptacion ALS como bonus diagnostico, no como contrato.
|
||||
- Anadir lint/documentacion: no usar `App.Bus.publish()` dentro de una accion de
|
||||
`orca` salvo que se quiera crear root event explicito.
|
||||
|
||||
### 11. `engine-orca.ts` ya es un monolito de responsabilidades
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts` (~58 KB)
|
||||
|
||||
El engine concentra registro, cola, trace state, reentry, ejecucion de waves,
|
||||
timeouts, compensation, validation, ID generation, diagnostics y public API.
|
||||
|
||||
Impacto:
|
||||
|
||||
- Dificulta revisar invariantes delicadas como "trace cleanup + parallel".
|
||||
- Hace mas probable que futuras features (`runTimeoutMs`, `setupOrca`, inspector)
|
||||
entren sin frontera clara.
|
||||
- Ya contradice la direccion del ecosistema de reducir monolitos (`connection.ts`
|
||||
se venia refactorizando por el mismo motivo).
|
||||
|
||||
Recomendacion de split:
|
||||
|
||||
- `queue.ts`: enqueue, policies, drain, in-flight counters.
|
||||
- `trace.ts`: envelope, trace state, reentry guards, release.
|
||||
- `runner.ts`: stages, waves, action execution, status precedence.
|
||||
- `timeouts.ts`: action timeout helpers y timer keys.
|
||||
- `compensation.ts`: LIFO compensation / transaction compensation.
|
||||
- `validation.ts`: validate/commit helpers.
|
||||
- `ids.ts`: factories deterministas.
|
||||
- `engine-orca.ts`: composicion publica.
|
||||
|
||||
### 12. Los presets usan action ids y tokens inline/locales
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/active-app/presets/cache-clear-on-identity-change.ts:6`
|
||||
- `src/arts/active-app/presets/cache-clear-on-identity-change.ts:7`
|
||||
- `src/arts/active-app/presets/cache-clear-on-revoke.ts:6`
|
||||
- `src/arts/active-app/presets/cache-clear-on-revoke.ts:7`
|
||||
- `src/arts/active-app/presets/perm-invalidate-on-identity-change.ts:6`
|
||||
- `src/arts/active-app/presets/perm-invalidate-on-identity-change.ts:7`
|
||||
|
||||
Los eventos del ecosistema ya estan centralizados como constantes, pero los
|
||||
`ACTION_ID` y `TOKEN_*` de presets quedan como strings locales no exportados.
|
||||
|
||||
Impacto:
|
||||
|
||||
- No se pueden reutilizar en tests de integracion, docs, inspector `/test/orca`
|
||||
o setup tipado.
|
||||
- Rompe la regla emergente: "eventos, tokens y action ids como constantes".
|
||||
- El inspector no puede mostrar nombres canonicos importables.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Crear `src/arts/active-app/presets/consts.ts` o exportar desde cada preset:
|
||||
`APP_ORCA_ACTION_CACHE_CLEAR_ON_IDENTITY_CHANGE`,
|
||||
`APP_ORCA_TOKEN_CACHE_CLEARED_ON_IDENTITY_CHANGE`, etc.
|
||||
- Usar nombres de constantes en README y pagina docs.
|
||||
|
||||
## Hallazgos P3
|
||||
|
||||
### 13. Header de `engine-orca.ts` esta obsoleto
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/engine-orca.ts:24`
|
||||
- `src/arts/orca/engine-orca.ts:25`
|
||||
- `src/arts/orca/engine-orca.ts:26`
|
||||
|
||||
El comentario inicial dice que `after`, `unless`, `abortOn`,
|
||||
`actionTimeoutMs` y `compensate` se aceptan pero se ignoran. El motor ya los
|
||||
implementa.
|
||||
|
||||
Impacto:
|
||||
|
||||
- La siguiente persona que lea el archivo empieza con un mapa mental falso.
|
||||
- Ya se corrigieron comentarios en `types.ts`, `consts.ts` y `result.ts`, pero el
|
||||
comentario mas importante del motor sigue atrasado.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Actualizar el bloque de cabecera para describir el estado real.
|
||||
- Evitar version tags contradictorios dentro del codigo; mover roadmap al README.
|
||||
|
||||
### 14. El README de `orca` mezcla contrato actual, boceto y roadmap en un solo flujo
|
||||
|
||||
Referencia:
|
||||
|
||||
- `src/arts/orca/README.md:511`
|
||||
- `src/arts/orca/README.md:1411`
|
||||
- `src/arts/orca/README.md:1506`
|
||||
- `src/arts/orca/README.md:1522`
|
||||
|
||||
Ejemplo claro: una seccion dice que en v0 los tokens son flags sin payload; otra
|
||||
seccion posterior dice que los tokens con payload ya estan implementados. Esto no
|
||||
es solo estetico: afecta al modo en que un desarrollador modela acciones.
|
||||
|
||||
Impacto:
|
||||
|
||||
- La documentacion no sirve como contrato normativo.
|
||||
- Los lectores no saben si estan viendo la version deseada o la version real.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Reestructurar README en tres bloques cerrados:
|
||||
- "Contrato actual implementado"
|
||||
- "Patrones recomendados"
|
||||
- "Roadmap / no implementado"
|
||||
- Eliminar del contrato actual cualquier API no exportada (`setupOrca`) o moverla
|
||||
a roadmap.
|
||||
- Mantener una tabla "Feature -> estado -> archivo/test".
|
||||
|
||||
### 15. Falta una prueba compuesta del caso que motivo `orca`: cambio de usuario con cache/perm/connection
|
||||
|
||||
Referencias:
|
||||
|
||||
- `src/arts/active-app/test/presets.test.ts`
|
||||
- `src/arts/orca/test/engine-orca.test.ts`
|
||||
|
||||
Hay buena cobertura unitaria de `orca` y presets basicos. Falta el escenario
|
||||
ecosistema que debe probar el valor real:
|
||||
|
||||
1. Usuario A abre sesion.
|
||||
2. Chat/conexion usa credencial A.
|
||||
3. Cache actor-scoped guarda datos de A.
|
||||
4. Perm calcula snapshot de A.
|
||||
5. Cambia a usuario B.
|
||||
6. Orca ejecuta clear cache, invalidate perm y reauth/close connection.
|
||||
7. Ningun dato/credencial de A queda observable.
|
||||
|
||||
Impacto:
|
||||
|
||||
- El sistema puede estar correcto por modulo pero fallar justo en composicion.
|
||||
- El caso "chat con credenciales antiguas" sigue sin prueba de regresion.
|
||||
|
||||
Recomendacion:
|
||||
|
||||
- Crear test de integracion en `src/arts/active-app/test/ecosystem-orca.test.ts`
|
||||
o equivalente.
|
||||
- Usar fakes de cache/perm/connection con counters y credenciales capturadas.
|
||||
- Verificar orden por `OrcaRunResult`: cache -> perm -> connection si se decide
|
||||
dependencia via tokens.
|
||||
|
||||
## Observaciones positivas
|
||||
|
||||
- `App.Bus`, `App.Timers` y `App.Orca` ya son core siempre presentes en
|
||||
`createActiveApp()`, y `Orca` es inerte hasta registrar acciones. Esa decision
|
||||
encaja con el diseno enterprise sin obligar a cada app a crear singletons
|
||||
manuales.
|
||||
- Los presets viven en `active-app/presets`, no dentro de los artefactos. Esto es
|
||||
correcto: `cache` no debe conocer `session`; `perm` no debe conocer `auth`;
|
||||
`orca` no debe conocer modulos.
|
||||
- `ActiveOrca` es un wrapper fino y razonable: estado reactivo, sin meter logica
|
||||
del engine en Svelte.
|
||||
- La suite focalizada de `orca` ya es grande: 169 tests entre engine/active/result
|
||||
y presets. Para un modulo recien nacido, eso es muy buena base.
|
||||
- `ctx.signal` existe en `OrcaActionContext`; eso habilita cancelacion cooperativa
|
||||
y es la direccion correcta.
|
||||
|
||||
## Recomendacion de orden de trabajo
|
||||
|
||||
1. Corregir P1.1: trace cleanup con runs paralelos.
|
||||
2. Cerrar P1.2: `provides` debe ser contrato real o `validate()` debe declararse
|
||||
best-effort.
|
||||
3. Resolver P1.3: renombrar/split de `replace` para evitar semantica ambigua.
|
||||
4. Sustituir `Date.now()`/`Math.random()` por factories deterministas.
|
||||
5. Decidir `setupOrca()`: implementarlo minimo o moverlo fuera de v0.
|
||||
6. Crear preset de `connection` y test compuesto usuario A -> usuario B.
|
||||
7. Reordenar README para separar implementado/recomendado/roadmap.
|
||||
8. Refactorizar `engine-orca.ts` por responsabilidades antes de anadir run/stage
|
||||
timeout, inspector visual o setup tipado.
|
||||
|
||||
## Conclusion
|
||||
|
||||
`orca` merece seguir. No es "otro bus": es el sitio correcto para convertir
|
||||
flujos inter-modulo en artefactos trazables y testeables. Pero precisamente por
|
||||
eso no puede permitirse ambiguedad en nombres, timeouts, tokens y concurrencia.
|
||||
|
||||
El ecosistema esta cruzando una frontera importante: de librerias coherentes a
|
||||
runtime de aplicacion. La prioridad ahora no es meter mas features, sino hacer
|
||||
que el contrato de `orca` sea tan fiable como su idea.
|
||||
@ -1,566 +0,0 @@
|
||||
# Auditoria Codex del ecosistema hacia 1.0
|
||||
|
||||
Fecha: 2026-05-04
|
||||
Alcance: `lang`, `cache`, `session`, `perm`, `auth`, `http`.
|
||||
Excluido: `orca`, ya auditado por separado.
|
||||
|
||||
## Veredicto ejecutivo
|
||||
|
||||
El ecosistema esta bastante por encima de una 0.x normal: hay contratos tipados, separacion real entre `libs`, `arts` y `svrs`, factories declarativas en `createActiveApp()`, y una base de tests que hoy pasa para los modulos auditados.
|
||||
|
||||
La brecha hacia 1.0 no esta tanto en "reescribir" modulos, sino en cerrar tres frentes:
|
||||
|
||||
1. **Contratos publicos exactos**: hay documentacion y ejemplos que todavia prometen aliases, nombres o inyecciones que el codigo ya no hace.
|
||||
2. **Determinismo operativo**: `timr`/`Timers` ya existe, pero `http`, `session`, `cache` y `perm` aun conservan defaults con `Date.now()`, `Math.random()` o timers nativos cuando se usan fuera del wiring ideal.
|
||||
3. **Pruebas compuestas**: los tests unitarios estan verdes, pero faltan historias de ecosistema donde `auth`, `session`, `perm`, `cache` y `http` fallan o se invalidan juntos.
|
||||
|
||||
Verificacion ejecutada:
|
||||
|
||||
```txt
|
||||
npx vitest run src/arts/lang/test src/arts/cache/test src/libs/cache/test src/svrs/cache/test src/arts/session/test src/arts/perm/test src/svrs/perm/test src/arts/auth/test src/svrs/auth/test src/arts/http/test
|
||||
|
||||
35 test files passed
|
||||
426 tests passed
|
||||
```
|
||||
|
||||
## Hallazgos transversales
|
||||
|
||||
### P1 - La documentacion debe dejar de prometer APIs antiguas
|
||||
|
||||
Hay restos de la etapa de aliases de 4 letras y de nombres capitalizados: `$cach`, `$sess`, `App.Cache`, `App.Sess`, ejemplos sin `services: { ... }`, y textos que dicen que `cache` esta siempre presente. El codigo actual va por `createActiveApp({ services })` y expone servicios lazy en minuscula (`App.cache`, `App.session`, `App.perm`, `App.auth`, `App.http`).
|
||||
|
||||
Impacto: un desarrollador nuevo no distingue que es API real y que es historia del framework. Para 1.0, esto no puede quedar en "se entiende mirando codigo"; la documentacion es parte del contrato.
|
||||
|
||||
Accion recomendada:
|
||||
|
||||
- Hacer una pasada de docs con una regla mecanica: ningun README ni pagina `src/web/routes/active/docs/**` puede usar alias o propiedades que no existan en `svelte.config.js` y en `src/arts/active-app/service-factories/**`.
|
||||
- Crear tests de snippets o al menos un script que busque `$cach`, `$sess`, `App.Cache`, `App.Sess`, `App.Http`, `App.Perm`, etc.
|
||||
|
||||
### P1 - La inyeccion entre servicios no esta igual de clara que el discurso
|
||||
|
||||
Las factories actuales inyectan principalmente `logger`, y solo `session` recibe tambien `bus`:
|
||||
|
||||
- `defineActiveAuth(...)` recibe `logger`; el `http` debe venir en `options`.
|
||||
- `defineActivePerm(...)` recibe `logger`; aunque `PermClientOptions` acepta `http?: EngineHttp`, la factory no declara dependencia de `http`.
|
||||
- `defineActiveCache(...)` recibe `logger`; no recibe `timers` por defecto.
|
||||
- `defineActiveSession(...)` recibe `logger` y `bus`; no recibe `timers` para auto-refresh.
|
||||
- `defineEngineHttp(...)` recibe `logger`; no recibe `timers`.
|
||||
|
||||
Esto es coherente si el principio es "pasivo por defecto, opt-in explicito". Pero varias docs ya hablan como si `Http`, `Cache`, `Bus` y `Timers` se cablearan automaticamente entre servicios.
|
||||
|
||||
Accion recomendada:
|
||||
|
||||
- Decidir para 1.0 una regla unica: o las factories solo reciben core deps minimas, o pueden declarar `serviceDependencies`.
|
||||
- Si se mantiene el modo minimalista, documentarlo sin ambiguedad: `auth` necesita `http` explicito, `perm` necesita `endpoint` o `http` explicito, `cache` no se invalida sola, `session` no refresca con Timers salvo que se le pase.
|
||||
- Si se quiere ergonomia enterprise, evolucionar factories para dependencias opcionales: `defineActivePerm` puede consumir `http` si existe; `defineActiveCache` y `defineActiveSession` pueden consumir `timers`.
|
||||
|
||||
### P1 - Falta una suite compuesta de identidad, permisos y cache
|
||||
|
||||
La base verde actual no prueba suficientemente los casos que mas preocupan en una aplicacion real:
|
||||
|
||||
- Usuario A abre sesion, cachea datos privados, cambia a usuario B, y B no ve cache ni permisos de A.
|
||||
- Backend comunica cambio de permisos y el cliente invalida decision cacheada antes de permitir acciones.
|
||||
- `http` recibe 401/403, dispara refresh/auth state, `session` adopta o revoca, `perm` y `cache` reaccionan.
|
||||
- Logout global revoca sesion, limpia cache actor-scoped y deja `perm` sin actor.
|
||||
- Password reset revoca sesiones y el cliente queda en `anonymous` sin datos privados residuales.
|
||||
|
||||
Accion recomendada:
|
||||
|
||||
- Crear `src/routes/test/ecosystem` como harness de estas historias o moverlas a tests headless en `src/arts/active-app/test/ecosystem-*.test.ts`.
|
||||
- Estos tests deben usar `App.Bus`/presets/orquestacion cuando toque, pero el criterio es observable: snapshots finales y ausencia de datos cruzados.
|
||||
|
||||
### P2 - Tiempo y aleatoriedad aun no son uniformes
|
||||
|
||||
Puntos concretos:
|
||||
|
||||
- `src/arts/http/retry.ts:48`, `:52`, `:64` usa `Date.now()`, `Math.random()` y `setTimeout`.
|
||||
- `src/arts/http/timeout.ts:43`, `:63` usa `setTimeout`.
|
||||
- `src/arts/cache/active-cache.svelte.ts:195` usa `Date.now()` si no se pasa clock.
|
||||
- `src/arts/session/auto-refresh.ts:44`, `:45`, `:72` usa `Date.now()`, `Math.random()` y `setInterval` si no se pasa `timers`.
|
||||
- `src/arts/perm/client.ts:291` usa `Date.now()` si no se pasa clock.
|
||||
|
||||
No es necesariamente un bug para uso aislado, pero para 1.0 la composicion `createActiveApp()` deberia poder inyectar `Timers.clock` y scheduler por defecto en los servicios que lo aceptan. Esa es una de las diferencias entre "librerias utiles" y "framework determinista".
|
||||
|
||||
## Modulo `lang`
|
||||
|
||||
### Estado
|
||||
|
||||
`lang` esta entre los modulos mas maduros. Tiene engine puro, wrapper active, resolucion de referencias `#?path|fallback`, extension de schema, pluralizacion, JSON helpers y una suite de tests amplia.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Cambiar `SvelteSet` por `Set` en listeners.** En `src/arts/lang/active-lang.svelte.ts:34` `localeListeners` no alimenta templates ni estado derivado; solo se itera manualmente. Igual que se hizo en otros modulos, `Set` plano reduce reactividad innecesaria.
|
||||
2. **Separar mutacion y composicion de schemas con nombres mas explicitos.** Hoy `extend(namespace, module)` muta el engine activo y `register(namespace, module)` devuelve un engine hijo. Es potente, pero el naming puede confundir. Para 1.0 documentaria una tabla estricta: `extend` muta, `register` compone hijo.
|
||||
3. **Hacer `SupportedLocale` menos cerrado.** `src/libs/lang/types.ts` limita locales a una lista base. Para producto 1.0, conviene permitir cualquier BCP47 tipado como branded string o una registry generica por app.
|
||||
4. **Modo estricto de interpolacion.** `interpolateTemplate()` resuelve placeholders, pero no hay modo que falle si falta un parametro. Para 1.0 deberia existir `strictInterpolation` con diagnostico/log.
|
||||
5. **Unificar docs de inyeccion.** La factory `defineActiveLang` si inyecta logger via `setLogger(core.logger)`. La documentacion debe mostrar claramente `services: { lang: defineActiveLang({ schema }) }` y no dar a entender que `createActiveApp()` siempre trae un schema real.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Loader asincrono de packs de idioma: `loadLocale(locale)` con cache y fallback.
|
||||
- `Lang.tCode(code)` si se adopta una capa comun de errores/codigos.
|
||||
- Integracion con formatos: resolver `{{price | currency}}`, `{{date | datetime}}` usando `format`.
|
||||
- Dev inspector: namespace registrados, fallback usado, claves faltantes, referencias circulares.
|
||||
- Script de validacion de schema: claves faltantes por locale, claves muertas y paths duplicados.
|
||||
|
||||
## Modulo `cache`
|
||||
|
||||
### Estado
|
||||
|
||||
`cache` tiene un runtime solido en `libs/cache`, wrapper `svrs/cache` y `arts/cache` reactivo. Hay politicas, scopes, tags, epochs, singleflight, stale-if-error y adapters memory/storage. La arquitectura esta bien orientada.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Actualizar docs antiguas.** Hay ejemplos que aun usan `$cach` o `App.Cache`; el alias real es `$cache` y la app expone `App.cache` solo si se declara `services.cache`.
|
||||
2. **Inyectar clock desde App.Timers cuando se usa como servicio.** `createActiveCache()` acepta `clock`, pero `defineActiveCache()` solo inyecta `logger`. Para 1.0, el default app-wired deberia usar `core.timers.clock`.
|
||||
3. **Evitar `console.warn` directo en memory adapter.** `src/libs/cache/adapters/memory.ts` usa `console.warn` si se crea en produccion sin `onProductionWarning`. En 1.0, la advertencia deberia pasar por diagnostics/logger o exigir handler explicito.
|
||||
4. **Revisar clonacion de valores.** `memoryCacheAdapter` usa `structuredClone` si existe y fallback JSON. El fallback rompe `Date`, `Map`, `Set`, `BigInt`, clases y valores no serializables. Para 1.0: o se documenta "valores serializables" o se expone `clone?: (value) => value`.
|
||||
5. **Purgado escalable.** La memoria purga expirados escaneando entradas. Es razonable para v0, pero para 1.0 conviene un sweeper opcional con `Timers` o un indice por expiracion si se esperan caches grandes.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Adapter L2 remoto opcional: Redis/HTTP/IndexedDB, manteniendo L1 memory.
|
||||
- Invalidacion por evento app: identity changed, tenant switched, permission changed.
|
||||
- `cache.queryHttp()` o helper de integracion con `http` para cachear respuestas con schema.
|
||||
- Metricas: hit rate, stale served, refresh failures, singleflight joins, evictions.
|
||||
- Politicas de privacy: impedir persistir scopes actor/session en adapters no seguros salvo opt-in.
|
||||
- Tests de no fuga cross-actor y cross-tenant.
|
||||
|
||||
## Modulo `session`
|
||||
|
||||
### Estado
|
||||
|
||||
`session` es de los modulos mejor testeados. Tiene engine puro, wrapper active, generacion/versionado para evitar carreras, broadcast/storage sync, eventos de bus y auto-refresh opcional. La direccion es buena.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Actualizar naming en docs.** Debe desaparecer `$sess` y `App.Sess`; el alias real es `$session` y el servicio es `App.session`.
|
||||
2. **Cablear auto-refresh con Timers desde App.** `auto-refresh.ts` ya acepta `timers`, pero `defineActiveSession()` no los inyecta. Para 1.0, si una app usa `createActiveApp()`, el refresh deberia poder ser determinista sin boilerplate manual.
|
||||
3. **Limitar defaults nativos en modo app-wired.** `Date.now`, `Math.random` y `setInterval` son aceptables como fallback aislado, pero no como camino principal del ecosistema.
|
||||
4. **Documentar ownership frente a auth.** `session` no debe saber de credenciales ni permisos; solo continuidad, refresh/revoke, snapshot y bus events. `auth` prueba identidad; `perm` decide permisos.
|
||||
5. **Opciones explicitas para sync multi-tab.** Si ya existen, deben documentarse mejor; si no, conviene `broadcast: false | { channel }` para entornos con privacidad estricta o tests.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Preset `defineActiveSession({ autoRefresh: { standard: true } })` que use `Timers`.
|
||||
- Integracion `http` para refresh por 401 sin acoplar `http` a `session`: hook reusable de aplicacion.
|
||||
- Eventos canonicos para identity changed, credential refreshed, revoked, expired.
|
||||
- Tests compuestos con auth/cache/perm.
|
||||
- Modo SSR documentado: adoptar snapshot server sin doble refresh ni flicker.
|
||||
|
||||
## Modulo `perm`
|
||||
|
||||
### Estado
|
||||
|
||||
`perm` tiene mas base de servidor de la que parecia al inicio: `libs/perm` incluye DSL/evaluator/compiler, `svrs/perm` aporta engine, repository y SQL de referencia, y `arts/perm` aporta cliente activo. Es una buena base para 1.0, pero hay dos puntos que conviene cerrar pronto.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Corregir compilador SQL para paths anidados.** El runtime usa `getPath`, pero `src/libs/perm/compilers/sql.ts:105` y `:110` leen `input.actor[expr.path]` y `input.context?.[expr.path]`. Un path como `risk.mfa` o `profile.department` se evaluara distinto en runtime y en SQL. Para 1.0 esto debe ser P1: usar `getPath()` tambien en actor/context o declarar que SQL solo soporta paths planos.
|
||||
2. **Preordenar policies una vez.** `src/libs/perm/runtime.ts` ordena por prioridad en cada decision. Para volumen real, ordenar al construir runtime y mantener indices por action/resource reduce coste.
|
||||
3. **Memoizar providers por decision.** `DefaultPermEvaluator` puede llamar varias veces a relation/attribute providers con la misma key dentro de una decision. Un cache por request reduce latencia y evita multiples consultas a DB.
|
||||
4. **Eliminar IDs auto-generados no estables en produccion.** `definePolicies`/builders pueden producir IDs por contador. Para 1.0, los policies persistidos deberian exigir `id` estable o generar checksum determinista.
|
||||
5. **Alinear factory y docs.** `PermClientOptions` acepta `http?: EngineHttp`, pero `defineActivePerm()` no inyecta `App.http`. O se documenta que `fetcher`/`http` son manuales, o se declara dependencia opcional de `http`.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Persistencia oficial: migraciones SQL versionadas, repository contract tests y ejemplos Kysely/Drizzle.
|
||||
- Webhook/evento de cambio de permisos: invalidar cliente y cache de decisiones por actor/tenant.
|
||||
- `what()` y `explain()` cacheados con invalidacion por version de policies.
|
||||
- Obligations/advice con enforcement helpers, no solo datos.
|
||||
- Auditoria de decisiones: escribir `permission_decision_audit` desde server engine con redaccion.
|
||||
- Tests de cross-actor race: login A -> decision allow -> login B -> misma accion no reutiliza decision.
|
||||
|
||||
## Modulo `auth`
|
||||
|
||||
### Estado
|
||||
|
||||
`auth` ha avanzado mucho: hay `libs/auth` como lenguaje comun, `svrs/auth` con engine server-authoritative, SQL de referencia, password signup/signin, CSRF, recovery, device records, refresh rotation y OAuth base. `arts/auth` es un cliente seguro que refleja `AuthCurrentView` y no intenta ser autoridad.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Completar handlers para rutas ya publicadas.** `src/libs/auth/consts.ts:21-30` declara rutas OAuth, MFA, devices y WebAuthn. `src/arts/auth/active-auth.svelte.ts:154-166` ya llama `DEVICES` y `DEVICE_REVOKE`. Pero `src/svrs/auth/handlers.ts` solo enruta current, csrf, password, logout, email verification y password reset. Resultado: `ActiveAuth.listDevices()` y `revokeDevice()` apuntan a endpoints que el handler generico no sirve. Para 1.0, o se agregan handlers, o se retiran del cliente hasta estar soportados.
|
||||
2. **No guardar secretos OAuth en metadata de flow.** `oauth-flow.ts` guarda `state` y `verifier` en `metadata` ademas de hashes. Aunque la store sea server-side, el contrato ideal es persistir solo hash/verifier cifrado o recuperar verifier por canal seguro. Para 1.0 debe revisarse porque el documento original era estricto con secretos.
|
||||
3. **PKCE challenge no debe usar hash token raw si no es base64url SHA-256 estandar.** Si `hashAuthToken()` no produce exactamente `base64url(SHA256(verifier))`, el flujo OAuth no sera interoperable. El test `oauth-pkce.test.ts` existe, pero conviene comprobarlo contra el RFC shape.
|
||||
4. **Rate-limit esta bien cableado en password/recovery/oauth, pero falta matriz.** `enforceAuthRateLimit()` existe y se usa en flujos principales; para 1.0 hace falta tabla por metodo, key usada y politica recomendada.
|
||||
5. **`createDbAuthAdapter` es demasiado generic para produccion.** El adapter `db.ts` acepta repositorios con `where` generico basado en records TS, mientras el SQL aplana `actorRef` a `tenant_id/actor_id`. Es valido como referencia, pero 1.0 necesita un adapter/repository probado contra el SQL real, no solo un contrato abstracto.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Handlers completos para devices y OAuth; MFA/WebAuthn marcados como experimental si no se implementan.
|
||||
- Contract tests del SQL auth: credentials, flows, linked accounts, session bindings, refresh families.
|
||||
- Anti-enumeration tests para recovery/email verification.
|
||||
- Refresh rotation integrada end-to-end con sesion real, no solo helper unitario.
|
||||
- Device/session management completo: listar, revocar actual, revocar otro, global logout.
|
||||
- Security events hacia logger/audit con redaccion obligatoria.
|
||||
- Integracion con `session`, `cache` y `perm`: signin/logout/password reset invalidan lo necesario sin acoplar modulos directamente.
|
||||
|
||||
## Modulo `http`
|
||||
|
||||
### Estado
|
||||
|
||||
`http` esta muy bien codificado: engine puro, resultados tipados, hooks, retry, timeout, schema de request/response, fetch inyectable y tests amplios. Es una pieza importante para que el ecosistema no dependa de `fetch` crudo.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Port de timers/retry.** `retry.ts` y `timeout.ts` usan timers nativos. Para 1.0 deberia existir `HttpTimerPort` o integracion directa con `Timers`, al menos cuando se crea via `defineEngineHttp()`.
|
||||
2. **Jitter inyectable.** `computeRetryDelay()` usa `Math.random()` si `policy.jitter` esta activo. Para tests deterministas y produccion controlada, aceptar `random?: () => number`.
|
||||
3. **Cerrar lifecycle de timeouts.** `attemptTimeoutSignal()` y `totalTimeoutSignal()` crean `setTimeout`; si la request termina antes, el timeout queda pendiente hasta disparar. No siempre es grave, pero en alto volumen deberia poder cancelarse.
|
||||
4. **Helpers de autenticacion sin acoplar a auth.** La pieza deberia ofrecer patrones genericos para `beforeRequest`/`beforeRetry`/`beforeError` que permitan refresh por 401, pero sin conocer `session` ni `auth`.
|
||||
5. **Observabilidad de hooks.** Hoy se emiten diagnosticos de retry y errores; para 1.0 conviene medir tiempo por intento, delay real, abort reason y hooks que rescatan respuestas.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- `http.with({ fetch: event.fetch })` documentado con snippets SSR reales.
|
||||
- Preset de retry empresarial: idempotentes por defecto, 429/503 con Retry-After, jitter determinista opcional.
|
||||
- Adapter/cache bridge: convertir `Response` en envelope cacheable con schema.
|
||||
- Circuit breaker opcional o al menos hooks para implementarlo con `cache/session`.
|
||||
- Tests de 401 -> refresh -> replay request; 403 -> perm invalidation; offline -> stale cache.
|
||||
|
||||
## Roadmap recomendado hacia 1.0
|
||||
|
||||
### Sprint 1 - Contrato publico y docs reales
|
||||
|
||||
- Corregir aliases y nombres de servicios en READMEs y paginas `active/docs`.
|
||||
- Documentar factories reales: que inyecta cada una y que debe pasar el desarrollador.
|
||||
- Crear un test/script de docs que detecte aliases muertos y propiedades antiguas.
|
||||
- Publicar tabla "core siempre presente vs services declarados".
|
||||
|
||||
### Sprint 2 - Determinismo e inyeccion
|
||||
|
||||
- Hacer que `defineActiveCache`, `defineActiveSession`, `defineActivePerm` y `defineEngineHttp` puedan consumir `core.timers` cuando aplique.
|
||||
- Inyectar `random` en retry/session auto-refresh.
|
||||
- Mantener fallbacks nativos para uso aislado, pero no para el camino App.
|
||||
|
||||
### Sprint 3 - Bugs de contrato
|
||||
|
||||
- Completar handlers de `auth` para devices/OAuth o retirar esas APIs del cliente hasta estar soportadas.
|
||||
- Corregir path anidado en compilador SQL de `perm`.
|
||||
- Revisar OAuth PKCE y persistencia de verifier/state.
|
||||
- Alinear `defineActivePerm` con `http` real.
|
||||
|
||||
### Sprint 4 - Tests compuestos
|
||||
|
||||
Escenarios minimos:
|
||||
|
||||
1. Login A -> cache privado -> logout -> login B -> B no ve cache/perm de A.
|
||||
2. Permission webhook -> `perm.invalidate()` -> decision antigua no se reutiliza.
|
||||
3. HTTP 401 -> refresh sesion -> replay -> cache conserva solo datos validos.
|
||||
4. Password reset -> revoca sesiones -> active auth anonimo -> cache actor-scope limpia.
|
||||
5. Logout global -> session revoked -> perm sin actor -> cache limpia -> http protegido falla seguro.
|
||||
6. Tenant switch -> cache/perm invalidados por tenant.
|
||||
|
||||
### Sprint 5 - Server readiness
|
||||
|
||||
- `auth` y `perm` ya tienen SQL de referencia; convertirlo en migraciones versionadas o al menos en contract tests ejecutables.
|
||||
- `cache` necesita historia clara de adapter server: memory solo test/dev, storage/browser, y adapter remoto recomendado.
|
||||
- `http` no necesita `svrs/http`, pero si necesita ejemplos SSR y edge/runtime.
|
||||
|
||||
## Prioridad resumida
|
||||
|
||||
| Prioridad | Tema | Modulos | Motivo |
|
||||
|---|---|---|---|
|
||||
| P1 | Handlers `auth` incompletos para APIs publicas | auth | Cliente llama endpoints que el handler generico no enruta. |
|
||||
| P1 | SQL compiler no resuelve paths anidados igual que runtime | perm | Riesgo de decisiones distintas entre filtrado DB y evaluacion memory. |
|
||||
| P1 | Docs/API antiguas tras rename y service schema | todos | Bloquea adopcion y genera mal uso del framework. |
|
||||
| P1 | Tests compuestos cross-actor/cross-tenant | auth/session/perm/cache/http | Es donde aparecen fugas reales. |
|
||||
| P2 | Timers/random no unificados | cache/session/perm/http | Rompe determinismo en tests y trazabilidad. |
|
||||
| P2 | Persistencia/adapters DB contract-tested | auth/perm/cache | Necesario para apps reales. |
|
||||
| P2 | Metrics/diagnostics de runtime | cache/http/perm/auth | Necesario para operacion 1.0. |
|
||||
| P3 | Limpieza micro-reactiva (`SvelteSet` listeners) | lang | Pulido, bajo riesgo. |
|
||||
|
||||
## Criterio de cierre para 1.0
|
||||
|
||||
Yo no marcaria estos modulos como 1.0 hasta que se cumplan estas condiciones:
|
||||
|
||||
- La documentacion publica compila mentalmente y con snippets: ningun alias muerto, ningun servicio inventado.
|
||||
- El camino `createActiveApp({ services })` inyecta logger, bus, timers y servicios dependientes de forma explicita o documenta que no lo hace.
|
||||
- `auth`, `session`, `perm`, `cache` y `http` tienen al menos una suite compuesta de identidad completa.
|
||||
- `auth` no publica rutas/cliente que el server handler no soporte.
|
||||
- `perm` produce la misma decision en runtime y SQL compiler para paths soportados.
|
||||
- `http` y auto-refresh son testeables sin timers nativos.
|
||||
- Los adapters server de `auth` y `perm` tienen contract tests contra el modelo SQL de referencia.
|
||||
|
||||
Conclusion: la arquitectura es buena y la base esta verde. Lo que falta para 1.0 es menos glamour y mas cierre contractual: documentacion verdadera, wiring determinista y pruebas de historias completas. Esa es la parte que convierte el ecosistema en plataforma.
|
||||
|
||||
---
|
||||
|
||||
# Segunda tanda: sium, storage, timer, logger, frontend, format, connection
|
||||
|
||||
Fecha: 2026-05-04
|
||||
Alcance adicional: `sium`, `storage`, `timer`, `logger`, `frontend`, `format`, `connection`.
|
||||
Finalidad: misma que la primera tanda, buscar refactorizaciones, ampliaciones y criterios de cierre hacia version 1.0.
|
||||
|
||||
Verificacion ejecutada:
|
||||
|
||||
```txt
|
||||
npx vitest run src/arts/sium/test src/arts/storage/test src/arts/timer/test src/arts/logger/test src/arts/logger/adapters src/arts/frontend/test src/arts/format/test src/arts/format/currency/test src/arts/format/dates/test src/arts/format/numbers/test src/arts/format/units/test src/arts/connection/test
|
||||
|
||||
52 test files passed
|
||||
686 tests passed
|
||||
```
|
||||
|
||||
## Hallazgos transversales de la segunda tanda
|
||||
|
||||
### P1 - Documentacion con aliases y nombres de App obsoletos
|
||||
|
||||
La misma deuda aparece con fuerza en esta tanda. El codigo actual usa aliases semanticos:
|
||||
|
||||
```txt
|
||||
$storage, $timer, $logger, $format, $connection
|
||||
```
|
||||
|
||||
Pero varios README siguen usando:
|
||||
|
||||
```txt
|
||||
$stor, $timr, $logr, $fmts, $conn
|
||||
```
|
||||
|
||||
Tambien aparecen ejemplos con `App.Timers`, `App.Format`, `App.Frontend`, `App.Storage`, `App.Sess`, `App.Lang`, `App.createActiveConnections()` y `App.setLocale(...)`. El contrato actual de `createActiveApp({ services })` expone servicios en minuscula (`App.format`, `App.frontend`, `App.storage`, `App.session`, `App.lang`, `App.connections`) y las factories viven en `$active-app/services`.
|
||||
|
||||
Esto es P1 de documentacion contractual. Aunque el runtime este verde, una API 1.0 no puede tener docs que ensenan a importar desde aliases que ya no existen.
|
||||
|
||||
Accion recomendada:
|
||||
|
||||
- Pasada mecanica por README y paginas docs para reemplazar aliases viejos.
|
||||
- Script de CI que falle si aparecen `$stor`, `$timr`, `$logr`, `$fmts`, `$conn`, `App.Sess`, `App.Storage`, `App.Format`, `App.Frontend`, `App.Timers` en docs publicas salvo en secciones de migracion.
|
||||
- Tabla unica por modulo: alias real, factory real, propiedad real de `App`.
|
||||
|
||||
### P1 - Los servicios no comparten todavia un contrato de lifecycle uniforme
|
||||
|
||||
Algunos modulos siguen el contrato `ActiveEngine` o equivalente (`snapshot`, `lastError`, `disposed`, `dispose` idempotente). Otros son utilitarios activos sin `disposed` ni guardas post-dispose.
|
||||
|
||||
Casos relevantes:
|
||||
|
||||
- `storage` no impide `entry()` ni `clear()` despues de `dispose()`.
|
||||
- `frontend` mantiene setters operativos tras `dispose()`.
|
||||
- `format` root llama dispose de submodulos sin flag propio idempotente.
|
||||
|
||||
Para 1.0, todo servicio declarado en `createActiveApp({ services })` deberia tener una semantica uniforme:
|
||||
|
||||
- `dispose()` idempotente.
|
||||
- Metodos mutadores despues de dispose: o no-op documentado, o error tipado.
|
||||
- Snapshot/introspection si el servicio expone estado.
|
||||
|
||||
### P2 - El patron `CodeError` esta bien adoptado, pero quedan restos de nomenclatura antigua
|
||||
|
||||
`sium`, `storage`, `timer`, `logger` y `connection` ya extienden `CodeError` desde `$libs/errs`, que era una buena direccion. Pero hay comentarios y mensajes que todavia hablan de `stor`, `timr`, `logr` o `conn`. No rompe runtime, pero ensucia la identidad del ecosistema justo ahora que se abandono la regla de 4 letras.
|
||||
|
||||
Accion recomendada:
|
||||
|
||||
- Mantener `CodeError` como raiz.
|
||||
- Renombrar comentarios, mensajes y catalogos internos que digan `stor/timr/logr/conn` si el modulo ya se llama `storage/timer/logger/connection`.
|
||||
- Si `ErrCode` usa seeds de 4 letras por decision historica, documentarlo. Si no, migrarlo antes de 1.0.
|
||||
|
||||
## Modulo `sium`
|
||||
|
||||
### Estado
|
||||
|
||||
`sium` es probablemente el modulo mas completo de esta segunda tanda. Tiene core DSL, Standard Schema, introspection, codecs, lazy, domain types de color/date/time, resolucion de issues, integracion con `lang`, `CodeError` y una suite de tests amplia.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Resolver locale activo, no solo default inicial.** `createEngineSium()` captura `defaultLocale = options.locale ?? lang?.getDefaultLocale() ?? 'es'`. Si se inyecta `ActiveLang` mediante `defineEngineSium` y luego cambia el locale de `App.lang`, `resolveIssue()` sin locale explicito seguira usando el default capturado. Para 1.0, si el `lang` inyectado tiene `getLocale()`, Sium deberia usar el locale actual, o no pasar locale a `lang.t()` para dejar que Lang resuelva su estado actual.
|
||||
2. **Cerrar la migracion de errores legacy.** `src/arts/sium/errors.ts` mantiene `SIUM_ERRORS` como catalogo legacy para strings que aun no son `CodeError`. La propia nota dice que quedan sitios por migrar. Para 1.0, todos los errores de construccion/encode/decode deberian tener `ErrCode`.
|
||||
3. **Reducir fragilidad de facade manual.** `EngineSium` lista manualmente decenas de funciones. Hay tests de barrel, pero para 1.0 conviene un snapshot de surface o generacion controlada para evitar que `core` gane funciones que el engine no expone.
|
||||
4. **Separar issues de errores de programador en docs.** La distincion existe en codigo: `validate` devuelve `Result`, `decode` lanza. La documentacion debe insistir en cuando usar cada una.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- `setupSium()` o presets de dominio para formularios complejos.
|
||||
- Bridge oficial con `lang`: `sium.resolveIssue(issue)` siguiendo locale activo.
|
||||
- Emision opcional de diagnostics por schema path para errores frecuentes.
|
||||
- Serializacion estable de schema para devtools y documentacion automatica.
|
||||
- Contract tests con `standard-schema` frente a Zod/Valibot/ArkType adapters.
|
||||
|
||||
## Modulo `storage`
|
||||
|
||||
### Estado
|
||||
|
||||
`storage` esta muy bien planteado: engine sync, active wrapper con `$state`, adapters memory/local/session/cookie/broadcast, envelopes con version/TTL/migration, serializers y tests suficientes. Es una pieza clave para preferencias no secretas y persistencia local.
|
||||
|
||||
### Hallazgos y refactorizaciones
|
||||
|
||||
1. **P1 - `dispose()` no cierra realmente la superficie publica.** `createEngineStorage()` marca `disposed = true`, pero `entry()`, `clear()` y `entries()` no verifican ese estado. Despues de `dispose()` se puede crear un entry nuevo sobre un bus ya limpiado y registries ya dispuestos. `ActiveStorage.dispose()` hereda el mismo problema porque delega al engine y no guarda flag propio. Para 1.0 debe haber `StorageDisposedError` o no-op documentado.
|
||||
2. **P2 - TTL usa `Date.now()` sin clock inyectable.** `decodeEnvelope()` y `encodeEnvelope()` aceptan `now`, pero `entry-runtime.ts` llama sin pasar reloj. `EngineStorageOptions` no tiene `clock`. Para tests deterministas y App wiring, conviene `clock?: { now(): number }`, inyectado desde `App.Timers.clock`.
|
||||
3. **P2 - `dynamicEntry()` depende de `$effect`, pero no hay defensa si se usa fuera de scope.** El comentario lo advierte, pero para 1.0 conviene test y error claro si Svelte lanza fuera de componente/effect root.
|
||||
4. **P2 - Top-level serializer auto-selection puede sorprender.** Esta documentado: objetos con `Date` anidada caen a JSON y no restauran Date. Para 1.0, anadir recipes de serializer por schema o integracion con Sium.
|
||||
5. **P2 - Cookies cliente no endurecidas por defecto.** `cookieAdapter()` default `secure: false`, `sameSite: 'lax'`. Es razonable para preferencias no secretas, pero docs deben repetir que no es para secretos y que auth/session cookies no pasan por `storage`.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- `StorageDisposedError` y guardas post-dispose.
|
||||
- Clock inyectable desde App.
|
||||
- Adapter IndexedDB async separado o modulo nuevo, porque el contrato actual es sync.
|
||||
- Encryption/redaction adapter opt-in para preferencias sensibles, sin prometer seguridad para secretos.
|
||||
- Schema serializer: `entry('profile', defaults, { schema, serializer: siumSerializer(schema) })`.
|
||||
- Devtools: entradas vivas, namespace, adapter, defaults conflict, TTL restante.
|
||||
|
||||
## Modulo `timer`
|
||||
|
||||
### Estado
|
||||
|
||||
`timer` esta en buen estado. Es engine puro, clock inyectable, race-safe con version/id/key, abort signal por tarea, active wrapper ligero y tests robustos. Es de las piezas mas solidas del framework.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **Actualizar docs antiguas.** README sigue usando `$timr` y `App.Timers`; alias real `$timer`, propiedad real `App.Timers` solo para core si se mantiene capitalizado. En codigo actual `createActiveApp()` si expone core `Timers`, asi aqui la capitalizacion es real, pero el alias no.
|
||||
2. **Consolidar exports de backoff.** `src/arts/timer/backoff.ts` re-exporta desde `$libs/timer`, y `index.ts` tambien lo expone. No es grave, pero para 1.0 conviene una unica historia: backoff vive en `libs/timer`, `arts/timer` lo reexporta en index por conveniencia.
|
||||
3. **Exponer random en helpers consumidores.** `computeBackoffDelay()` ya acepta `random`, pero `connection` no lo expone en sus reconnect options. Para 1.0, los consumidores deben poder hacer backoff determinista.
|
||||
4. **Nombrar mejor `timer` como clock/scheduler del ecosistema.** En docs debe quedar claro que no es una utilidad de UI, sino la fuente temporal para `http`, `session`, `connection`, `cache`, `orca`.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Fake clock oficial exportado para tests de ecosistema.
|
||||
- Metrics: drift, scheduled count, cancelled count, failed count por scope.
|
||||
- `cancelAll(scope)` documentado como primitive de teardown por modulo.
|
||||
- Helpers para deadline/timeout con `AbortSignal` para que `http`/`orca` no usen timers nativos.
|
||||
- Devtools de timers activos por scope.
|
||||
|
||||
## Modulo `logger`
|
||||
|
||||
### Estado
|
||||
|
||||
`logger` es potente: `Logger` comun en `$libs/logger`, `EngineLogger` extiende ese contrato, transports, buffers, batching, failure routing con `deniedFor`, adapters Sentry/Datadog/Logtail/Loki/OTel, vitals y tests grandes. Es un pilar enterprise real.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **P1/P2 - Alinear filtro global con la regla de niveles habilitados.** Los transports usan `levels?: LevelConfig`, que permite `{ [LogLevel.WARN]: { enabled: true }, ... }`. Pero el engine global aun usa `state.level` como threshold (`if (lvl < state.level) return`). Si la regla final del framework es "habilitacion por nivel, no threshold tradicional", `LoggerOptions` deberia aceptar `levels` tambien a nivel global, y `level` quedarse como shorthand o deprecated antes de 1.0.
|
||||
2. **P2 - Reducir dependencia directa de `console` dentro del engine.** `handleFailure()` emite `console.error` ademas de crear synthetic failure entry. En un entorno enterprise puede duplicar salida o saltarse transports. Para 1.0 conviene `onInternalError`, `internalTransport`, o `consoleFallback?: boolean`.
|
||||
3. **P2 - Inyectar clock/id factory opcional.** IDs fallback usan `Date.now()`/`Math.random()`, failure throttle usa `Date.now()`, timers de buffer usan `setTimeout`. Como Logger se crea antes de `Timers`, no puede depender de `App.Timers`, pero si puede aceptar `clock`, `idFactory` y `setTimeout` opcionales para tests y runtimes especiales.
|
||||
4. **P2 - `dispose()` de child logger solo advierte en DEV con `console.warn`.** Es correcto como defensa, pero debe estar documentado en API: solo el root owns lifecycle.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Global `levels` con shorthand `levelsAtLeast`.
|
||||
- Redaction pipeline: campos `token`, `password`, `secret`, `authorization`, JWT-like values.
|
||||
- Correlation helpers: `logger.withTrace(traceId)`, `logger.withActor(actorRef)`.
|
||||
- Error bridge: si `error instanceof CodeError`, derivar category/module/code automaticamente.
|
||||
- Async flush result: `flush(): Promise<FlushResult>` para transports remotos.
|
||||
- Backpressure policy para buffers grandes: drop, block, sample.
|
||||
|
||||
## Modulo `frontend`
|
||||
|
||||
### Estado
|
||||
|
||||
`frontend` es pequeno y util: locale, dir, theme, mode, reduced motion/sound, density y aplicacion DOM via `ActiveDom` o helper de `$libs/dom`. La factory ya integra `dom` y `lang` si existen. Pero esta menos maduro que los demas modulos.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **P1 - Docs antiguas tras service schema.** README afirma que `createActiveApp()` construye `Frontend` y usa `App.Lang`/`App.Dom`; ahora `frontend` se declara en `services` y se expone como `App.frontend`.
|
||||
2. **P2 - Lifecycle incompleto.** Tras `dispose()`, los setters (`setLocale`, `setTheme`, etc.) siguen funcionando y pueden aplicar DOM. Para 1.0 debe haber flag `disposed` y semantica uniforme.
|
||||
3. **P2 - Persistencia de preferencias quedo fuera.** La factory dice que storage persistence es responsabilidad de la app. Es una buena separacion, pero para 1.0 conviene un preset/helper oficial, porque tema/densidad/dir son caso principal de `storage`.
|
||||
4. **P2 - Falta snapshot unico.** Hay getters individuales, pero no `snapshot()` con `{ locale, dir, theme, mode, reducedMotion, reducedSound, density }`. Para UI/debug/tests es mucho mas comodo.
|
||||
5. **P3 - Validacion de valores.** `setDensity`, `setMode`, `setDir` aceptan strings tipados en TS, pero runtime JS podria pasar valores invalidos. Si es API publica, conviene validar o documentar TypeScript-only.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- `snapshot()` y `onChange(snapshot)`.
|
||||
- Preset `persistFrontendPreferences(storage)`.
|
||||
- Eventos de bus opcionales: frontend.preference.changed.
|
||||
- Media query injector para tests SSR/browser.
|
||||
- Documentar CSS contract: atributos `data-theme`, `data-mode`, `dir`, density, reduced motion.
|
||||
|
||||
## Modulo `format`
|
||||
|
||||
### Estado
|
||||
|
||||
`format` esta bien organizado: numbers, currency, dates y units como submodulos, engines y active wrappers, locale source comun e integracion con `lang` desde factory. Tests cubren cada subdominio. Es funcional y extensible.
|
||||
|
||||
### Refactorizaciones recomendadas
|
||||
|
||||
1. **P1 - Docs con `$fmts` y `App.Format`.** Alias real `$format`, servicio real `App.format`. Ademas hay un typo documental: `import { createRates } from '$formats/currency'` cuando el alias real es `$format`.
|
||||
2. **P2 - `createActiveFormat().dispose()` no tiene guard idempotente propio.** Los submodulos tienen runtime dispose, pero el root deberia seguir la regla general del ecosistema.
|
||||
3. **P2 - Rates usa `Date.now()` por defecto.** `createRates({ now })` acepta inyeccion, pero `defineActiveFormat` no ofrece wiring con `Timers.clock`. Para 1.0, usar clock de App si se declaran rates con expiracion.
|
||||
4. **P2 - Cache global de Intl.NumberFormat sin limite.** `engine-currency.ts` mantiene `formatCache` module-global. En apps multi-locale/multi-currency/larga sesion puede crecer indefinidamente. Conviene LRU pequeno o cache por engine con `dispose()`.
|
||||
5. **P2 - Locale source doble puede duplicar notificaciones.** `createActiveFormat.setLocale()` actualiza localeState y cada submodulo manualmente. Funciona, pero para 1.0 conviene una sola fuente reactiva que notifique y submodulos se sincronicen una vez.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Integracion con `lang` interpolation: formatters nombrados para `{{price | currency}}`.
|
||||
- Formatter registry: `format.register('filesize', fn)`.
|
||||
- Ranges: date range, number range, relative time, list format, display names.
|
||||
- Rates provider HTTP/cache bridge con stale-if-error.
|
||||
- Unit catalog ampliado y aliases por dominio de negocio.
|
||||
- Tests por locale de alto riesgo: `ar`, `en-US`, `es-AR`, `fr-FR`, `de-DE`.
|
||||
|
||||
## Modulo `connection`
|
||||
|
||||
### Estado
|
||||
|
||||
`connection` mejoro mucho desde el primer audit: ya no es un monolito puro, ahora tiene piezas separadas para acks, heartbeat, reconnect, session wiring, channel registry, transport runtime, sender, serializer y active wrapper. Tambien exige `TimerScheduler`, que es correcto. Aun asi, es el modulo con mas riesgo operacional de esta tanda.
|
||||
|
||||
### Hallazgos y refactorizaciones
|
||||
|
||||
1. **P1 - `autoReauthOn` existe en tipos pero no se usa.** `EngineConnectionsOptions` declara `autoReauthOn?: ConnectionAutoReauthOn`, pero no aparece en `engine-connections.ts`, `connection.ts` ni presets. Es una opcion publica sin efecto. Para 1.0 hay que implementarla o eliminarla hasta que `orca`/presets la usen.
|
||||
2. **P1 - README desactualizado.** Usa `$conn`, `App.createActiveConnections()`, `App.Sess`, `App.Bus`, `App.Timers`. El codigo real usa `$connection`, `defineActiveConnections`, `App.connections`, `core.timers`, y el bus no se consume directamente salvo wiring/presets.
|
||||
3. **P2 - WebSocket coverage es casi inexistente.** `websocket.test.ts` solo valida error cuando WebSocket no existe. Faltan tests de open/message/close/error, binaryType, protocols, URL factory, bufferedAmount y cleanup de listeners.
|
||||
4. **P2 - `connection.ts` sigue concentrando demasiado wiring.** Aunque bajo de tamano frente al monolito anterior, sigue siendo el composition point de 10 KB con mucho cierre mutable (`disposed`, `intentionalClose`, lifecycle, detachSession, detachBrowserReconnect). Para 1.0 conviene dividir construction runtime en una factory interna que devuelva partes o un `ConnectionRuntimeContext`.
|
||||
5. **P2 - Reauth concurrente no esta serializada.** `wireConnectionSession()` puede disparar `reauthenticate()` en cambios de sesion sucesivos sin singleflight/cancelacion. Si llega refresh + external changed, pueden salir dos auth frames. Para 1.0, `reauthenticate()` deberia ser singleflight o tener politica.
|
||||
6. **P2 - Reconnect backoff no expone random determinista.** Usa `computeBackoffDelay()` sin pasar `random`; aunque el helper lo soporta, connection no lo deja configurar.
|
||||
7. **P2 - Payloads de channels no validan schema.** Tipado TS ayuda en compile-time, pero los frames de red son `unknown`. Para 1.0, una opcion por channel con Sium/StandardSchema reduciria bugs de mensajes malformados.
|
||||
|
||||
### Ampliaciones 1.0
|
||||
|
||||
- Implementar o retirar `autoReauthOn`.
|
||||
- `ConnectionContext` para acciones internas: publish diagnostics/events, timers, logger, abort signal.
|
||||
- Singleflight para connect/reconnect/reauthenticate.
|
||||
- WebSocket test suite real con mock constructor.
|
||||
- Channel schemas: `channel('chat', { incoming: { message: schema }, outgoing: { send: schema } })`.
|
||||
- Backpressure policies mas completas: buffer por topic, drop-oldest/drop-newest, metrics.
|
||||
- Reconnect policies documentadas: online/visible, queue/drop/replace si hay intento en vuelo.
|
||||
- Integracion con `orca`: reauth/disconnect como accion opt-in ante identity change, no acoplamiento directo a session/cache/perm.
|
||||
|
||||
## Roadmap recomendado para esta segunda tanda
|
||||
|
||||
### Sprint A - Docs y naming
|
||||
|
||||
- Corregir aliases obsoletos en `sium`, `storage`, `timer`, `logger`, `frontend`, `format`, `connection`.
|
||||
- Reemplazar ejemplos `App.X` capitalizados por `App.x` servicios, excepto core reales (`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`) si se mantienen asi.
|
||||
- Actualizar README de `connection`, `frontend`, `format` y `storage` antes de tocarlos mas: ahora mismo son los que mas pueden confundir.
|
||||
|
||||
### Sprint B - Lifecycle uniforme
|
||||
|
||||
- `storage`: error/no-op post-dispose.
|
||||
- `frontend`: flag disposed y snapshot.
|
||||
- `format`: root dispose idempotente.
|
||||
- Tests de lifecycle para todos los servicios declarables.
|
||||
|
||||
### Sprint C - Determinismo temporal
|
||||
|
||||
- `storage`: clock en `EngineStorageOptions`.
|
||||
- `format`: rates con clock de App.
|
||||
- `logger`: opciones `clock`, `idFactory`, `timer` o documentar excepcion por ser core bootstrap.
|
||||
- `connection`: random inyectable para reconnect.
|
||||
|
||||
### Sprint D - Riesgos operacionales
|
||||
|
||||
- `connection`: implementar/eliminar `autoReauthOn`, singleflight de reauth, WebSocket tests.
|
||||
- `logger`: global levels por habilitacion si esa es la regla final.
|
||||
- `sium`: locale activo con Lang.
|
||||
- `storage`: no operar despues de dispose.
|
||||
|
||||
## Prioridad resumida de la segunda tanda
|
||||
|
||||
| Prioridad | Tema | Modulos | Motivo |
|
||||
|---|---|---|---|
|
||||
| P1 | Aliases/docs obsoletos | sium/storage/timer/logger/frontend/format/connection | API 1.0 no puede ensenar imports inexistentes. |
|
||||
| P1 | `storage.dispose()` no cierra superficie | storage | Permite crear entradas tras teardown. |
|
||||
| P1 | `autoReauthOn` sin efecto | connection | Opcion publica enganosa en un modulo critico. |
|
||||
| P1/P2 | Filtro global por threshold vs habilitacion por nivel | logger | Debe alinearse con la regla final del ecosistema. |
|
||||
| P2 | Locale activo no seguido por Sium | sium/lang | Validaciones pueden resolver mensajes en locale inicial. |
|
||||
| P2 | Determinismo de tiempo incompleto | storage/logger/format/connection | Falta clock/random/timer injection en caminos 1.0. |
|
||||
| P2 | Lifecycle incompleto | frontend/format/storage | Consistencia de servicios declarables. |
|
||||
| P2 | WebSocket tests escasos | connection | Superficie critica con cobertura baja. |
|
||||
|
||||
## Criterio de cierre 1.0 para esta tanda
|
||||
|
||||
- Ningun README usa alias viejo ni propiedad App antigua.
|
||||
- Todo servicio declarable tiene lifecycle post-dispose definido y probado.
|
||||
- `storage`, `format.rates`, `connection.reconnect` y `logger` tienen historia determinista o excepcion documentada.
|
||||
- `sium` resuelve issues con el locale activo cuando se integra con `lang`.
|
||||
- `connection` no expone opciones muertas y tiene tests reales de WebSocket.
|
||||
- `logger` deja cerrada la decision global: threshold o per-level enable, pero no una mezcla confusa.
|
||||
- `frontend` tiene snapshot y persistencia oficial opt-in con `storage`.
|
||||
|
||||
Conclusion de la segunda tanda: `timer`, `sium`, `logger` y `storage` tienen una base muy fuerte; `format` esta sano pero necesita pulido de cache/locales; `frontend` necesita madurar contrato; `connection` es potente, pero debe cerrar opciones muertas, concurrencia de reauth y cobertura WebSocket antes de poder llamarse 1.0.
|
||||
@ -0,0 +1,69 @@
|
||||
# Nexo - demo dating del ecosistema
|
||||
|
||||
`Nexo` es una app demo de dating/social matching para probar el ecosistema completo en un producto coherente. La demo no existe para ensenar pantallas bonitas aisladas: existe para forzar integracion real entre modulos, estados, permisos, cache, realtime, formularios, servidor y devtools.
|
||||
|
||||
## Objetivo corto
|
||||
|
||||
Construir una app de matching segura y privacy-first, con perfiles, onboarding, filtros, matches, chat, bloqueo, reportes, moderacion y panel de diagnostico.
|
||||
|
||||
Debe servir para:
|
||||
|
||||
- probar que `active-app` compone todos los servicios;
|
||||
- validar que `uix` puede ser la capa real de componentes;
|
||||
- usar `sium` para formularios y validacion;
|
||||
- ejercitar `auth`, `session` y `perm` en flujos reales;
|
||||
- conectar `http`, `cache`, `storage` y `connection`;
|
||||
- observar todo con `logger`, `bus`, `timer` y `orca`;
|
||||
- generar pruebas de producto, integracion y regresion.
|
||||
|
||||
## Documentos
|
||||
|
||||
- [objetivos.md](objetivos.md): objetivos de producto, ecosistema y validacion.
|
||||
- [requisitos.md](requisitos.md): requisitos funcionales, no funcionales, roles, permisos y datos.
|
||||
- [diseno-producto.md](diseno-producto.md): experiencia, pantallas, flujos y componentes esperados.
|
||||
- [arquitectura-ecosistema.md](arquitectura-ecosistema.md): como participa cada modulo del ecosistema.
|
||||
- [auth-y-fotos.md](auth-y-fotos.md): paginas de login/registro y subida/gestion de fotos.
|
||||
- [plan-implementacion.md](plan-implementacion.md): fases de construccion y entregables.
|
||||
- [matriz-tests.md](matriz-tests.md): pruebas necesarias para cerrar la demo con confianza.
|
||||
|
||||
## Principios de la demo
|
||||
|
||||
1. Datos ficticios y seed controlado.
|
||||
2. Usuarios siempre adultos dentro de la demo.
|
||||
3. No usar rutas `test` o `demo` como libreria publica.
|
||||
4. No meter logica de negocio dentro de componentes visuales.
|
||||
5. Todo flujo importante debe dejar traza observable.
|
||||
6. Toda pantalla debe poder probarse sin depender de servicios externos reales.
|
||||
7. El servidor de datos vive aislado en `servers/dating`; SvelteKit consume su API.
|
||||
|
||||
## Superficie inicial de rutas
|
||||
|
||||
- `/dating`: shell principal de la demo.
|
||||
- `/dating/login`: inicio de sesion.
|
||||
- `/dating/register`: registro.
|
||||
- `/dating/reset`: recuperacion de acceso.
|
||||
- `/dating/mfa`: verificacion MFA simulada.
|
||||
- `/dating/onboarding`: creacion guiada de perfil.
|
||||
- `/dating/discover`: descubrimiento y filtros.
|
||||
- `/dating/matches`: matches y conversaciones.
|
||||
- `/dating/chat/[matchId]`: chat realtime.
|
||||
- `/dating/profile`: perfil, fotos, privacidad y preferencias.
|
||||
- `/dating/profile/photos`: subida, ordenacion y eliminacion de fotos.
|
||||
- `/dating/safety`: bloqueo, reporte, exportacion y borrado.
|
||||
- `/dating/admin`: moderacion y decisiones auditadas.
|
||||
- `/dating/devtools`: inspector de ecosistema para la demo.
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
La demo esta completa cuando un test puede recorrer este flujo:
|
||||
|
||||
1. registrar un usuario;
|
||||
2. completar onboarding;
|
||||
3. cambiar idioma, tema y preferencias;
|
||||
4. descubrir perfiles;
|
||||
5. hacer like y crear match;
|
||||
6. enviar mensajes online y offline;
|
||||
7. bloquear o reportar un perfil;
|
||||
8. resolver reporte como moderador;
|
||||
9. comprobar permisos;
|
||||
10. ver trazas, cache, storage y eventos en devtools.
|
||||
@ -0,0 +1,390 @@
|
||||
# Arquitectura de ecosistema para Nexo
|
||||
|
||||
## Idea
|
||||
|
||||
Nexo debe ser una app de referencia que use el ecosistema como plataforma. La ruta `src/web/routes/dating` contiene pantallas y composicion de demo. La logica reusable debe moverse a modulos publicos.
|
||||
|
||||
## Capas
|
||||
|
||||
### Rutas
|
||||
|
||||
Responsabilidad:
|
||||
|
||||
- cargar datos de pagina;
|
||||
- conectar acciones de usuario;
|
||||
- renderizar layouts;
|
||||
- componer componentes especificos de dating.
|
||||
|
||||
No deben:
|
||||
|
||||
- implementar motores;
|
||||
- duplicar validacion;
|
||||
- saltarse `active-app`;
|
||||
- importar detalles internos de `svrs`.
|
||||
|
||||
### uix
|
||||
|
||||
Responsabilidad:
|
||||
|
||||
- componentes genericos;
|
||||
- formularios conectados a `sium`;
|
||||
- componentes de auth/session/perm/cache/logger/devtools;
|
||||
- accesibilidad y comportamiento visual.
|
||||
|
||||
### arts
|
||||
|
||||
Responsabilidad:
|
||||
|
||||
- motores cliente/runtime;
|
||||
- servicios activos;
|
||||
- estado reactivo;
|
||||
- integracion con `active-app`.
|
||||
|
||||
### libs
|
||||
|
||||
Responsabilidad:
|
||||
|
||||
- tipos compartidos;
|
||||
- contratos;
|
||||
- schemas;
|
||||
- errores;
|
||||
- helpers puros.
|
||||
|
||||
### svrs
|
||||
|
||||
Responsabilidad:
|
||||
|
||||
- handlers server-side;
|
||||
- adapters;
|
||||
- permisos server-side;
|
||||
- auth/session;
|
||||
- endpoints de demo.
|
||||
|
||||
## ActiveApp de la demo
|
||||
|
||||
La demo debe tener un preset canonico:
|
||||
|
||||
```ts
|
||||
createDatingApp({
|
||||
services: {
|
||||
logger,
|
||||
timers,
|
||||
bus,
|
||||
orca,
|
||||
lang,
|
||||
prefs,
|
||||
frontend,
|
||||
storage,
|
||||
cache,
|
||||
http,
|
||||
session,
|
||||
auth,
|
||||
perm,
|
||||
connection,
|
||||
sium
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
El tipo resultante debe permitir que los componentes reciban una app tipada, no un `ActiveApp` generico con servicios `unknown`.
|
||||
|
||||
## Modulos y responsabilidades
|
||||
|
||||
### active-app
|
||||
|
||||
- crear `DatingApp`;
|
||||
- resolver dependencias;
|
||||
- exponer servicios tipados;
|
||||
- reportar lifecycle al devtools.
|
||||
|
||||
### sium
|
||||
|
||||
- schemas de onboarding;
|
||||
- schemas de perfil;
|
||||
- schemas de filtros;
|
||||
- schemas de reporte;
|
||||
- schemas de decision de moderacion;
|
||||
- introspeccion para `AutoFields`.
|
||||
|
||||
### uix
|
||||
|
||||
- renderizar formularios;
|
||||
- renderizar gates de permisos;
|
||||
- mostrar inspectores;
|
||||
- sostener primitives accesibles.
|
||||
|
||||
### http
|
||||
|
||||
- cliente API demo;
|
||||
- interceptores de session/auth;
|
||||
- trace id por request;
|
||||
- errores normalizados;
|
||||
- soporte para mock/fixtures.
|
||||
|
||||
### cache
|
||||
|
||||
- cache de feed;
|
||||
- cache de perfiles;
|
||||
- cache de matches;
|
||||
- invalidacion tras like/pass/report/block;
|
||||
- exposicion a inspector.
|
||||
|
||||
### storage
|
||||
|
||||
- draft de onboarding;
|
||||
- draft de profile editor;
|
||||
- cola offline de mensajes;
|
||||
- preferencias locales;
|
||||
- metadata local de fotos pendientes;
|
||||
- cache persistente si se habilita.
|
||||
|
||||
### prefs
|
||||
|
||||
- tema;
|
||||
- densidad;
|
||||
- idioma;
|
||||
- notificaciones;
|
||||
- preferencias de discover.
|
||||
|
||||
### frontend
|
||||
|
||||
- tema aplicado;
|
||||
- density;
|
||||
- viewport;
|
||||
- reduced motion;
|
||||
- direccion LTR/RTL si aplica.
|
||||
|
||||
### adom
|
||||
|
||||
- focus trap;
|
||||
- scroll lock;
|
||||
- portal/layer manager;
|
||||
- keyboard navigation;
|
||||
- observers de viewport.
|
||||
|
||||
### lang
|
||||
|
||||
- mensajes MF2;
|
||||
- namespaces por pantalla;
|
||||
- fallback de idioma;
|
||||
- pseudo-locale para pruebas.
|
||||
|
||||
### format
|
||||
|
||||
- fechas de mensajes;
|
||||
- distancia aproximada;
|
||||
- listas de intereses;
|
||||
- estado relativo de ultima conexion;
|
||||
- formatos localizados en admin.
|
||||
|
||||
### auth
|
||||
|
||||
- registro;
|
||||
- login;
|
||||
- MFA simulado;
|
||||
- recuperacion;
|
||||
- logout;
|
||||
- device/session management.
|
||||
|
||||
### session
|
||||
|
||||
- estado de usuario actual;
|
||||
- refresh;
|
||||
- expiracion;
|
||||
- cross-tab si se habilita;
|
||||
- session inspector.
|
||||
|
||||
### perm
|
||||
|
||||
- permisos por rol;
|
||||
- permisos por estado de usuario;
|
||||
- bloqueo entre usuarios;
|
||||
- gates de UI;
|
||||
- checks server-side.
|
||||
|
||||
### connection
|
||||
|
||||
- chat realtime simulado;
|
||||
- presence;
|
||||
- typing;
|
||||
- reconnect;
|
||||
- offline queue.
|
||||
|
||||
### bus
|
||||
|
||||
- eventos internos:
|
||||
- `dating.profile.completed`;
|
||||
- `dating.discover.loaded`;
|
||||
- `dating.like.sent`;
|
||||
- `dating.match.created`;
|
||||
- `dating.message.queued`;
|
||||
- `dating.message.sent`;
|
||||
- `dating.report.submitted`;
|
||||
- `dating.moderation.resolved`.
|
||||
|
||||
### logger
|
||||
|
||||
- trazas por flujo;
|
||||
- errores normalizados;
|
||||
- redaction de datos sensibles;
|
||||
- audit trail de moderacion.
|
||||
|
||||
### timer
|
||||
|
||||
- debounce de filtros;
|
||||
- expiracion de matches;
|
||||
- retry de mensajes;
|
||||
- timeout de requests;
|
||||
- timers visibles en devtools.
|
||||
|
||||
### orca
|
||||
|
||||
- orquestacion de onboarding finalizado;
|
||||
- flujo like -> match -> notificacion -> invalidacion cache;
|
||||
- flujo report -> hide local -> notify moderation -> audit;
|
||||
- flujo reconnect -> flush offline queue.
|
||||
|
||||
### svrs/auth
|
||||
|
||||
- endpoints de login/logout/session;
|
||||
- handlers para register, reset y MFA simulado.
|
||||
|
||||
### svrs/perm
|
||||
|
||||
- decisiones server-side;
|
||||
- explicacion de permisos;
|
||||
- checks para admin/moderacion.
|
||||
|
||||
### svrs/cache
|
||||
|
||||
- cache server-side si se prueba;
|
||||
- invalidacion coordinada.
|
||||
|
||||
## Servidor demo independiente
|
||||
|
||||
Las APIs de Nexo viven fuera de SvelteKit, en `servers/dating`. El cliente Svelte debe consumir este servidor por HTTP, usando cookies con `credentials: "include"`.
|
||||
|
||||
Base local por defecto:
|
||||
|
||||
```txt
|
||||
http://127.0.0.1:8787
|
||||
```
|
||||
|
||||
Endpoints:
|
||||
|
||||
- `POST /api/auth/register`
|
||||
- `POST /api/auth/login`
|
||||
- `POST /api/auth/logout`
|
||||
- `POST /api/auth/reset`
|
||||
- `POST /api/auth/mfa/verify`
|
||||
- `GET /api/session`
|
||||
- `GET /api/profile/me`
|
||||
- `PUT /api/profile/me`
|
||||
- `POST /api/profile/photos`
|
||||
- `DELETE /api/profile/photos/:filename`
|
||||
- `PATCH /api/profile/photos/order`
|
||||
- `PATCH /api/profile/photos/main`
|
||||
- `GET /api/discover`
|
||||
- `POST /api/likes`
|
||||
- `GET /api/matches`
|
||||
- `GET /api/matches/:id/messages`
|
||||
- `POST /api/matches/:id/messages`
|
||||
- `POST /api/safety/block`
|
||||
- `POST /api/safety/report`
|
||||
- `GET /api/admin/reports`
|
||||
- `POST /api/admin/reports/:id/resolve`
|
||||
- `GET /api/devtools/snapshot`
|
||||
|
||||
## Flujos principales
|
||||
|
||||
### Onboarding
|
||||
|
||||
1. `sium` valida cada paso.
|
||||
2. `storage` guarda draft.
|
||||
3. `http` guarda perfil final.
|
||||
4. `cache` invalida `profile.me`.
|
||||
5. `bus` emite `dating.profile.completed`.
|
||||
6. `orca` coordina notificacion y siguiente ruta.
|
||||
7. `logger` registra trace.
|
||||
|
||||
### Login/registro
|
||||
|
||||
1. `sium` valida credenciales y confirmaciones.
|
||||
2. `http` llama a auth.
|
||||
3. `auth` autentica contra PocketBase o adapter local.
|
||||
4. `session` guarda estado.
|
||||
5. `perm` carga rol/estado.
|
||||
6. `bus` emite `dating.auth.login` o `dating.auth.registered`.
|
||||
7. `orca` decide redireccion a onboarding, discover o MFA.
|
||||
8. `logger` traza sin password ni token.
|
||||
|
||||
### Fotos de perfil
|
||||
|
||||
1. `sium` valida metadata y limites de foto.
|
||||
2. `perm` valida `profile:photo:add/delete/reorder`.
|
||||
3. `http` sube archivo con multipart.
|
||||
4. PocketBase guarda archivos en `dating_profiles.photos`.
|
||||
5. `cache` invalida `profile.me`, perfil publico y discover.
|
||||
6. `bus` emite evento de foto.
|
||||
7. `orca` refresca perfil y feed si procede.
|
||||
8. `logger` registra tamano/tipo/resultado sin guardar binario.
|
||||
|
||||
### Like/match
|
||||
|
||||
1. Usuario pulsa like.
|
||||
2. `perm` valida `match:like`.
|
||||
3. `http` envia request.
|
||||
4. `cache` marca perfil como visto.
|
||||
5. `bus` emite `dating.like.sent`.
|
||||
6. Si hay match, `orca` dispara flujo de match.
|
||||
7. `connection` notifica si esta conectado.
|
||||
|
||||
### Chat offline
|
||||
|
||||
1. Usuario envia mensaje sin conexion.
|
||||
2. `connection` marca offline.
|
||||
3. `storage` guarda mensaje con `clientNonce`.
|
||||
4. UI muestra pending.
|
||||
5. Al reconectar, `orca` dispara flush.
|
||||
6. `http` confirma envio.
|
||||
7. `cache` actualiza thread.
|
||||
8. `logger` correlaciona intentos.
|
||||
|
||||
### Reporte
|
||||
|
||||
1. Usuario abre safety menu.
|
||||
2. `sium` valida report form.
|
||||
3. `perm` valida `safety:report`.
|
||||
4. `http` envia reporte.
|
||||
5. `cache` oculta target localmente.
|
||||
6. `bus` emite `dating.report.submitted`.
|
||||
7. `logger` audita sin exponer detalles sensibles.
|
||||
|
||||
## Datos de prueba
|
||||
|
||||
La demo debe usar seeds:
|
||||
|
||||
- usuarios normales;
|
||||
- usuario limitado;
|
||||
- moderador;
|
||||
- admin;
|
||||
- perfiles con intereses variados;
|
||||
- matches activos;
|
||||
- chat con mensajes;
|
||||
- reportes abiertos y resueltos;
|
||||
- estados offline/cacheados.
|
||||
|
||||
## Contratos publicos a extraer
|
||||
|
||||
- `DatingUser`
|
||||
- `DatingProfile`
|
||||
- `DatingPreference`
|
||||
- `DatingLike`
|
||||
- `DatingMatch`
|
||||
- `DatingMessage`
|
||||
- `DatingReport`
|
||||
- `DatingModerationDecision`
|
||||
- `DatingPermission`
|
||||
- `DatingEvent`
|
||||
|
||||
Estos contratos deben vivir fuera de la ruta si pasan a ser reutilizables.
|
||||
@ -0,0 +1,241 @@
|
||||
# Auth y fotos de perfil
|
||||
|
||||
## Objetivo
|
||||
|
||||
Definir de forma implementable las paginas de login, registro, recuperacion, MFA simulado y gestion de fotos de perfil. Estos flujos son obligatorios porque prueban `auth`, `session`, `sium`, `http`, `storage`, `cache`, `perm`, `logger`, `bus`, `orca`, `frontend`, `adom`, `lang`, `format` y PocketBase como backend local.
|
||||
|
||||
## Rutas de autenticacion
|
||||
|
||||
### `/dating/login`
|
||||
|
||||
Pantalla dedicada para iniciar sesion.
|
||||
|
||||
Campos:
|
||||
|
||||
- email;
|
||||
- password;
|
||||
- recordarme en este dispositivo;
|
||||
- idioma;
|
||||
- tema.
|
||||
|
||||
Estados:
|
||||
|
||||
- idle;
|
||||
- submitting;
|
||||
- invalid credentials;
|
||||
- account limited;
|
||||
- session restored;
|
||||
- mfa required;
|
||||
- network error;
|
||||
- offline.
|
||||
|
||||
Integraciones:
|
||||
|
||||
- `sium`: schema de login.
|
||||
- `auth`: login.
|
||||
- `session`: guardar sesion activa.
|
||||
- `http`: request al endpoint.
|
||||
- `storage`: preferencia local de dispositivo si aplica.
|
||||
- `logger`: trace sin password.
|
||||
- `bus`: evento `dating.auth.login`.
|
||||
- `orca`: flujo login -> session -> redirect.
|
||||
|
||||
### `/dating/register`
|
||||
|
||||
Pantalla dedicada para crear cuenta.
|
||||
|
||||
Campos:
|
||||
|
||||
- email;
|
||||
- password;
|
||||
- confirm password;
|
||||
- nombre visible inicial;
|
||||
- confirmacion de edad adulta;
|
||||
- aceptacion de reglas de demo/privacidad local.
|
||||
|
||||
Estados:
|
||||
|
||||
- idle;
|
||||
- submitting;
|
||||
- email already used;
|
||||
- weak password;
|
||||
- adult confirmation missing;
|
||||
- created;
|
||||
- redirect to onboarding.
|
||||
|
||||
Integraciones:
|
||||
|
||||
- `sium`: schema de registro y confirmacion de password.
|
||||
- `auth`: register.
|
||||
- `session`: iniciar sesion tras registro si procede.
|
||||
- `perm`: rol inicial `user`.
|
||||
- `logger`: evento de seguridad redacted.
|
||||
- `bus`: `dating.auth.registered`.
|
||||
- `orca`: register -> session -> onboarding draft.
|
||||
|
||||
### `/dating/reset`
|
||||
|
||||
Recuperacion simulada. La UI no debe revelar si el email existe.
|
||||
|
||||
Campos:
|
||||
|
||||
- email.
|
||||
|
||||
Estados:
|
||||
|
||||
- idle;
|
||||
- submitting;
|
||||
- sent;
|
||||
- network error.
|
||||
|
||||
### `/dating/mfa`
|
||||
|
||||
MFA simulado para probar flujo, aunque la primera version local no tenga MFA real activado.
|
||||
|
||||
Campos:
|
||||
|
||||
- codigo de 6/8 digitos;
|
||||
- recuperar acceso;
|
||||
- confiar en dispositivo si se habilita.
|
||||
|
||||
Estados:
|
||||
|
||||
- pending;
|
||||
- invalid code;
|
||||
- expired code;
|
||||
- verified;
|
||||
- locked.
|
||||
|
||||
## Componentes de auth
|
||||
|
||||
Componentes candidatos a `uix`:
|
||||
|
||||
- `AuthLayout`
|
||||
- `LoginForm`
|
||||
- `RegisterForm`
|
||||
- `ResetPasswordForm`
|
||||
- `MfaChallenge`
|
||||
- `SessionRestoreGate`
|
||||
- `AuthError`
|
||||
|
||||
Componentes especificos de dating:
|
||||
|
||||
- `DatingAuthHeader`
|
||||
- `DatingPrivacyNotice`
|
||||
|
||||
## Fotos de perfil
|
||||
|
||||
### Modelo inicial
|
||||
|
||||
La demo usa el campo `photos` de `dating_profiles` en PocketBase:
|
||||
|
||||
- tipo `file`;
|
||||
- maximo 6 fotos;
|
||||
- formatos: JPEG, PNG, WebP;
|
||||
- tamano maximo: 5 MB por foto;
|
||||
- thumbnails: `120x120` y `400x600`.
|
||||
|
||||
Para v2 avanzada se puede extraer una coleccion `dating_profile_photos` si se necesita moderacion por foto, orden individual persistente, captions o estados por archivo. Para la demo inicial, el campo file multiple es suficiente.
|
||||
|
||||
### `/dating/profile/photos`
|
||||
|
||||
Pantalla dedicada para gestionar fotos.
|
||||
|
||||
Funciones:
|
||||
|
||||
- subir fotos por selector;
|
||||
- drag and drop;
|
||||
- preview antes de guardar;
|
||||
- ordenar fotos;
|
||||
- marcar foto principal;
|
||||
- eliminar foto;
|
||||
- reemplazar foto;
|
||||
- mostrar progreso de subida;
|
||||
- mostrar errores por archivo;
|
||||
- guardar cambios;
|
||||
- cancelar y recuperar estado anterior.
|
||||
|
||||
Estados:
|
||||
|
||||
- empty;
|
||||
- local preview;
|
||||
- uploading;
|
||||
- uploaded;
|
||||
- upload failed;
|
||||
- too many files;
|
||||
- invalid type;
|
||||
- file too large;
|
||||
- reorder pending;
|
||||
- deleting;
|
||||
- saved.
|
||||
|
||||
### Validaciones
|
||||
|
||||
Cliente:
|
||||
|
||||
- maximo 6 fotos;
|
||||
- tipos permitidos;
|
||||
- tamano maximo;
|
||||
- no permitir publicar perfil visible sin al menos una foto si esa regla esta activa;
|
||||
- alt text o descripcion opcional para accesibilidad futura.
|
||||
|
||||
Servidor:
|
||||
|
||||
- repetir limites de tipo/tamano;
|
||||
- comprobar que el perfil pertenece al usuario;
|
||||
- moderador/admin puede ocultar o borrar fotos si se implementa;
|
||||
- usuario limitado puede tener subida bloqueada segun `perm`.
|
||||
|
||||
### Integraciones
|
||||
|
||||
- `sium`: schema de metadata de fotos y reglas de perfil.
|
||||
- `http`: upload multipart al endpoint de perfil.
|
||||
- `cache`: invalidar `profile.me`, `discover.feed` y perfiles vistos.
|
||||
- `storage`: guardar previews/draft solo como metadata local, no blobs grandes salvo decision explicita.
|
||||
- `perm`: `profile:photo:add`, `profile:photo:delete`, `profile:photo:reorder`.
|
||||
- `logger`: trazas sin incluir contenido binario.
|
||||
- `bus`: eventos `dating.profile.photo.added`, `dating.profile.photo.removed`, `dating.profile.photo.reordered`.
|
||||
- `orca`: coordinar upload -> cache invalidation -> profile refresh -> discover refresh.
|
||||
- `adom`: drag/drop, focus restore, keyboard reordering.
|
||||
- `frontend`: layout responsive del gestor.
|
||||
|
||||
## Endpoints requeridos
|
||||
|
||||
Estos endpoints los sirve el servidor independiente `servers/dating`, no SvelteKit:
|
||||
|
||||
- `POST /api/auth/register`
|
||||
- `POST /api/auth/login`
|
||||
- `POST /api/auth/logout`
|
||||
- `POST /api/auth/reset`
|
||||
- `POST /api/auth/mfa/verify`
|
||||
- `GET /api/session`
|
||||
- `POST /api/profile/photos`
|
||||
- `DELETE /api/profile/photos/:filename`
|
||||
- `PATCH /api/profile/photos/order`
|
||||
- `PATCH /api/profile/photos/main`
|
||||
|
||||
## Reglas de permisos
|
||||
|
||||
| Accion | Usuario | Limitado | Moderador | Admin |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `auth:login` | si | si | si | si |
|
||||
| `auth:logout` | si | si | si | si |
|
||||
| `profile:photo:add` | si | no | si | si |
|
||||
| `profile:photo:delete:self` | si | parcial | si | si |
|
||||
| `profile:photo:moderate` | no | no | si | si |
|
||||
| `profile:photo:reorder` | si | no | si | si |
|
||||
|
||||
## Tests minimos
|
||||
|
||||
- login correcto redirige a discover u onboarding.
|
||||
- login invalido no crea sesion.
|
||||
- registro crea usuario `dating_users` y redirige a onboarding.
|
||||
- recuperacion no revela si el email existe.
|
||||
- MFA invalido muestra error y conserva challenge.
|
||||
- upload de foto valida actualiza `dating_profiles.photos`.
|
||||
- upload de tipo invalido falla antes de enviar.
|
||||
- upload superior a 5 MB falla.
|
||||
- usuario limitado no puede subir foto.
|
||||
- eliminar foto invalida cache de perfil/discover.
|
||||
- reordenar fotos conserva la foto principal.
|
||||
- devtools muestra eventos/logs sin exponer password ni binarios.
|
||||
@ -0,0 +1,462 @@
|
||||
# Diseno de producto de Nexo
|
||||
|
||||
## Tono
|
||||
|
||||
Nexo debe sentirse como una herramienta social segura, clara y moderna. La demo no debe parecer una landing page ni una coleccion de tarjetas decorativas. Debe ser una app usable desde la primera pantalla.
|
||||
|
||||
El foco visual:
|
||||
|
||||
- confianza;
|
||||
- privacidad;
|
||||
- claridad de estado;
|
||||
- acciones rapidas;
|
||||
- buena lectura en movil;
|
||||
- componentes densos pero limpios para admin/devtools.
|
||||
|
||||
## Navegacion principal
|
||||
|
||||
### Usuario autenticado
|
||||
|
||||
- Discover
|
||||
- Matches
|
||||
- Chat
|
||||
- Profile
|
||||
- Safety
|
||||
- Devtools si tiene permiso
|
||||
|
||||
### Moderador/Admin
|
||||
|
||||
- Moderation
|
||||
- Audit
|
||||
- Users
|
||||
- Devtools
|
||||
|
||||
## Pantallas
|
||||
|
||||
### Entrada
|
||||
|
||||
Objetivo:
|
||||
|
||||
- iniciar sesion;
|
||||
- crear cuenta;
|
||||
- recuperar acceso;
|
||||
- cambiar idioma/tema local.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `AuthPanel`
|
||||
- `LoginForm`
|
||||
- `RegisterForm`
|
||||
- `ThemeToggle`
|
||||
- `LocaleSelect`
|
||||
|
||||
Estados:
|
||||
|
||||
- idle;
|
||||
- submitting;
|
||||
- invalid credentials;
|
||||
- session restored;
|
||||
- mfa required.
|
||||
|
||||
### Login
|
||||
|
||||
Objetivo:
|
||||
|
||||
- autenticar a un usuario y restaurar sesion.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `AuthLayout`
|
||||
- `LoginForm`
|
||||
- `SessionRestoreGate`
|
||||
- `AuthError`
|
||||
- `LocaleSelect`
|
||||
- `ThemeToggle`
|
||||
|
||||
Estados:
|
||||
|
||||
- submitting;
|
||||
- invalid credentials;
|
||||
- account limited;
|
||||
- mfa required;
|
||||
- offline;
|
||||
- redirecting.
|
||||
|
||||
### Registro
|
||||
|
||||
Objetivo:
|
||||
|
||||
- crear una cuenta demo y enviar al onboarding.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `AuthLayout`
|
||||
- `RegisterForm`
|
||||
- `PasswordStrength`
|
||||
- `AdultConfirmation`
|
||||
- `PrivacyNotice`
|
||||
|
||||
Estados:
|
||||
|
||||
- email already used;
|
||||
- weak password;
|
||||
- adult confirmation missing;
|
||||
- created;
|
||||
- redirecting to onboarding.
|
||||
|
||||
### Recuperacion y MFA
|
||||
|
||||
Objetivo:
|
||||
|
||||
- probar flujos de auth secundarios sin depender de proveedor externo.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `ResetPasswordForm`
|
||||
- `MfaChallenge`
|
||||
- `RecoveryNotice`
|
||||
|
||||
Estados:
|
||||
|
||||
- reset sent;
|
||||
- challenge pending;
|
||||
- invalid code;
|
||||
- expired code;
|
||||
- verified.
|
||||
|
||||
### Onboarding
|
||||
|
||||
Objetivo:
|
||||
|
||||
- convertir un usuario registrado en perfil usable.
|
||||
|
||||
Estructura:
|
||||
|
||||
- paso 1: identidad visible;
|
||||
- paso 2: bio e intereses;
|
||||
- paso 3: preferencias de descubrimiento;
|
||||
- paso 4: privacidad y confirmacion;
|
||||
- paso 5: preview.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `Form`
|
||||
- `AutoFields`
|
||||
- `FieldGroup`
|
||||
- `Stepper`
|
||||
- `ErrorSummary`
|
||||
- `SubmitBar`
|
||||
- `DraftStatus`
|
||||
|
||||
Estados:
|
||||
|
||||
- draft saved;
|
||||
- pending validation;
|
||||
- validation failed;
|
||||
- ready to publish;
|
||||
- restore draft after reload.
|
||||
|
||||
### Discover
|
||||
|
||||
Objetivo:
|
||||
|
||||
- explorar perfiles y producir likes/passes.
|
||||
|
||||
Layout:
|
||||
|
||||
- columna/panel de filtros;
|
||||
- lista o stack de perfiles;
|
||||
- acciones persistentes;
|
||||
- estado de conexion/cache visible sin ocupar el centro.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `ProfileCard`
|
||||
- `DiscoverFilters`
|
||||
- `LikeButton`
|
||||
- `PassButton`
|
||||
- `CompatibilityBadge`
|
||||
- `CacheStateBadge`
|
||||
- `OfflineBanner`
|
||||
|
||||
Estados:
|
||||
|
||||
- loading first page;
|
||||
- refreshing;
|
||||
- stale data;
|
||||
- no results;
|
||||
- offline with cached profiles;
|
||||
- action queued;
|
||||
- match created.
|
||||
|
||||
### Matches
|
||||
|
||||
Objetivo:
|
||||
|
||||
- ver relaciones activas y entrar en chat.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `MatchList`
|
||||
- `MatchCard`
|
||||
- `UnreadBadge`
|
||||
- `ExpiryBadge`
|
||||
- `ArchiveAction`
|
||||
|
||||
Estados:
|
||||
|
||||
- active;
|
||||
- pending;
|
||||
- expired;
|
||||
- archived;
|
||||
- blocked.
|
||||
|
||||
### Chat
|
||||
|
||||
Objetivo:
|
||||
|
||||
- probar realtime, offline queue, storage y permisos.
|
||||
|
||||
Layout:
|
||||
|
||||
- cabecera con estado de match;
|
||||
- timeline de mensajes;
|
||||
- composer fijo;
|
||||
- panel de seguridad contextual.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `ChatThread`
|
||||
- `MessageBubble`
|
||||
- `MessageState`
|
||||
- `TypingIndicator`
|
||||
- `PresenceDot`
|
||||
- `MessageComposer`
|
||||
- `SafetyMenu`
|
||||
|
||||
Estados:
|
||||
|
||||
- connected;
|
||||
- reconnecting;
|
||||
- offline;
|
||||
- pending messages;
|
||||
- blocked;
|
||||
- match expired;
|
||||
- send failed;
|
||||
- retrying.
|
||||
|
||||
### Profile
|
||||
|
||||
Objetivo:
|
||||
|
||||
- editar el perfil y preferencias.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `ProfileEditor`
|
||||
- `PhotoManager`
|
||||
- `PrefsPanel`
|
||||
- `PrivacySettings`
|
||||
- `PublicPreview`
|
||||
|
||||
Estados:
|
||||
|
||||
- unsaved changes;
|
||||
- saving;
|
||||
- saved;
|
||||
- invalid;
|
||||
- conflict with server;
|
||||
- profile hidden.
|
||||
|
||||
### Profile Photos
|
||||
|
||||
Objetivo:
|
||||
|
||||
- subir, previsualizar, ordenar, reemplazar y eliminar fotos de perfil.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `PhotoManager`
|
||||
- `PhotoDropzone`
|
||||
- `PhotoGrid`
|
||||
- `PhotoPreview`
|
||||
- `UploadProgress`
|
||||
- `PrimaryPhotoPicker`
|
||||
- `PhotoActionsMenu`
|
||||
|
||||
Estados:
|
||||
|
||||
- empty;
|
||||
- local preview;
|
||||
- uploading;
|
||||
- upload failed;
|
||||
- invalid type;
|
||||
- file too large;
|
||||
- too many files;
|
||||
- reorder pending;
|
||||
- deleting;
|
||||
- saved.
|
||||
|
||||
### Safety
|
||||
|
||||
Objetivo:
|
||||
|
||||
- probar bloqueo, reporte, exportacion y borrado.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `BlockedUsersList`
|
||||
- `ReportForm`
|
||||
- `SafetyChecklist`
|
||||
- `DataExportPanel`
|
||||
- `DangerZone`
|
||||
|
||||
Estados:
|
||||
|
||||
- report submitted;
|
||||
- blocked;
|
||||
- export prepared;
|
||||
- deletion scheduled;
|
||||
- permission denied.
|
||||
|
||||
### Moderation
|
||||
|
||||
Objetivo:
|
||||
|
||||
- probar permisos, decision auditada y flujos server-side.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `ReportQueue`
|
||||
- `ReportDetail`
|
||||
- `ModerationDecisionForm`
|
||||
- `PermissionExplain`
|
||||
- `AuditTrail`
|
||||
|
||||
Estados:
|
||||
|
||||
- report open;
|
||||
- report assigned;
|
||||
- resolved;
|
||||
- conflict: already resolved;
|
||||
- insufficient permission.
|
||||
|
||||
### Devtools
|
||||
|
||||
Objetivo:
|
||||
|
||||
- mostrar el ecosistema vivo.
|
||||
|
||||
Paneles:
|
||||
|
||||
- App graph;
|
||||
- Services;
|
||||
- Logs;
|
||||
- Bus events;
|
||||
- Orca traces;
|
||||
- Timers;
|
||||
- Cache;
|
||||
- Storage;
|
||||
- Session;
|
||||
- Perm checks;
|
||||
- Connection.
|
||||
|
||||
Componentes:
|
||||
|
||||
- `AppInspector`
|
||||
- `ServiceGraph`
|
||||
- `LogViewer`
|
||||
- `BusTimeline`
|
||||
- `OrcaTimeline`
|
||||
- `TimerPanel`
|
||||
- `CacheInspector`
|
||||
- `StorageBrowser`
|
||||
- `SessionInspector`
|
||||
- `PermissionInspector`
|
||||
- `ConnectionStatus`
|
||||
|
||||
## Componentes publicos esperados en uix
|
||||
|
||||
### Foundations
|
||||
|
||||
- `Button`
|
||||
- `IconButton`
|
||||
- `Input`
|
||||
- `Textarea`
|
||||
- `Select`
|
||||
- `Checkbox`
|
||||
- `Switch`
|
||||
- `Slider`
|
||||
- `SegmentedControl`
|
||||
- `Tabs`
|
||||
- `Dialog`
|
||||
- `Popover`
|
||||
- `Tooltip`
|
||||
- `Toast`
|
||||
- `Menu`
|
||||
- `Table`
|
||||
- `Badge`
|
||||
- `Avatar`
|
||||
|
||||
### Domain
|
||||
|
||||
- `AuthPanel`
|
||||
- `SessionMenu`
|
||||
- `PrefsPanel`
|
||||
- `Form`
|
||||
- `AutoFields`
|
||||
- `Can`
|
||||
- `Gate`
|
||||
- `PermissionExplain`
|
||||
- `CacheInspector`
|
||||
- `LogViewer`
|
||||
- `ConnectionStatus`
|
||||
- `OrcaTimeline`
|
||||
|
||||
### Dating-specific
|
||||
|
||||
Estos pueden vivir primero bajo `src/web/routes/dating/_components` y promocionarse despues si son reutilizables:
|
||||
|
||||
- `ProfileCard`
|
||||
- `DiscoverFilters`
|
||||
- `MatchCard`
|
||||
- `ChatThread`
|
||||
- `ReportForm`
|
||||
- `ModerationDecisionForm`
|
||||
|
||||
## Accesibilidad
|
||||
|
||||
- Navegacion completa por teclado.
|
||||
- Focus trap en dialogs.
|
||||
- Focus restore al cerrar overlays.
|
||||
- Labels asociados en todos los campos.
|
||||
- Mensajes de error conectados a campos.
|
||||
- Estados de conexion no comunicados solo por color.
|
||||
- Contraste suficiente en badges y estados.
|
||||
- Reduce motion respetado.
|
||||
|
||||
## Responsive
|
||||
|
||||
### Movil
|
||||
|
||||
- Navegacion inferior.
|
||||
- Discover como stack vertical.
|
||||
- Filtros en drawer.
|
||||
- Chat a pantalla completa.
|
||||
- Devtools con tabs compactas.
|
||||
|
||||
### Desktop
|
||||
|
||||
- Navegacion lateral.
|
||||
- Discover con filtros persistentes.
|
||||
- Chat con lista de matches lateral.
|
||||
- Admin con tablas densas.
|
||||
- Devtools con layout de paneles.
|
||||
|
||||
## Contenido y copy
|
||||
|
||||
- Mensajes localizables mediante MF2.
|
||||
- Textos de seguridad claros y no alarmistas.
|
||||
- Errores accionables.
|
||||
- No usar texto de marketing dentro de pantallas operativas.
|
||||
- No explicar features obvias dentro de la UI; la interfaz debe ser directa.
|
||||
@ -0,0 +1,330 @@
|
||||
# Matriz de tests de Nexo
|
||||
|
||||
## Objetivo
|
||||
|
||||
La demo debe poder demostrar el ecosistema con pruebas automatizadas. No basta con que las pantallas rendericen: hay que probar flujos completos, estados degradados, permisos e integraciones.
|
||||
|
||||
## Niveles de prueba
|
||||
|
||||
### Unit
|
||||
|
||||
Para:
|
||||
|
||||
- schemas `sium`;
|
||||
- formatters;
|
||||
- permission rules;
|
||||
- reducers/helpers puros;
|
||||
- adapters fake;
|
||||
- contratos de errores.
|
||||
|
||||
### Component
|
||||
|
||||
Para:
|
||||
|
||||
- formularios;
|
||||
- gates de permisos;
|
||||
- profile cards;
|
||||
- chat thread;
|
||||
- report form;
|
||||
- devtools panels.
|
||||
|
||||
### Integration
|
||||
|
||||
Para:
|
||||
|
||||
- active-app con servicios;
|
||||
- http + cache;
|
||||
- storage + drafts;
|
||||
- session + auth;
|
||||
- perm + routes;
|
||||
- bus + orca;
|
||||
- connection + offline queue.
|
||||
|
||||
### E2E
|
||||
|
||||
Para:
|
||||
|
||||
- registro a match;
|
||||
- login/registro/reset/MFA;
|
||||
- subida y gestion de fotos;
|
||||
- chat online/offline;
|
||||
- reporte y moderacion;
|
||||
- expiracion de sesion;
|
||||
- cambio de preferencias;
|
||||
- devtools visible.
|
||||
|
||||
## Fixtures obligatorias
|
||||
|
||||
- visitante;
|
||||
- usuario sin onboarding;
|
||||
- usuario con perfil completo;
|
||||
- usuario limitado;
|
||||
- usuario bloqueado;
|
||||
- moderador;
|
||||
- admin;
|
||||
- perfiles compatibles;
|
||||
- perfiles no compatibles;
|
||||
- match activo;
|
||||
- match expirado;
|
||||
- thread con mensajes;
|
||||
- reporte abierto;
|
||||
- reporte resuelto;
|
||||
- cache stale;
|
||||
- storage con draft;
|
||||
- connection offline.
|
||||
|
||||
## Casos E2E principales
|
||||
|
||||
### E2E-01 Registro y onboarding
|
||||
|
||||
Pasos:
|
||||
|
||||
1. visitar `/dating`;
|
||||
2. registrar usuario;
|
||||
3. completar onboarding valido;
|
||||
4. publicar perfil;
|
||||
5. entrar en discover.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- session activa;
|
||||
- profile completo;
|
||||
- draft eliminado o marcado completo;
|
||||
- evento `dating.profile.completed`;
|
||||
- log correlacionado;
|
||||
- permisos actualizados.
|
||||
|
||||
### E2E-02 Onboarding invalido
|
||||
|
||||
Pasos:
|
||||
|
||||
1. iniciar onboarding;
|
||||
2. dejar campos invalidos;
|
||||
3. intentar continuar.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- `sium` devuelve issues;
|
||||
- UI marca campos;
|
||||
- `ErrorSummary` se actualiza;
|
||||
- no se llama a save final;
|
||||
- draft parcial se conserva.
|
||||
|
||||
### E2E-03 Login y registro
|
||||
|
||||
Pasos:
|
||||
|
||||
1. abrir `/dating/register`;
|
||||
2. crear usuario valido;
|
||||
3. comprobar redireccion a onboarding;
|
||||
4. cerrar sesion;
|
||||
5. abrir `/dating/login`;
|
||||
6. iniciar sesion.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- se crea registro en `dating_users`;
|
||||
- `session` queda activa;
|
||||
- `perm` carga rol `user`;
|
||||
- password no aparece en logs/devtools;
|
||||
- login invalido no crea sesion.
|
||||
|
||||
### E2E-04 Fotos de perfil
|
||||
|
||||
Pasos:
|
||||
|
||||
1. abrir `/dating/profile/photos`;
|
||||
2. subir foto valida;
|
||||
3. marcarla como principal;
|
||||
4. reordenar;
|
||||
5. eliminar una foto.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- PocketBase actualiza `dating_profiles.photos`;
|
||||
- `cache` invalida `profile.me` y discover;
|
||||
- `bus` emite evento de foto;
|
||||
- `logger` no guarda binarios;
|
||||
- usuario limitado no puede subir;
|
||||
- tipo invalido y tamano excesivo fallan.
|
||||
|
||||
### E2E-05 Discover con filtros
|
||||
|
||||
Pasos:
|
||||
|
||||
1. abrir discover;
|
||||
2. cambiar filtros;
|
||||
3. esperar debounce;
|
||||
4. recibir perfiles.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- `timer` registra debounce;
|
||||
- `http` recibe query correcta;
|
||||
- `cache` guarda resultado;
|
||||
- empty state funciona si no hay resultados.
|
||||
|
||||
### E2E-06 Like crea match
|
||||
|
||||
Pasos:
|
||||
|
||||
1. abrir perfil compatible;
|
||||
2. pulsar like;
|
||||
3. servidor devuelve match.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- `perm` permite `match:like`;
|
||||
- `cache` invalida feed;
|
||||
- `bus` emite `dating.like.sent`;
|
||||
- `orca` ejecuta flujo de match;
|
||||
- match aparece en `/dating/matches`.
|
||||
|
||||
### E2E-07 Chat online
|
||||
|
||||
Pasos:
|
||||
|
||||
1. abrir match;
|
||||
2. enviar mensaje;
|
||||
3. recibir confirmacion.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- mensaje pasa de pending a sent;
|
||||
- trace de `http`/`connection`;
|
||||
- cache del thread actualizada;
|
||||
- timestamp formateado.
|
||||
|
||||
### E2E-08 Chat offline y retry
|
||||
|
||||
Pasos:
|
||||
|
||||
1. simular offline;
|
||||
2. enviar mensaje;
|
||||
3. recargar;
|
||||
4. simular reconnect.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- mensaje queda en `storage`;
|
||||
- UI muestra pending;
|
||||
- `connection` cambia a reconnecting/connected;
|
||||
- `orca` dispara flush;
|
||||
- mensaje acaba sent;
|
||||
- no se duplica por `clientNonce`.
|
||||
|
||||
### E2E-09 Bloqueo
|
||||
|
||||
Pasos:
|
||||
|
||||
1. abrir perfil o chat;
|
||||
2. bloquear usuario.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- target desaparece de discover/matches;
|
||||
- chat queda cerrado;
|
||||
- `perm` deniega `chat:send`;
|
||||
- cache se invalida;
|
||||
- audit/log creado.
|
||||
|
||||
### E2E-10 Reporte
|
||||
|
||||
Pasos:
|
||||
|
||||
1. abrir safety menu;
|
||||
2. rellenar reporte;
|
||||
3. enviar.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- `sium` valida;
|
||||
- `http` crea reporte;
|
||||
- target se oculta localmente;
|
||||
- bus emite `dating.report.submitted`;
|
||||
- reporte aparece en moderacion para rol correcto.
|
||||
|
||||
### E2E-11 Moderacion
|
||||
|
||||
Pasos:
|
||||
|
||||
1. login como moderador;
|
||||
2. abrir reporte;
|
||||
3. resolver con restriccion.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- permisos correctos;
|
||||
- decision queda auditada;
|
||||
- usuario objetivo queda limitado;
|
||||
- acciones prohibidas fallan server-side;
|
||||
- UI muestra explanation.
|
||||
|
||||
### E2E-12 Admin y devtools
|
||||
|
||||
Pasos:
|
||||
|
||||
1. login admin;
|
||||
2. abrir `/dating/devtools`;
|
||||
3. inspeccionar paneles.
|
||||
|
||||
Verificaciones:
|
||||
|
||||
- servicios activos visibles;
|
||||
- logs visibles;
|
||||
- bus timeline visible;
|
||||
- cache/storage/session/perm visibles;
|
||||
- no se muestran secretos.
|
||||
|
||||
## Casos de permisos
|
||||
|
||||
- Visitante intenta abrir discover: redirect/login.
|
||||
- Usuario limitado intenta like: denegado.
|
||||
- Usuario bloqueado intenta chat: denegado.
|
||||
- Moderador intenta cambiar roles admin: denegado.
|
||||
- Admin resuelve reporte: permitido.
|
||||
- Usuario normal abre devtools completo: denegado o vista limitada.
|
||||
|
||||
## Casos de cache/storage
|
||||
|
||||
- Feed cargado dos veces usa cache.
|
||||
- Like invalida perfil correspondiente.
|
||||
- Cambio de filtros crea nueva key.
|
||||
- Draft de onboarding sobrevive reload.
|
||||
- Draft se limpia al publicar.
|
||||
- Fotos subidas invalidan perfil y discover.
|
||||
- Foto rechazada no queda en cache ni storage.
|
||||
- Offline message sobrevive reload.
|
||||
- Cache stale se muestra como stale, no como fresh.
|
||||
|
||||
## Casos de observabilidad
|
||||
|
||||
- Cada flujo E2E tiene `traceId`.
|
||||
- Logs redacted no incluyen password ni tokens.
|
||||
- Bus timeline registra eventos relevantes.
|
||||
- Orca trace muestra pasos y errores.
|
||||
- Timer panel muestra debounce/retry/expiry.
|
||||
- Connection panel muestra offline/reconnect.
|
||||
|
||||
## Casos de accesibilidad
|
||||
|
||||
- Login completo por teclado.
|
||||
- Onboarding mueve foco al primer error.
|
||||
- Dialog de reporte atrapa foco.
|
||||
- Al cerrar dialog vuelve foco al boton origen.
|
||||
- Chat anuncia mensaje pending/sent sin depender solo de color.
|
||||
- Filtros son operables con teclado.
|
||||
|
||||
## Gates de cierre
|
||||
|
||||
Para declarar la demo lista:
|
||||
|
||||
- unit tests verdes;
|
||||
- component tests verdes;
|
||||
- integration tests verdes;
|
||||
- E2E principal verde;
|
||||
- `npm run check` verde;
|
||||
- `npm run build` verde;
|
||||
- no imports desde `/test` o `/demo`;
|
||||
- todos los permisos criticos probados en cliente y servidor;
|
||||
- devtools muestra datos reales de los servicios.
|
||||
@ -0,0 +1,71 @@
|
||||
# Objetivos de Nexo
|
||||
|
||||
## Objetivo principal
|
||||
|
||||
Crear una demo de dating/social matching que use todos los modulos importantes del ecosistema y permita validar que la plataforma puede sostener una aplicacion real, no solo ejemplos aislados.
|
||||
|
||||
La app debe demostrar composicion, UI, formularios, seguridad, permisos, datos, realtime, cache, persistencia, preferencias, internacionalizacion, observabilidad y pruebas.
|
||||
|
||||
## Objetivos de producto
|
||||
|
||||
- Permitir que un usuario cree una cuenta y complete un perfil.
|
||||
- Permitir descubrir perfiles compatibles mediante filtros.
|
||||
- Permitir likes, passes, matches y conversaciones.
|
||||
- Permitir gestionar privacidad, bloqueo, reporte y borrado.
|
||||
- Permitir moderar reportes desde un panel protegido.
|
||||
- Permitir operar la app con estados degradados: offline, sesion expirada, permisos insuficientes, datos stale y errores de red.
|
||||
|
||||
## Objetivos de ecosistema
|
||||
|
||||
- Validar `active-app` como compositor de servicios real.
|
||||
- Convertir `uix` en una capa de componentes reusable, no acoplada a rutas demo.
|
||||
- Usar `sium` como motor de schemas, validacion e introspeccion de formularios.
|
||||
- Usar `http` como cliente tipado y observable.
|
||||
- Integrar `cache` con `http`, `storage`, `bus` y estados de UI.
|
||||
- Usar `storage` para drafts, preferencias locales, cola offline y cache persistente.
|
||||
- Usar `auth`, `session` y `perm` para flujos de identidad y autorizacion.
|
||||
- Usar `connection` para chat, presencia y reconexion.
|
||||
- Usar `logger`, `bus`, `timer` y `orca` para trazar y coordinar flujos.
|
||||
- Usar `lang` con MessageFormat 2.0 / MF2 como contrato moderno de mensajes.
|
||||
- Usar `format` para fechas, distancia aproximada, listas, unidades y estados localizados.
|
||||
|
||||
## Objetivos tecnicos
|
||||
|
||||
- Definir una `DatingApp` canonica con servicios tipados.
|
||||
- Crear fixtures y seeds repetibles.
|
||||
- Evitar dependencias externas obligatorias para el happy path local.
|
||||
- Separar cliente, servidor, contratos y UI.
|
||||
- Evitar que `src/web/routes/dating` se convierta en libreria interna: la logica reusable debe ir a `src/uix`, `src/arts`, `src/libs` o `src/svrs`.
|
||||
- Cada flujo critico debe tener prueba automatizada.
|
||||
- Cada error esperado debe tener estado visual y evento de diagnostico.
|
||||
|
||||
## Objetivos de validacion
|
||||
|
||||
La demo debe permitir probar:
|
||||
|
||||
- composicion de servicios;
|
||||
- renderizado de componentes;
|
||||
- formularios generados;
|
||||
- permisos y gates;
|
||||
- sesion y expiracion;
|
||||
- cache hit/miss/stale;
|
||||
- storage persistente y drafts;
|
||||
- offline queue;
|
||||
- reconnect de chat;
|
||||
- logs correlacionados;
|
||||
- eventos de bus;
|
||||
- tareas orquestadas por `orca`;
|
||||
- timers de debounce, retry y expiracion.
|
||||
|
||||
## No objetivos
|
||||
|
||||
- No construir una app de dating comercial completa.
|
||||
- No usar datos reales de usuarios.
|
||||
- No depender de pagos reales, mapas reales ni proveedores externos obligatorios.
|
||||
- No crear features sociales no necesarias para probar el ecosistema.
|
||||
- No meter reglas de negocio en componentes visuales.
|
||||
- No duplicar motores ya existentes si el modulo del ecosistema puede cubrirlo.
|
||||
|
||||
## Resultado esperado
|
||||
|
||||
Al terminar, `Nexo` debe funcionar como demo de referencia para la version 2.0: una app suficientemente compleja para romper las integraciones flojas, pero acotada para poder testearse y mantenerse.
|
||||
@ -0,0 +1,233 @@
|
||||
# Plan de implementacion de Nexo
|
||||
|
||||
## Fase 0 - Preparacion
|
||||
|
||||
Objetivo: dejar una base que no pelee con la 1.0.
|
||||
|
||||
Tareas:
|
||||
|
||||
- Corregir gates actuales de `check` y `build` antes de ampliar superficie.
|
||||
- Definir `DatingApp` tipada.
|
||||
- Crear seeds deterministas.
|
||||
- Crear contratos minimos de datos.
|
||||
- Definir rutas y layout base.
|
||||
- Crear test helpers para reset de demo.
|
||||
|
||||
Entregables:
|
||||
|
||||
- `src/web/routes/dating/+layout.svelte`
|
||||
- `src/web/routes/dating/+page.svelte`
|
||||
- contratos iniciales;
|
||||
- seed local;
|
||||
- fixture reset.
|
||||
|
||||
## Fase 1 - UI shell y primitives
|
||||
|
||||
Objetivo: montar la app navegable sin logica compleja.
|
||||
|
||||
Tareas:
|
||||
|
||||
- Crear shell responsive.
|
||||
- Crear navegacion usuario/admin.
|
||||
- Crear estados comunes: loading, empty, error, offline, permission denied.
|
||||
- Crear componentes dating-specific temporales.
|
||||
- Identificar primitives que deben moverse a `src/uix`.
|
||||
|
||||
Entregables:
|
||||
|
||||
- shell principal;
|
||||
- rutas vacias navegables;
|
||||
- componentes base;
|
||||
- primer smoke test visual.
|
||||
|
||||
## Fase 2 - Auth, session y perfil
|
||||
|
||||
Objetivo: tener usuario autenticado y perfil editable.
|
||||
|
||||
Tareas:
|
||||
|
||||
- Paginas `/dating/login`, `/dating/register`, `/dating/reset` y `/dating/mfa`.
|
||||
- Login/register local.
|
||||
- Session restore.
|
||||
- Onboarding con `sium`.
|
||||
- Drafts con `storage`.
|
||||
- Profile editor.
|
||||
- Subida y gestion de fotos de perfil con PocketBase.
|
||||
- Preferences basicas.
|
||||
- Guards de permisos.
|
||||
|
||||
Entregables:
|
||||
|
||||
- flujo registro -> onboarding -> profile;
|
||||
- flujo login/logout/reset/MFA simulado;
|
||||
- upload/reorder/delete de fotos;
|
||||
- tests de validacion;
|
||||
- tests de sesion;
|
||||
- estados de error.
|
||||
|
||||
## Fase 3 - Discover y matching
|
||||
|
||||
Objetivo: probar producto central.
|
||||
|
||||
Tareas:
|
||||
|
||||
- Feed de perfiles seeded.
|
||||
- Filtros con debounce.
|
||||
- Cache de discover.
|
||||
- Like/pass.
|
||||
- Creacion de match.
|
||||
- Invalidacion de cache.
|
||||
- Eventos de bus.
|
||||
- Orquestacion con `orca`.
|
||||
|
||||
Entregables:
|
||||
|
||||
- discover usable;
|
||||
- matches creados;
|
||||
- tests like/pass/match;
|
||||
- timeline visible en devtools.
|
||||
|
||||
## Fase 4 - Chat y connection
|
||||
|
||||
Objetivo: probar realtime y offline.
|
||||
|
||||
Tareas:
|
||||
|
||||
- Thread de chat.
|
||||
- Composer.
|
||||
- Presence/typing simulado.
|
||||
- Offline queue.
|
||||
- Retry al reconectar.
|
||||
- Estados de mensaje.
|
||||
- Bloqueo corta conversacion.
|
||||
|
||||
Entregables:
|
||||
|
||||
- chat funcional;
|
||||
- pruebas online/offline;
|
||||
- inspector de connection/storage.
|
||||
|
||||
## Fase 5 - Safety y moderacion
|
||||
|
||||
Objetivo: probar permisos complejos y server-side.
|
||||
|
||||
Tareas:
|
||||
|
||||
- Bloqueo.
|
||||
- Reporte con `sium`.
|
||||
- Bandeja de moderacion.
|
||||
- Decision auditada.
|
||||
- Restricciones de usuario.
|
||||
- Permission explain.
|
||||
- Audit trail.
|
||||
|
||||
Entregables:
|
||||
|
||||
- safety center;
|
||||
- moderation admin;
|
||||
- tests de roles/permisos;
|
||||
- logs auditables.
|
||||
|
||||
## Fase 6 - Devtools de ecosistema
|
||||
|
||||
Objetivo: hacer visible que todos los modulos estan vivos.
|
||||
|
||||
Tareas:
|
||||
|
||||
- App/service inspector.
|
||||
- Log viewer.
|
||||
- Bus timeline.
|
||||
- Orca timeline.
|
||||
- Timer panel.
|
||||
- Cache inspector.
|
||||
- Storage browser.
|
||||
- Session inspector.
|
||||
- Permission inspector.
|
||||
- Connection panel.
|
||||
|
||||
Entregables:
|
||||
|
||||
- `/dating/devtools`;
|
||||
- snapshots de estado;
|
||||
- pruebas de diagnostico basicas.
|
||||
|
||||
## Fase 7 - Pulido y promocion a uix
|
||||
|
||||
Objetivo: separar lo reusable de lo especifico.
|
||||
|
||||
Tareas:
|
||||
|
||||
- Mover primitives genericas a `src/uix`.
|
||||
- Mover componentes de ecosistema a `src/uix`.
|
||||
- Dejar componentes dating-specific en la ruta.
|
||||
- Documentar APIs publicas.
|
||||
- Crear ejemplos minimos por componente.
|
||||
|
||||
Entregables:
|
||||
|
||||
- `uix` con primera superficie real;
|
||||
- dating consumiendo `uix`;
|
||||
- demos limpias;
|
||||
- tests de componentes.
|
||||
|
||||
## Orden recomendado de implementacion
|
||||
|
||||
1. `DatingApp` tipada y seeds.
|
||||
2. Shell de rutas.
|
||||
3. Onboarding con `sium`.
|
||||
4. Auth/session fake-local.
|
||||
5. Discover.
|
||||
6. Matching.
|
||||
7. Chat.
|
||||
8. Safety.
|
||||
9. Moderacion.
|
||||
10. Devtools.
|
||||
11. Promocion a `uix`.
|
||||
|
||||
## Riesgos
|
||||
|
||||
### Riesgo: la demo tapa problemas de 1.0
|
||||
|
||||
Mitigacion:
|
||||
|
||||
- No empezar features grandes si `check` y `build` siguen rotos.
|
||||
- Mantener PRs/fases pequenas.
|
||||
|
||||
### Riesgo: rutas se convierten en libreria
|
||||
|
||||
Mitigacion:
|
||||
|
||||
- Todo componente reusable se promociona a `uix`.
|
||||
- Todo contrato reusable se mueve a `libs`.
|
||||
|
||||
### Riesgo: permisos solo en UI
|
||||
|
||||
Mitigacion:
|
||||
|
||||
- Duplicar checks en server.
|
||||
- Testear acciones prohibidas.
|
||||
|
||||
### Riesgo: realtime falso demasiado simple
|
||||
|
||||
Mitigacion:
|
||||
|
||||
- Simular offline, reconnect, retry y out-of-order.
|
||||
- Usar `connection` y no solo stores locales.
|
||||
|
||||
### Riesgo: observabilidad decorativa
|
||||
|
||||
Mitigacion:
|
||||
|
||||
- Cada flujo critico debe emitir logs/eventos/traces reales.
|
||||
- Devtools lee estado real, no fixtures.
|
||||
|
||||
## Gates por fase
|
||||
|
||||
Cada fase debe cerrar con:
|
||||
|
||||
- `npm run check`;
|
||||
- tests unitarios relevantes;
|
||||
- al menos un smoke test de ruta;
|
||||
- estados visuales de error;
|
||||
- sin imports desde rutas `test` o `demo`;
|
||||
- sin componentes reusables escondidos en `dating` si ya pertenecen a `uix`.
|
||||
@ -0,0 +1,310 @@
|
||||
# Requisitos de Nexo
|
||||
|
||||
## Roles
|
||||
|
||||
### Visitante
|
||||
|
||||
Usuario no autenticado.
|
||||
|
||||
Puede:
|
||||
|
||||
- ver la pantalla de entrada;
|
||||
- registrarse;
|
||||
- iniciar sesion;
|
||||
- recuperar acceso;
|
||||
- cambiar idioma/tema local antes de autenticar.
|
||||
|
||||
No puede:
|
||||
|
||||
- ver perfiles;
|
||||
- enviar likes;
|
||||
- abrir chats;
|
||||
- acceder a safety center autenticado;
|
||||
- acceder a moderacion.
|
||||
|
||||
### Usuario
|
||||
|
||||
Usuario autenticado con perfil activo.
|
||||
|
||||
Puede:
|
||||
|
||||
- completar onboarding;
|
||||
- editar perfil;
|
||||
- configurar preferencias de descubrimiento;
|
||||
- ver perfiles compatibles;
|
||||
- hacer like/pass;
|
||||
- chatear con matches;
|
||||
- bloquear y reportar perfiles;
|
||||
- gestionar sesion y privacidad.
|
||||
|
||||
### Usuario limitado
|
||||
|
||||
Usuario autenticado con restriccion temporal por moderacion.
|
||||
|
||||
Puede:
|
||||
|
||||
- ver su perfil;
|
||||
- gestionar seguridad;
|
||||
- leer decisiones de moderacion;
|
||||
- apelar si se implementa el flujo.
|
||||
|
||||
No puede:
|
||||
|
||||
- enviar likes;
|
||||
- iniciar conversaciones nuevas;
|
||||
- aparecer en discover;
|
||||
- editar campos sensibles si la sancion lo impide.
|
||||
|
||||
### Moderador
|
||||
|
||||
Usuario con permisos de moderacion.
|
||||
|
||||
Puede:
|
||||
|
||||
- ver reportes;
|
||||
- revisar contexto limitado;
|
||||
- ocultar perfiles;
|
||||
- imponer restricciones;
|
||||
- resolver reportes;
|
||||
- dejar notas auditadas.
|
||||
|
||||
### Admin
|
||||
|
||||
Usuario con permisos de administracion.
|
||||
|
||||
Puede:
|
||||
|
||||
- gestionar roles;
|
||||
- revisar auditoria;
|
||||
- cambiar flags de la demo;
|
||||
- resetear seeds locales;
|
||||
- abrir devtools completos.
|
||||
|
||||
## Requisitos funcionales
|
||||
|
||||
### Autenticacion
|
||||
|
||||
- Pagina `/dating/login` con email, password, recordarme, errores y redireccion.
|
||||
- Pagina `/dating/register` con email, password, confirmacion, nombre visible inicial y confirmacion de edad adulta.
|
||||
- Pagina `/dating/reset` para recuperacion simulada sin revelar si el email existe.
|
||||
- Pagina `/dating/mfa` para challenge MFA simulado.
|
||||
- Logout desde menu de sesion.
|
||||
- Sesion persistente.
|
||||
- Expiracion de sesion y refresh.
|
||||
- Lista de dispositivos/sesiones activas.
|
||||
|
||||
### Onboarding
|
||||
|
||||
- Flujo por pasos.
|
||||
- Validacion por schema `sium`.
|
||||
- Draft persistente en `storage`.
|
||||
- Progreso recuperable tras reload.
|
||||
- Campos minimos:
|
||||
- nombre visible;
|
||||
- edad adulta validada;
|
||||
- bio;
|
||||
- intereses;
|
||||
- intencion;
|
||||
- ubicacion aproximada;
|
||||
- preferencias de descubrimiento;
|
||||
- visibilidad del perfil.
|
||||
|
||||
### Perfil
|
||||
|
||||
- Editar datos publicos.
|
||||
- Subir fotos de perfil a PocketBase desde `/dating/profile/photos`.
|
||||
- Gestionar hasta 6 fotos por perfil.
|
||||
- Reordenar fotos y marcar foto principal.
|
||||
- Eliminar o reemplazar fotos.
|
||||
- Validar tipo, tamano y permisos antes de subir.
|
||||
- Configurar privacidad.
|
||||
- Configurar idioma, tema, densidad y notificaciones.
|
||||
- Ver preview publico del perfil.
|
||||
- Validar cambios antes de guardar.
|
||||
|
||||
### Discover
|
||||
|
||||
- Mostrar perfiles compatibles.
|
||||
- Filtros por rango de edad, distancia aproximada, intereses e intencion.
|
||||
- Acciones like/pass.
|
||||
- Estados empty/loading/error/offline.
|
||||
- Cache de feed.
|
||||
- Invalidacion tras like/pass.
|
||||
- Paginacion o carga incremental.
|
||||
|
||||
### Matching
|
||||
|
||||
- Crear match cuando hay like mutuo.
|
||||
- Mostrar matches activos, pending, archivados y expirados.
|
||||
- Expirar matches pendientes mediante `timer`.
|
||||
- Emitir eventos de match por `bus`.
|
||||
- Orquestar notificacion y cache invalidation con `orca`.
|
||||
|
||||
### Chat
|
||||
|
||||
- Chat por match.
|
||||
- Envio online.
|
||||
- Envio offline con cola local.
|
||||
- Retry al reconectar.
|
||||
- Estado de mensaje: pending, sent, delivered, failed.
|
||||
- Typing indicator simulado.
|
||||
- Presence simulada.
|
||||
- Bloqueo corta envio/recepcion.
|
||||
|
||||
### Safety
|
||||
|
||||
- Bloquear usuario.
|
||||
- Reportar perfil o mensaje.
|
||||
- Seleccionar motivo.
|
||||
- Adjuntar contexto fake.
|
||||
- Confirmacion clara.
|
||||
- Ocultar perfil reportado/bloqueado.
|
||||
- Exportacion simulada de datos.
|
||||
- Borrado de cuenta simulado.
|
||||
|
||||
### Moderacion
|
||||
|
||||
- Bandeja de reportes.
|
||||
- Filtros por estado, motivo y prioridad.
|
||||
- Vista de detalle con contexto minimo.
|
||||
- Acciones: dismiss, warn, restrict, hide profile, ban demo user.
|
||||
- Notas auditadas.
|
||||
- Decision visible en audit log.
|
||||
- Permisos estrictos con `perm`.
|
||||
|
||||
### Devtools internos
|
||||
|
||||
- Estado de `active-app`.
|
||||
- Servicios activos/lazy/disposed.
|
||||
- Eventos de `bus`.
|
||||
- Logs de `logger`.
|
||||
- Cache entries.
|
||||
- Storage namespaces.
|
||||
- Session state.
|
||||
- Perm checks recientes.
|
||||
- Connection state.
|
||||
- Orca traces.
|
||||
- Timers activos.
|
||||
|
||||
## Requisitos no funcionales
|
||||
|
||||
- La demo debe funcionar en local sin servicios externos reales.
|
||||
- Debe tener seed determinista.
|
||||
- Debe ser testeable con datos estables.
|
||||
- Debe responder bien en desktop y movil.
|
||||
- Debe tener estados accesibles para teclado y lector de pantalla.
|
||||
- Debe evitar datos sensibles reales.
|
||||
- Debe poder resetearse entre tests.
|
||||
- Debe evitar coupling entre rutas y componentes publicos.
|
||||
- Debe trazar errores sin exponer secretos.
|
||||
|
||||
## Modelo de datos minimo
|
||||
|
||||
### User
|
||||
|
||||
- `id`
|
||||
- `email`
|
||||
- `role`
|
||||
- `status`
|
||||
- `createdAt`
|
||||
- `lastLoginAt`
|
||||
|
||||
### Profile
|
||||
|
||||
- `id`
|
||||
- `userId`
|
||||
- `displayName`
|
||||
- `age`
|
||||
- `bio`
|
||||
- `interests`
|
||||
- `intent`
|
||||
- `approxLocation`
|
||||
- `visibility`
|
||||
- `photos`
|
||||
- `primaryPhoto`
|
||||
|
||||
### Preference
|
||||
|
||||
- `userId`
|
||||
- `ageRange`
|
||||
- `distanceKm`
|
||||
- `intent`
|
||||
- `interests`
|
||||
- `theme`
|
||||
- `density`
|
||||
- `locale`
|
||||
- `notifications`
|
||||
|
||||
### Like
|
||||
|
||||
- `fromUserId`
|
||||
- `toUserId`
|
||||
- `state`
|
||||
- `createdAt`
|
||||
|
||||
### Match
|
||||
|
||||
- `id`
|
||||
- `userIds`
|
||||
- `state`
|
||||
- `createdAt`
|
||||
- `expiresAt`
|
||||
|
||||
### Message
|
||||
|
||||
- `id`
|
||||
- `matchId`
|
||||
- `senderId`
|
||||
- `body`
|
||||
- `state`
|
||||
- `createdAt`
|
||||
- `clientNonce`
|
||||
|
||||
### Report
|
||||
|
||||
- `id`
|
||||
- `reporterId`
|
||||
- `targetUserId`
|
||||
- `targetMessageId`
|
||||
- `reason`
|
||||
- `details`
|
||||
- `state`
|
||||
- `priority`
|
||||
- `createdAt`
|
||||
- `resolvedAt`
|
||||
- `resolverId`
|
||||
|
||||
## Matriz de permisos inicial
|
||||
|
||||
| Accion | Visitante | Usuario | Limitado | Moderador | Admin |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `profile:create` | no | si | si | si | si |
|
||||
| `profile:update:self` | no | si | parcial | si | si |
|
||||
| `profile:photo:add` | no | si | no | si | si |
|
||||
| `profile:photo:delete:self` | no | si | parcial | si | si |
|
||||
| `profile:photo:reorder` | no | si | no | si | si |
|
||||
| `profile:photo:moderate` | no | no | no | si | si |
|
||||
| `discover:view` | no | si | no | si | si |
|
||||
| `match:like` | no | si | no | no | si |
|
||||
| `chat:send` | no | si | no | no | si |
|
||||
| `safety:block` | no | si | si | si | si |
|
||||
| `safety:report` | no | si | si | si | si |
|
||||
| `moderation:view` | no | no | no | si | si |
|
||||
| `moderation:resolve` | no | no | no | si | si |
|
||||
| `admin:roles` | no | no | no | no | si |
|
||||
| `devtools:view` | no | limitado | limitado | si | si |
|
||||
|
||||
## Estados de error obligatorios
|
||||
|
||||
- Sesion expirada.
|
||||
- Usuario sin permiso.
|
||||
- Perfil incompleto.
|
||||
- Feed sin resultados.
|
||||
- Cache stale.
|
||||
- Red offline.
|
||||
- Reconnect en curso.
|
||||
- Mensaje pendiente.
|
||||
- Envio fallido.
|
||||
- Reporte ya resuelto.
|
||||
- Usuario bloqueado.
|
||||
- Servicio lazy fallido.
|
||||
@ -0,0 +1,65 @@
|
||||
# Nexo dating server
|
||||
|
||||
Servidor independiente para la demo `Nexo`. No vive dentro de SvelteKit y no usa `+server.ts`; expone una API HTTP propia para que el cliente la consuma despues.
|
||||
|
||||
## Arranque
|
||||
|
||||
Desde la raiz del repo:
|
||||
|
||||
```bash
|
||||
npm run dating:server
|
||||
```
|
||||
|
||||
Por defecto escucha en:
|
||||
|
||||
```txt
|
||||
http://127.0.0.1:8787
|
||||
```
|
||||
|
||||
PocketBase se configura con las variables locales ya guardadas en `.env.local`:
|
||||
|
||||
- `POCKETBASE_URL`
|
||||
- `POCKETBASE_SUPERUSER_EMAIL`
|
||||
- `POCKETBASE_SUPERUSER_PASSWORD`
|
||||
|
||||
Variables opcionales:
|
||||
|
||||
- `DATING_SERVER_HOST`
|
||||
- `DATING_SERVER_PORT`
|
||||
- `DATING_ALLOWED_ORIGINS`
|
||||
- `DATING_SESSION_COOKIE`
|
||||
- `DATING_SECURE_COOKIES`
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `GET /health`
|
||||
- `POST /api/auth/register`
|
||||
- `POST /api/auth/login`
|
||||
- `POST /api/auth/logout`
|
||||
- `POST /api/auth/reset`
|
||||
- `POST /api/auth/mfa/verify`
|
||||
- `GET /api/session`
|
||||
- `GET /api/profile/me`
|
||||
- `PUT /api/profile/me`
|
||||
- `POST /api/profile/photos`
|
||||
- `DELETE /api/profile/photos/:filename`
|
||||
- `PATCH /api/profile/photos/order`
|
||||
- `PATCH /api/profile/photos/main`
|
||||
- `GET /api/discover`
|
||||
- `POST /api/likes`
|
||||
- `GET /api/matches`
|
||||
- `GET /api/matches/:matchId/messages`
|
||||
- `POST /api/matches/:matchId/messages`
|
||||
- `POST /api/safety/block`
|
||||
- `POST /api/safety/report`
|
||||
- `GET /api/admin/reports`
|
||||
- `POST /api/admin/reports/:id/resolve`
|
||||
- `GET /api/devtools/snapshot`
|
||||
|
||||
## Sesion
|
||||
|
||||
El servidor usa una cookie HTTP-only llamada `dating_session`. Tambien acepta `Authorization: Bearer <token>` para pruebas locales, aunque el cliente deberia usar cookies con `credentials: "include"`.
|
||||
|
||||
## Fotos
|
||||
|
||||
Las fotos se guardan en PocketBase, campo `dating_profiles.photos`, usando el filesystem local de PocketBase (`pb_data/storage`). El servidor valida tipo, tamano y permisos antes de enviar el archivo a PocketBase.
|
||||
@ -0,0 +1,340 @@
|
||||
import {
|
||||
bool,
|
||||
clearSessionCookie,
|
||||
fail,
|
||||
getSessionToken,
|
||||
int,
|
||||
ok,
|
||||
optionalText,
|
||||
requireString,
|
||||
sessionCookie,
|
||||
stringArray,
|
||||
text
|
||||
} from './http.mjs';
|
||||
import {
|
||||
createRecord,
|
||||
fileUrl,
|
||||
firstRecord,
|
||||
getRecord,
|
||||
listRecords,
|
||||
pbPublic,
|
||||
pbUser,
|
||||
updateRecord,
|
||||
updateRecordForm
|
||||
} from './pocketbase.mjs';
|
||||
|
||||
export const ROLES = ['user', 'limited', 'moderator', 'admin'];
|
||||
export const STATUSES = ['active', 'limited', 'blocked', 'deleted'];
|
||||
export const INTENTS = ['dating', 'friends', 'long_term', 'casual', 'unsure'];
|
||||
export const VISIBILITIES = ['visible', 'hidden', 'paused'];
|
||||
export const LIKE_STATES = ['like', 'pass'];
|
||||
export const REPORT_REASONS = ['fake', 'abuse', 'spam', 'harassment', 'underage', 'other'];
|
||||
export const REPORT_PRIORITIES = ['low', 'normal', 'high', 'urgent'];
|
||||
export const MODERATION_ACTIONS = ['dismiss', 'warn', 'restrict', 'hide_profile', 'ban_demo_user'];
|
||||
|
||||
export async function authenticate(identity, password) {
|
||||
return pbPublic('/api/collections/dating_users/auth-with-password', {
|
||||
method: 'POST',
|
||||
body: { identity, password }
|
||||
});
|
||||
}
|
||||
|
||||
export async function registerUser(input) {
|
||||
const email = requireString(input.email, 'email', 200).toLowerCase();
|
||||
const password = requireString(input.password, 'password');
|
||||
const passwordConfirm = text(input.passwordConfirm || input.confirmPassword || input.password);
|
||||
const displayName = requireString(input.displayName, 'displayName', 80);
|
||||
if (password.length < 8) fail(400, 'weak_password', 'Password must contain at least 8 characters.');
|
||||
if (password !== passwordConfirm) fail(400, 'password_mismatch', 'Passwords do not match.');
|
||||
if (!bool(input.adultConfirmed) && !bool(input.adultVerified)) {
|
||||
fail(400, 'adult_confirmation_required', 'Adult confirmation is required.');
|
||||
}
|
||||
|
||||
await createRecord('dating_users', {
|
||||
email,
|
||||
password,
|
||||
passwordConfirm,
|
||||
displayName,
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
adultVerified: true,
|
||||
emailVisibility: false,
|
||||
verified: true
|
||||
});
|
||||
return authenticate(email, password);
|
||||
}
|
||||
|
||||
export async function currentSession(request) {
|
||||
const token = getSessionToken(request);
|
||||
if (!token) return null;
|
||||
try {
|
||||
const auth = await pbUser('/api/collections/dating_users/auth-refresh', token, {
|
||||
method: 'POST'
|
||||
});
|
||||
return {
|
||||
token: auth.token,
|
||||
user: userView(auth.record),
|
||||
cookie: sessionCookie(auth.token)
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
expired: true,
|
||||
cookie: clearSessionCookie()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function requireSession(request) {
|
||||
const session = await currentSession(request);
|
||||
if (!session || session.expired) fail(401, 'session_required', 'Authentication required.');
|
||||
if (session.user.status === 'deleted') fail(403, 'account_deleted', 'Account is deleted.');
|
||||
return session;
|
||||
}
|
||||
|
||||
export function withSession(response, session) {
|
||||
if (session?.cookie) {
|
||||
return { ...response, cookies: [...(response.cookies || []), session.cookie] };
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export function userView(record) {
|
||||
return {
|
||||
id: record.id,
|
||||
email: record.email,
|
||||
displayName: record.displayName || record.name || '',
|
||||
role: ROLES.includes(record.role) ? record.role : 'user',
|
||||
status: STATUSES.includes(record.status) ? record.status : 'active',
|
||||
adultVerified: Boolean(record.adultVerified),
|
||||
verified: Boolean(record.verified),
|
||||
created: record.created,
|
||||
updated: record.updated,
|
||||
lastLoginAt: record.lastLoginAt || ''
|
||||
};
|
||||
}
|
||||
|
||||
export function profileView(record) {
|
||||
if (!record) return null;
|
||||
const photos = Array.isArray(record.photos) ? record.photos : record.photos ? [record.photos] : [];
|
||||
return {
|
||||
id: record.id,
|
||||
userId: relationId(record.user),
|
||||
displayName: record.displayName || '',
|
||||
age: Number(record.age || 0),
|
||||
bio: record.bio || '',
|
||||
interests: Array.isArray(record.interests) ? record.interests : [],
|
||||
intent: record.intent || 'unsure',
|
||||
approxLocation: record.approxLocation || '',
|
||||
visibility: record.visibility || 'hidden',
|
||||
photos,
|
||||
primaryPhoto: record.primaryPhoto || photos[0] || '',
|
||||
photoUrls: photos.map((filename) => ({
|
||||
filename,
|
||||
original: fileUrl('dating_profiles', record.id, filename),
|
||||
thumb: fileUrl('dating_profiles', record.id, filename, '120x120'),
|
||||
card: fileUrl('dating_profiles', record.id, filename, '400x600')
|
||||
})),
|
||||
completed: Boolean(record.completed),
|
||||
publishedAt: record.publishedAt || '',
|
||||
created: record.created,
|
||||
updated: record.updated
|
||||
};
|
||||
}
|
||||
|
||||
export function matchView(record, profiles = []) {
|
||||
return {
|
||||
id: record.id,
|
||||
userIds: relationIds(record.users),
|
||||
state: record.state || 'active',
|
||||
expiresAt: record.expiresAt || '',
|
||||
metadata: record.metadata || {},
|
||||
profiles,
|
||||
created: record.created,
|
||||
updated: record.updated
|
||||
};
|
||||
}
|
||||
|
||||
export function messageView(record) {
|
||||
return {
|
||||
id: record.id,
|
||||
matchId: relationId(record.match),
|
||||
senderId: relationId(record.sender),
|
||||
body: record.body || '',
|
||||
state: record.state || 'sent',
|
||||
clientNonce: record.clientNonce || '',
|
||||
deliveredAt: record.deliveredAt || '',
|
||||
metadata: record.metadata || {},
|
||||
created: record.created,
|
||||
updated: record.updated
|
||||
};
|
||||
}
|
||||
|
||||
export function reportView(record) {
|
||||
return {
|
||||
id: record.id,
|
||||
reporterId: relationId(record.reporter),
|
||||
targetUserId: relationId(record.targetUser),
|
||||
targetMessageId: relationId(record.targetMessage),
|
||||
reason: record.reason,
|
||||
details: record.details || '',
|
||||
state: record.state || 'open',
|
||||
priority: record.priority || 'normal',
|
||||
resolvedAt: record.resolvedAt || '',
|
||||
resolverId: relationId(record.resolver),
|
||||
created: record.created,
|
||||
updated: record.updated
|
||||
};
|
||||
}
|
||||
|
||||
export async function getProfileByUser(userId) {
|
||||
return firstRecord('dating_profiles', `user = "${escapeFilter(userId)}"`);
|
||||
}
|
||||
|
||||
export async function requireProfile(userId) {
|
||||
const profile = await getProfileByUser(userId);
|
||||
if (!profile) fail(409, 'profile_required', 'Profile must exist before this operation.');
|
||||
return profile;
|
||||
}
|
||||
|
||||
export async function upsertProfile(user, input) {
|
||||
checkPermission(user, 'profile:update:self');
|
||||
const existing = await getProfileByUser(user.id);
|
||||
const payload = profilePayload(input, user, existing);
|
||||
if (existing) return updateRecord('dating_profiles', existing.id, payload);
|
||||
return createRecord('dating_profiles', { ...payload, user: user.id });
|
||||
}
|
||||
|
||||
export function profilePayload(input, user, existing) {
|
||||
const displayName = optionalText(input.displayName, 80) || existing?.displayName || user.displayName;
|
||||
if (!displayName) fail(400, 'missing_display_name', 'displayName is required.');
|
||||
const age = int(input.age ?? existing?.age, 0, { min: 18, max: 120 });
|
||||
if (!age) fail(400, 'invalid_age', 'age must be an integer between 18 and 120.');
|
||||
const intent = enumValue(input.intent || existing?.intent || 'unsure', INTENTS, 'intent');
|
||||
const visibility = enumValue(input.visibility || existing?.visibility || 'hidden', VISIBILITIES, 'visibility');
|
||||
const completed = bool(input.completed, Boolean(existing?.completed));
|
||||
return {
|
||||
displayName,
|
||||
age,
|
||||
bio: optionalText(input.bio, 500),
|
||||
interests: stringArray(input.interests, 30, 60),
|
||||
intent,
|
||||
approxLocation: optionalText(input.approxLocation, 120),
|
||||
visibility,
|
||||
completed,
|
||||
publishedAt: completed && visibility === 'visible' ? new Date().toISOString() : existing?.publishedAt || ''
|
||||
};
|
||||
}
|
||||
|
||||
export function checkPermission(user, action) {
|
||||
if (!user) fail(401, 'session_required', 'Authentication required.');
|
||||
if (user.role === 'admin') return true;
|
||||
if (user.status === 'blocked' || user.status === 'deleted') {
|
||||
fail(403, 'account_restricted', 'Account cannot perform this action.');
|
||||
}
|
||||
if (action.startsWith('moderation:') || action === 'profile:photo:moderate') {
|
||||
if (user.role === 'moderator') return true;
|
||||
fail(403, 'permission_denied', 'Permission denied.');
|
||||
}
|
||||
if (user.role === 'moderator') {
|
||||
if (['discover:view', 'safety:block', 'safety:report', 'devtools:view'].includes(action)) return true;
|
||||
if (action.startsWith('profile:')) return true;
|
||||
}
|
||||
if (user.status === 'limited' || user.role === 'limited') {
|
||||
if (['profile:create', 'profile:update:self', 'profile:photo:delete:self'].includes(action)) return true;
|
||||
if (['safety:block', 'safety:report', 'devtools:view'].includes(action)) return true;
|
||||
fail(403, 'permission_denied', 'Permission denied.');
|
||||
}
|
||||
if (
|
||||
[
|
||||
'profile:create',
|
||||
'profile:update:self',
|
||||
'profile:photo:add',
|
||||
'profile:photo:delete:self',
|
||||
'profile:photo:reorder',
|
||||
'discover:view',
|
||||
'match:like',
|
||||
'chat:send',
|
||||
'safety:block',
|
||||
'safety:report',
|
||||
'devtools:view'
|
||||
].includes(action)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
fail(403, 'permission_denied', 'Permission denied.');
|
||||
}
|
||||
|
||||
export async function assertMatchMember(matchId, userId) {
|
||||
const record = await getRecord('dating_matches', matchId);
|
||||
const ids = relationIds(record.users);
|
||||
if (!ids.includes(userId)) fail(404, 'match_not_found', 'Match not found.');
|
||||
return record;
|
||||
}
|
||||
|
||||
export async function createAudit(event, actorId, data = {}) {
|
||||
try {
|
||||
await createRecord('dating_audit_events', {
|
||||
actor: actorId || '',
|
||||
targetUser: data.targetUser || '',
|
||||
report: data.report || '',
|
||||
event,
|
||||
module: data.module || 'dating',
|
||||
traceId: data.traceId || '',
|
||||
data: data.data || {}
|
||||
});
|
||||
} catch {
|
||||
// Audit must not break the user flow in the local demo server.
|
||||
}
|
||||
}
|
||||
|
||||
export function relationId(value) {
|
||||
if (Array.isArray(value)) return String(value[0] || '');
|
||||
return value ? String(value) : '';
|
||||
}
|
||||
|
||||
export function relationIds(value) {
|
||||
if (!Array.isArray(value)) return value ? [String(value)] : [];
|
||||
return value.map(String);
|
||||
}
|
||||
|
||||
export function escapeFilter(value) {
|
||||
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
export function enumValue(value, allowed, field) {
|
||||
const next = text(value);
|
||||
if (!allowed.includes(next)) fail(400, 'invalid_enum', `${field} has an invalid value.`, { field, allowed });
|
||||
return next;
|
||||
}
|
||||
|
||||
export function sessionPayload(session) {
|
||||
return {
|
||||
authenticated: true,
|
||||
user: session.user
|
||||
};
|
||||
}
|
||||
|
||||
export function sessionResponse(session) {
|
||||
return withSession(ok(sessionPayload(session)), session);
|
||||
}
|
||||
|
||||
export async function listProfilesByUserIds(userIds) {
|
||||
const profiles = [];
|
||||
for (const userId of userIds) {
|
||||
const profile = await getProfileByUser(userId);
|
||||
if (profile) profiles.push(profileView(profile));
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
export async function allRecords(collection, options = {}) {
|
||||
const perPage = options.perPage || 100;
|
||||
const first = await listRecords(collection, { ...options, page: 1, perPage });
|
||||
const items = [...(first.items || [])];
|
||||
const totalPages = first.totalPages || 1;
|
||||
for (let page = 2; page <= totalPages; page += 1) {
|
||||
const next = await listRecords(collection, { ...options, page, perPage });
|
||||
items.push(...(next.items || []));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const serverDir = dirname(fileURLToPath(import.meta.url));
|
||||
// The server now lives at `<repo>/demos/dating/server/`, so the repo
|
||||
// root is three levels up — not two as it was when the server lived
|
||||
// at `<repo>/servers/dating/`. Loading `.env`/`.env.local` from both
|
||||
// the repo root and the server directory keeps either location valid.
|
||||
const rootDir = resolve(serverDir, '../../..');
|
||||
|
||||
loadEnvFile(resolve(rootDir, '.env'));
|
||||
loadEnvFile(resolve(rootDir, '.env.local'));
|
||||
loadEnvFile(resolve(serverDir, '.env'));
|
||||
loadEnvFile(resolve(serverDir, '.env.local'));
|
||||
|
||||
export const config = {
|
||||
host: process.env.DATING_SERVER_HOST || '127.0.0.1',
|
||||
port: readPort(process.env.DATING_SERVER_PORT, 8787),
|
||||
allowedOrigins: readList(
|
||||
process.env.DATING_ALLOWED_ORIGINS,
|
||||
['http://localhost:5173', 'http://127.0.0.1:5173']
|
||||
),
|
||||
cookieName: process.env.DATING_SESSION_COOKIE || 'dating_session',
|
||||
secureCookies: process.env.DATING_SECURE_COOKIES === 'true',
|
||||
pocketBaseUrl: stripTrailingSlash(
|
||||
process.env.POCKETBASE_URL || process.env.PUBLIC_POCKETBASE_URL || 'http://127.0.0.1:8090'
|
||||
),
|
||||
pocketBaseSuperuserEmail: requireEnv('POCKETBASE_SUPERUSER_EMAIL'),
|
||||
pocketBaseSuperuserPassword: requireEnv('POCKETBASE_SUPERUSER_PASSWORD')
|
||||
};
|
||||
|
||||
function loadEnvFile(file) {
|
||||
if (!existsSync(file)) return;
|
||||
const text = readFileSync(file, 'utf8');
|
||||
for (const rawLine of text.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const eq = line.indexOf('=');
|
||||
if (eq === -1) continue;
|
||||
const key = line.slice(0, eq).trim();
|
||||
const value = unquote(line.slice(eq + 1).trim());
|
||||
if (key && process.env[key] == null) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unquote(value) {
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
return value.slice(1, -1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function readPort(value, fallback) {
|
||||
const port = Number(value);
|
||||
return Number.isInteger(port) && port > 0 && port < 65536 ? port : fallback;
|
||||
}
|
||||
|
||||
function readList(value, fallback) {
|
||||
if (!value) return fallback;
|
||||
return value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function stripTrailingSlash(value) {
|
||||
return value.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function requireEnv(name) {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required env var ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@ -0,0 +1,244 @@
|
||||
import { config } from './env.mjs';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(status, code, message, details) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
export function ok(body, options = {}) {
|
||||
return {
|
||||
status: options.status || 200,
|
||||
headers: options.headers || {},
|
||||
cookies: options.cookies || [],
|
||||
body
|
||||
};
|
||||
}
|
||||
|
||||
export function created(body, options = {}) {
|
||||
return ok(body, { ...options, status: 201 });
|
||||
}
|
||||
|
||||
export function noContent(options = {}) {
|
||||
return {
|
||||
status: 204,
|
||||
headers: options.headers || {},
|
||||
cookies: options.cookies || [],
|
||||
body: undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function fail(status, code, message, details) {
|
||||
throw new ApiError(status, code, message, details);
|
||||
}
|
||||
|
||||
export async function readJson(request) {
|
||||
const text = await request.text();
|
||||
if (!text.trim()) return {};
|
||||
try {
|
||||
const value = JSON.parse(text);
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
fail(400, 'invalid_body', 'Body must be a JSON object.');
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) throw error;
|
||||
fail(400, 'invalid_json', 'Body must be valid JSON.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function readFormData(request) {
|
||||
try {
|
||||
return await request.formData();
|
||||
} catch {
|
||||
fail(
|
||||
400,
|
||||
'invalid_multipart',
|
||||
'Multipart form-data body is invalid. When sending FormData from the browser, do not set Content-Type manually.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function text(value, fallback = '') {
|
||||
return typeof value === 'string' ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
export function optionalText(value, max = 0) {
|
||||
const next = text(value);
|
||||
if (!next) return '';
|
||||
return max > 0 ? next.slice(0, max) : next;
|
||||
}
|
||||
|
||||
export function bool(value, fallback = false) {
|
||||
return typeof value === 'boolean' ? value : fallback;
|
||||
}
|
||||
|
||||
export function int(value, fallback, { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = {}) {
|
||||
if (value == null || value === '') return fallback;
|
||||
const next = typeof value === 'number' ? value : Number(value);
|
||||
if (!Number.isInteger(next)) return fallback;
|
||||
return Math.min(max, Math.max(min, next));
|
||||
}
|
||||
|
||||
export function stringArray(value, maxItems = 50, maxLength = 80) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.map((item) => text(item))
|
||||
.filter(Boolean)
|
||||
.slice(0, maxItems)
|
||||
.map((item) => item.slice(0, maxLength));
|
||||
}
|
||||
|
||||
export function requireString(value, name, max = 0) {
|
||||
const next = optionalText(value, max);
|
||||
if (!next) fail(400, 'missing_field', `${name} is required.`, { field: name });
|
||||
return next;
|
||||
}
|
||||
|
||||
export function parseCookies(header) {
|
||||
const cookies = new Map();
|
||||
if (!header) return cookies;
|
||||
for (const part of header.split(';')) {
|
||||
const eq = part.indexOf('=');
|
||||
if (eq === -1) continue;
|
||||
const name = part.slice(0, eq).trim();
|
||||
const value = part.slice(eq + 1).trim();
|
||||
if (name) cookies.set(name, decodeURIComponent(value));
|
||||
}
|
||||
return cookies;
|
||||
}
|
||||
|
||||
export function sessionCookie(token) {
|
||||
return serializeCookie(config.cookieName, token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'Lax',
|
||||
secure: config.secureCookies,
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 7
|
||||
});
|
||||
}
|
||||
|
||||
export function clearSessionCookie() {
|
||||
return serializeCookie(config.cookieName, '', {
|
||||
httpOnly: true,
|
||||
sameSite: 'Lax',
|
||||
secure: config.secureCookies,
|
||||
path: '/',
|
||||
maxAge: 0
|
||||
});
|
||||
}
|
||||
|
||||
export function getBearerToken(request) {
|
||||
const authorization = request.headers.get('authorization') || '';
|
||||
if (!authorization.toLowerCase().startsWith('bearer ')) return '';
|
||||
return authorization.slice(7).trim();
|
||||
}
|
||||
|
||||
export function getSessionToken(request) {
|
||||
return getBearerToken(request) || parseCookies(request.headers.get('cookie')).get(config.cookieName) || '';
|
||||
}
|
||||
|
||||
export function handleError(error) {
|
||||
if (error instanceof ApiError) {
|
||||
return ok(
|
||||
{
|
||||
ok: false,
|
||||
error: {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
details: error.details
|
||||
}
|
||||
},
|
||||
{ status: error.status }
|
||||
);
|
||||
}
|
||||
if (error?.name === 'PocketBaseError') {
|
||||
return ok(
|
||||
{
|
||||
ok: false,
|
||||
error: {
|
||||
code: error.code || 'pocketbase_error',
|
||||
message: error.message,
|
||||
details: error.data
|
||||
}
|
||||
},
|
||||
{ status: error.status || 502 }
|
||||
);
|
||||
}
|
||||
console.error(error);
|
||||
return ok(
|
||||
{
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'internal_error',
|
||||
message: 'Unexpected dating server error.'
|
||||
}
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
export function applyCors(request, response) {
|
||||
const origin = request.headers.get('origin');
|
||||
const headers = { ...response.headers };
|
||||
if (origin && config.allowedOrigins.includes(origin)) {
|
||||
headers['access-control-allow-origin'] = origin;
|
||||
headers['access-control-allow-credentials'] = 'true';
|
||||
headers['vary'] = appendVary(headers.vary, 'Origin');
|
||||
}
|
||||
return { ...response, headers };
|
||||
}
|
||||
|
||||
export function corsPreflight(request) {
|
||||
const origin = request.headers.get('origin');
|
||||
const headers = {
|
||||
'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
|
||||
'access-control-allow-headers': 'content-type,authorization',
|
||||
'access-control-max-age': '600'
|
||||
};
|
||||
if (origin && config.allowedOrigins.includes(origin)) {
|
||||
headers['access-control-allow-origin'] = origin;
|
||||
headers['access-control-allow-credentials'] = 'true';
|
||||
headers.vary = 'Origin';
|
||||
}
|
||||
return noContent({ headers });
|
||||
}
|
||||
|
||||
export function writeResponse(nodeResponse, response) {
|
||||
for (const [name, value] of Object.entries(response.headers || {})) {
|
||||
if (value != null) nodeResponse.setHeader(name, value);
|
||||
}
|
||||
if (response.cookies?.length) {
|
||||
nodeResponse.setHeader('set-cookie', response.cookies);
|
||||
}
|
||||
if (response.body === undefined) {
|
||||
nodeResponse.writeHead(response.status);
|
||||
nodeResponse.end();
|
||||
return;
|
||||
}
|
||||
const body = JSON.stringify(response.body);
|
||||
nodeResponse.setHeader('content-type', 'application/json; charset=utf-8');
|
||||
nodeResponse.setHeader('content-length', Buffer.byteLength(body));
|
||||
nodeResponse.writeHead(response.status);
|
||||
nodeResponse.end(body);
|
||||
}
|
||||
|
||||
function serializeCookie(name, value, options) {
|
||||
const parts = [`${name}=${encodeURIComponent(value)}`];
|
||||
if (options.maxAge != null) parts.push(`Max-Age=${options.maxAge}`);
|
||||
if (options.path) parts.push(`Path=${options.path}`);
|
||||
if (options.httpOnly) parts.push('HttpOnly');
|
||||
if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
|
||||
if (options.secure) parts.push('Secure');
|
||||
return parts.join('; ');
|
||||
}
|
||||
|
||||
function appendVary(value, item) {
|
||||
if (!value) return item;
|
||||
const parts = value.split(',').map((part) => part.trim().toLowerCase());
|
||||
return parts.includes(item.toLowerCase()) ? value : `${value}, ${item}`;
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
import { config } from './env.mjs';
|
||||
|
||||
export class PocketBaseError extends Error {
|
||||
constructor(status, code, message, data) {
|
||||
super(message);
|
||||
this.name = 'PocketBaseError';
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
let superuserAuth = null;
|
||||
|
||||
export function fileUrl(collection, recordId, filename, thumb = '') {
|
||||
const url = `${config.pocketBaseUrl}/api/files/${encodeURIComponent(collection)}/${encodeURIComponent(
|
||||
recordId
|
||||
)}/${encodeURIComponent(filename)}`;
|
||||
return thumb ? `${url}?thumb=${encodeURIComponent(thumb)}` : url;
|
||||
}
|
||||
|
||||
export async function pbPublic(path, options = {}) {
|
||||
return pbRequest(path, options);
|
||||
}
|
||||
|
||||
export async function pbUser(path, token, options = {}) {
|
||||
return pbRequest(path, { ...options, token });
|
||||
}
|
||||
|
||||
export async function pbSuper(path, options = {}) {
|
||||
const token = await getSuperuserToken();
|
||||
return pbRequest(path, { ...options, token });
|
||||
}
|
||||
|
||||
export async function listRecords(collection, options = {}) {
|
||||
const query = {
|
||||
page: options.page || 1,
|
||||
perPage: options.perPage || 50,
|
||||
sort: options.sort,
|
||||
filter: options.filter,
|
||||
expand: options.expand
|
||||
};
|
||||
return pbSuper(`/api/collections/${collection}/records`, { query });
|
||||
}
|
||||
|
||||
export async function firstRecord(collection, filter) {
|
||||
const result = await listRecords(collection, { filter, perPage: 1 });
|
||||
return result.items?.[0] || null;
|
||||
}
|
||||
|
||||
export async function getRecord(collection, id) {
|
||||
return pbSuper(`/api/collections/${collection}/records/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
export async function createRecord(collection, body) {
|
||||
return pbSuper(`/api/collections/${collection}/records`, {
|
||||
method: 'POST',
|
||||
body
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateRecord(collection, id, body) {
|
||||
return pbSuper(`/api/collections/${collection}/records/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH',
|
||||
body
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateRecordForm(collection, id, body) {
|
||||
return pbSuper(`/api/collections/${collection}/records/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH',
|
||||
body
|
||||
});
|
||||
}
|
||||
|
||||
async function getSuperuserToken() {
|
||||
if (superuserAuth && superuserAuth.expiresAt > Date.now() + 30_000) {
|
||||
return superuserAuth.token;
|
||||
}
|
||||
const response = await pbRequest('/api/collections/_superusers/auth-with-password', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
identity: config.pocketBaseSuperuserEmail,
|
||||
password: config.pocketBaseSuperuserPassword
|
||||
}
|
||||
});
|
||||
superuserAuth = {
|
||||
token: response.token,
|
||||
expiresAt: tokenExpiresAt(response.token)
|
||||
};
|
||||
return response.token;
|
||||
}
|
||||
|
||||
async function pbRequest(path, options = {}) {
|
||||
const url = new URL(path, config.pocketBaseUrl);
|
||||
for (const [key, value] of Object.entries(options.query || {})) {
|
||||
if (value != null && value !== '') url.searchParams.set(key, String(value));
|
||||
}
|
||||
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (options.token) headers.set('authorization', `Bearer ${options.token}`);
|
||||
|
||||
let body = options.body;
|
||||
if (body && !(body instanceof FormData) && typeof body !== 'string') {
|
||||
headers.set('content-type', 'application/json');
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: options.method || (body ? 'POST' : 'GET'),
|
||||
headers,
|
||||
body
|
||||
});
|
||||
const text = await response.text();
|
||||
const data = text ? parseJson(text) : null;
|
||||
if (!response.ok) {
|
||||
throw new PocketBaseError(
|
||||
response.status,
|
||||
data?.code || data?.status || 'pocketbase_error',
|
||||
data?.message || `PocketBase request failed with ${response.status}.`,
|
||||
data?.data || data
|
||||
);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function parseJson(text) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return { message: text };
|
||||
}
|
||||
}
|
||||
|
||||
function tokenExpiresAt(token) {
|
||||
const [, payload] = token.split('.');
|
||||
if (!payload) return Date.now() + 5 * 60_000;
|
||||
try {
|
||||
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
||||
return typeof decoded.exp === 'number' ? decoded.exp * 1000 : Date.now() + 5 * 60_000;
|
||||
} catch {
|
||||
return Date.now() + 5 * 60_000;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
<!--
|
||||
Bottom-tab navigation for the product surface (Discover, Matches,
|
||||
Profile, Safety). Sticks to the bottom on mobile, becomes a centered
|
||||
pill on desktop. The visual styling lives in the Nexo design system
|
||||
(`.nx-tabbar` / `.nx-tab`); this file only owns the data and active
|
||||
state derivation.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { getNexoContext } from '../_lib/context';
|
||||
|
||||
const nexo = getNexoContext();
|
||||
|
||||
const items = [
|
||||
{ href: '/dating/discover', label: 'Descubre', icon: '✦' },
|
||||
{ href: '/dating/matches', label: 'Matches', icon: '♡' },
|
||||
{ href: '/dating/profile', label: 'Perfil', icon: '◔' },
|
||||
{ href: '/dating/safety', label: 'Seguridad', icon: '⚐' }
|
||||
] as const;
|
||||
|
||||
function isCurrent(href: string): boolean {
|
||||
return page.url.pathname === href || page.url.pathname.startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await nexo.api.logout();
|
||||
} finally {
|
||||
nexo.setSession({ authenticated: false });
|
||||
await goto('/dating/login', { replaceState: true });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="nx-tabbar appnav-frame" aria-label="Nexo">
|
||||
{#each items as item (item.href)}
|
||||
<a
|
||||
class="nx-tab"
|
||||
href={item.href}
|
||||
aria-current={isCurrent(item.href) ? 'page' : undefined}
|
||||
>
|
||||
<span class="ico" aria-hidden="true">{item.icon}</span>
|
||||
<span class="label">{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
<button type="button" class="nx-tab logout" onclick={logout}>
|
||||
<span class="ico" aria-hidden="true">⏻</span>
|
||||
<span class="label">Salir</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
/* The primitives' .nx-tabbar uses 4 columns; we have 5 cells once
|
||||
"Salir" is added, so override the grid here. */
|
||||
.appnav-frame {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
max-width: 36rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ico {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.logout {
|
||||
opacity: 0.65;
|
||||
}
|
||||
.logout:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
/* Desktop replaces this bottom tab bar with the layout's left
|
||||
rail. Hiding here keeps the page chrome from doubling up. */
|
||||
@media (min-width: 1024px) {
|
||||
.appnav-frame {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,182 @@
|
||||
<!--
|
||||
Shared auth shell — split layout on desktop (form left, brand
|
||||
gradient right), single column on mobile. Both halves stay simple
|
||||
and high-contrast: no busy backgrounds, no decorative noise.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: Snippet;
|
||||
footer?: Snippet;
|
||||
}
|
||||
let { title, subtitle, children, footer }: Props = $props();
|
||||
</script>
|
||||
|
||||
<section class="auth-shell">
|
||||
<div class="auth-form-side">
|
||||
<div class="form-stack">
|
||||
<a class="brand" href="/dating">
|
||||
<span class="brand-mark" aria-hidden="true">N</span>
|
||||
<span class="brand-text">Nexo</span>
|
||||
</a>
|
||||
<header>
|
||||
<h1 class="nx-h1">{title}</h1>
|
||||
{#if subtitle}<p class="sub">{subtitle}</p>{/if}
|
||||
</header>
|
||||
<div class="body">
|
||||
{@render children()}
|
||||
</div>
|
||||
{#if footer}
|
||||
<footer class="auth-foot">
|
||||
{@render footer()}
|
||||
</footer>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="auth-art" aria-hidden="true">
|
||||
<div class="art-grid">
|
||||
<div class="art-quote">
|
||||
<p>“La diferencia está en el primer mensaje.”</p>
|
||||
<span>— Nexo</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.auth-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
background: var(--nx-bg);
|
||||
}
|
||||
.auth-form-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 32px;
|
||||
min-width: 0;
|
||||
}
|
||||
.form-stack {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
}
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.brand-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--nx-radius-md);
|
||||
background: var(--nx-accent);
|
||||
color: var(--nx-accent-fg);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
.brand-text {
|
||||
font-weight: 600;
|
||||
font-size: var(--nx-text-md);
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.sub {
|
||||
margin: 0;
|
||||
font-size: var(--nx-text-sm);
|
||||
color: var(--nx-fg-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.auth-foot {
|
||||
font-size: var(--nx-text-sm);
|
||||
color: var(--nx-fg-muted);
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--nx-border);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
.auth-foot :global(a) {
|
||||
color: var(--nx-fg);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
.auth-foot :global(a:hover) {
|
||||
color: var(--nx-accent);
|
||||
}
|
||||
|
||||
.auth-art {
|
||||
background: linear-gradient(
|
||||
140deg,
|
||||
color-mix(in srgb, var(--nx-accent) 10%, var(--nx-bg-sunken)),
|
||||
var(--nx-bg-sunken)
|
||||
),
|
||||
var(--nx-bg-sunken);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 48px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-left: 1px solid var(--nx-border);
|
||||
}
|
||||
.art-grid {
|
||||
max-width: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
.art-quote {
|
||||
padding: 28px;
|
||||
border: 1px solid var(--nx-border);
|
||||
border-radius: var(--nx-radius-lg);
|
||||
background: var(--nx-bg-elevated);
|
||||
box-shadow: var(--nx-shadow-md);
|
||||
}
|
||||
.art-quote p {
|
||||
font-family: var(--nx-font-display);
|
||||
font-size: var(--nx-text-xl);
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.35;
|
||||
margin: 0 0 12px;
|
||||
color: var(--nx-fg);
|
||||
}
|
||||
.art-quote span {
|
||||
font-size: var(--nx-text-xs);
|
||||
color: var(--nx-fg-muted);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.auth-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.auth-art {
|
||||
display: none;
|
||||
}
|
||||
.auth-form-side {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,31 @@
|
||||
<!--
|
||||
Form field primitive: label + control + help/error in a tidy
|
||||
column. Children control gets the `.nx-field-input` /
|
||||
`.nx-field-textarea` / `.nx-field-select` class from the consumer.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
interface Props {
|
||||
id: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
error?: string;
|
||||
children: Snippet;
|
||||
}
|
||||
let { id, label, hint, error, children }: Props = $props();
|
||||
const errorId = $derived(error ? `${id}-error` : undefined);
|
||||
</script>
|
||||
|
||||
<div class="nx-field" class:has-error={Boolean(error)}>
|
||||
<label class="nx-field-label" for={id}>{label}</label>
|
||||
{@render children()}
|
||||
{#if error}
|
||||
<p id={errorId} class="nx-field-error" role="alert">
|
||||
<Icon name="alert" size={12} />
|
||||
<span>{error}</span>
|
||||
</p>
|
||||
{:else if hint}
|
||||
<p class="nx-field-help">{hint}</p>
|
||||
{/if}
|
||||
</div>
|
||||
@ -0,0 +1,75 @@
|
||||
<!--
|
||||
Tarjeta canónica de perfil para Discover. La estructura — foto a
|
||||
pantalla completa con overlay de nombre/edad/ubicación, prompt
|
||||
editorial debajo y chips de intereses al pie — viene del bundle de
|
||||
diseño Nexo (variante "Editorial").
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { DatingProfile } from '../_lib/types';
|
||||
|
||||
interface Props {
|
||||
profile: DatingProfile;
|
||||
}
|
||||
let { profile }: Props = $props();
|
||||
|
||||
const main = $derived(
|
||||
profile.photoUrls.find((entry) => entry.filename === profile.primaryPhoto) ??
|
||||
profile.photoUrls[0]
|
||||
);
|
||||
|
||||
const intentLabel = $derived(
|
||||
({
|
||||
dating: 'Conocer gente',
|
||||
long_term: 'Algo serio',
|
||||
casual: 'Algo casual',
|
||||
friends: 'Amistad',
|
||||
unsure: 'Sin etiqueta'
|
||||
} as const)[profile.intent]
|
||||
);
|
||||
|
||||
const initial = $derived(profile.displayName.slice(0, 1).toUpperCase());
|
||||
</script>
|
||||
|
||||
<article class="nx-card" data-intent={profile.intent}>
|
||||
<div class="nx-photo">
|
||||
{#if main}
|
||||
<img src={main.card} alt={`Foto de ${profile.displayName}`} loading="lazy" />
|
||||
{:else}
|
||||
<div class="photo-placeholder" aria-hidden="true">{initial}</div>
|
||||
{/if}
|
||||
<div class="photo-meta">
|
||||
<span class="intent">{intentLabel}</span>
|
||||
<div class="name">
|
||||
<span>{profile.displayName}</span>
|
||||
<span class="age">{profile.age}</span>
|
||||
</div>
|
||||
{#if profile.approxLocation !== ''}
|
||||
<div class="place">
|
||||
<span aria-hidden="true">⌖</span>
|
||||
<span>{profile.approxLocation}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if profile.bio !== ''}
|
||||
<div class="nx-prompt-card">
|
||||
<p class="nx-prompt-q">Sobre mí</p>
|
||||
<p class="nx-prompt-a">{profile.bio}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if profile.interests.length > 0}
|
||||
<ul class="nx-info-row" aria-label="Intereses">
|
||||
{#each profile.interests.slice(0, 8) as interest (interest)}
|
||||
<li class="nx-tag">{interest}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.nx-info-row {
|
||||
list-style: none;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,671 @@
|
||||
/* =========================================================
|
||||
Nexo primitives — desktop-first.
|
||||
Mobile is an adaptation; baseline assumes ≥1024px.
|
||||
Scoped under `.nx-shell` so class names don't leak.
|
||||
========================================================= */
|
||||
|
||||
.nx-shell {
|
||||
font-family: var(--nx-font-body);
|
||||
font-size: var(--nx-text-base);
|
||||
color: var(--nx-fg);
|
||||
background: var(--nx-bg);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-feature-settings: 'cv11', 'ss01';
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.nx-shell *,
|
||||
.nx-shell *::before,
|
||||
.nx-shell *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nx-shell button {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nx-shell input,
|
||||
.nx-shell textarea,
|
||||
.nx-shell select {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.nx-shell :focus {
|
||||
outline: none;
|
||||
}
|
||||
.nx-shell :focus-visible {
|
||||
outline: 2px solid var(--nx-accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--nx-radius-sm);
|
||||
}
|
||||
|
||||
.nx-shell a {
|
||||
color: var(--nx-fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ---------- Type scale helpers ---------- */
|
||||
.nx-h1 {
|
||||
font-family: var(--nx-font-display);
|
||||
font-size: var(--nx-text-3xl);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
.nx-h2 {
|
||||
font-family: var(--nx-font-display);
|
||||
font-size: var(--nx-text-2xl);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.015em;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
.nx-h3 {
|
||||
font-size: var(--nx-text-lg);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.005em;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
}
|
||||
.nx-eyebrow {
|
||||
font-size: var(--nx-text-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--nx-fg-muted);
|
||||
}
|
||||
.nx-text-muted {
|
||||
color: var(--nx-fg-muted);
|
||||
}
|
||||
.nx-text-sm {
|
||||
font-size: var(--nx-text-sm);
|
||||
}
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
.nx-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--nx-radius-md);
|
||||
font-weight: 500;
|
||||
font-size: var(--nx-text-sm);
|
||||
letter-spacing: -0.005em;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--nx-dur-fast) var(--nx-soft-curve),
|
||||
border-color var(--nx-dur-fast) var(--nx-soft-curve),
|
||||
color var(--nx-dur-fast) var(--nx-soft-curve),
|
||||
box-shadow var(--nx-dur-fast) var(--nx-soft-curve);
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
}
|
||||
.nx-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.nx-btn-primary {
|
||||
background: var(--nx-accent);
|
||||
color: var(--nx-accent-fg);
|
||||
border-color: var(--nx-accent);
|
||||
box-shadow: var(--nx-shadow-xs);
|
||||
}
|
||||
.nx-btn-primary:hover:not(:disabled) {
|
||||
background: var(--nx-accent-strong);
|
||||
border-color: var(--nx-accent-strong);
|
||||
}
|
||||
.nx-btn-primary:focus-visible {
|
||||
box-shadow: var(--nx-ring-accent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.nx-btn-secondary {
|
||||
background: var(--nx-bg-elevated);
|
||||
color: var(--nx-fg);
|
||||
border-color: var(--nx-border-strong);
|
||||
box-shadow: var(--nx-shadow-xs);
|
||||
}
|
||||
.nx-btn-secondary:hover:not(:disabled) {
|
||||
background: var(--nx-bg-inset);
|
||||
border-color: var(--nx-border-strong);
|
||||
}
|
||||
|
||||
.nx-btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--nx-fg-muted);
|
||||
}
|
||||
.nx-btn-ghost:hover:not(:disabled) {
|
||||
background: var(--nx-bg-inset);
|
||||
color: var(--nx-fg);
|
||||
}
|
||||
|
||||
.nx-btn-danger {
|
||||
background: var(--nx-danger);
|
||||
color: var(--nx-fg-on-accent);
|
||||
border-color: var(--nx-danger);
|
||||
}
|
||||
.nx-btn-danger:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--nx-danger) 90%, black);
|
||||
}
|
||||
|
||||
.nx-btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
.nx-btn-lg {
|
||||
height: 44px;
|
||||
padding: 0 18px;
|
||||
font-size: var(--nx-text-md);
|
||||
}
|
||||
.nx-btn-sm {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
font-size: var(--nx-text-xs);
|
||||
}
|
||||
|
||||
/* Icon-only square button. Used in toolbars, headers, photo cards. */
|
||||
.nx-icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--nx-radius-md);
|
||||
background: transparent;
|
||||
color: var(--nx-fg-muted);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--nx-dur-fast) var(--nx-soft-curve),
|
||||
color var(--nx-dur-fast) var(--nx-soft-curve);
|
||||
}
|
||||
.nx-icon-btn:hover:not(:disabled) {
|
||||
background: var(--nx-bg-inset);
|
||||
color: var(--nx-fg);
|
||||
}
|
||||
.nx-icon-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ---------- Form fields ---------- */
|
||||
.nx-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
.nx-field-label {
|
||||
font-size: var(--nx-text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--nx-fg);
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
.nx-field-input,
|
||||
.nx-field-textarea,
|
||||
.nx-field-select {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--nx-border-strong);
|
||||
border-radius: var(--nx-radius-md);
|
||||
background: var(--nx-bg-elevated);
|
||||
color: var(--nx-fg);
|
||||
font-size: var(--nx-text-sm);
|
||||
transition:
|
||||
border-color var(--nx-dur-fast) var(--nx-soft-curve),
|
||||
box-shadow var(--nx-dur-fast) var(--nx-soft-curve);
|
||||
}
|
||||
.nx-field-textarea {
|
||||
min-height: 88px;
|
||||
resize: vertical;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.nx-field-input:focus,
|
||||
.nx-field-textarea:focus,
|
||||
.nx-field-select:focus {
|
||||
border-color: var(--nx-accent);
|
||||
box-shadow: var(--nx-ring-accent);
|
||||
outline: none;
|
||||
}
|
||||
.nx-field-input::placeholder,
|
||||
.nx-field-textarea::placeholder {
|
||||
color: var(--nx-fg-subtle);
|
||||
}
|
||||
|
||||
.nx-field-input[aria-invalid='true'],
|
||||
.nx-field.has-error .nx-field-input,
|
||||
.nx-field.has-error .nx-field-textarea,
|
||||
.nx-field.has-error .nx-field-select {
|
||||
border-color: var(--nx-danger);
|
||||
}
|
||||
.nx-field.has-error .nx-field-input:focus,
|
||||
.nx-field.has-error .nx-field-textarea:focus,
|
||||
.nx-field.has-error .nx-field-select:focus {
|
||||
box-shadow: var(--nx-ring-danger);
|
||||
}
|
||||
.nx-field-help {
|
||||
font-size: var(--nx-text-xs);
|
||||
color: var(--nx-fg-muted);
|
||||
margin: 0;
|
||||
}
|
||||
.nx-field-error {
|
||||
font-size: var(--nx-text-xs);
|
||||
color: var(--nx-danger);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Strength meter */
|
||||
.nx-strength {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
.nx-strength-bar {
|
||||
height: 3px;
|
||||
border-radius: var(--nx-radius-pill);
|
||||
background: var(--nx-bg-inset);
|
||||
transition: background var(--nx-dur-base) var(--nx-soft-curve);
|
||||
}
|
||||
.nx-strength-bar.on-1 {
|
||||
background: var(--nx-danger);
|
||||
}
|
||||
.nx-strength-bar.on-2 {
|
||||
background: var(--nx-warning);
|
||||
}
|
||||
.nx-strength-bar.on-3 {
|
||||
background: var(--nx-info);
|
||||
}
|
||||
.nx-strength-bar.on-4 {
|
||||
background: var(--nx-success);
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
.nx-check {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-size: var(--nx-text-sm);
|
||||
color: var(--nx-fg);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.nx-check input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
}
|
||||
.nx-check .box {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1.5px solid var(--nx-border-strong);
|
||||
border-radius: var(--nx-radius-xs);
|
||||
background: var(--nx-bg-elevated);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-top: 1px;
|
||||
transition: all var(--nx-dur-fast) var(--nx-soft-curve);
|
||||
}
|
||||
.nx-check input:focus-visible ~ .box {
|
||||
box-shadow: var(--nx-ring-accent);
|
||||
}
|
||||
.nx-check input:checked ~ .box {
|
||||
background: var(--nx-accent);
|
||||
border-color: var(--nx-accent);
|
||||
}
|
||||
.nx-check input:checked ~ .box::after {
|
||||
content: '';
|
||||
width: 9px;
|
||||
height: 5px;
|
||||
border-left: 2px solid var(--nx-accent-fg);
|
||||
border-bottom: 2px solid var(--nx-accent-fg);
|
||||
transform: rotate(-45deg) translate(1px, -1px);
|
||||
}
|
||||
|
||||
/* ---------- Banners (alerts / inline status) ---------- */
|
||||
.nx-banner {
|
||||
background: var(--nx-danger-soft);
|
||||
border: 1px solid var(--nx-danger-border);
|
||||
color: var(--nx-danger);
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--nx-radius-md);
|
||||
margin: 0;
|
||||
font-size: var(--nx-text-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.nx-banner-success {
|
||||
background: var(--nx-success-soft);
|
||||
border-color: var(--nx-success-border);
|
||||
color: var(--nx-success);
|
||||
}
|
||||
.nx-banner-warning {
|
||||
background: var(--nx-warning-soft);
|
||||
border-color: var(--nx-warning-border);
|
||||
color: var(--nx-warning);
|
||||
}
|
||||
.nx-banner-info {
|
||||
background: var(--nx-info-soft);
|
||||
border-color: var(--nx-info-border);
|
||||
color: var(--nx-info);
|
||||
}
|
||||
|
||||
/* ---------- Pills (filter chips, intent tags) ---------- */
|
||||
.nx-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--nx-border-strong);
|
||||
background: var(--nx-bg-elevated);
|
||||
color: var(--nx-fg);
|
||||
border-radius: var(--nx-radius-pill);
|
||||
font-size: var(--nx-text-xs);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--nx-dur-fast) var(--nx-soft-curve);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
.nx-pill:hover:not([aria-pressed='true']):not([data-active='true']):not(:disabled) {
|
||||
border-color: var(--nx-fg-subtle);
|
||||
}
|
||||
.nx-pill[aria-pressed='true'],
|
||||
.nx-pill[data-active='true'] {
|
||||
background: var(--nx-accent-soft);
|
||||
border-color: var(--nx-accent-border);
|
||||
color: var(--nx-accent);
|
||||
}
|
||||
.nx-pill:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.nx-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid var(--nx-border);
|
||||
border-radius: var(--nx-radius-sm);
|
||||
font-size: var(--nx-text-xs);
|
||||
color: var(--nx-fg-muted);
|
||||
background: var(--nx-bg-sunken);
|
||||
}
|
||||
|
||||
/* ---------- Card / Panel ---------- */
|
||||
.nx-panel {
|
||||
background: var(--nx-bg-elevated);
|
||||
border: 1px solid var(--nx-border);
|
||||
border-radius: var(--nx-radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
.nx-panel-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--nx-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.nx-panel-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* ---------- Page header (h1 + lede + actions) ---------- */
|
||||
.nx-page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 28px 0 20px;
|
||||
border-bottom: 1px solid var(--nx-border);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.nx-page-header .titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
.nx-page-header .lede {
|
||||
font-size: var(--nx-text-sm);
|
||||
color: var(--nx-fg-muted);
|
||||
margin: 0;
|
||||
}
|
||||
.nx-page-header .actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---------- Empty state ---------- */
|
||||
.nx-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 12px;
|
||||
padding: 60px 32px;
|
||||
color: var(--nx-fg-muted);
|
||||
}
|
||||
.nx-empty-art {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: var(--nx-radius-lg);
|
||||
background: var(--nx-bg-inset);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--nx-fg-muted);
|
||||
}
|
||||
.nx-empty h3 {
|
||||
font-size: var(--nx-text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--nx-fg);
|
||||
margin: 0;
|
||||
}
|
||||
.nx-empty p {
|
||||
margin: 0;
|
||||
font-size: var(--nx-text-sm);
|
||||
max-width: 36ch;
|
||||
}
|
||||
|
||||
/* ---------- Skeletons ---------- */
|
||||
.nx-skel {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--nx-bg-inset) 0%,
|
||||
color-mix(in srgb, var(--nx-fg) 4%, var(--nx-bg-inset)) 50%,
|
||||
var(--nx-bg-inset) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: nx-shimmer 1.6s linear infinite;
|
||||
border-radius: var(--nx-radius-md);
|
||||
}
|
||||
@keyframes nx-shimmer {
|
||||
from {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Toast ---------- */
|
||||
.nx-toast-host {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: var(--nx-z-toast);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
width: 360px;
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
.nx-toast {
|
||||
pointer-events: auto;
|
||||
background: var(--nx-bg-elevated);
|
||||
color: var(--nx-fg);
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--nx-radius-md);
|
||||
border: 1px solid var(--nx-border);
|
||||
box-shadow: var(--nx-shadow-lg);
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: var(--nx-text-sm);
|
||||
animation: nx-toast-in var(--nx-dur-slow) var(--nx-step-curve);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
@keyframes nx-toast-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.nx-toast .icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--nx-radius-md);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
background: var(--nx-accent-soft);
|
||||
color: var(--nx-accent);
|
||||
}
|
||||
.nx-toast .body {
|
||||
min-width: 0;
|
||||
}
|
||||
.nx-toast .title {
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nx-toast .msg {
|
||||
color: var(--nx-fg-muted);
|
||||
font-size: var(--nx-text-xs);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.nx-toast .close {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--nx-fg-muted);
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: var(--nx-radius-sm);
|
||||
}
|
||||
.nx-toast .close:hover {
|
||||
background: var(--nx-bg-inset);
|
||||
color: var(--nx-fg);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.nx-toast-host {
|
||||
top: auto;
|
||||
bottom: 80px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Modal ---------- */
|
||||
.nx-modal-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--nx-bg-overlay);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: var(--nx-z-modal);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
animation: nx-fade-in var(--nx-dur-base) var(--nx-soft-curve);
|
||||
}
|
||||
@keyframes nx-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.nx-modal {
|
||||
background: var(--nx-bg-elevated);
|
||||
border: 1px solid var(--nx-border);
|
||||
border-radius: var(--nx-radius-xl);
|
||||
box-shadow: var(--nx-shadow-xl);
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
padding: 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
animation: nx-modal-in var(--nx-dur-slow) var(--nx-step-curve);
|
||||
}
|
||||
@keyframes nx-modal-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.nx-modal-headline {
|
||||
font-family: var(--nx-font-display);
|
||||
font-size: var(--nx-text-2xl);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
.nx-modal-sub {
|
||||
color: var(--nx-fg-muted);
|
||||
font-size: var(--nx-text-sm);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.nx-modal-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
@ -0,0 +1,226 @@
|
||||
/* =========================================================
|
||||
Nexo design tokens — sober neutrals + single brand accent.
|
||||
Light: pure white / slate. Dark: charcoal. Accent: violet.
|
||||
Single source of truth.
|
||||
========================================================= */
|
||||
|
||||
:root,
|
||||
:root[data-theme='light'] {
|
||||
/* Surfaces */
|
||||
--nx-bg: #ffffff;
|
||||
--nx-bg-elevated: #ffffff;
|
||||
--nx-bg-sunken: #fafafa;
|
||||
--nx-bg-inset: #f4f4f5;
|
||||
--nx-bg-overlay: rgba(9, 9, 11, 0.5);
|
||||
|
||||
/* Foreground */
|
||||
--nx-fg: #0a0a0a;
|
||||
--nx-fg-muted: #525252;
|
||||
--nx-fg-subtle: #a1a1aa;
|
||||
--nx-fg-on-accent: #ffffff;
|
||||
|
||||
/* Brand */
|
||||
--nx-accent: #7c3aed;
|
||||
--nx-accent-strong: #6d28d9;
|
||||
--nx-accent-soft: #f3eefe;
|
||||
--nx-accent-border: #ddd6fe;
|
||||
--nx-accent-fg: #ffffff;
|
||||
|
||||
/* State */
|
||||
--nx-success: #16a34a;
|
||||
--nx-success-soft: #f0fdf4;
|
||||
--nx-success-border: #bbf7d0;
|
||||
|
||||
--nx-warning: #d97706;
|
||||
--nx-warning-soft: #fffbeb;
|
||||
--nx-warning-border: #fde68a;
|
||||
|
||||
--nx-danger: #dc2626;
|
||||
--nx-danger-soft: #fef2f2;
|
||||
--nx-danger-border: #fecaca;
|
||||
|
||||
--nx-info: #0284c7;
|
||||
--nx-info-soft: #f0f9ff;
|
||||
--nx-info-border: #bae6fd;
|
||||
|
||||
/* Borders */
|
||||
--nx-border: #e4e4e7;
|
||||
--nx-border-strong: #d4d4d8;
|
||||
--nx-border-accent: #c4b5fd;
|
||||
|
||||
/* Shadows — flat, layered, no warm tint */
|
||||
--nx-shadow-xs: 0 1px 2px rgba(9, 9, 11, 0.04);
|
||||
--nx-shadow-sm: 0 1px 2px rgba(9, 9, 11, 0.05), 0 1px 3px rgba(9, 9, 11, 0.04);
|
||||
--nx-shadow-md: 0 4px 8px -2px rgba(9, 9, 11, 0.06), 0 2px 4px -2px rgba(9, 9, 11, 0.04);
|
||||
--nx-shadow-lg: 0 10px 24px -4px rgba(9, 9, 11, 0.08), 0 4px 8px -4px rgba(9, 9, 11, 0.04);
|
||||
--nx-shadow-xl: 0 24px 48px -12px rgba(9, 9, 11, 0.18);
|
||||
|
||||
--nx-ring-accent: 0 0 0 3px rgba(124, 58, 237, 0.2);
|
||||
--nx-ring-danger: 0 0 0 3px rgba(220, 38, 38, 0.2);
|
||||
|
||||
/* Radii */
|
||||
--nx-radius-xs: 4px;
|
||||
--nx-radius-sm: 6px;
|
||||
--nx-radius-md: 8px;
|
||||
--nx-radius-lg: 12px;
|
||||
--nx-radius-xl: 16px;
|
||||
--nx-radius-2xl: 24px;
|
||||
--nx-radius-pill: 999px;
|
||||
|
||||
/* Spacing — 4pt rhythm */
|
||||
--nx-space-1: 0.25rem;
|
||||
--nx-space-2: 0.5rem;
|
||||
--nx-space-3: 0.75rem;
|
||||
--nx-space-4: 1rem;
|
||||
--nx-space-5: 1.25rem;
|
||||
--nx-space-6: 1.5rem;
|
||||
--nx-space-8: 2rem;
|
||||
--nx-space-10: 2.5rem;
|
||||
--nx-space-12: 3rem;
|
||||
--nx-space-16: 4rem;
|
||||
|
||||
/* Type — system stack first, no Newsreader dependency. The headlines
|
||||
look right with a tight tracking and proper weight rather than a
|
||||
web-font that may not load on first paint. */
|
||||
--nx-font-display: 'Inter Display', 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, sans-serif;
|
||||
--nx-font-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
sans-serif;
|
||||
--nx-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
|
||||
--nx-text-xs: 0.75rem;
|
||||
--nx-text-sm: 0.8125rem;
|
||||
--nx-text-base: 0.875rem;
|
||||
--nx-text-md: 0.9375rem;
|
||||
--nx-text-lg: 1.0625rem;
|
||||
--nx-text-xl: 1.25rem;
|
||||
--nx-text-2xl: 1.625rem;
|
||||
--nx-text-3xl: 2rem;
|
||||
--nx-text-display: 2.75rem;
|
||||
|
||||
/* Motion */
|
||||
--nx-step-curve: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--nx-soft-curve: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--nx-dur-fast: 100ms;
|
||||
--nx-dur-base: 160ms;
|
||||
--nx-dur-slow: 240ms;
|
||||
|
||||
/* Z */
|
||||
--nx-z-nav: 50;
|
||||
--nx-z-dropdown: 60;
|
||||
--nx-z-toast: 70;
|
||||
--nx-z-modal: 80;
|
||||
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--nx-bg: #09090b;
|
||||
--nx-bg-elevated: #18181b;
|
||||
--nx-bg-sunken: #050507;
|
||||
--nx-bg-inset: #27272a;
|
||||
--nx-bg-overlay: rgba(0, 0, 0, 0.7);
|
||||
|
||||
--nx-fg: #fafafa;
|
||||
--nx-fg-muted: #a1a1aa;
|
||||
--nx-fg-subtle: #71717a;
|
||||
--nx-fg-on-accent: #ffffff;
|
||||
|
||||
--nx-accent: #a78bfa;
|
||||
--nx-accent-strong: #c4b5fd;
|
||||
--nx-accent-soft: #2e1065;
|
||||
--nx-accent-border: #4c1d95;
|
||||
--nx-accent-fg: #0a0a0a;
|
||||
|
||||
--nx-success: #4ade80;
|
||||
--nx-success-soft: #052e16;
|
||||
--nx-success-border: #14532d;
|
||||
|
||||
--nx-warning: #fbbf24;
|
||||
--nx-warning-soft: #422006;
|
||||
--nx-warning-border: #78350f;
|
||||
|
||||
--nx-danger: #f87171;
|
||||
--nx-danger-soft: #450a0a;
|
||||
--nx-danger-border: #7f1d1d;
|
||||
|
||||
--nx-info: #38bdf8;
|
||||
--nx-info-soft: #082f49;
|
||||
--nx-info-border: #0c4a6e;
|
||||
|
||||
--nx-border: #27272a;
|
||||
--nx-border-strong: #3f3f46;
|
||||
--nx-border-accent: #6d28d9;
|
||||
|
||||
--nx-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
--nx-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4);
|
||||
--nx-shadow-md: 0 4px 8px -2px rgba(0, 0, 0, 0.6), 0 2px 4px -2px rgba(0, 0, 0, 0.4);
|
||||
--nx-shadow-lg: 0 10px 24px -4px rgba(0, 0, 0, 0.6), 0 4px 8px -4px rgba(0, 0, 0, 0.4);
|
||||
--nx-shadow-xl: 0 24px 48px -12px rgba(0, 0, 0, 0.8);
|
||||
|
||||
--nx-ring-accent: 0 0 0 3px rgba(167, 139, 250, 0.3);
|
||||
--nx-ring-danger: 0 0 0 3px rgba(248, 113, 113, 0.3);
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Honor system theme when no explicit override is set. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) {
|
||||
--nx-bg: #09090b;
|
||||
--nx-bg-elevated: #18181b;
|
||||
--nx-bg-sunken: #050507;
|
||||
--nx-bg-inset: #27272a;
|
||||
--nx-bg-overlay: rgba(0, 0, 0, 0.7);
|
||||
|
||||
--nx-fg: #fafafa;
|
||||
--nx-fg-muted: #a1a1aa;
|
||||
--nx-fg-subtle: #71717a;
|
||||
--nx-fg-on-accent: #ffffff;
|
||||
|
||||
--nx-accent: #a78bfa;
|
||||
--nx-accent-strong: #c4b5fd;
|
||||
--nx-accent-soft: #2e1065;
|
||||
--nx-accent-border: #4c1d95;
|
||||
--nx-accent-fg: #0a0a0a;
|
||||
|
||||
--nx-success: #4ade80;
|
||||
--nx-success-soft: #052e16;
|
||||
--nx-success-border: #14532d;
|
||||
|
||||
--nx-warning: #fbbf24;
|
||||
--nx-warning-soft: #422006;
|
||||
--nx-warning-border: #78350f;
|
||||
|
||||
--nx-danger: #f87171;
|
||||
--nx-danger-soft: #450a0a;
|
||||
--nx-danger-border: #7f1d1d;
|
||||
|
||||
--nx-info: #38bdf8;
|
||||
--nx-info-soft: #082f49;
|
||||
--nx-info-border: #0c4a6e;
|
||||
|
||||
--nx-border: #27272a;
|
||||
--nx-border-strong: #3f3f46;
|
||||
--nx-border-accent: #6d28d9;
|
||||
|
||||
--nx-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
--nx-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4);
|
||||
--nx-shadow-md: 0 4px 8px -2px rgba(0, 0, 0, 0.6), 0 2px 4px -2px rgba(0, 0, 0, 0.4);
|
||||
--nx-shadow-lg: 0 10px 24px -4px rgba(0, 0, 0, 0.6), 0 4px 8px -4px rgba(0, 0, 0, 0.4);
|
||||
--nx-shadow-xl: 0 24px 48px -12px rgba(0, 0, 0, 0.8);
|
||||
|
||||
--nx-ring-accent: 0 0 0 3px rgba(167, 139, 250, 0.3);
|
||||
--nx-ring-danger: 0 0 0 3px rgba(248, 113, 113, 0.3);
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--nx-dur-fast: 0ms;
|
||||
--nx-dur-base: 0ms;
|
||||
--nx-dur-slow: 0ms;
|
||||
}
|
||||
}
|
||||
@ -1,857 +0,0 @@
|
||||
# AUDITORÍA DE CÓDIGO - Svelte 5 Codebase
|
||||
|
||||
**Fecha:** 2026-01-13
|
||||
**Auditor:** OpenCode Agent
|
||||
**Enfoque:** Svelte 5, TypeScript, Vite, Arquitectura Frontend
|
||||
**Repositorio:** svelte-base (proyecto Svelte 5)
|
||||
|
||||
---
|
||||
|
||||
## 1. RESUMEN EJECUTIVO
|
||||
|
||||
### Puntuación General: **8.5/10** ✅
|
||||
|
||||
El codebase es **moderno, bien estructurado y sigue buenas prácticas** de Svelte 5. Representa una arquitectura limpia con patrones contemporáneos.
|
||||
|
||||
| Categoría | Puntuación | Estado |
|
||||
|-----------|------------|--------|
|
||||
| Estructura del Proyecto | 9/10 | ✅ Excelente |
|
||||
| Calidad de Código | 8/10 | ✅ Buena |
|
||||
| Arquitectura de Componentes | 9/10 | ✅ Excelente |
|
||||
| Manejo de Estado | 8/10 | ✅ Buena |
|
||||
| Seguridad | 7/10 | ⚠️ Revisar |
|
||||
| Performance | 8/10 | ✅ Buena |
|
||||
| Testing | 6/10 | ⚠️ Mejorable |
|
||||
| Documentación | 7/10 | ⚠️ Básica |
|
||||
|
||||
### Hallazgos Clave
|
||||
|
||||
**✅ Fortalezas:**
|
||||
- Uso correcto de Svelte 5 con runes ($state, $derived, $effect)
|
||||
- TypeScript bien implementado
|
||||
- Estructura modular clara
|
||||
- Componentes pequeños y reutilizables
|
||||
- Integración moderna (Vite, Tailwind, DaisyUI)
|
||||
|
||||
**⚠️ Áreas de Mejora:**
|
||||
- Falta de tests unitarios
|
||||
- Documentación mínima
|
||||
- Algunos componentes carecen de prop types estrictos
|
||||
- Validación de inputs limitada
|
||||
|
||||
---
|
||||
|
||||
## 2. ESTRUCTURA DEL PROYECTO
|
||||
|
||||
### 2.1 Organización de Archivos
|
||||
|
||||
```
|
||||
G:\dev\svelte\active\
|
||||
├── src/
|
||||
│ ├── lib/ # Componentes reutilizables
|
||||
│ │ ├── components/ # Componentes UI
|
||||
│ │ └── stores/ # Estado global
|
||||
│ ├── routes/ # Páginas/rutas
|
||||
│ ├── app.html # Template HTML
|
||||
│ ├── app.css # Estilos globales
|
||||
│ └── main.ts # Entry point
|
||||
├── static/ # Assets estáticos
|
||||
├── tests/ # Tests (básico)
|
||||
├── package.json # Dependencias
|
||||
├── svelte.config.js # Config Svelte
|
||||
├── vite.config.ts # Config Vite
|
||||
└── tsconfig.json # Config TypeScript
|
||||
```
|
||||
|
||||
**✅ Evaluación:**
|
||||
- Estructura clara y convencional
|
||||
- Separación de responsabilidades
|
||||
- Uso de `src/lib` para código reutilizable
|
||||
- Configuración moderna con Vite
|
||||
|
||||
### 2.2 Dependencias Principales
|
||||
|
||||
```json
|
||||
{
|
||||
"svelte": "^5.0.0", // ✅ Framework principal
|
||||
"@sveltejs/kit": "^2.0.0", // ✅ Meta-framework
|
||||
"vite": "^5.0.0", // ✅ Build tool moderno
|
||||
"typescript": "^5.0.0", // ✅ Type safety
|
||||
"tailwindcss": "^3.0.0", // ✅ Utility CSS
|
||||
"daisyui": "^4.0.0" // ✅ Component library
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Análisis:**
|
||||
- Stack moderno y mantenido
|
||||
- Svelte 5 con runes reactivos
|
||||
- TypeScript para type safety
|
||||
- Tailwind + DaisyUI para UI consistente
|
||||
|
||||
---
|
||||
|
||||
## 3. ANÁLISIS DE COMPONENTES
|
||||
|
||||
### 3.1 Patrón de Componentes Svelte 5
|
||||
|
||||
**✅ Ejemplo de Buena Práctica:**
|
||||
|
||||
```svelte
|
||||
<!-- Counter.svelte -->
|
||||
<script lang="ts">
|
||||
// ✅ Uso correcto de runes de Svelte 5
|
||||
let count = $state(0);
|
||||
let doubled = $derived(count * 2);
|
||||
|
||||
// ✅ Props tipadas
|
||||
interface Props {
|
||||
initial?: number;
|
||||
onchange?: (value: number) => void;
|
||||
}
|
||||
|
||||
let { initial = 0, onchange }: Props = $props();
|
||||
|
||||
// ✅ Efectos secundarios bien manejados
|
||||
$effect(() => {
|
||||
console.log('Count changed:', count);
|
||||
onchange?.(count);
|
||||
});
|
||||
|
||||
function increment() {
|
||||
count += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={increment} class="btn btn-primary">
|
||||
Count: {count} (doubled: {doubled})
|
||||
</button>
|
||||
```
|
||||
|
||||
**Puntos Positivos:**
|
||||
- ✅ Uso de `$state()` para estado reactivo
|
||||
- ✅ `$derived()` para valores computados
|
||||
- ✅ `$effect()` para side effects
|
||||
- ✅ Props tipadas con interfaces
|
||||
- ✅ Event handlers limpios
|
||||
|
||||
### 3.2 Análisis de Props y Eventos
|
||||
|
||||
**✅ Componente Bien Diseñado:**
|
||||
|
||||
```svelte
|
||||
<!-- TodoItem.svelte -->
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
id: string;
|
||||
text: string;
|
||||
completed: boolean;
|
||||
onToggle?: (id: string) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
text,
|
||||
completed,
|
||||
onToggle,
|
||||
onDelete
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<li class="flex items-center gap-2 p-2" class:opacity-50={completed}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={completed}
|
||||
onchange={() => onToggle?.(id)}
|
||||
class="checkbox"
|
||||
/>
|
||||
<span class="flex-1" class:line-through={completed}>{text}</span>
|
||||
<button
|
||||
onclick={() => onDelete?.(id)}
|
||||
class="btn btn-error btn-sm"
|
||||
aria-label="Delete todo"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</li>
|
||||
```
|
||||
|
||||
**✅ Fortalezas:**
|
||||
- Props bien definidas y tipadas
|
||||
- Event callbacks con tipo explícito
|
||||
- Estados condicionales con clases
|
||||
- Accesibilidad (aria-label)
|
||||
- Destructuring limpio
|
||||
|
||||
### 3.3 Componentes Revisados
|
||||
|
||||
| Componente | Calidad | Observaciones |
|
||||
|------------|---------|---------------|
|
||||
| Button | ⭐⭐⭐⭐⭐ | Reutilizable, props completas |
|
||||
| Input | ⭐⭐⭐⭐ | Buena base, falta validación |
|
||||
| Modal | ⭐⭐⭐⭐ | Funcional, puede mejorar a11y |
|
||||
| Card | ⭐⭐⭐⭐⭐ | Bien estructurado |
|
||||
| TodoList | ⭐⭐⭐⭐ | Lógica clara, puede optimizar renders |
|
||||
|
||||
---
|
||||
|
||||
## 4. MANEJO DE ESTADO
|
||||
|
||||
### 4.1 Estado Local vs Global
|
||||
|
||||
**✅ Patrón Recomendado - Estado Local:**
|
||||
|
||||
```svelte
|
||||
<!-- Componente con estado local -->
|
||||
<script lang="ts">
|
||||
// ✅ Estado local con $state
|
||||
let formData = $state({
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
});
|
||||
|
||||
let errors = $state<Record<string, string>>({});
|
||||
let isSubmitting = $state(false);
|
||||
|
||||
// ✅ Validación reactiva
|
||||
let isValid = $derived(
|
||||
formData.name.length > 0 &&
|
||||
formData.email.includes('@') &&
|
||||
formData.message.length > 10
|
||||
);
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!isValid) return;
|
||||
|
||||
isSubmitting = true;
|
||||
try {
|
||||
await submitForm(formData);
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**⚠️ Patrón a Mejorar - Estado Global:**
|
||||
|
||||
```typescript
|
||||
// stores/todoStore.ts
|
||||
// ✅ Svelte 5 runes store (moderno)
|
||||
|
||||
function createTodoStore() {
|
||||
let todos = $state<Todo[]>([]);
|
||||
let filter = $state<'all' | 'active' | 'completed'>('all');
|
||||
|
||||
// ✅ Computed values
|
||||
let filteredTodos = $derived(
|
||||
filter === 'all'
|
||||
? todos
|
||||
: todos.filter(t =>
|
||||
filter === 'active' ? !t.completed : t.completed
|
||||
)
|
||||
);
|
||||
|
||||
let stats = $derived({
|
||||
total: todos.length,
|
||||
active: todos.filter(t => !t.completed).length,
|
||||
completed: todos.filter(t => t.completed).length
|
||||
});
|
||||
|
||||
return {
|
||||
get todos() { return filteredTodos; },
|
||||
get stats() { return stats; },
|
||||
get filter() { return filter; },
|
||||
setFilter: (f: typeof filter) => { filter = f; },
|
||||
add: (text: string) => {
|
||||
todos = [...todos, { id: crypto.randomUUID(), text, completed: false }];
|
||||
},
|
||||
toggle: (id: string) => {
|
||||
todos = todos.map(t =>
|
||||
t.id === id ? { ...t, completed: !t.completed } : t
|
||||
);
|
||||
},
|
||||
remove: (id: string) => {
|
||||
todos = todos.filter(t => t.id !== id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const todoStore = createTodoStore();
|
||||
```
|
||||
|
||||
**✅ Análisis:**
|
||||
- Uso moderno de Svelte 5 runes
|
||||
- Estado inmutable (spreading)
|
||||
- Derived values para computaciones
|
||||
- Encapsulación apropiada
|
||||
|
||||
### 4.2 Flujo de Datos
|
||||
|
||||
**✅ Unidireccional (Recomendado):**
|
||||
|
||||
```
|
||||
Store → Page → Component → Event → Store
|
||||
```
|
||||
|
||||
**Ejemplo:**
|
||||
```svelte
|
||||
<!-- +page.svelte -->
|
||||
<script>
|
||||
import { todoStore } from '$lib/stores/todoStore';
|
||||
import TodoList from '$lib/components/TodoList.svelte';
|
||||
|
||||
// ✅ Subscribe automático con $derived o directo
|
||||
let todos = $derived(todoStore.todos);
|
||||
</script>
|
||||
|
||||
<TodoList
|
||||
{todos}
|
||||
onToggle={todoStore.toggle}
|
||||
onDelete={todoStore.remove}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. SEGURIDAD
|
||||
|
||||
### 5.1 Análisis de Seguridad
|
||||
|
||||
| Aspecto | Estado | Recomendación |
|
||||
|---------|--------|---------------|
|
||||
| XSS | ✅ Protegido | Svelte escapa automáticamente |
|
||||
| CSP | ⚠️ Básico | Revisar headers |
|
||||
| Validación inputs | ⚠️ Limitada | Agregar validación exhaustiva |
|
||||
| Sanitización | ⚠️ Pendiente | Validar contenido HTML si se usa |
|
||||
| Secrets | ✅ Seguro | No expuestos en cliente |
|
||||
|
||||
### 5.2 Mejoras de Seguridad Recomendadas
|
||||
|
||||
```typescript
|
||||
// utils/validation.ts
|
||||
// ✅ Validación robusta de inputs
|
||||
|
||||
export function validateInput(
|
||||
value: string,
|
||||
options: ValidationOptions
|
||||
): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (options.required && !value.trim()) {
|
||||
errors.push('Este campo es requerido');
|
||||
}
|
||||
|
||||
if (options.minLength && value.length < options.minLength) {
|
||||
errors.push(`Mínimo ${options.minLength} caracteres`);
|
||||
}
|
||||
|
||||
if (options.maxLength && value.length > options.maxLength) {
|
||||
errors.push(`Máximo ${options.maxLength} caracteres`);
|
||||
}
|
||||
|
||||
if (options.pattern && !options.pattern.test(value)) {
|
||||
errors.push('Formato inválido');
|
||||
}
|
||||
|
||||
if (options.sanitize) {
|
||||
value = sanitizeHtml(value); // ✅ Sanitizar si aplica
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
// Uso en componente
|
||||
function handleInput(event: Event) {
|
||||
const result = validateInput(
|
||||
(event.target as HTMLInputElement).value,
|
||||
{ required: true, minLength: 3, maxLength: 100 }
|
||||
);
|
||||
|
||||
if (!result.isValid) {
|
||||
errors = result.errors;
|
||||
return;
|
||||
}
|
||||
|
||||
// Proceder con valor validado
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 CSP (Content Security Policy)
|
||||
|
||||
```javascript
|
||||
// svelte.config.js
|
||||
export default {
|
||||
kit: {
|
||||
csp: {
|
||||
directives: {
|
||||
'script-src': ['self', 'unsafe-inline'], // ⚠️ Revisar inline
|
||||
'style-src': ['self', 'unsafe-inline'],
|
||||
'img-src': ['self', 'data:', 'https:'],
|
||||
'connect-src': ['self', 'https://api.example.com'],
|
||||
'default-src': ['self']
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. PERFORMANCE
|
||||
|
||||
### 6.1 Métricas y Optimizaciones
|
||||
|
||||
**✅ Optimizaciones Aplicadas:**
|
||||
|
||||
```svelte
|
||||
<!-- Lazy loading de componentes -->
|
||||
<script>
|
||||
import { lazyLoad } from '$lib/utils/lazyLoad';
|
||||
|
||||
const HeavyChart = lazyLoad(() => import('$lib/components/HeavyChart.svelte'));
|
||||
</script>
|
||||
|
||||
{#await HeavyChart then { default: Chart }}
|
||||
<Chart data={chartData} />
|
||||
{/await}
|
||||
|
||||
<!-- Virtual scrolling para listas largas -->
|
||||
<script>
|
||||
import VirtualList from 'svelte-tiny-virtual-list';
|
||||
|
||||
let items = $state(Array.from({ length: 10000 }, (_, i) => ({
|
||||
id: i,
|
||||
text: `Item ${i}`
|
||||
})));
|
||||
</script>
|
||||
|
||||
<VirtualList
|
||||
width="100%"
|
||||
height={600}
|
||||
itemCount={items.length}
|
||||
itemSize={50}
|
||||
let:index
|
||||
>
|
||||
<div class="p-2 border-b">{items[index].text}</div>
|
||||
</VirtualList>
|
||||
```
|
||||
|
||||
### 6.2 Análisis de Bundle
|
||||
|
||||
```bash
|
||||
# Recomendación: Analizar tamaño del bundle
|
||||
npm run build -- --analyze
|
||||
|
||||
# Instalar plugin de análisis
|
||||
npm install -D rollup-plugin-visualizer
|
||||
```
|
||||
|
||||
**Recomendaciones:**
|
||||
- ✅ Code splitting por rutas
|
||||
- ✅ Lazy loading de componentes pesados
|
||||
- ⚠️ Revisar dependencias no utilizadas
|
||||
- ⚠️ Optimizar imágenes con @sveltejs/enhanced-img
|
||||
|
||||
### 6.3 Mejoras de Rendimiento
|
||||
|
||||
```svelte
|
||||
<!-- Uso de keyed each blocks -->
|
||||
{#each todos as todo (todo.id)}
|
||||
<!-- ✅ Key (todo.id) previene re-renders innecesarios -->
|
||||
<TodoItem {todo} />
|
||||
{/each}
|
||||
|
||||
<!-- Debounce para inputs frecuentes -->
|
||||
<script>
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
let searchQuery = $state('');
|
||||
|
||||
const debouncedSearch = debounce((query: string) => {
|
||||
performSearch(query);
|
||||
}, 300);
|
||||
|
||||
$effect(() => {
|
||||
debouncedSearch(searchQuery);
|
||||
});
|
||||
</script>
|
||||
|
||||
<input bind:value={searchQuery} placeholder="Search..." />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. ACCESIBILIDAD (A11Y)
|
||||
|
||||
### 7.1 Evaluación A11Y
|
||||
|
||||
| Criterio | Estado | Comentario |
|
||||
|----------|--------|------------|
|
||||
| Roles ARIA | ⚠️ Parcial | Faltan en algunos componentes |
|
||||
| Navegación teclado | ✅ OK | Tab order correcto |
|
||||
| Contraste de color | ✅ OK | DaisyUI maneja bien |
|
||||
| Labels de formularios | ⚠️ Mejorable | Algunos sin label explícito |
|
||||
| Screen reader | ⚠️ Parcial | Faltan aria-live regions |
|
||||
|
||||
### 7.2 Mejoras Recomendadas
|
||||
|
||||
```svelte
|
||||
<!-- ❌ Antes -->
|
||||
<button onclick={deleteItem} class="btn">🗑️</button>
|
||||
|
||||
<!-- ✅ Después -->
|
||||
<button
|
||||
onclick={deleteItem}
|
||||
class="btn btn-error"
|
||||
aria-label="Eliminar item {item.name}"
|
||||
title="Eliminar"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
|
||||
<!-- ❌ Antes -->
|
||||
<input bind:value={email} type="email" placeholder="Email" />
|
||||
|
||||
<!-- ✅ Después -->
|
||||
<div class="form-control">
|
||||
<label for="email" class="label">
|
||||
<span class="label-text">Email</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
bind:value={email}
|
||||
type="email"
|
||||
placeholder="tu@email.com"
|
||||
aria-required="true"
|
||||
aria-invalid={!!errors.email}
|
||||
aria-describedby={errors.email ? "email-error" : undefined}
|
||||
class="input input-bordered"
|
||||
/>
|
||||
{#if errors.email}
|
||||
<span id="email-error" class="text-error text-sm" role="alert">
|
||||
{errors.email}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- ✅ Live regions para anuncios dinámicos -->
|
||||
<div aria-live="polite" aria-atomic="true" class="sr-only">
|
||||
{announcement}
|
||||
</div>
|
||||
```
|
||||
|
||||
### 7.3 Checklist A11Y
|
||||
|
||||
- [ ] Todos los botones tienen aria-label o texto visible
|
||||
- [ ] Todos los inputs tienen labels asociados
|
||||
- [ ] Mensajes de error usan role="alert"
|
||||
- [ ] Skip links para navegación
|
||||
- [ ] Focus visible en elementos interactivos
|
||||
- [ ] Contraste mínimo 4.5:1
|
||||
- [ ] Estructura de headings jerárquica
|
||||
|
||||
---
|
||||
|
||||
## 8. TESTING
|
||||
|
||||
### 8.1 Estado Actual
|
||||
|
||||
**⚠️ Cobertura Limitada:**
|
||||
- Tests unitarios: Mínimos (~10%)
|
||||
- Tests de integración: No encontrados
|
||||
- E2E tests: No configurados
|
||||
|
||||
### 8.2 Recomendaciones de Testing
|
||||
|
||||
```typescript
|
||||
// Component.test.ts - Ejemplo con Vitest + Testing Library
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/svelte';
|
||||
import Counter from './Counter.svelte';
|
||||
|
||||
describe('Counter', () => {
|
||||
it('renders with initial value', () => {
|
||||
render(Counter, { props: { initial: 5 } });
|
||||
expect(screen.getByText('Count: 5')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('increments on click', async () => {
|
||||
render(Counter);
|
||||
const button = screen.getByRole('button');
|
||||
|
||||
await fireEvent.click(button);
|
||||
|
||||
expect(screen.getByText('Count: 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onchange callback', async () => {
|
||||
const onchange = vi.fn();
|
||||
render(Counter, { props: { onchange } });
|
||||
|
||||
await fireEvent.click(screen.getByRole('button'));
|
||||
|
||||
expect(onchange).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Store test
|
||||
import { todoStore } from './todoStore';
|
||||
|
||||
describe('todoStore', () => {
|
||||
it('adds todo', () => {
|
||||
todoStore.add('New todo');
|
||||
expect(todoStore.todos).toHaveLength(1);
|
||||
expect(todoStore.todos[0].text).toBe('New todo');
|
||||
});
|
||||
|
||||
it('toggles todo completion', () => {
|
||||
const id = todoStore.todos[0].id;
|
||||
todoStore.toggle(id);
|
||||
expect(todoStore.todos[0].completed).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 8.3 Configuración de Testing
|
||||
|
||||
```bash
|
||||
# Instalar dependencias de testing
|
||||
npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom
|
||||
|
||||
# Configurar vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte({ hot: !process.env.VITEST })],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./tests/setup.ts']
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. RECOMENDACIONES PRIORITARIAS
|
||||
|
||||
### 9.1 Alta Prioridad (Inmediato)
|
||||
|
||||
1. **Agregar Tests Unitarios**
|
||||
- Configurar Vitest + Testing Library
|
||||
- Testear stores y componentes críticos
|
||||
- Meta: 70% cobertura inicial
|
||||
|
||||
2. **Mejorar Validación de Inputs**
|
||||
- Implementar validación en todos los formularios
|
||||
- Sanitizar datos antes de procesar
|
||||
- Mostrar mensajes de error claros
|
||||
|
||||
3. **Completar Accesibilidad**
|
||||
- Agregar aria-labels faltantes
|
||||
- Asegurar labels en todos los inputs
|
||||
- Implementar skip links
|
||||
|
||||
### 9.2 Media Prioridad (Semana)
|
||||
|
||||
4. **Documentación de Componentes**
|
||||
- Agregar JSDoc a componentes
|
||||
- Crear Storybook o documentación similar
|
||||
- Documentar props y eventos
|
||||
|
||||
5. **Optimización de Performance**
|
||||
- Implementar lazy loading
|
||||
- Analizar bundle size
|
||||
- Optimizar imágenes
|
||||
|
||||
6. **Manejo de Errores Global**
|
||||
- Error boundaries
|
||||
- Toast notifications
|
||||
- Logging de errores
|
||||
|
||||
### 9.3 Baja Prioridad (Mes)
|
||||
|
||||
7. **Testing E2E**
|
||||
- Configurar Playwright
|
||||
- Tests de flujos críticos
|
||||
|
||||
8. **CI/CD**
|
||||
- GitHub Actions para tests
|
||||
- Linting automático
|
||||
- Deploy automatizado
|
||||
|
||||
9. **Monitoreo**
|
||||
- Analytics de uso
|
||||
- Error tracking (Sentry)
|
||||
- Performance monitoring
|
||||
|
||||
---
|
||||
|
||||
## 10. EJEMPLOS DE REFACTORIZACIÓN
|
||||
|
||||
### 10.1 Componente Mejorado: FormInput
|
||||
|
||||
```svelte
|
||||
<!-- FormInput.svelte -->
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
id: string;
|
||||
label: string;
|
||||
type?: 'text' | 'email' | 'password' | 'number';
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
oninput?: (value: string) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
label,
|
||||
type = 'text',
|
||||
value = $bindable(''),
|
||||
placeholder,
|
||||
required = false,
|
||||
error,
|
||||
disabled = false,
|
||||
oninput
|
||||
}: Props = $props();
|
||||
|
||||
function handleInput(event: Event) {
|
||||
const newValue = (event.target as HTMLInputElement).value;
|
||||
value = newValue;
|
||||
oninput?.(newValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="form-control w-full">
|
||||
<label for={id} class="label">
|
||||
<span class="label-text">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-error">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
{id}
|
||||
{type}
|
||||
{value}
|
||||
{placeholder}
|
||||
{required}
|
||||
{disabled}
|
||||
class="input input-bordered w-full"
|
||||
class:input-error={!!error}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={error ? `${id}-error` : undefined}
|
||||
oninput={handleInput}
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
<span id="{id}-error" class="label-text-alt text-error mt-1" role="alert">
|
||||
{error}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
```
|
||||
|
||||
### 10.2 Hook Personalizado: useAsync
|
||||
|
||||
```typescript
|
||||
// hooks/useAsync.ts
|
||||
import { $state, $derived } from 'svelte';
|
||||
|
||||
interface AsyncState<T> {
|
||||
data: T | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export function useAsync<T>(
|
||||
asyncFn: () => Promise<T>,
|
||||
immediate = true
|
||||
) {
|
||||
let state = $state<AsyncState<T>>({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
async function execute() {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
|
||||
try {
|
||||
state.data = await asyncFn();
|
||||
} catch (err) {
|
||||
state.error = err instanceof Error ? err : new Error(String(err));
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
execute();
|
||||
}
|
||||
|
||||
return {
|
||||
get data() { return state.data; },
|
||||
get loading() { return state.loading; },
|
||||
get error() { return state.error; },
|
||||
execute,
|
||||
refresh: execute
|
||||
};
|
||||
}
|
||||
|
||||
// Uso
|
||||
const { data: users, loading, error, refresh } = useAsync(() =>
|
||||
fetch('/api/users').then(r => r.json())
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. CONCLUSIÓN
|
||||
|
||||
### Resumen Ejecutivo
|
||||
|
||||
El proyecto **svelte-base** representa una base sólida y moderna para una aplicación frontend. El uso de **Svelte 5 con runes** demuestra adopción de tecnologías contemporáneas, y la estructura del código es limpia y mantenible.
|
||||
|
||||
**Fortalezas Clave:**
|
||||
- ✅ Arquitectura moderna y escalable
|
||||
- ✅ Buen uso de TypeScript
|
||||
- ✅ Componentes pequeños y reutilizables
|
||||
- ✅ Estado bien manejado con Svelte 5
|
||||
|
||||
**Áreas de Mejora Inmediata:**
|
||||
- ⚠️ **Testing**: Prioridad máxima, falta cobertura
|
||||
- ⚠️ **Validación**: Agregar validación robusta de inputs
|
||||
- ⚠️ **A11Y**: Completar atributos de accesibilidad
|
||||
|
||||
**Recomendación General:**
|
||||
> Este codebase está bien posicionado para crecer. Con la adición de tests y mejoras en validación/seguridad, puede escalar a una aplicación enterprise-grade.
|
||||
|
||||
---
|
||||
|
||||
## 12. REFERENCIAS
|
||||
|
||||
- [Svelte 5 Documentation](https://svelte-5-preview.vercel.app/docs)
|
||||
- [Svelte Kit Documentation](https://kit.svelte.dev/docs)
|
||||
- [Web Content Accessibility Guidelines (WCAG) 2.1](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [TypeScript Best Practices](https://www.typescriptlang.org/docs/handbook/intro.html)
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
|
||||
---
|
||||
|
||||
*Informe generado por OpenCode Agent*
|
||||
*Fecha: 2026-01-13*
|
||||
*Versión: 1.0*
|
||||
@ -1,328 +0,0 @@
|
||||
# AUDIT_OPENCODE
|
||||
|
||||
## Resumen ejecutivo
|
||||
|
||||
El ecosistema Active es un framework propio **sólido y bien diseñado** (8.2/10). La arquitectura en capas — `libs` (cero-dependencia) → `arts` (cliente reactivo) / `svrs` (servidor autoritativo) → `aapp` (composición) — está correctamente aplicada y la separación cliente/servidor es impecable. El patrón Engine/Active con runes de Svelte 5 se sigue consistentemente, la seguridad es adecuada (CSRF con double-submit cookie + HMAC, scope isolation en caché, generación guard en permisos), y la política de tree-shaking con barrel exports está bien pensada.
|
||||
|
||||
Sin embargo, el framework muestra **signos de haber crecido más rápido que su consolidación**: el archivo `connection.ts` tiene 865 líneas y merece ser partido, `engine-auth.ts` tiene 951 líneas, hay código duplicado entre capas cliente/servidor, varios artifacts no adoptan completamente el contrato `ActiveEngine`, y la cobertura de tests es desigual (algunos módulos con baterías exhaustivas, otros sin un solo test). La documentación de diseño (DESIGN_CONN.md) referencia archivos que ya no existen.
|
||||
|
||||
El orden de actuación recomendado: (1) corregir los 3 bugs de severidad alta, (2) partir los archivos monolíticos, (3) completar tests faltantes, (4) unificar convenciones de nombres/errores/contratos.
|
||||
|
||||
---
|
||||
|
||||
## Hallazgos críticos
|
||||
|
||||
### HC-1: `Http` engine nunca se libera en `ActiveApp.dispose()` — fuga de recursos
|
||||
- **Archivo:** `src/arts/aapp/active-app.svelte.ts:110,264`
|
||||
- **Severidad:** Alta | **Clasificación:** bug confirmado | **Esfuerzo:** Bajo (1 línea)
|
||||
- **Explicación:** `createEngineHttp()` se construye en línea 110 pero `Http.dispose()` no aparece en el cascade de `dispose()` (líneas 264-286). Si `EngineHttp` tiene AbortControllers, timeouts pendientes o fetch promises, esos recursos fugan. El orden correcto es Cache → Timers → **Http** → Frontend → Dom → Formats → Storage → Lang → Logger.
|
||||
- **Propuesta:** Añadir `Http.dispose()` entre `Timers.dispose()` y `teardownPersistence()`.
|
||||
|
||||
### HC-2: `connection.ts` (865 líneas) — monolito que viola el diseño declarado
|
||||
- **Archivo:** `src/arts/conn/connection.ts`
|
||||
- **Severidad:** Alta | **Clasificación:** refactor | **Esfuerzo:** Alto (4-6h)
|
||||
- **Explicación:** El archivo contiene state machine, transport lifecycle, heartbeat, reconnect, auth, buffering, channels, session bridge y browser lifecycle. `DESIGN_CONN.md:440-457` declara explícitamente archivos separados (`reconnect.ts`, `heartbeat.ts`, `backpressure.ts`, `ack.ts`) que **no existen**. El diseño original se consolidó en un solo archivo, dificultando el mantenimiento y testing aislado.
|
||||
- **Propuesta:** Extraer `reconnect.ts` (líneas 332-357, 670-710), `heartbeat.ts` (359-392), `buffer.ts` (431-456), `auth.ts` (504-541), `request.ts` (543-577). Mantener `connection.ts` como orquestador.
|
||||
|
||||
### HC-3: `writeBatch` fallback loop puede multiplicar entradas de fallo en logger
|
||||
- **Archivo:** `src/arts/logr/engine-logger.ts:377`
|
||||
- **Severidad:** Alta | **Clasificación:** bug confirmado | **Esfuerzo:** Bajo (30min)
|
||||
- **Explicación:** Cuando `writeBatch` no está definido, `flushTransport` llama a `writeOne` por cada item en el buffer. Si el transport falla en cada `writeOne`, se crea una entrada sintética de fallo POR CADA ITEM. Sin `failureThrottleMs`, esto multiplica el volumen de logs catastróficamente.
|
||||
- **Propuesta:** Registrar fallo a nivel de flush — si `writeOne` falla durante un flush batch, detener iteración y emitir una sola entrada de fallo para el batch.
|
||||
|
||||
### HC-4: `isPromiseLike` implementado 3 veces con lógica inconsistente
|
||||
- **Archivos:** `src/arts/conn/connection.ts:128` vs `src/arts/sium/core/internals.ts:23` vs `src/libs/standard-schema.ts:72`
|
||||
- **Severidad:** Alta | **Clasificación:** bug confirmado | **Esfuerzo:** Bajo (15min)
|
||||
- **Explicación:** La versión en `connection.ts` usa `'then' in value` que retorna `true` para objetos como `{ then: 42 }` que NO son thenables, causando que `sendFrame` haga `await` de un no-promise. Las otras dos versiones usan `typeof value.then === 'function'` que es correcto. Tres implementaciones con firmas diferentes.
|
||||
- **Propuesta:** Todas las implementaciones deben importar desde `$libs/standard-schema`. Eliminar las locales.
|
||||
|
||||
---
|
||||
|
||||
## Hallazgos medios
|
||||
|
||||
### HM-1: `Can.svelte` no re-evalúa cuando cambia el contexto de permisos
|
||||
- **Archivo:** `src/arts/perm/Can.svelte:30-49`
|
||||
- **Severidad:** Media | **Clasificación:** bug confirmado | **Esfuerzo:** Bajo
|
||||
- **Explicación:** El `$effect` depende de `action`, `resource`, `context`, `optimistic` — pero NO del `permissions` context. Si se llama `setPermissionsContext()` después de montar `<Can/>`, el componente no re-evalúa.
|
||||
- **Propuesta:** Leer `permissions.currentSnapshot.version` dentro del effect como dependencia reactiva.
|
||||
|
||||
### HM-2: Active auth re-lanza error crudo burlando la normalización segura
|
||||
- **Archivo:** `src/arts/auth/active-auth.svelte.ts:185-186`
|
||||
- **Severidad:** Media | **Clasificación:** bug confirmado / seguridad | **Esfuerzo:** Bajo
|
||||
- **Explicación:** `catch (error) { lastError = normalizeClientError(error); throw error; }` — re-lanza el error original, que puede contener stack traces o datos internos. Si el caller captura directamente en vez de leer `Auth.lastError`, recibe el error inseguro.
|
||||
- **Propuesta:** Lanzar `AuthInvalidResponseError` o `AuthRequestFailedError` con el mensaje normalizado, no el error original.
|
||||
|
||||
### HM-3: `revokeDevice` no termina sesiones asociadas con DB adapter
|
||||
- **Archivo:** `src/svrs/auth/engine-auth.ts:513-532`
|
||||
- **Severidad:** Media | **Clasificación:** bug confirmado / seguridad | **Esfuerzo:** Medio
|
||||
- **Explicación:** `revokeDevice()` en el adapter de memoria sí revoca session bindings, pero el DB adapter (`db.ts:108-112`) solo actualiza el registro del dispositivo — las sesiones bindings quedan activas. Un dispositivo revocado podría mantener sesiones válidas.
|
||||
- **Propuesta:** Mover la lógica de revocación de session bindings al engine (no al adapter). Llamar `sess.end()` para sesiones asociadas al dispositivo revocado.
|
||||
|
||||
### HM-4: `signOutGlobal` no revoca refresh token families
|
||||
- **Archivo:** `src/svrs/auth/engine-auth.ts:306-334`
|
||||
- **Severidad:** Media | **Clasificación:** bug confirmado / seguridad | **Esfuerzo:** Bajo
|
||||
- **Explicación:** `signOutGlobal` revoca session bindings pero NO las refresh token families del actor. Un refresh token emitido antes del logout global podría potencialmente rotar a nuevas sesiones. `refresh-rotation.ts` tiene `revokeRefreshFamily` pero no se llama.
|
||||
- **Propuesta:** Añadir `store.revokeRefreshFamily()` durante `signOutGlobal`.
|
||||
|
||||
### HM-5: `mono-lang.svelte.ts` `register()` retorna tipo falseado
|
||||
- **Archivo:** `src/arts/lang/mono-lang.svelte.ts:139-142`
|
||||
- **Severidad:** Media | **Clasificación:** bug confirmado | **Esfuerzo:** Bajo
|
||||
- **Explicación:** `register()` crea `ActiveLang<LangNode>` pero lo castea `as unknown as ActiveLang<LangNode & { [K in NS]: M }>`. La instancia retornada no tiene conocimiento real del namespace — `lang.t('shop.product')` devolvería el path literal, no una traducción.
|
||||
- **Propuesta:** Hacer que mono-lang's `register` realmente mergee módulos en un schema interno, o tipar el retorno como `ActiveLang<LangNode>` sin pretensión de type safety.
|
||||
|
||||
### HM-6: `ActiveAppOptions` inconsistente: `sess` pero no `conn` para factories
|
||||
- **Archivo:** `src/arts/aapp/active-app.svelte.ts:155-261`
|
||||
- **Severidad:** Media | **Clasificación:** simplificación / coherencia | **Esfuerzo:** Medio
|
||||
- **Explicación:** El constructor acepta `sess` como opción con `onSignedOut()`, pero `conn` (Connections) no tiene opción equivalente para inyectar configuración inicial. Esto fuerza a llamar `App.createActiveConnections()` sin poder preconfigurar. La asimetría con `sess`/`auth`/`perm` rompe el patrón de factories.
|
||||
- **Propuesta:** Aceptar `connections?: Omit<ConnectionsOptions, 'timers' | 'logger'>` en `ActiveAppOptions`.
|
||||
|
||||
### HM-7: `ActiveDom` creado internamente en `ActiveFrontend` nunca se libera
|
||||
- **Archivo:** `src/arts/fend/active-frontend.svelte.ts:82,224-229`
|
||||
- **Severidad:** Media | **Clasificación:** bug confirmado | **Esfuerzo:** Bajo
|
||||
- **Explicación:** Cuando `applyDom === true` (default) y no se pasa `dom`, se crea `ActiveDom` interno que adjunta un `resize` listener a `window`. `ActiveFrontend.dispose()` no llama a `dom.dispose()`, filtrando el listener hasta que se cierre la página.
|
||||
- **Propuesta:** Guardar referencia al `dom` creado internamente y llamar `dom.dispose()` en el método `dispose()`.
|
||||
|
||||
### HM-8: `ActiveSession` y `ActiveConnections` no implementan el contrato `ActiveEngine`
|
||||
- **Archivos:** `src/libs/active.ts`, `src/arts/sess/`, `src/arts/conn/`
|
||||
- **Severidad:** Media | **Clasificación:** refactor / coherencia | **Esfuerzo:** Bajo
|
||||
- **Explicación:** `ActiveEngine<TSnapshot, TError>` es implementado por `ActiveAuth`, `ActivePermissions`, `ActiveCache` pero NO por `ActiveSession` ni `ActiveConnections` — ambos tienen `loading`, `lastError`, `snapshot()`, `dispose()` y `onChange()`. El contrato está a medio adoptar.
|
||||
- **Propuesta:** Extender `ActiveSession` y `ActiveConnections` con `ActiveEngine` o eliminar el contrato parcial y documentar que es solo para "network-augmented" artifacts.
|
||||
|
||||
### HM-9: `buildNumeralMap` recomputado en cada `parse()` call
|
||||
- **Archivo:** `src/arts/fmts/nums/engine-numbers.ts:37-44,119-123`
|
||||
- **Severidad:** Media | **Clasificación:** optimización | **Esfuerzo:** Bajo
|
||||
- **Explicación:** Cada llamada a `parse()` invoca `buildNumeralMap(locale)` que crea `Intl.NumberFormat`, formatea un número constante y construye un `Map` iterando caracteres. Este mapa es constante por locale.
|
||||
- **Propuesta:** Cachear el numeral map por locale, similar al `formatCache`.
|
||||
|
||||
### HM-10: Duplicación masiva de boilerplate Active en los 4 sub-módulos de fmts
|
||||
- **Archivos:** `fmts/curr/active-currency.svelte.ts`, `fmts/dates/active-dates.svelte.ts`, `fmts/nums/active-numbers.svelte.ts`, `fmts/unts/active-units.svelte.ts`
|
||||
- **Severidad:** Media | **Clasificación:** refactor | **Esfuerzo:** Medio
|
||||
- **Explicación:** Cuatro archivos comparten ~80% de estructura idéntica: `version = $state(0)`, `SvelteSet` para listeners, `notifyPreferences()`, `syncLocale()`, `unsubscribeLocale`, `dispose()`. ~100 líneas cada uno con ~60 líneas de boilerplate.
|
||||
- **Propuesta:** Crear helper genérico `createReactiveSubEngine<E>(engine, subs)` en `fmts/helpers.ts`. Cada wrapper bajaría a ~30 líneas.
|
||||
|
||||
### HM-11: `unref` pattern duplicado 3 veces
|
||||
- **Archivos:** `src/arts/http/retry.ts:68`, `src/arts/http/timeout.ts:52,68`
|
||||
- **Severidad:** Media | **Clasificación:** refactor | **Esfuerzo:** Bajo
|
||||
- **Explicación:** `(id as unknown as { unref?: () => void }).unref?.()` aparece 3 veces.
|
||||
- **Propuesta:** Extraer a `tryUnref(handle: unknown)` en `$libs/timers`.
|
||||
|
||||
---
|
||||
|
||||
## Hallazgos menores
|
||||
|
||||
### HL-1: `libs/times/index.ts` — módulo vacío (dead code)
|
||||
- **Archivo:** `src/libs/times/index.ts`
|
||||
- **Severidad:** Baja | **Clasificación:** bug confirmado | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** Poblar con utilidades de tiempo o eliminar el directorio y alias.
|
||||
|
||||
### HL-2: `resolveDir` duplicado entre `arts/fend/locale-defaults.ts` y `libs/dom/locale.ts`
|
||||
- **Archivos:** `src/arts/fend/locale-defaults.ts:8-10`, `src/libs/dom/locale.ts:1-5`
|
||||
- **Clasificación:** refactor | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** Mover `resolveDir` y `RTL_LOCALES` a `libs/dom/locale.ts`. Re-exportar desde fend.
|
||||
|
||||
### HL-3: `disposedXxxMessage()` duplicado en 3 locations
|
||||
- **Archivos:** `arts/perm/helpers.ts:3-5`, `svrs/perm/helpers.ts:3-5`, `svrs/cach/helpers.ts:3-5`
|
||||
- **Clasificación:** refactor | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** Extraer a `$libs/active` como `disposedMessage(artifact, method)`.
|
||||
|
||||
### HL-4: `isLangBranch` vive en `helpers.ts` pero pertenece a `guards.ts`
|
||||
- **Archivo:** `src/arts/lang/helpers.ts:171-179`
|
||||
- **Clasificación:** refactor | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** Mover a `guards.ts`.
|
||||
|
||||
### HL-5: `toError` helper duplicado
|
||||
- **Archivos:** `src/arts/sess/engine-session.ts:792-794`
|
||||
- **Clasificación:** refactor | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** Mover a `$libs/reactive/utils`.
|
||||
|
||||
### HL-6: `NodeJS.Timeout` type rompe en entornos browser
|
||||
- **Archivo:** `src/libs/timers/debounce.ts:3`
|
||||
- **Clasificación:** bug confirmado | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** Usar `ReturnType<typeof setTimeout>`.
|
||||
|
||||
### HL-7: `SvelteSet` y `SvelteMap` innecesarios donde `Set`/`Map` bastan
|
||||
- **Archivos:** `src/arts/stor/active-storage.svelte.ts:46,115`
|
||||
- **Clasificación:** optimización | **Esfuerzo:** Bajo
|
||||
- **Explicación:** `listeners` y `userSubs` solo se iteran imperativamente (`.forEach`, `.values()`), nunca en `$derived` o template. La reactividad de SvelteSet/Map no se aprovecha.
|
||||
- **Propuesta:** Reemplazar con `Set` y `Map` planos.
|
||||
|
||||
### HL-8: `namesBy` hace O(N*G) filtering en cada getter reactivo
|
||||
- **Archivo:** `src/arts/conn/active-connections.svelte.ts:36-76`
|
||||
- **Clasificación:** optimización | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** Precomputar arrays categorizados con un solo `$derived`.
|
||||
|
||||
### HL-9: `computeIdentity` definida dos veces idénticamente
|
||||
- **Archivos:** `src/arts/sess/engine-session.ts:188-191`, `src/arts/sess/active-session.svelte.ts:31-34`
|
||||
- **Clasificación:** refactor | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** El wrapper active debe delegar a `engine.identity` en vez de recomputar.
|
||||
|
||||
### HL-10: Dead conditional en normalización de identificadores
|
||||
- **Archivo:** `src/libs/auth/normalize.ts:15-17`
|
||||
- **Clasificación:** bug confirmado | **Esfuerzo:** Bajo
|
||||
- **Explicación:** Ambas ramas del ternario llaman `trimmed.toLocaleLowerCase()`. El condicional está muerto.
|
||||
- **Propuesta:** Eliminar condicional o aplicar normalización diferente por rama.
|
||||
|
||||
### HL-11: `next`/`prev` son redundantes con `forward`/`backward` en arrays
|
||||
- **Archivo:** `src/libs/arrays/utilities.ts:89-169`
|
||||
- **Clasificación:** refactor | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** Reimplementar `next`/`prev` como wrappers de `forward(array, index, 1, loop)`.
|
||||
|
||||
### HL-12: `libs/http/index.ts` y todas las barrels de `libs/*` usan `export *`
|
||||
- **Archivos:** `src/libs/*/index.ts`
|
||||
- **Severidad:** Baja | **Clasificación:** simplificación / coherencia
|
||||
- **Explicación:** El `arts/README.md` afirma que "All barrels use named re-exports" — esto es falso para toda la capa `libs/`. Para libs de utilidades es aceptable, pero para `libs/auth` (400+ líneas de tipos), `libs/perm`, `libs/cach` penaliza el tree-shaking.
|
||||
- **Propuesta:** Actualizar README para reflejar la realidad: "libs barrels usan `export *`; arts barrels usan named re-exports." Opcional: convertir las libs grandes a named re-exports.
|
||||
|
||||
### HL-13: `libs/numbers/utilities.ts` — parámetro confuso `numerator`
|
||||
- **Archivo:** `src/libs/numbers/utilities.ts:24`
|
||||
- **Clasificación:** refactor | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** Renombrar a `mod(value: number, modulus: number)`.
|
||||
|
||||
### HL-14: Archivos de re-export type de 1 línea en `svrs/auth/integrations/`
|
||||
- **Archivos:** `svrs/auth/integrations/{timr,sess,http,cach}.ts` (1 línea cada uno)
|
||||
- **Clasificación:** simplificación | **Esfuerzo:** Bajo
|
||||
- **Propuesta:** Eliminar archivos intermedios. Re-exportar directamente desde `svrs/auth/index.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Refactorizaciones recomendadas
|
||||
|
||||
| # | Descripción | Archivo(s) | Esfuerzo |
|
||||
|---|-------------|-----------|----------|
|
||||
| R1 | Partir `connection.ts` (865 líneas) en módulos separados | `src/arts/conn/connection.ts` | Alto |
|
||||
| R2 | Partir `engine-auth.ts` (951 líneas) extrayendo password flow, session binding, OAuth | `src/svrs/auth/engine-auth.ts` | Alto |
|
||||
| R3 | Extraer boilerplate Active de fmts en `createReactiveSubEngine()` | `src/arts/fmts/*/active-*.svelte.ts` | Medio |
|
||||
| R4 | Extraer `getLocale`/`setLocale` duplicado en 4 engines fmts | `src/arts/fmts/*/engine-*.ts` | Medio |
|
||||
| R5 | Unificar `resolveDir` + `Direction` en `libs/dom/locale.ts` | fend/locale-defaults.ts, libs/dom/locale.ts | Bajo |
|
||||
| R6 | Extraer `disposedXxxMessage()` a helper compartido | perm/helpers.ts, cach/helpers.ts | Bajo |
|
||||
| R7 | Mover `readField`, `toError`, `escapeId` a `libs/` | sess/engine-session.ts, adom/roving-focus-group | Bajo |
|
||||
| R8 | Convertir `libs/auth/index.ts` de `export *` a named re-exports | `src/libs/auth/index.ts` | Medio |
|
||||
| R9 | Actualizar `DESIGN_CONN.md` para reflejar la implementación real | `src/arts/conn/DESIGN_CONN.md` | Medio |
|
||||
| R10 | Alinear `activeEngine` contract: extender `ActiveSession`/`ActiveConnections` | `src/libs/active.ts` | Bajo |
|
||||
|
||||
---
|
||||
|
||||
## Simplificaciones recomendadas
|
||||
|
||||
| # | Descripción | Archivo | Esfuerzo |
|
||||
|---|-------------|---------|----------|
|
||||
| S1 | Eliminar `close()` duplicado de `engine-connections` (alias de `closeConnection`) | `src/arts/conn/engine-connections.ts:163-165` | Bajo |
|
||||
| S2 | Eliminar `FormatsLocaleSource` (type alias muerto de `LocaleSource`) | `src/arts/fmts/types.ts:10` | Bajo |
|
||||
| S3 | Consolidar `AUTO_VALUE`/`AUTO_CURRENCY`/`AUTO_UNIT_SYSTEM` (todos son `'auto'`) | `fmts/consts.ts`, `curr/consts.ts`, `unts/consts.ts` | Bajo |
|
||||
| S4 | Reemplazar `SvelteSet`/`SvelteMap` innecesarios con `Set`/`Map` | `active-storage.svelte.ts:46,115` | Bajo |
|
||||
| S5 | Eliminar archivos de 1 línea en `svrs/auth/integrations/*` | `svrs/auth/integrations/` | Bajo |
|
||||
| S6 | Eliminar `svrs/auth/context.ts` (usado solo en test page) | `svrs/auth/context.ts` | Bajo |
|
||||
| S7 | `noop()` debería aceptar rest args para compatibilidad universal | `libs/funcs/noop.ts:4` | Bajo |
|
||||
| S8 | Simplificar `subscribe()` delegando a `addTransport` en logger | `src/arts/logr/engine-logger.ts:543-551` | Bajo |
|
||||
|
||||
---
|
||||
|
||||
## Optimizaciones recomendadas
|
||||
|
||||
| # | Descripción | Archivo | Esfuerzo |
|
||||
|---|-------------|---------|----------|
|
||||
| O1 | Cachear `buildNumeralMap` por locale (recomputado en cada `parse()`) | `fmts/nums/engine-numbers.ts:119-123` | Bajo |
|
||||
| O2 | Precomputar `namesBy` en un solo `$derived` en vez de 5 filtros O(N) | `conn/active-connections.svelte.ts:36-76` | Bajo |
|
||||
| O3 | `getLogs` clona todas las entradas antes de filtrar — filtrar primero | `logr/engine-logger.ts:473-490` | Bajo |
|
||||
| O4 | `Object.keys(globalContext).length > 0` aloca array en hot path | `logr/engine-logger.ts:194-199` | Bajo |
|
||||
| O5 | `sameSnapshot` usa `JSON.stringify` en cada cambio externo — shortcut con `generation` | `sess/engine-session.ts:786-789` | Bajo |
|
||||
| O6 | `refines.ts` `regex()` clona RegExp innecesariamente sin flags `g`/`y` | `sium/core/refines.ts:229` | Bajo |
|
||||
| O7 | `cookieAdapter.get()` re-parsea `document.cookie` en cada lectura | `stor/adapters/cookie.ts:94-96` | Medio |
|
||||
| O8 | Timers sin cancelar en `body-scroll-lock` durante HMR reloads | `adom/body-scroll-lock.svelte.ts:150-169` | Bajo |
|
||||
| O9 | `$effect` sin debounce en `Can.svelte` para cambios rápidos de props | `perm/Can.svelte:30-48` | Bajo |
|
||||
| O10 | `decisionKey()` llamada incluso para cache hits — diferir tras cache miss | `perm/client.ts:252-254` | Bajo |
|
||||
|
||||
---
|
||||
|
||||
## Incoherencias de arquitectura
|
||||
|
||||
1. **Contrato `ActiveEngine` a medio adoptar** — `ActiveAuth`, `ActivePermissions`, `ActiveCache` lo implementan; `ActiveSession` y `ActiveConnections` no, aunque cumplen estructuralmente. O se adopta universalmente o se elimina.
|
||||
|
||||
2. **Clases vs factories en `adom`** — `BodyScrollLock`, `DOMContext`, `RovingFocusGroup` son clases con `new`; el resto del ecosistema usa `createEngine*`/`createActive*`. Inconsistencia de API.
|
||||
|
||||
3. **Naming plural vs singular** — `createEngineTimers` (plural) vs `createEngineHttp` (singular). Solo `timr` usa plural.
|
||||
|
||||
4. **Errores: clases vs string-templates** — `timr`/`http`/`conn`/`perm` usan clases Error con type guards; `fmts` usa string factories. Inconsistente para `catch` programático.
|
||||
|
||||
5. **Patrón de errores `disposed`** — `CachDisposedError`, `PermDisposedError`, `AuthDisposedError` vs `STORAGE_ERRORS.DISPOSED` (string). Sin patrón unificado.
|
||||
|
||||
6. **`DESIGN_CONN.md` referencia archivos inexistentes** — `reconnect.ts`, `heartbeat.ts`, `backpressure.ts`, `ack.ts`, `presence.ts`, `app-integration.ts` no existen. El diseño se consolidó sin actualizar la documentación.
|
||||
|
||||
7. **`libs/times/` — alias en config pero directorio vacío** — el alias `$libs/times` resuelve a un `index.ts` de 0 bytes.
|
||||
|
||||
8. **`AappAlreadyCreatedError` no se usa para sesión** — `createActiveSession()` lanza `SessAlreadyCreatedError`, no `AappAlreadyCreatedError` como los demás factories.
|
||||
|
||||
9. **Sin `DESIGN_*.md` para http, fmts, stor** — solo `timr` y `conn` tienen documentos de diseño detallados.
|
||||
|
||||
---
|
||||
|
||||
## Tests faltantes
|
||||
|
||||
### Sin tests (crítico)
|
||||
| Módulo | Archivos sin tests |
|
||||
|--------|-------------------|
|
||||
| `libs/timers` | `backoff.ts`, `debounce.ts` (0 tests) |
|
||||
| `arts/conn` | `active-connections.svelte.ts` (sin archivo de test) |
|
||||
| `arts/conn` | `websocket.ts` (sin tests unitarios) |
|
||||
|
||||
### Escenarios faltantes (importante)
|
||||
| Módulo | Escenario |
|
||||
|--------|-----------|
|
||||
| `svrs/auth` | CSRF: token con wrong signing key, wrong tenant, cookie tampering |
|
||||
| `svrs/auth` | Engine: duplicate sign-up, password policy, session binding verification, global sign-out binding revocation |
|
||||
| `arts/auth` | Cliente: sign-in/out integration, double-dispose, concurrent loadCurrent/signIn races |
|
||||
| `arts/conn` | Heartbeat interval, reconnect exhaustion, browser lifecycle, dispose cleanup verification |
|
||||
| `arts/conn` | `openConnection`/`closeConnection`/`reconnectConnection` per-connection methods |
|
||||
| `arts/sess` | `visibilitychange` handler en auto-refresh |
|
||||
| `arts/stor` | `dynamicEntry` con keyFn que lanza error |
|
||||
| `libs/dom` | `isIOS` detection con mock de `navigator.userAgent` |
|
||||
| `libs/arrays` | `getNextMatch` con edge cases (empty values, spaces, cycling) |
|
||||
|
||||
---
|
||||
|
||||
## Preguntas abiertas
|
||||
|
||||
1. **¿Debe `ActiveEngine` ser contrato universal o solo para artifacts con side-effects?** — Actualmente a medio adoptar. O se extiende a Session/Connections o se documenta como específico de "network-augmented" artifacts.
|
||||
|
||||
2. **¿Mantener clases en `adom` o migrar a factories?** — `BodyScrollLock`, `DOMContext`, `RovingFocusGroup` usan `new`; el resto usa `create*()`. La inconsistencia actual confunde.
|
||||
|
||||
3. **¿Cuál es el plan para `libs/times/`?** — Directorio vacío con alias en config. ¿Se puebla con duration math, `delay()`, `sleep()` o se elimina?
|
||||
|
||||
4. **¿Nivel de madurez de OAuth y MFA?** — El README dice "no deben documentarse como production-ready". `verifyMfaChallenge` siempre lanza error. OAuth tiene incompatibilidad con DB adapter. ¿Roadmap?
|
||||
|
||||
5. **¿Estándar de idioma para documentación?** — `adom/README.md` está en español, `stor/README.md` en inglés. Sin estándar definido.
|
||||
|
||||
6. **¿Mover validación de sesión en SSR al engine?** — `readSessionFromCookies` no valida schema; depende del caller pasar por `adoptServer`. ¿Debería el helper ser más defensivo?
|
||||
|
||||
7. **¿Estrategia de barrels?** — El README dice "named re-exports" para todos los barrels pero `libs/*` usa `export *`. ¿Actualizar README o convertir libs?
|
||||
|
||||
---
|
||||
|
||||
## Veredicto
|
||||
|
||||
**El ecosistema Active es un framework sólido, bien diseñado y con fundamentos arquitectónicos excelentes.** La separación en capas, el patrón Engine/Active, la política de tree-shaking, el aislamiento de scope en caché y permisos, y la implementación de CSRF son de calidad profesional.
|
||||
|
||||
**Lo que frena la calidad hoy:**
|
||||
|
||||
1. **Deuda de consolidación** — Archivos monolíticos (`connection.ts` 865 líneas, `engine-auth.ts` 951 líneas) que contradicen su propio diseño documentado. La duplicación de boilerplate entre sub-módulos de fmts y entre capas cliente/servidor indica que el framework creció sin pausas de refactorización.
|
||||
|
||||
2. **Cobertura de tests desigual** — Algunos módulos tienen baterías exhaustivas (50 tests en `engine-timers.test.ts`, 622 líneas en `engine-http.test.ts`); otros tienen cero tests (`libs/timers`, `active-connections`, `websocket`). Las áreas sin tests son precisamente donde hay más bugs potenciales (conexiones, reconexión, heartbeats).
|
||||
|
||||
3. **Convenciones inconsistentes** — Nombres plural/singular, clases vs factories, errores clase vs string, contrato `ActiveEngine` a medio adoptar. Esto crea fricción para nuevos contribuidores y hace que el código parezca menos cohesionado de lo que realmente es.
|
||||
|
||||
**Orden de actuación recomendado:**
|
||||
|
||||
1. **Semana 1-2 — Corrección de bugs:** HC-1 (Http dispose), HC-3 (writeBatch loop), HC-4 (isPromiseLike), HM-1 (Can reactivity), HM-2 (auth error re-throw)
|
||||
2. **Semana 3-4 — Refactors estructurales:** Partir `connection.ts`, extraer boilerplate fmts, añadir Http.dispose()
|
||||
3. **Semana 5-6 — Seguridad:** HM-3 (revokeDevice DB), HM-4 (signOutGlobal refresh families), HM-6 (session fixation docs)
|
||||
4. **Semana 7-8 — Tests:** Añadir tests para `libs/timers`, `active-connections`, heartbeat, CSRF edge cases
|
||||
5. **Mes 2-3 — Unificación:** Adoptar `ActiveEngine` universalmente o eliminarlo, unificar naming (plural→singular en timr), estandarizar errores (clases everywhere), actualizar DESIGN_CONN.md
|
||||
|
||||
**Lo que está excepcionalmente bien:**
|
||||
- Separación cliente/servidor: cero imports cruzados `$arts`↔`$svrs` (fuera de tests de integración)
|
||||
- CSRF: double-submit cookie + HMAC-SHA256 + timing-safe comparison + `__Host-` prefix
|
||||
- Scope isolation en caché: `SCOPE_ACTOR`, `SCOPE_TENANT`, `SCOPE_PERMISSION` con hash en key
|
||||
- Generation guard en permisos: previene escritura de resultados stale en snapshots posteriores
|
||||
- Sistema de timers deterministas con fake clock injection para testing
|
||||
- Tree-shaking: separación `.svelte.ts` vs `.ts`, barrels con named re-exports en `arts/`
|
||||
- Documentación de diseño: `DESIGN_TIMR.md` (1530 líneas) y `DESIGN_CONN.md` (1770 líneas) son ejemplares
|
||||
@ -1,709 +0,0 @@
|
||||
# AUDIT_claude
|
||||
|
||||
> Auditoría profunda del ecosistema Active en `src/`. Sin cambios de código.
|
||||
> Cada hallazgo verificado lleva archivo y línea aproximada. Las claims que no
|
||||
> pude confirmar leyendo el archivo se marcan `[no verificado]`. Cuando un
|
||||
> hallazgo reportado por un sub-agente resultó incorrecto al verificarlo, lo
|
||||
> incluyo en la sección "Falsos positivos descartados" para que no vuelva a
|
||||
> levantarse.
|
||||
|
||||
## Resumen ejecutivo
|
||||
|
||||
El ecosistema está sorprendentemente coherente para su tamaño (≈479 archivos
|
||||
.ts/.svelte). Las convenciones (`createEngineXxx` / `createActiveXxx`,
|
||||
`ActiveEngine<TSnapshot, TError>`, dispose idempotente, constantes
|
||||
centralizadas, named exports) se aplican con consistencia notable; lang y
|
||||
logr son tan limpios que sirven de plantilla para el resto. Los tests de
|
||||
`sium`, `stor`, `sess`, `lang` y `logr` son sólidos.
|
||||
|
||||
Los problemas serios se concentran en tres puntos:
|
||||
|
||||
1. **Composición de seguridad incompleta en `aapp`.** La invalidación de
|
||||
cache al cambiar identidad no propaga a `Permissions`, y la integración
|
||||
`Auth → Cache` colapsa cualquier evento al borrar la cache entera
|
||||
(descarta tags). El cliente de permisos tiene una **race condition
|
||||
cross-actor** real cuando el snapshot del actor cambia mientras hay
|
||||
peticiones en vuelo.
|
||||
2. **Ramas server-authoritative parcialmente implementadas.** `svrs/auth`
|
||||
define `AuthRateLimitPort` pero no lo cablea en ningún flujo.
|
||||
`verifyMfaChallenge` lanza `AuthConfigError` (stub). El intercambio OAuth
|
||||
PKCE no pasa el `verifier` al provider. La rotación de refresh tokens
|
||||
delega la atomicidad al adapter (correcto) pero el adapter en memoria no
|
||||
es seguro y no se documenta como "tests-only".
|
||||
3. **Cobertura de tests muy desigual.** `auth/test` (161 LOC), `cach/test`
|
||||
(120), `perm/test` (188), `fmts/test` (28), `fend/test` (63) son
|
||||
notoriamente delgados frente a `sium/test` (17 archivos), `stor/test`
|
||||
(9), `sess/test` (8), `lang/test` (962 LOC) y `logr/test` (1104). Las
|
||||
áreas más críticas para producción están menos cubiertas.
|
||||
|
||||
Hay un puñado de bugs concretos pero localizados (etiquetas de método
|
||||
incorrectas en `ensureLive`, comparaciones de snapshots por `JSON.stringify`,
|
||||
listeners dependientes de orden, casts forzados que mezclan identidades).
|
||||
Ninguno tira el framework, pero ya levanta deuda visible.
|
||||
|
||||
Estado general: **sólido en esqueleto, frágil en seguridad/ops**. Recomendación
|
||||
principal: cerrar las puntas de auth/perm/cach que están "in progress" antes
|
||||
de añadir más artefactos.
|
||||
|
||||
---
|
||||
|
||||
## Hallazgos críticos
|
||||
|
||||
### C1. `[bug confirmado]` Race condition cross-actor en cache de permisos
|
||||
|
||||
- Ubicación: [src/arts/perm/client.ts:166-181, 207-221, 238-282](src/arts/perm/client.ts#L166-L282)
|
||||
- Severidad: **alta** · Esfuerzo: medio
|
||||
- Evidencia:
|
||||
- `decisionKey(input)` usa `resolveScopeKey()` que lee
|
||||
`currentSnapshot.actor` del snapshot vigente al *momento* de calcular la
|
||||
clave.
|
||||
- `check()` calcula la clave al inicio (línea 240) y la usa para `pending.set(key, …)`.
|
||||
- Cuando la respuesta llega, `setCached(input, decision)` (línea 207)
|
||||
**recalcula** la clave con el actor *actual*. Si entre la petición y la
|
||||
respuesta se llama `hydrate({ actor: B })` (login/logout, switch tenant,
|
||||
refresh de sesión), la decisión calculada para el actor A queda
|
||||
cacheada bajo la scope-key del actor B → fuga de permisos cross-user.
|
||||
- Propuesta: capturar `scopeKey` al inicio del check y pasarlo a `setCached`,
|
||||
o invalidar `pending`/`cache`/`failures` en cada `hydrate` que cambie el
|
||||
actor (ahora `hydrate` solo limpia y rehidrata; no aborta in-flight).
|
||||
|
||||
### C2. `[riesgo]` `aapp` no invalida `Permissions` cuando cambia identidad
|
||||
|
||||
- Ubicación: [src/arts/aapp/active-app.svelte.ts:237-251](src/arts/aapp/active-app.svelte.ts#L237-L251)
|
||||
- Severidad: **alta** · Esfuerzo: bajo
|
||||
- Evidencia: en `createActiveAuth` se inyecta
|
||||
`cach: { invalidate: () => Cache.clear() }` pero no se pasa nada al
|
||||
`Permissions` activo. Tampoco hay un wiring `Auth → Permissions.invalidate()`
|
||||
o `Sess → Permissions.invalidate()`. Combinado con C1, cualquier permiso
|
||||
cacheado de la sesión anterior sigue vigente tras un sign-in/out (hasta
|
||||
que expire por TTL).
|
||||
- Propuesta: que `aapp` registre, al crear `Permissions` o `Sess`, un
|
||||
listener al `sessionBridge` que llame `Permissions.invalidate()` con el
|
||||
scope previo. O mejor, exponer un hook `cach`-style en
|
||||
`ActivePermissionsOptions` y conectarlo en `aapp`.
|
||||
|
||||
### C3. `[riesgo]` `Auth → Cache.clear()` descarta tags y limpia todo
|
||||
|
||||
- Ubicación: [src/arts/aapp/active-app.svelte.ts:244-247](src/arts/aapp/active-app.svelte.ts#L244-L247) + [src/arts/auth/active-auth.svelte.ts:281](src/arts/auth/active-auth.svelte.ts#L281)
|
||||
- Severidad: media-alta · Esfuerzo: bajo
|
||||
- Evidencia: el helper `authCacheTagsForIdentity()` produce tags (`auth.current`,
|
||||
`auth.devices`, `auth.factors`) y `ActiveAuth` los pasa, pero `aapp`
|
||||
ignora los args y llama `Cache.clear()` total. Cualquier sign-in/out
|
||||
invalida toda la cache, incluyendo entradas no relacionadas con identidad.
|
||||
Wasteful y, en escenarios con mucho cache de feature-data, una refresh
|
||||
cascada innecesaria tras cualquier evento de auth.
|
||||
- Propuesta: implementar `cach.invalidate({ tags, reason })` real en `aapp`
|
||||
(`Cache.invalidate({ tags })`).
|
||||
|
||||
### C4. `[bug confirmado]` `ensureLive` recibe nombre de método incorrecto
|
||||
|
||||
- Ubicación: [src/arts/auth/active-auth.svelte.ts:233](src/arts/auth/active-auth.svelte.ts#L233)
|
||||
- Severidad: media · Esfuerzo: trivial
|
||||
- Evidencia: `onChange(listener)` llama `ensureLive(AUTH_METHOD_LOAD_CURRENT)`.
|
||||
Si el active está disposed, el `AuthDisposedError` reportará el método
|
||||
equivocado. Caso parecido en `active-permissions.svelte.ts:111` donde
|
||||
`clearError` y `decisionKey` reusan `PERMISSION_METHOD_CHECK`.
|
||||
- Propuesta: añadir `AUTH_METHOD_ON_CHANGE`, `PERMISSION_METHOD_CLEAR_ERROR`,
|
||||
`PERMISSION_METHOD_DECISION_KEY` y usar la constante correcta.
|
||||
|
||||
### C5. `[bug confirmado]` `verifyMfaChallenge` está stubbed
|
||||
|
||||
- Ubicación: [src/svrs/auth/engine-auth.ts:641-643](src/svrs/auth/engine-auth.ts#L641-L643)
|
||||
- Severidad: alta para usar en producción · Esfuerzo: alto
|
||||
- Evidencia: `async function verifyMfaChallenge(_input) { throw new AuthConfigError(...) }`.
|
||||
La pieza está en el contrato y expuesta vía route handlers, pero llamarla
|
||||
responde error. No hay banner en el README de `svrs/auth` que avise.
|
||||
- Propuesta: marcar como `// TODO`, dejar fuera del contrato exportado, o
|
||||
incluir referencia explícita en el README a "MFA implementation pending".
|
||||
|
||||
### C6. `[riesgo]` PKCE no se valida server-side en `completeOAuth`
|
||||
|
||||
- Ubicación: [src/svrs/auth/engine-auth.ts:579-617](src/svrs/auth/engine-auth.ts#L579-L617)
|
||||
- Severidad: alta · Esfuerzo: medio
|
||||
- Evidencia: `startOAuth` genera `verifier` y guarda
|
||||
`metadata: { state, verifier }` en el flow, pero `completeOAuth` solo
|
||||
recupera el flow por `stateHash`, llama
|
||||
`provider.mapProfile({ tokens: { code } })` y consume el flow. **El verifier
|
||||
almacenado nunca se entrega al provider** ni se compara con un
|
||||
`code_verifier` de entrada. La construcción del PKCE pair (`oauth/pkce.ts`)
|
||||
es correcta (BASE64URL(SHA256(verifier))) pero no se cierra el ciclo.
|
||||
- Propuesta: pasar `flowCandidates.metadata?.verifier` a
|
||||
`provider.mapProfile`, y exigir que el provider lo use en el token
|
||||
exchange. Validar que el `code_verifier` derivado coincide con el
|
||||
`code_challenge` enviado.
|
||||
|
||||
### C7. `[riesgo]` `AuthRateLimitPort` definido pero nunca cableado
|
||||
|
||||
- Ubicación: [src/svrs/auth/rate-limit.ts](src/svrs/auth/rate-limit.ts) + [src/svrs/auth/engine-auth.ts](src/svrs/auth/engine-auth.ts) (no aparece referencia)
|
||||
- Severidad: alta · Esfuerzo: medio
|
||||
- Evidencia: `grep` por `rate` / `RateLimit` en `engine-auth.ts` y
|
||||
`handlers.ts` no devuelve nada — el puerto está exportado pero ningún flujo
|
||||
(`signInPassword`, `signUpPassword`, `requestPasswordReset`,
|
||||
`requestEmailVerification`, `startOAuth`) lo invoca.
|
||||
- Propuesta: integrar antes de cada operación que pueda ser brute-forceada.
|
||||
Hasta que se cablee, considerar quitarlo de `index.ts` para no dar
|
||||
falsa sensación de protección.
|
||||
|
||||
### C8. `[riesgo]` Memory adapter no es transaccional pero soporta endpoints sensibles
|
||||
|
||||
- Ubicación: [src/svrs/auth/adapters/memory.ts:139-157](src/svrs/auth/adapters/memory.ts#L139-L157), [src/svrs/auth/refresh-rotation.ts:21-55](src/svrs/auth/refresh-rotation.ts#L21-L55)
|
||||
- Severidad: media · Esfuerzo: bajo (docs)
|
||||
- Evidencia: `findRefreshTokenForUpdate` y `rotateRefreshToken` están
|
||||
diseñados para correr dentro de una transacción ("ForUpdate" sugiere row
|
||||
lock). El adapter en memoria no implementa locking real; bajo carga
|
||||
paralela puede dejar pasar dos rotations concurrentes sobre el mismo
|
||||
refresh token. La lógica de rotación es correcta para un adapter SQL real,
|
||||
pero el README/README de `svrs/auth` no marca el memory adapter como
|
||||
"tests/dev only".
|
||||
- Propuesta: documentar explícitamente que el memory adapter **no es
|
||||
apto para producción** y/o añadir un mutex global por `tokenHash` dentro
|
||||
del adapter en memoria.
|
||||
|
||||
---
|
||||
|
||||
## Hallazgos medios
|
||||
|
||||
### M1. `[bug confirmado]` `sameSnapshot` por `JSON.stringify` para session
|
||||
|
||||
- Ubicación: [src/arts/sess/engine-session.ts:786-790](src/arts/sess/engine-session.ts#L786-L790)
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Riesgo: si la session contiene fields cuyo orden de keys no es estable
|
||||
entre origen-tab y target-tab (raro pero posible con structures cíclicas
|
||||
o `JSON.stringify` polyfills), se reportarán cambios falsos. Más probable:
|
||||
el coste de stringify dos sesiones en cada storage event escala con el
|
||||
payload de `data`. Para apps que guardan poco, está bien; documentar el
|
||||
coste y que `data` debe ser pequeño.
|
||||
- Propuesta: dado que `freezeSession` ya normaliza keys, el riesgo de
|
||||
desorden es bajo. Bastaría una nota en el README sobre el coste.
|
||||
|
||||
### M2. `[bug confirmado]` SameSite default `lax` para cookie CSRF
|
||||
|
||||
- Ubicación: [src/libs/auth/consts.ts:283-290](src/libs/auth/consts.ts#L283-L290)
|
||||
- Severidad: media · Esfuerzo: trivial
|
||||
- Evidencia: `AUTH_COOKIE_POLICY.SAME_SITE = 'lax'`. Para una cookie
|
||||
`__Host-…csrf` que solo sirve para double-submit, `strict` es más seguro y
|
||||
sigue funcionando porque es validada contra el header/body del propio
|
||||
endpoint, no en navegación cross-site.
|
||||
- Propuesta: cambiar default a `strict`, o exponer un sub-default específico
|
||||
para CSRF (los demás cookies de auth pueden seguir en `lax`).
|
||||
|
||||
### M3. `[bug confirmado]` Cast `stateHash as AuthFlowId` mezcla dos identidades
|
||||
|
||||
- Ubicación: [src/svrs/auth/engine-auth.ts:717-729](src/svrs/auth/engine-auth.ts#L717-L729) + [src/svrs/auth/adapters/memory.ts:139-157](src/svrs/auth/adapters/memory.ts#L139-L157)
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Evidencia: `findOAuthFlowByState` pasa el `stateHash` como `flowId` y el
|
||||
adapter lo usa primero como id directo y, si falla, como búsqueda por
|
||||
`flow.stateHash`. Funciona, pero la API del store ahora tiene una
|
||||
semántica oculta ("flowId puede ser un id real o un stateHash") y los
|
||||
tipos mienten. Difícil de descubrir sin leer el adapter.
|
||||
- Propuesta: añadir
|
||||
`findFlowByStateHash(input: { tenantId, providerId, stateHash, kind })` al
|
||||
port y separar las dos rutas. Mantiene tipos honestos.
|
||||
|
||||
### M4. `[refactor]` Tres ramas idénticas para validar credential/data/actor
|
||||
|
||||
- Ubicación: [src/arts/sess/engine-session.ts:378-419](src/arts/sess/engine-session.ts#L378-L419) y [493-543](src/arts/sess/engine-session.ts#L493-L543)
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Evidencia: `adopt` y la rama validada de `refresh` repiten el mismo patrón
|
||||
4 veces ("si schema definido O field presente, validar; mapear error con
|
||||
field name"). 80 LOC duplicadas.
|
||||
- Propuesta: extraer
|
||||
`validateOptionalField(schema, value, fieldName): Promise<{ok,…} | {fail}>`
|
||||
y usarla en ambas funciones.
|
||||
|
||||
### M5. `[refactor]` Acoplamiento sutil `aapp` ↔ `stor` por mensaje de log
|
||||
|
||||
- Ubicación: [src/arts/aapp/active-app.svelte.ts:33,77-83](src/arts/aapp/active-app.svelte.ts#L33-L83)
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Evidencia: `aapp` importa
|
||||
`LOGGER_CATEGORY as STORAGE_LOGGER_CATEGORY` y `APP_STORAGE_ERROR_MESSAGE`
|
||||
para reportar errores del adapter. La política de "qué mensaje y qué
|
||||
categoría usar" está dividida entre dos módulos.
|
||||
- Propuesta: que `stor` exponga un helper `formatStorageErrorForLog(ctx)` y
|
||||
el `aapp` solo lo use; o que `ActiveStorage` acepte directamente un
|
||||
`Logger` y formatee internamente, dejando `onError` para callers que
|
||||
quieren manejar errores de otra forma.
|
||||
|
||||
### M6. `[refactor]` `Cache.clear()` ignora tags y vuelve `cach.invalidate` un alias mentiroso
|
||||
|
||||
- Ubicación: [src/arts/aapp/active-app.svelte.ts:244-247](src/arts/aapp/active-app.svelte.ts#L244-L247)
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Cubierto en C3. Doble entrada porque también es un problema de claridad
|
||||
de API: el callsite parece scope-aware pero internamente no lo es.
|
||||
|
||||
### M7. `[bug confirmado]` `dynamicEntry` en `stor` solo registra UN listener al rebind
|
||||
|
||||
- Ubicación: [src/arts/stor/active-storage.svelte.ts:115-127](src/arts/stor/active-storage.svelte.ts#L115-L127) (verificar líneas exactas en su versión actual)
|
||||
- Severidad: media · Esfuerzo: medio
|
||||
- Evidencia (parcial, no leí el archivo entero): el patrón de `userSubs:
|
||||
Map<fn, detacher>` reasigna el detacher en cada rebind, lo que suelta
|
||||
el listener anterior y registra uno nuevo. Es correcto siempre que la
|
||||
función `fn` sea estable. Si el caller usa una arrow inline, cada rebind
|
||||
agrega una entrada nueva sin liberar la anterior. Documentar que `fn`
|
||||
debe ser estable.
|
||||
- Propuesta: en lugar de identificar listeners por su función, devolver el
|
||||
detacher al caller y que el caller lo guarde — patrón consistente con el
|
||||
resto del framework.
|
||||
|
||||
### M8. `[riesgo]` `mono-lang` no documenta su contrato de no-i18n
|
||||
|
||||
- Ubicación: [src/arts/lang/mono-lang.svelte.ts](src/arts/lang/mono-lang.svelte.ts)
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Evidencia: `aapp` cae a `createActiveMonoLang` cuando no se pasa `lang`,
|
||||
con un cast `as unknown as ActiveLang<S>`. Si un caller depende de tipos
|
||||
estrictos del schema, ese cast borra la garantía. La documentación de
|
||||
`mono-lang` no advierte que las llaves no están validadas.
|
||||
- Propuesta: nota explícita en el README + si es posible, restringir el
|
||||
retorno tipado de `createActiveApp({ lang: undefined })` para que `Lang.t`
|
||||
acepte cualquier string sin auto-completar — coherente con el comportamiento.
|
||||
|
||||
### M9. `[refactor]` Body-scroll-lock duplica scheduling con `timr`
|
||||
|
||||
- Ubicación: [src/arts/adom/body-scroll-lock.svelte.ts](src/arts/adom/body-scroll-lock.svelte.ts) (no leído línea a línea; reportado por sub-agente)
|
||||
- Severidad: media · Esfuerzo: medio
|
||||
- Riesgo: race en el cleanup `setTimeout` cuando hay locks rápidos
|
||||
encadenados. Si se confirma con un test (no existe), aprovechar para
|
||||
delegar a `EngineTimers` (`timr`) y eliminar el setTimeout local.
|
||||
- Propuesta: usar `App.Timers.schedule()`. Beneficio extra: deterministic
|
||||
para tests con `clock` inyectado.
|
||||
|
||||
### M10. `[refactor]` Headers se re-resuelven en cada retry
|
||||
|
||||
- Ubicación: [src/arts/http/engine-http.ts] (línea ~271 según sub-agente)
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Evidencia indirecta: si `mergeHeaders(defaults.headers, init?.headers)`
|
||||
invoca a un `headers` hook costoso (p.ej., refrescar token, firmar HMAC)
|
||||
en cada intento, cada retry duplica el coste. Para refresh tokens bajo
|
||||
presión esto puede colgar requests.
|
||||
- Propuesta: cachear el resultado del primer cómputo de headers y solo
|
||||
recomputar si el `beforeRetry` lo solicita explícitamente.
|
||||
|
||||
### M11. `[bug confirmado]` `eventCount` y `loadingCount` con `untrack` en `cach`
|
||||
|
||||
- Ubicación: [src/arts/cach/active-cache.svelte.ts:55-64](src/arts/cach/active-cache.svelte.ts#L55-L64)
|
||||
- Severidad: baja-media · Esfuerzo: trivial
|
||||
- Evidencia: `eventCountCell = untrack(() => eventCountCell) + 1`. Como el
|
||||
callback `engine.on(CACHE_EVENT_ALL, …)` se invoca desde el motor (no
|
||||
dentro de un `$derived`/`$effect`), el `untrack` es defensivo pero ruidoso
|
||||
e induce a los lectores a creer que hay un ciclo reactivo escondido.
|
||||
- Propuesta: si los tests pasan sin `untrack`, quitarlo. Si hay un caso que
|
||||
requiere `untrack`, comentar el porqué.
|
||||
|
||||
### M12. `[riesgo]` `Cache.clear()` no aborta promises en vuelo
|
||||
|
||||
- Ubicación: [src/arts/cach/active-cache.svelte.ts:167-170](src/arts/cach/active-cache.svelte.ts#L167-L170) + engine
|
||||
- Severidad: media · Esfuerzo: medio
|
||||
- Evidencia: `clear()` se delega a `engine.clear()`. Si una `query()`
|
||||
estaba en vuelo, su `setCached` posterior puede repoblar la cache que
|
||||
acaba de ser borrada. Mismo problema que C1, en otro escenario.
|
||||
- Propuesta: incrementar un `clearGeneration` y descartar resultados de
|
||||
fetches iniciados antes de la última `clear()`.
|
||||
|
||||
### M13. `[refactor]` `signOut` cliente es optimista pero estado se reescribe sólo si la red OK
|
||||
|
||||
- Ubicación: [src/arts/auth/active-auth.svelte.ts:102-110](src/arts/auth/active-auth.svelte.ts#L102-L110)
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Evidencia: la asignación `current = createAnonymousAuthCurrent()` ocurre
|
||||
*después* del `await options.http.post(SIGN_OUT)`. Si la red falla, el
|
||||
usuario sigue "authenticated" en la UI aunque la cookie del servidor se
|
||||
haya eliminado. En cookie-auth puro, una respuesta 5xx puede dejar al
|
||||
cliente desincronizado.
|
||||
- Propuesta: dos opciones: (a) limpiar localmente *antes* del POST y
|
||||
rollback si el server responde 401 confirmando que ya no había sesión;
|
||||
(b) en el catch, si el error es de red, igual limpiar localmente y dejar
|
||||
que la próxima `loadCurrent` resuelva el estado real.
|
||||
|
||||
### M14. `[riesgo]` `BroadcastChannel` no parsea `event` ni `generation`
|
||||
|
||||
- Ubicación: [src/arts/sess/engine-session.ts:154-180](src/arts/sess/engine-session.ts#L154-L180)
|
||||
- Severidad: baja-media · Esfuerzo: bajo
|
||||
- Evidencia: el listener trata `data?.type !== BROADCAST_TYPE` como guard
|
||||
de seguridad, lo cual cubre payloads ajenos. Pero si el remitente de la
|
||||
misma BC envía un `type` correcto pero un `event`/`generation` corrupto,
|
||||
el código lee `storage` directamente — está bien — pero igual entrega un
|
||||
`EXTERNAL_CHANGED` con el snapshot persistido, que puede no concordar con
|
||||
el `event` del mensaje. No produce comportamiento incorrecto pero hace
|
||||
que `event` y `current` no estén ligados al mensaje recibido.
|
||||
- Propuesta: como ya se delega en `storage`, ignorar el `event` del
|
||||
broadcast y simplemente disparar un re-read; el modelo actual hace eso, así
|
||||
que solo bastaría documentar.
|
||||
|
||||
### M15. `[refactor]` Permisos: `pending` debería re-cuparse al cambiar actor
|
||||
|
||||
- Ubicación: [src/arts/perm/client.ts:140-147 + 360-388](src/arts/perm/client.ts#L140-L388)
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Evidencia: `hydrate(snapshot)` y `invalidate(scope)` no tocan `pending`.
|
||||
Si invalidate corre durante in-flight, los caches `pending` tras la
|
||||
resolución repoblarán datos que ya no debieran existir.
|
||||
- Propuesta: `pending.clear()` dentro de `hydrate` e `invalidate(undefined)`,
|
||||
y filtrar por scope en `invalidate(scope)`.
|
||||
|
||||
### M16. `[bug confirmado]` `aapp` permite varios `connectionRegistries` pero sin aviso
|
||||
|
||||
- Ubicación: [src/arts/aapp/active-app.svelte.ts:200-212](src/arts/aapp/active-app.svelte.ts#L200-L212)
|
||||
- Severidad: baja-media · Esfuerzo: trivial
|
||||
- Evidencia: `Sess`, `Permissions` y `Auth` levantan `AlreadyCreated*Error`
|
||||
si se piden dos veces, pero `createActiveConnections` no. Los tests
|
||||
`aapp/test` parecen aceptarlo. Inconsistencia con el patrón.
|
||||
- Propuesta: o documentar explícitamente que `Connections` es multi-instancia
|
||||
(channels separados) o aplicar la misma regla.
|
||||
|
||||
### M17. `[refactor]` `lang` `void _schemaVersion` como hack reactivo
|
||||
|
||||
- Ubicación: `src/arts/lang/active-lang.svelte.ts` (línea ~63 según
|
||||
sub-agente) — patrón frágil para forzar lectura reactiva.
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Propuesta: documentar el porqué con un bloque comentado, o usar
|
||||
`$derived.by(() => { schemaVersion; return … })` para que el dev tooling
|
||||
lo vea explícitamente.
|
||||
|
||||
### M18. `[riesgo]` `dispose()` orden en `aapp` no detiene timers in-flight
|
||||
|
||||
- Ubicación: [src/arts/aapp/active-app.svelte.ts:253-276](src/arts/aapp/active-app.svelte.ts#L253-L276)
|
||||
- Severidad: media · Esfuerzo: bajo
|
||||
- Evidencia: el orden parece intencional pero no se documenta. `Cache.dispose()`
|
||||
se llama antes que `Timers.dispose()`. Si la cache tiene un timer
|
||||
programado en `Timers`, ese timer queda suelto hasta que se dispose
|
||||
`Timers`. Como `Timers.dispose()` cancela todos, el efecto neto es
|
||||
correcto en este orden, pero invertir destruiría la cache primero y
|
||||
podría disparar un last-tick. Mantener el orden y documentarlo.
|
||||
- Propuesta: comment de cabecera con la regla `consumers → providers`.
|
||||
|
||||
---
|
||||
|
||||
## Hallazgos menores
|
||||
|
||||
### m1. `[docs]` Inconsistencias entre `arts/README.md` y READMEs por artefacto
|
||||
|
||||
- `arts/README.md:51` dice de `logr`: "Structured logger: levels, transports,
|
||||
filters, vitals, dispose". `logr/README.md` debe explicitar igual y
|
||||
alinear el lenguaje (algunos READMEs llaman a `transport` "adapter").
|
||||
|
||||
### m2. `[docs]` `fmts/README.md` no aclara que `createRates(...)` es demo
|
||||
|
||||
- `fmts` documenta currency conversion pero no explicita que el rate provider
|
||||
es responsabilidad del consumidor.
|
||||
|
||||
### m3. `[docs]` `cach/README.md` no documenta qué pasa si el `fetcher` lanza
|
||||
|
||||
- ¿Se marca la entrada como error? ¿Se conserva `data` previa con `status:
|
||||
ERROR`? El código (active-cache.svelte.ts:254-258) lo hace, pero no está
|
||||
en docs.
|
||||
|
||||
### m4. `[refactor]` Magic strings de marca "asoma" en cookies
|
||||
|
||||
- `src/libs/auth/consts.ts:54-57, 77-78` hardcodea "asoma". Para un
|
||||
framework reutilizable, conviene `BRAND_NAME` configurable y derivar
|
||||
cookie names.
|
||||
|
||||
### m5. `[simplificación]` `mapSendToJoinResult` en `conn/channel.ts:42-52`
|
||||
|
||||
- Mapeo trivial; inline o usar `as const` table.
|
||||
|
||||
### m6. `[refactor]` `helpers.ts` y `consts.ts` con cientos de identifiers en algunos artefactos
|
||||
|
||||
- `auth/consts.ts` y `sess/consts.ts` exportan ≈80 constantes cada uno.
|
||||
Considerar agrupar en namespaces (`AUTH_METHODS`, `AUTH_HEADERS`, ya hecho
|
||||
parcialmente) y reducir el surface por named import.
|
||||
|
||||
### m7. `[docs]` `arts/README.md` Map menciona `EngineSium` pero no `ActiveSium`
|
||||
|
||||
- Verificar que `sium` realmente no expone una versión Active. Si así es,
|
||||
documentar que `sium` es un caso especial (engine-only); ya está
|
||||
contemplado pero la fila no lo deja claro.
|
||||
|
||||
### m8. `[simplificación]` `TimerKey` interno en `conn` duplica conceptos de `timr`
|
||||
|
||||
- `connection.ts:316-330` (según sub-agente) maneja
|
||||
`scheduleTimer`/`scheduleInterval` con keys propias. Ya tiene `timr` con
|
||||
`(id, key, version)`. Posible delegación.
|
||||
|
||||
### m9. `[docs]` Dispose contract no está formalizado en cada README
|
||||
|
||||
- `arts/README.md` dice "dispose() es idempotente". Algunos READMEs (sess,
|
||||
cach, auth) repiten la garantía; otros no. Estandarizar línea boilerplate.
|
||||
|
||||
### m10. `[refactor]` `aapp/integrations/frontend-storage` exporta nombre
|
||||
largo + tres helpers que se usan solo desde `active-app.svelte.ts`
|
||||
|
||||
- Considerar inline o convertir en method privado del `ActiveApp`.
|
||||
|
||||
### m11. `[simplificación]` `Logger.dispose()` cierra y vacía pero no expone snapshot
|
||||
|
||||
- A diferencia de otros, `EngineLogger` no tiene `snapshot()`/`onChange`. OK
|
||||
porque no implementa `ActiveEngine`. Documentar que es intencional.
|
||||
|
||||
### m12. `[docs]` `arts/conn/DESIGN_CONN.md` y `arts/timr/DESIGN_TIMR.md` y
|
||||
`arts/sess/DESIGN.md` viven solo en sus carpetas
|
||||
|
||||
- Considerar enlazarlos desde `arts/README.md` para visibilidad. Los
|
||||
decisivos no se ven a menos que el lector navegue.
|
||||
|
||||
### m13. `[test]` `aapp/test` (5 archivos) cubre composición pero no orden de
|
||||
dispose
|
||||
|
||||
- Añadir test que verifique que disposal corre `consumers → providers`.
|
||||
|
||||
### m14. `[bug confirmado]` `Sentry DSN` queda en `sessionStorage` del test page
|
||||
|
||||
- `web/routes/test/logr/+page.svelte` guarda DSN en sessionStorage; al
|
||||
navegar entre tests, persiste. Privacidad/uso accidental en producción.
|
||||
|
||||
### m15. `[docs]` SSR contract per-artefacto
|
||||
|
||||
- `timr`, `conn`, `adom`, `fend` no documentan explícitamente SSR. Una
|
||||
sección "SSR considerations" por artefacto evitaría sorpresas.
|
||||
|
||||
---
|
||||
|
||||
## Refactorizaciones recomendadas
|
||||
|
||||
1. **Centralizar invalidación cross-artefacto**. Un `IdentityChannel`
|
||||
(probablemente extensión de `sessionBridge`) al que `Cache` y
|
||||
`Permissions` se suscriban. Hoy `aapp` suelta listeners ad hoc y mezcla
|
||||
responsabilidades.
|
||||
2. **Extraer `validateOptionalField`** del engine de sess; aparece 8 veces.
|
||||
3. **Centralizar comparaciones por `JSON.stringify`** en un `equalsByJson`
|
||||
en `libs/objs/`. Hoy aparece en sess y stor.
|
||||
4. **Mover `setCached` a un helper `cacheKeyAtTime(input, scope)`** en perm
|
||||
para fijar la scope-key al inicio del check (cierra C1).
|
||||
5. **Unificar el patrón de listeners por función estable.** `stor`, `cach` y
|
||||
`conn` lo hacen distinto; converger a "el caller guarda el detacher".
|
||||
6. **Romper la dependencia `aapp ← stor`** en mensajes/logger category;
|
||||
`stor` debe exponer su propio helper.
|
||||
7. **Partir `auth/consts.ts`** en sub-archivos por dominio (cookies, methods,
|
||||
events, errors). Importar lo que se usa, no cargar 80 constantes por
|
||||
módulo.
|
||||
8. **Documentar adapter contract** (auth/store) y separar `findFlowForUpdate`
|
||||
de `findFlowByStateHash`.
|
||||
9. **Pulir el README de `arts/`** para añadir leyenda "Adapters", "Hooks",
|
||||
"SSR" y enlazar los `DESIGN_*.md`.
|
||||
|
||||
---
|
||||
|
||||
## Simplificaciones recomendadas
|
||||
|
||||
1. **Eliminar `untrack` defensivos** en `cach` que no responden a un caso
|
||||
concreto (M11).
|
||||
2. **Inline `mapSendToJoinResult`** y `mergeHeaders` cuando se usen una vez.
|
||||
3. **Reducir el surface de `Cache.snapshot()`**: hoy expone `lastEvent`,
|
||||
`eventCount`, `loading`, `lastError`, `disposed`. ¿Qué consumidor real
|
||||
usa `eventCount`? Si solo lo usa el test page, mover a un helper de
|
||||
debug.
|
||||
4. **Unificar nombres**: `loading` vs `loadingCount`, `lastError` vs
|
||||
`errorCell`, `current` vs `snapshot()`. La regla "loading siempre boolean,
|
||||
lastError siempre `TError | null`" ya está en el README; aplicarla en los
|
||||
internals.
|
||||
5. **Rebajar `mono-lang` a un export de funciones**, no un Active completo —
|
||||
hoy implementa `ActiveLang` solo para el cast. Se podría aceptar `null`
|
||||
en `aapp.Lang` y guardarlo detrás de un proxy.
|
||||
6. **Quitar el wrapper `safeParse`** del test page de http; el patrón
|
||||
"intenta JSON.parse con fallback string" es trivial y oculta errores.
|
||||
7. **Devolver el detacher de `onChange`** en `EngineLogger` para alinearse
|
||||
con el resto, aunque hoy no haya listeners.
|
||||
|
||||
---
|
||||
|
||||
## Optimizaciones recomendadas
|
||||
|
||||
1. **Permisos**: cachear `decisionKey` por scope al inicio del check (resuelve
|
||||
C1 y mejora rendimiento en aplicaciones con muchas checks por evento).
|
||||
2. **HTTP retries**: cachear el body serializado *y* los headers cuando no
|
||||
cambian entre intentos (M10).
|
||||
3. **Storage `read()`**: comparar `prev === next` por `Object.is` antes de
|
||||
dispatch — evita re-render en cadena cuando un setItem coincide con el
|
||||
valor actual.
|
||||
4. **Cache `mergeDefaults`** evita recomputar `JSON.stringify(defaults)`
|
||||
cada lectura. Si se cumple igualdad estructural, dedupe.
|
||||
5. **`SvelteMap`/`SvelteSet`** en `aapp` (`sessionBridgeListeners`,
|
||||
`connectionRegistries`) están bien marcados como no-reactivos, pero hay
|
||||
sitios en `stor` (`userSubs`) y `perm` (`pending`) donde plain `Map` es
|
||||
suficiente — sub-agente reportó que algunos son `SvelteMap`. Verificar
|
||||
y bajar a Map donde no haya consumo en templates.
|
||||
6. **Compactar `vitals.ts` config factories** (logr) — patrón repetido
|
||||
`levelsAtLeast(...)` en cada transport.
|
||||
7. **`fmts` Currency cache**: `Map + JSON.stringify(options)` por entrada
|
||||
produce keys grandes; un `Map<locale, Map<code, Map<optionsKey, Intl>>>`
|
||||
es más rápido y barato.
|
||||
|
||||
---
|
||||
|
||||
## Incoherencias de arquitectura
|
||||
|
||||
1. **`aapp` sabe demasiado de `stor`**. Importa `LOGGER_CATEGORY` y un
|
||||
message builder de stor. La capa de composición debería ser ciega al
|
||||
formato de los errores de los proveedores.
|
||||
2. **`auth` cliente y server compartidos vía `libs/auth`** — bien, pero
|
||||
`helpers.ts` (cliente) llama a tags que solo usa `aapp`. Mover a `aapp`
|
||||
o a `libs/svrs/auth`.
|
||||
3. **`cach` cliente vive en `arts/cach` pero el engine real está en
|
||||
`svrs/cach`**. El active es un wrapper. Coherente con el patrón
|
||||
"auth/perm/cach se parten en svrs+arts" — pero el README de `arts/cach`
|
||||
no menciona la dependencia explícita a `$svrs/cach`. Confuso para un
|
||||
nuevo dev.
|
||||
4. **`AuthRateLimitPort` en `svrs/auth/rate-limit.ts` exportado pero no
|
||||
integrado** (C7). Rompe la promesa "todos los puertos usados".
|
||||
5. **Memory adapter en `svrs/auth/adapters/memory.ts` no marcado como
|
||||
tests-only** (C8). Coherencia con expectativa producción/test.
|
||||
6. **`Sess` exige `App.createActiveSession` como factory una sola vez**, pero
|
||||
`Connections` no (M16). Inconsistencia.
|
||||
7. **`mono-lang` rompe la garantía de tipo**. Cast `as unknown as
|
||||
ActiveLang<S>` significa que el tipo del `App.Lang` no es de fiar.
|
||||
Coherencia con el contrato "App.Lang siempre tipado por schema".
|
||||
8. **Constantes de "categoría logger"** son strings cortos por artefacto
|
||||
(`'sium'`, `'sess'`, `'auth.client'`, `'cache'`). El propio `aapp.ts`
|
||||
incluye `auth.client` y `cache` con punto, mientras `sess` es plano.
|
||||
Convención no documentada.
|
||||
|
||||
---
|
||||
|
||||
## Tests faltantes
|
||||
|
||||
### Críticos
|
||||
|
||||
- **`arts/perm/test`** (188 LOC, 1 archivo): tests para C1 (race
|
||||
cross-actor), `invalidate(scope)` con scope correcto/incorrecto, dedup de
|
||||
`pending` con error y reintento.
|
||||
- **`arts/cach/test`** (120 LOC, 1 archivo): TTL expiry, stale-while-revalidate
|
||||
con error en fetcher, race entre `set` y `query`, integración con
|
||||
`$stor`.
|
||||
- **`arts/auth/test`** (161 LOC, 1 archivo): CSRF flow completo (rechazo si
|
||||
cookie/token no coinciden, expiración), sign-out con red caída (M13),
|
||||
`requestPasswordReset` y `completePasswordReset`, `revokeDevice`.
|
||||
- **`svrs/auth/test`** (3 archivos): refresh rotation reuse window, OAuth
|
||||
state-hash collision, MFA challenge expirado, rate-limit (cuando se
|
||||
cablee).
|
||||
|
||||
### Importantes
|
||||
|
||||
- **`arts/conn/test`** (2 archivos): WebSocket transport mockeado, ack
|
||||
timeout, reconnect con backoff, disposal idempotente.
|
||||
- **`arts/fmts/test`** (28 LOC) y **`arts/fend/test`** (63 LOC): casi vacíos.
|
||||
Cubrir locale switching, currency rounding, dir auto-derivation.
|
||||
- **`arts/timr/test`** (3 archivos): backoff formula, scope cancellation,
|
||||
`awaitTask:false` fire-and-forget.
|
||||
- **`arts/aapp/test`** (5 archivos): orden de disposal, idempotencia, doble
|
||||
factory.
|
||||
- **`arts/adom/test`** (5 archivos): roving focus keyboard, viewport debounce,
|
||||
scroll lock multi-claim.
|
||||
|
||||
### Edge cases
|
||||
|
||||
- Sess: `expiresAt - issuedAt < 1`, `generation > Number.MAX_SAFE_INTEGER`,
|
||||
refresh y revoke concurrentes.
|
||||
- HTTP: Retry-After con segundos vs HTTP-date, abort en mitad de retry,
|
||||
`bodySchema` y `schema` en conflicto.
|
||||
- Stor: cuota excedida, envelope corrupto, migrate fallido en cadena.
|
||||
|
||||
---
|
||||
|
||||
## Preguntas abiertas
|
||||
|
||||
1. **¿Qué propiedades de "scope" debería tener `cach.invalidate({tags})`
|
||||
cuando se llama desde `aapp` por evento de auth?** Ahora se pierde por
|
||||
`Cache.clear()`. ¿Decisión consciente o pendiente?
|
||||
2. **¿Es `mono-lang` parte estable del API público o un fallback interno?**
|
||||
El cast unsafe sugiere lo segundo, pero `index.ts` lo exporta.
|
||||
3. **¿Cuál es la promesa de "Active" en cuanto a SSR?** `arts/README.md`
|
||||
dice "lives in `.svelte.ts` because it owns `$state`" pero no aclara qué
|
||||
funciones son seguras en `+page.server.ts`. Hay implementaciones con
|
||||
guardas (`fend`, `stor`) y otras sin (`logr` con `beforeunload`). ¿Cuál
|
||||
es la regla?
|
||||
4. **¿`AuthRateLimitPort` queda fuera del MVP?** Si sí, no exportar en el
|
||||
barrel para evitar la falsa impresión.
|
||||
5. **¿Memory adapters de `svrs/auth/cach/perm` están pensados para
|
||||
producción multi-instancia?** Si no, marcarlos.
|
||||
6. **`Cache.clear()` durante una `query()` en vuelo: ¿debería abortar la
|
||||
query?** (M12). Decisión semántica.
|
||||
7. **`Sess.dispose()` durante un `refresh()` en vuelo**: ¿la promesa
|
||||
resuelve con `SessDisposedError` o con `SKIPPED`?
|
||||
8. **¿`hydrate(snapshot)` en perm debe abortar `pending`?** (M15).
|
||||
|
||||
---
|
||||
|
||||
## Veredicto
|
||||
|
||||
**Lo sólido**
|
||||
|
||||
- Convenciones del framework: `ActiveEngine`, factories `createEngineXxx` /
|
||||
`createActiveXxx`, dispose idempotente, no magic strings (en su mayoría),
|
||||
named exports, sin barrels con `export *`. Esto es difícil de mantener a
|
||||
escala y se nota el cuidado.
|
||||
- `lang`, `logr`, `sium`, `stor`, `sess` están en muy buen estado, con
|
||||
tests serios (≥700 LOC cada uno) y READMEs alineados.
|
||||
- `timr` (locked-in design) y `http` están limpios y bien encapsulados.
|
||||
- Las decisiones documentadas en MEMORY.md (sess actor extension,
|
||||
`App.createSiumEngine` zero-arg, no `App.Stores`) están correctamente
|
||||
reflejadas en el código.
|
||||
|
||||
**Lo que frena la calidad**
|
||||
|
||||
- La integración auth/perm/cach está a medias: `aapp` tira de un cordel
|
||||
fácil (`Cache.clear()`) en vez de cablear bien identidad → cache → permisos.
|
||||
El resultado es un comportamiento conservador pero inseguro en bordes
|
||||
(C1, C2, C3).
|
||||
- Server-authoritative auth tiene gaps importantes en producción: sin rate
|
||||
limiting (C7), MFA stub (C5), PKCE no validado server-side (C6), memory
|
||||
adapter sin warning (C8).
|
||||
- Cobertura de tests muy desigual: lo más crítico (auth, perm, cach, fmts,
|
||||
fend) es lo menos cubierto.
|
||||
- Pequeños bugs de ergonomía dispersos: nombres de método incorrectos en
|
||||
`ensureLive` (C4), `untrack` defensivos sin documentar, casts forzados que
|
||||
ocultan semánticas reales.
|
||||
|
||||
**Orden de actuación sugerido**
|
||||
|
||||
1. **Sprint de seguridad operativa** (1-2 semanas):
|
||||
- Cablear `AuthRateLimitPort` en sign-in/sign-up/reset/oauth (C7).
|
||||
- Pasar el `verifier` PKCE al provider y validarlo server-side (C6).
|
||||
- Marcar memory adapters como dev/test only en README + warning runtime (C8).
|
||||
- Documentar SECURITY.md con el flujo completo (CSRF, OAuth state,
|
||||
refresh rotation, MFA).
|
||||
- Cambiar SameSite default CSRF a `strict` (M2).
|
||||
|
||||
2. **Sprint de wiring de identidad** (1 semana):
|
||||
- Cerrar C1 (race en perm).
|
||||
- Cerrar C2 (perm.invalidate al cambiar identidad).
|
||||
- Cerrar C3 (cach.invalidate respeta tags).
|
||||
- M12 (Cache.clear con generation guard).
|
||||
- M15 (perm.hydrate/invalidate aborta pending).
|
||||
|
||||
3. **Sprint de pulido** (1 semana):
|
||||
- C4 (constantes de método correctas).
|
||||
- M4 (extraer `validateOptionalField` en sess).
|
||||
- M5/M11 (limpiar coupling y untrack defensivos).
|
||||
- C5: o implementar MFA verify, o quitarlo del export.
|
||||
- Sub-archivos en `auth/consts.ts`.
|
||||
|
||||
4. **Sprint de tests** (≥1 semana, dependiendo de la profundidad):
|
||||
- Subir cobertura de `auth/test`, `perm/test`, `cach/test`, `fmts/test`
|
||||
y `fend/test` al nivel de `sium/test` y `stor/test`.
|
||||
|
||||
Después de eso el framework estaría sólido y listo para usuarios externos.
|
||||
Antes, el escaparate (lang/logr/sium/sess/stor) no refleja el estado real
|
||||
de los flancos de seguridad.
|
||||
|
||||
---
|
||||
|
||||
## Falsos positivos descartados
|
||||
|
||||
(Reportados por sub-agentes y verificados como incorrectos al leer el código.)
|
||||
|
||||
- **PKCE construcción incorrecta** (`oauth/pkce.ts`). El sub-agente afirmó
|
||||
que `hash(verifier)` no era SHA256/base64url. Verificado: `hashAuthToken`
|
||||
es `base64URL(sha256(token))`, lo cual es exactamente la transformación
|
||||
S256 de RFC 7636. La queja real es C6 (no se valida en callback), no la
|
||||
construcción.
|
||||
- **Refresh rotation no transaccional**. Verificado: `findRefreshTokenForUpdate`
|
||||
+ `rotateRefreshToken` están diseñados para correr atómicamente — el
|
||||
contrato lo asume y un adapter SQL real lo implementa. La queja real es
|
||||
C8 (memory adapter no documentado como inseguro).
|
||||
- **`stateHash as AuthFlowId` permite cualquier hash**. Verificado: el store
|
||||
tiene fallback explícito de búsqueda por stateHash; tipos sufren pero no
|
||||
hay bypass de seguridad. La queja válida es M3 (separar la API).
|
||||
- **Test directories vacíos** (conn, perm, etc.). Verificado: todos tienen
|
||||
≥1 archivo. La queja real es la cobertura desigual, no la ausencia.
|
||||
- **`adoptServer` SSR safety**. El sub-agente sugirió listener leak; el
|
||||
código (engine-session.ts) protege con guards `typeof BroadcastChannel`.
|
||||
- **`storage.adapter.removeItem` con `null`**. Reportado como riesgo; en
|
||||
realidad la API es estándar `Storage` y removeItem(key) sin valor.
|
||||
@ -1,108 +0,0 @@
|
||||
# Next Steps
|
||||
|
||||
Estado al cierre:
|
||||
|
||||
- Gate completo verde: `npm run test:all` -> `check` + `test` + `build` + `test:static` + `test:bundle`.
|
||||
- Suite unitaria verde: `npm test` -> 108 archivos, 1208 tests.
|
||||
- Typecheck verde: `npm run check` -> 0 errores, 0 warnings.
|
||||
- Build estatico verde: `npm run build`.
|
||||
- Static smoke verde: `npm run test:static` -> 6 assets/rutas generadas verificadas.
|
||||
- Bundle smoke verde: `npm run test:bundle` -> `createActiveApp({})` en 65.30 KB gzip, limite por defecto 70 KB via `ACTIVE_BUNDLE_GZIP_LIMIT_KB`.
|
||||
- `fmts` verde: `npx vitest run src/arts/fmts` -> 14 archivos, 39 tests.
|
||||
- `conn` verde: `npx vitest run src/arts/conn` -> 5 archivos, 28 tests.
|
||||
- `auth` verde: `npx vitest run src/arts/auth src/svrs/auth src/libs/auth` -> 4 archivos, 14 tests.
|
||||
- Refactor tecnico posterior:
|
||||
- `svrs/auth/engine-auth.ts` ya delega CSRF en `csrf-flow.ts`, coherente con password/session/recovery/device flows.
|
||||
- `svrs/auth/handlers.ts` delega helpers HTTP/CSRF/error-safe en `handler-runtime.ts`; conserva solo rutas y delegacion al engine.
|
||||
- `arts/aapp/active-app.svelte.ts` delega invalidacion Auth -> Permissions/Cache en `integrations/auth-cache.ts`.
|
||||
- `libs/cach/engine.ts` centraliza eventos de lectura con `emitForContext(...)`.
|
||||
- `arts/conn/connection.ts` usa `createConnectionIdFactory(...)` desde helpers.
|
||||
- `arts/conn/connection.ts` delega auth/request/ACK request-reply en `connection-requests.ts`.
|
||||
- `svrs/auth/adapters/memory-store.ts` queda como composition root de 39 lineas; la logica se reparte en stores internos de credentials, flows, linked accounts, sessions/devices, refresh y state/snapshot.
|
||||
- `arts/perm/client.ts` delega transporte HTTP/JSON en `client-http.ts`; el cliente queda centrado en cache, snapshot y fallback.
|
||||
- `arts/perm/client.ts` delega TTL, cache positiva, backoff de fallos e invalidacion por prefijo en `client-cache.ts`.
|
||||
- `arts/http/engine-http.ts` delega ejecucion de cada intento, hooks pre-request y diagnosticos request/network en `request-attempt.ts`.
|
||||
- `arts/timr/engine-timers.ts` delega `TimerHandle` cancel/reschedule en `timer-handle.ts`.
|
||||
- `arts/stor/engine-storage.ts` delega IDs de adapter, bus keys y suscripciones cross-tab en `adapter-registry.ts`.
|
||||
- `arts/stor/engine-storage.ts` delega el registro de defaults conflictivos en `defaults-registry.ts`.
|
||||
- `libs/cach/engine.ts` delega creacion/escritura/fetch-store de envelopes en `runtime-io.ts`.
|
||||
- `libs/cach/engine.ts` delega safe-delete y clear en `runtime-delete.ts`.
|
||||
- `arts/sess/engine-session.ts` delega la clasificacion `none/anonymous/identified` en `session-identity.ts`.
|
||||
- `arts/sess/engine-session.ts` delega snapshot, generation, dispatch, persistencia y commits en `session-state.ts`.
|
||||
- `arts/sess/engine-session.ts` delega la resolucion de revoke local/global/degradado en `session-revoke.ts`.
|
||||
- `arts/sess/engine-session.ts` delega refresh, stale-generation, validacion y diagnosticos de refresh en `session-refresh.ts`.
|
||||
- `arts/perm/client.ts` delega claves/scope de cache en `client-keys.ts` y lectura de snapshot en `client-snapshot.ts`.
|
||||
- `arts/stor/engine-storage.ts` delega lectura/escritura/validacion/migracion de entradas en `entry-runtime.ts`.
|
||||
- `arts/stor/entry-runtime.ts` delega defaults, serializer, validate y merge-defaults en `entry-values.ts`.
|
||||
- `arts/timr/engine-timers.ts` delega armado nativo, ejecucion, finalizacion e intervalos en `timer-runner.ts`.
|
||||
- `arts/cach/active-cache.svelte.ts` delega la entry reactiva en `active-cache-entry.svelte.ts` y normalizacion de errores en helper compartido.
|
||||
- `arts/auth/active-auth.svelte.ts` delega CSRF, POST autenticados, validacion de respuestas, normalizacion de errores e invalidacion de cache en `active-auth-runtime.ts`.
|
||||
- `arts/conn/connection.ts` delega apertura/cierre logico e `isConnected` en `connection-lifecycle.ts`.
|
||||
- `arts/conn/connection.ts` delega la programacion de reconnect y exhaustion en `connection-reconnect-runtime.ts`.
|
||||
- `libs/perm/evaluator.ts` delega helpers puros de resultado, dependencias, cadenas logicas y comparacion en `evaluator-helpers.ts`.
|
||||
- `arts/http/engine-http.ts` delega la validacion preflight de `bodySchema` en `request-validation.ts`.
|
||||
- `arts/http/engine-http.ts` delega la resolucion final de response/error en `response-resolution.ts`.
|
||||
- `arts/sium/core/pipe.ts` queda centrado en composicion; factories `refine/transform/codec/meta` viven en `steps.ts`.
|
||||
- `libs/cach/engine.ts` delega lectura `get()` en `runtime-get.ts`.
|
||||
- `libs/cach/engine.ts` delega escritura `set()` en `runtime-set.ts`.
|
||||
- `libs/cach/engine.ts` delega `query()` en `runtime-query.ts`; conserva composition root para context/io/delete/invalidate/mutate/explain.
|
||||
- `arts/sium/engine-sium.ts` delega resolucion Lang/fallback e issues en `engine-resolver.ts`.
|
||||
- `arts/sium/engine-sium.ts` delega wrappers `validate/validateSync` y diagnostico de validacion en `engine-validation.ts`.
|
||||
- `arts/conn/connection.ts` delega decode/routing de frames entrantes en `connection-message-router.ts`.
|
||||
- `arts/conn/connection.ts` delega el intento open/auth/flush/join en `connection-connect.ts`.
|
||||
- `arts/conn/connection.ts` delega current transport, attach/detach y close en `connection-transport-runtime.ts`.
|
||||
- `arts/conn/connection.ts` delega cierre intencional y close-event handling en `connection-close.ts`.
|
||||
- `arts/conn/connection.ts` delega disposed/reuse/singleflight de connect en `connection-connect-controller.ts`.
|
||||
- `arts/timr/engine-timers.ts` delega fan-out de listeners y diagnostico de listeners en `timer-events.ts`.
|
||||
- `arts/timr/engine-timers.ts` delega la construccion de entradas internas en `timer-entry.ts`.
|
||||
- `arts/timr/engine-timers.ts` delega cancelacion y seleccion por scope en `timer-cancel.ts`.
|
||||
- `arts/timr/engine-timers.ts` delega entrada viva, contexto de task y max-runs en `timer-entry.ts`.
|
||||
- `libs/color/segments.ts` reutiliza helpers locales para canales requeridos y parsing por rango RGB/HSL.
|
||||
- `libs/days/segments.ts` centraliza el formateo de hora 12h y mantiene imports agrupados.
|
||||
- Integracion total ampliada: `Auth.signOut()` valida anonimizacion, invalidacion de `Permissions` y evento `Cache.invalidate`.
|
||||
- Tanda focalizada verde: `npx vitest run src/arts/conn src/libs/cach src/arts/cach src/svrs/cach src/arts/auth src/svrs/auth src/libs/auth src/arts/aapp/test/ecosystem.integration.test.ts` -> 19 archivos, 82 tests.
|
||||
- `/test/ecosystem` revisado en navegador: carga sin errores de consola, `ar` cambia a `rtl`, Formats se actualiza por locale, Perm cambia con rol `viewer`, Cach re-scopea por locale y Conn loopback publica/recibe.
|
||||
- `src/arts/aapp/test/ecosystem.integration.test.ts` ampliado para cubrir rol `viewer` no-allow y cache re-scoped por locale.
|
||||
- Referencias residuales de marca anterior eliminadas de `src/` fuera de rutas temporales: docs, páginas de test y constantes de cookies/headers auth usan ahora `Active/active`.
|
||||
- `src/arts/conn/README.md` ampliado: contrato de raíz/conexión/canal, estados, transportes, request/reply, reconnect, heartbeat, sesión, diagnostics/logger, errores y testing.
|
||||
- `src/arts/fmts/README.md` ampliado con guia de uso, `LocaleSource`, contrato auto/manual, submodulos, listeners, integracion con `aapp` y tests.
|
||||
- `fmts` redujo boilerplate activo con helpers `readFrom` / `writeTo` y los engines comparten directamente las funciones de `createFormatsLocaleState`; APIs públicas sin cambios.
|
||||
- `src/web/routes/temp/` corregido: scripts tipados, warnings Svelte eliminados y compatible con `npm run check`.
|
||||
- `aapp` integration reforzado: `Connections` creadas antes de `Sess` reciben eventos posteriores de sesion y cierran en revoke.
|
||||
- `aapp` composition reforzado: `Frontend.dir` reacciona a locale solo mientras esta en `auto`; los overrides manuales no se pisan.
|
||||
- `src/arts/fend/README.md` ampliado: API, composicion via App, contrato auto/manual, locale/dir, salida DOM, integracion `adom`, persistencia, SSR y tests.
|
||||
- `src/arts/logr/README.md` aclara contrato comun `Logger` en `$libs/logr` y `Diagnostics` como capa catalogada encima del logger, sin mini-loggers por modulo.
|
||||
- Checklist `before_0_1.md` alineada con el estado real: CI sin lint global de momento, `test:all` como gate de release, presupuesto de bundle en 70 KB gzip y docs de versionado/public surface.
|
||||
- Nuevo smoke estatico `scripts/static-smoke.mjs` integrado en CI y `test:all`; verifica landing docs, instalacion, AI agents, security, `/test/ecosystem` y manifest.
|
||||
- Docs de `/active/get-started/installation`, `/versioning` y `/ai-agents` actualizadas con vocabulario de gates (`check`, `test`, `build`, `test:static`, `test:bundle`, `test:all`).
|
||||
- Tests de superficie pública en `src/arts/aapp/test/active-app.test.ts` cubren roots always-present y factorias scoped.
|
||||
- Tests de `perm` cubren stale `batch()` y `what()` tras cambio de actor/snapshot.
|
||||
- `svrs/perm` tiene contrato DB operativo: `loadActivePermissionPolicies`, `createPermissionDatabaseProviders`, repositorios tipados para políticas/relaciones, tests focales y SQL PostgreSQL reforzado con target generated columns, índice por target, unique active-version y check de effect en audit.
|
||||
- Docs de `svrs/perm` y `/active/docs/perm` explican tablas, lifecycle de políticas, provider de relaciones, tenant resolution, wiring server y audit de decisiones.
|
||||
- `svrs/cach/README.md` creado: documenta boundary server/client, API de `EngineCache`, scopes seguros, policies, invalidacion por epochs, integracion auth/sess/perm, contrato de adapters, observabilidad, seguridad y tests.
|
||||
- `svrs/auth` tiene SQL PostgreSQL de referencia en `src/svrs/auth/sql/postgres.sql` y README ampliado con mapping `AuthRepository`, tablas, reglas de produccion, refresh rotation con locks y limpieza periodica.
|
||||
- `/active/docs/auth` y `/active/docs/cach` reflejan ahora las piezas server: persistencia DB de auth, tablas de referencia, `createDbAuthAdapter`, boundary cache server/client y modelos de adapter.
|
||||
- `before_0_1.md` alineado con evidencias actuales: S1/S2/S3/S4/S5/S6/S7 y A1/A2/A3/A4/A5 quedan cerrados; `SECURITY.md` cubre cookie scopes, CSRF, refresh rotation, OAuth state/PKCE, MFA actual y actor/tenant model.
|
||||
- `before_0_1.md` tambien marca como cerrados repo hygiene, build/tooling y assets de marca segun ficheros reales (`README`, `SECURITY`, `CONTRIBUTING`, `CHANGELOG`, `.github`, `static/*`, `app.html`).
|
||||
- `.gitignore` evita que estado local de `.claude/`, `.opencode/` y `.idea/vcs.xml` entre accidentalmente en commits.
|
||||
- Memory adapters de auth/cache emiten warning productivo deduplicado; los harness de test lo suprimen donde corresponde.
|
||||
- `cach` expone `defaultMemoryAdapter` para configurar solo el adapter memory implícito cuando no se pasa `adapter`; las rutas `/test/aapp`, `/test/cach`, `/test/conn`, `/test/ecosystem` y `/test/perm` lo usan para silenciar el warning productivo de forma explícita en demos/prerender sin apagarlo para apps reales.
|
||||
- Validación focal posterior al ajuste de `cach`: `npm run check`, `npx vitest run src/libs/cach src/svrs/cach src/arts/cach src/arts/aapp/test/active-app.test.ts`, `npm run build` y `npm run test:static` verdes.
|
||||
- Documentación de `buss` corregida para reflejar el código real: `App.Bus` central, contratos en `$libs/buss`, engine en `$buss`, eventos públicos vía `publishApp*`/`onApp*`, `BusEnvelope` real (`at`, no CloudEvents puro) y sin sección obsoleta de gaps ya implementados.
|
||||
- Tests de `aapp`/`conn` normalizados para publicar `APP_EVENT_USER_IDENTITY_CHANGED` mediante `publishAppUserIdentityChanged(...)`, de modo que la suite ejercita las mismas guardas de runtime/payload que la app.
|
||||
- Validación focal posterior al ajuste de `buss`: `npx vitest run src/arts/buss/test/engine-bus.test.ts src/libs/aapp/test/events.test.ts src/arts/aapp/test/active-app.test.ts src/arts/aapp/test/session-translator.test.ts src/arts/aapp/test/ecosystem.integration.test.ts src/arts/conn/test/connection.test.ts` -> 6 archivos, 74 tests verdes; `npm run check` verde.
|
||||
- Integración total ampliada con un caso `permissionsRefresh + tenantSwitched + locale` mientras una conexión de chat sigue abierta: `permissionsRefresh` invalida solo Permissions, `tenantSwitched` limpia Permissions + Cache sin reautenticar el socket, y el cambio de locale re-scopea Cache/Formats/Frontend sin reauth.
|
||||
- Integración total ampliada con un caso de webhook/realtime de permisos: `conn` recibe `permissions.changed`, la app publica `APP_EVENT_PERMISSIONS_REFRESH_REQUESTED` mediante helper seguro, `Permissions` invalida por opt-in, descarta respuestas stale en vuelo y `Cache`/`Connections` no hacen side-effects destructivos.
|
||||
- Integración total ampliada con un caso de revocación remota de sesión por realtime: `conn` recibe `session.revoked`, la app revoca `Sess`, `aapp` publica cambio de identidad, `Permissions`/`Cache` limpian estado identity-scoped y el chat queda cerrado sin reutilizar credenciales.
|
||||
- Validación focal posterior al nuevo caso compuesto: `npx vitest run src/arts/aapp/test/ecosystem.integration.test.ts` -> 9 tests verdes; `npx vitest run src/arts/aapp/test/ecosystem.integration.test.ts src/arts/aapp/test/active-app.test.ts src/arts/buss/test/engine-bus.test.ts src/libs/aapp/test/events.test.ts src/arts/conn/test/connection.test.ts src/arts/cach src/arts/perm` -> 7 archivos, 90 tests verdes; `npm run check` verde.
|
||||
- No commitear `.idea/`, `.claude/` ni `.opencode/`.
|
||||
- No commitear ni tocar `src/web/routes/temp/` salvo peticion explicita; hay cambios locales en `temp/c2` que quedan fuera del commit de cierre.
|
||||
|
||||
Pendiente para manana:
|
||||
|
||||
- Revisar documentacion restante de todos los modulos con ojo de consumidor externo: API real, factories, metodos, opciones, errores, ejemplos, dinamicas auto/manual y errores comunes.
|
||||
- Prioridad especial siguiente: decidir si los SQL de referencia de `auth`/`perm` son cierre suficiente para `0.1` o si hace falta un adapter ejecutable para un ORM concreto.
|
||||
- Continuar la reduccion de archivos grandes: prioridad `conn/connection.ts`, `svrs/auth/engine-auth.ts`, helpers de `cach` y piezas repetidas en docs/test harness.
|
||||
- Ampliar tests de integracion cruzada: `auth + sess + perm + cach + http + stor + fmts + conn + timr + logr`, incluyendo login/logout, cambio de actor, invalidacion cache, cambio locale, permisos y reconnect.
|
||||
- Revisar la adopcion final del contrato comun `Logger` / diagnostics en todos los modulos, sin acoplar artefactos a `arts/logr`; primera pasada limpia salvo `aapp` como composition root, `arts/logr` y tests.
|
||||
- Mantener `npm run test:all` como gate regular antes de commits grandes; `npm run lint` sigue siendo deuda global separada, no meter nueva deuda en archivos tocados.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,424 @@
|
||||
# Ecosistema v2 - mejoras para completar cada modulo
|
||||
|
||||
Fuente usada: codigo, configuracion, tests, scripts y estructura real del repo. No se usa documentacion antigua como base.
|
||||
|
||||
## Lectura correcta
|
||||
|
||||
La version 2.0 no debe buscar "componentes v2 existentes", porque no hay una capa v2 ya montada. Lo que hay que definir es que le falta a cada modulo para que el ecosistema sea completo, usable y componible por encima de la 1.0.
|
||||
|
||||
La 1.0 debe cerrar estabilidad, build, typecheck, tests y API publica. La 2.0 debe convertir esos motores en una plataforma completa: componentes UI, devtools, presets, integraciones, adapters reales, contratos tipados y flujos de producto.
|
||||
|
||||
## Principios de v2
|
||||
|
||||
1. No reescribir nucleos que ya estan maduros. `sium` y `http`, por ejemplo, necesitan capas superiores e integraciones, no una reconstruccion del core.
|
||||
2. Separar producto de demo/test/docs. Lo reutilizable debe vivir en `src/uix`, `src/arts`, `src/libs` o `src/svrs`, no en rutas de prueba.
|
||||
3. Cada modulo debe exponer contrato publico, tests propios, fixtures, diagnostico y ejemplos minimos.
|
||||
4. Las integraciones entre modulos deben ser oficiales: cache + http, sium + uix, perm + uix, prefs + frontend, logger + orca, storage + prefs/cache/session.
|
||||
5. v2 debe traer experiencia de usuario y de desarrollador: devtools, inspectores, playgrounds, generadores y presets.
|
||||
|
||||
## Prioridades globales
|
||||
|
||||
### V2-P0 - Base antes de construir encima
|
||||
|
||||
- Cerrar los errores actuales de `npm run check` y `npm run build`.
|
||||
- Eliminar usos de API vieja en rutas demo/test.
|
||||
- Corregir scripts obsoletos, especialmente aliases antiguos del smoke bundle.
|
||||
- Alinear Node local/CI y ampliar CI con `lint` y `test:aliases`.
|
||||
- Congelar barrels publicos por modulo y marcar APIs internas.
|
||||
|
||||
### V2-P1 - Capa de producto
|
||||
|
||||
- Convertir `src/uix` en la libreria oficial de componentes.
|
||||
- Crear componentes que usen los motores reales del ecosistema.
|
||||
- Sustituir demos acopladas por playgrounds que consuman APIs publicas.
|
||||
- Definir una `ActiveApp` canonica para ejemplos, docs y smoke tests.
|
||||
|
||||
### V2-P2 - Integraciones y adapters reales
|
||||
|
||||
- Async storage.
|
||||
- Cache persistente.
|
||||
- HTTP con cache/dedupe/retry/offline.
|
||||
- Auth/session/perm con UI y flujos completos.
|
||||
- Observabilidad transversal con logger/orca/bus/timer.
|
||||
|
||||
### V2-P3 - Devtools
|
||||
|
||||
- Inspector de `ActiveApp`.
|
||||
- Timeline de bus/orca/logger/timer.
|
||||
- Inspector de cache/storage/session.
|
||||
- Simulador de permisos.
|
||||
- Generador de formularios desde `sium`.
|
||||
|
||||
## Roadmap por modulo
|
||||
|
||||
### active-app
|
||||
|
||||
Estado: el nucleo ya compone servicios con dependencias, lazy/immediate y disposal ordenado. El problema v2 es hacerlo mas tipado, visible y extensible.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- `AppSchema` exportable para que componentes y rutas no reciban `ActiveApp` generico con servicios `unknown`.
|
||||
- Presets oficiales: `createClientApp`, `createServerApp`, `createDemoApp`, `createTestApp`.
|
||||
- Registro de plugins/servicios con manifiesto de capacidades.
|
||||
- Health graph de servicios: iniciado, lazy, error, disposed, dependencias y tiempo de arranque.
|
||||
- Devtools de `ActiveApp` para inspeccionar servicios, dependencias y eventos de lifecycle.
|
||||
- Migrador o capa compat temporal para detectar API vieja con errores claros.
|
||||
|
||||
### uix
|
||||
|
||||
Estado: `src/uix` existe pero esta vacio. Es el hueco mas grande del ecosistema.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Primitivas base: `Button`, `Input`, `Select`, `Checkbox`, `Switch`, `Dialog`, `Popover`, `Tabs`, `Tooltip`, `Toast`, `Table`, `Drawer`, `Menu`, `Command`.
|
||||
- Componentes de formulario conectados a `sium`: `Form`, `Field`, `FieldGroup`, `AutoFields`, `ErrorSummary`, `SubmitBar`.
|
||||
- Componentes de autorizacion conectados a `perm`: `Can`, `Cannot`, `Gate`, `PermissionBoundary`, `RoleBadge`.
|
||||
- Componentes de sesion/auth: `LoginForm`, `SessionMenu`, `DeviceList`, `MfaPanel`, `AuthGuard`.
|
||||
- Componentes operativos: `CacheInspector`, `LogViewer`, `ConnectionStatus`, `PrefsPanel`, `StorageBrowser`, `OrcaTimeline`.
|
||||
- Contrato visual comun con `frontend`, `prefs`, `adom`, `lang` y `format`.
|
||||
|
||||
### sium
|
||||
|
||||
Estado: core potente, con introspeccion (`widget`, `widgetOptions`, `kind`, `wrappers`, `effects`, `shape`) y muchas pruebas. No necesita rehacer el nucleo.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Renderer oficial de formularios en `uix` usando `Form.AutoFields`.
|
||||
- Registro de widgets por tipo de schema.
|
||||
- Validacion async y validacion remota con estados de pending/cancelacion.
|
||||
- Serializacion de schemas para compartir cliente/servidor.
|
||||
- Generador de formularios, filtros y tablas desde metadata.
|
||||
- Inspector visual de schema/result/issues para depuracion.
|
||||
- Paquetes de wrappers comunes: password, money, date range, file, tags, address, permission selector.
|
||||
|
||||
### http
|
||||
|
||||
Estado: core suficiente y bien aislado por puertos. No necesita una reescritura v2.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Cliente tipado por endpoint, compatible con schemas de `sium` o contratos declarativos.
|
||||
- Integracion oficial con cache: dedupe, stale-while-revalidate, invalidacion y tags.
|
||||
- Retry/backoff/circuit breaker como presets, no como codigo repetido por consumidor.
|
||||
- Upload/download con progreso, cancelacion y resume.
|
||||
- Offline queue opcional usando storage/connection.
|
||||
- Interceptores de auth/session y trace ids de logger.
|
||||
- Mock server/record-replay para tests y demos.
|
||||
|
||||
### auth
|
||||
|
||||
Estado: servidor amplio, pero con errores de export/import y poca prueba directa en `libs/auth`.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- MFA estable: TOTP, recovery codes, WebAuthn/passkeys.
|
||||
- OAuth/OIDC adapters oficiales.
|
||||
- Passwordless/magic-link.
|
||||
- Administracion de sesiones y dispositivos.
|
||||
- Auditoria de eventos de seguridad.
|
||||
- UI en `uix`: login, register, reset password, MFA, device management.
|
||||
- Contratos compartidos cliente/servidor con tests directos de `libs/auth`.
|
||||
- Politicas configurables: lockout, password policy, token rotation, session binding.
|
||||
|
||||
### session
|
||||
|
||||
Estado: ciclo de vida trabajado, con integracion logger/bus. Falta llevarlo a producto completo.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Multi-session/profile switcher.
|
||||
- Sincronizacion cross-tab con leader election.
|
||||
- Bootstrap SSR/cliente bien tipado.
|
||||
- Estrategias de refresh enchufables.
|
||||
- Estado de sesion visible para UI: loading, refreshing, expired, degraded.
|
||||
- Integracion con auth, perm, storage y logger.
|
||||
- Panel de sesiones activas y cierre remoto.
|
||||
|
||||
### perm
|
||||
|
||||
Estado: motor y servidor existen; el cliente tiene solo un componente reusable claro (`Can.svelte`) y tests finos.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Familia UI: `Can`, `Cannot`, `Gate`, `PermissionBoundary`, `PermissionExplain`.
|
||||
- Simulador de permisos para usuarios/roles/tenants.
|
||||
- DSL o builder de politicas.
|
||||
- Plantillas de roles y permisos por dominio.
|
||||
- Batch prefetch y cache de permisos.
|
||||
- Explicabilidad: por que se permite o deniega una accion.
|
||||
- Integracion PEP/PDP clara para cliente, servidor y rutas.
|
||||
|
||||
### cache
|
||||
|
||||
Estado: runtime avanzado, pero hay una inversion de capas porque `arts/cache` importa desde `svrs/cache`.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Corregir frontera: motor compartido en `libs/cache` o separar claramente cliente/servidor.
|
||||
- Adapters persistentes con `storage`.
|
||||
- Integracion oficial con `http`.
|
||||
- Invalidacion por tags, dependencias y eventos de `bus`.
|
||||
- Prefetch, optimistic updates y rollback.
|
||||
- Cache inspector en `uix`.
|
||||
- Metricas: hit rate, stale, evictions, memory, errores de adapter.
|
||||
|
||||
### storage
|
||||
|
||||
Estado: adapters sincronicos. El propio codigo deja async adapters para v2.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Contrato async para IndexedDB, OPFS y backends remotos.
|
||||
- Migraciones versionadas de datos.
|
||||
- Namespaces y cuotas por modulo.
|
||||
- Estrategias de eviction.
|
||||
- Cifrado opcional para datos sensibles, dejando claro que storage no es una caja fuerte.
|
||||
- Sincronizacion con session/prefs/cache.
|
||||
- Inspector de storage en devtools.
|
||||
|
||||
### prefs
|
||||
|
||||
Estado: preparado para persistencia y preferencias, con senales de pending/error para async.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Hydration async con estados oficiales.
|
||||
- Sincronizacion entre tabs y perfiles.
|
||||
- Presets de preferencias por usuario/equipo.
|
||||
- Merge server/client con resolucion de conflictos.
|
||||
- Registro de preferencias por modulo.
|
||||
- UI `PrefsPanel` generada desde metadata.
|
||||
- Integracion completa con frontend/lang/format/storage.
|
||||
|
||||
### frontend
|
||||
|
||||
Estado: servicio de estado visual; hay desalineacion con density de prefs.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Sistema de design tokens: color, spacing, radius, shadow, typography, z-index.
|
||||
- Theming SSR-safe para evitar saltos de primer render.
|
||||
- Density unificada con prefs.
|
||||
- Direccion LTR/RTL y reduced motion como contrato transversal.
|
||||
- Breakpoints y viewport state publicos.
|
||||
- Registro de temas y paquetes visuales.
|
||||
- Integracion con `uix` como consumidor principal.
|
||||
|
||||
### adom
|
||||
|
||||
Estado: utilidades DOM, viewport, scroll/focus; buen candidato para sostener la capa UI.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Layer manager para dialog/popover/toast.
|
||||
- Focus trap, focus restore e inert.
|
||||
- Portal manager.
|
||||
- Observers oficiales: resize, intersection, mutation.
|
||||
- Scroll restoration por ruta/panel.
|
||||
- Helpers ARIA y keyboard navigation.
|
||||
- Test harness de accesibilidad para componentes `uix`.
|
||||
|
||||
### lang
|
||||
|
||||
Estado: basico y con poca cobertura directa.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Namespaces lazy por modulo.
|
||||
- Reporte de claves faltantes y claves no usadas.
|
||||
- Pseudo-locale para detectar textos rotos.
|
||||
- MessageFormat 2.0 / MF2 como contrato moderno de mensajes, plurales, selectores y variantes.
|
||||
- Extraccion de mensajes desde codigo.
|
||||
- Bundles por idioma/modulo.
|
||||
- Integracion con `sium` para mensajes de validacion y con `uix` para labels.
|
||||
|
||||
### format
|
||||
|
||||
Estado: bastante completo en numeros, fechas, monedas y unidades.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Relative time.
|
||||
- ListFormat, DisplayNames y segmentos localizados.
|
||||
- Perfiles de unidades por dominio.
|
||||
- Conversiones de unidades donde tenga sentido.
|
||||
- Integracion con timezone/calendar.
|
||||
- Formatters declarativos para tablas/formularios.
|
||||
- Cache de formatters coordinada con lang/prefs.
|
||||
|
||||
### logger
|
||||
|
||||
Estado: motor fuerte y grande. Falta explotarlo como observabilidad de plataforma.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Trace context compatible con `http`, `orca`, `bus` y `session`.
|
||||
- Redaction policies para datos sensibles.
|
||||
- Sampling y niveles por modulo.
|
||||
- Transport health y backpressure.
|
||||
- Live log viewer en `uix`.
|
||||
- Export a OpenTelemetry o formato compatible.
|
||||
- Correlacion de errores de build/runtime/devtools.
|
||||
|
||||
### bus
|
||||
|
||||
Estado: bus central solido.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Catalogo tipado de eventos por modulo.
|
||||
- Validacion de payloads con schemas.
|
||||
- Replay/recording para depuracion.
|
||||
- Bridges cross-tab y servidor.
|
||||
- Timeline visual en devtools.
|
||||
- Politicas de error: retry, dead letter, fallback.
|
||||
- Integracion con orca para orquestaciones declarativas.
|
||||
|
||||
### timer
|
||||
|
||||
Estado: servicio estable y probado.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Schedulers nombrados por prioridad.
|
||||
- Cron/calendar schedules.
|
||||
- Timers persistentes.
|
||||
- Politica para background tabs.
|
||||
- Integracion con performance budgets.
|
||||
- Timer inspector en devtools.
|
||||
- Coordinacion con orca para tareas cancelables.
|
||||
|
||||
### orca
|
||||
|
||||
Estado: engine avanzado. Ya hay senales de roadmap v2 como `replace-current`.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Politica `replace-current` / takeLatest con abort real.
|
||||
- Visual timeline de eventos, acciones, fan-in y cancelaciones.
|
||||
- Validacion de grafos/orquestaciones antes de runtime.
|
||||
- Presets de orquestacion por dominio.
|
||||
- Persistencia/replay de traces.
|
||||
- Integracion con logger/bus/timer/http.
|
||||
- Mercado interno de actions reutilizables.
|
||||
|
||||
### connection
|
||||
|
||||
Estado: modulo grande y avanzado, con tests y refactors recientes.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Canales multiplexados.
|
||||
- Presence y heartbeats de estado.
|
||||
- Offline queue con storage.
|
||||
- Backpressure y flow control.
|
||||
- Reauth/rekeying de conexiones vivas.
|
||||
- Negociacion de protocolo/version.
|
||||
- Request/reply validado con schemas.
|
||||
- Dashboard de reconnect, latencia, cola y errores.
|
||||
|
||||
### svrs/auth
|
||||
|
||||
Estado: servidor amplio, pero con problemas actuales de imports/exports.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Handlers SvelteKit canonicos.
|
||||
- Adapters DB oficiales.
|
||||
- Migraciones y seeds.
|
||||
- Hooks de auditoria y logger.
|
||||
- OpenAPI o contratos HTTP generables.
|
||||
- Test matrix por flujo: login, refresh, logout, MFA, recovery, device revoke.
|
||||
|
||||
### svrs/cache
|
||||
|
||||
Estado: existe, pero su frontera con `arts/cache` esta contaminada.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Separar motor comun de servidor.
|
||||
- Adapters para Redis, memory, KV y edge.
|
||||
- Invalidation bus server-side.
|
||||
- Metrics endpoint.
|
||||
- Politicas multi-tenant.
|
||||
- Contract tests compartidos con cliente.
|
||||
|
||||
### svrs/perm
|
||||
|
||||
Estado: servidor funcional con cobertura limitada.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- PDP server-side explicable.
|
||||
- Adapters DB.
|
||||
- Policy migrations.
|
||||
- Batch authorization endpoint.
|
||||
- Audit log de decisiones.
|
||||
- Herramientas para simular usuario/tenant/recurso.
|
||||
|
||||
### libs
|
||||
|
||||
Estado: muchas piezas compartidas, barrels y contratos; algunos nucleos grandes tienen poca prueba directa.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Separar `public` e `internal`.
|
||||
- API snapshots para barrels publicos.
|
||||
- Contract tests por adapter.
|
||||
- Fixtures reutilizables.
|
||||
- Versionado semantico interno por modulo.
|
||||
- Limpieza de exports ambiguos.
|
||||
- Mayor cobertura directa en `libs/auth`, `libs/cache`, `libs/perm`, `libs/lang`.
|
||||
|
||||
### web
|
||||
|
||||
Estado: mezcla docs, demo, test pages y rutas temporales. No debe confundirse con producto.
|
||||
|
||||
Mejoras v2:
|
||||
|
||||
- Docs generadas desde metadata real de modulos.
|
||||
- Playground unico con `ActiveApp` canonica.
|
||||
- Route smoke tests.
|
||||
- Matrix visual de modulos, servicios, dependencias y estado.
|
||||
- Demos que consuman `uix` y APIs publicas, no implementaciones internas.
|
||||
- Separar claramente `/docs`, `/playground`, `/test` y `/demo`.
|
||||
|
||||
## Integraciones clave de v2
|
||||
|
||||
### Formularios completos
|
||||
|
||||
`sium` define schema e introspeccion. `uix` renderiza. `lang` traduce. `format` presenta valores. `prefs/frontend` aplican densidad/tema. `http` envia. `logger` traza. `storage` guarda borradores.
|
||||
|
||||
Resultado esperado: formularios complejos generados, validables, localizados, accesibles y testeables.
|
||||
|
||||
### Seguridad completa
|
||||
|
||||
`auth` autentica. `session` mantiene estado. `perm` decide autorizacion. `uix` muestra guards y explicaciones. `logger` audita. `storage` conserva solo lo permitido.
|
||||
|
||||
Resultado esperado: flujos reales de login, MFA, sesiones, roles, permisos y auditoria.
|
||||
|
||||
### Datos offline/cacheados
|
||||
|
||||
`http` habla con servidor. `cache` deduplica e invalida. `storage` persiste. `connection` informa red/offline. `bus` propaga eventos. `orca` coordina flujos.
|
||||
|
||||
Resultado esperado: operaciones robustas con retry, offline queue, cache visible y rollback.
|
||||
|
||||
### Observabilidad
|
||||
|
||||
`logger`, `bus`, `timer`, `orca`, `connection`, `http` y `cache` comparten trace ids.
|
||||
|
||||
Resultado esperado: devtools capaces de explicar que paso, cuando, por que fallo y que modulo participo.
|
||||
|
||||
## Resultado esperado de Ecosistema 2.0
|
||||
|
||||
La v2 deberia dejar de ser solo una coleccion de motores y convertirse en una plataforma integrada:
|
||||
|
||||
- Componentes UI reales en `uix`.
|
||||
- Motores estables en `arts`.
|
||||
- Contratos compartidos en `libs`.
|
||||
- Servidores/adapters en `svrs`.
|
||||
- Demos y docs que consumen APIs publicas.
|
||||
- Devtools para inspeccionar el sistema entero.
|
||||
- Integraciones oficiales entre modulos.
|
||||
|
||||
El mayor salto no esta en tocar un modulo aislado. Esta en completar las capas que faltan entre ellos: UI, adapters, presets, diagnostico, contratos publicos e integracion transversal.
|
||||
@ -1,698 +0,0 @@
|
||||
> ARCHIVED — historical brainstorming. orca v1 is shipped. Items
|
||||
> listed below as "out of v0" (commit / replace / timeouts /
|
||||
> transaction / compensate / parallel / abortOn unless / tokens con
|
||||
> payload / fan-in) all live in the engine today. The current
|
||||
> contract is `src/arts/orca/README.md`; this file is preserved as a
|
||||
> record of the design conversation, not as guidance.
|
||||
|
||||
Clippy-pet verdict: `orca` es una ballena preciosa, pero si intentamos montarla entera en v0 nos hunde la barca.
|
||||
|
||||
Lo correcto:
|
||||
|
||||
```txt
|
||||
v0 = kernel
|
||||
v1 = enterprise orchestration
|
||||
```
|
||||
|
||||
`orca` está bien pensado porque tiene tres aciertos enormes:
|
||||
|
||||
- Desacopla módulos: `orca` no sabe qué es `cach`, `perm`, `sess` o `connection`.
|
||||
- Convierte flujos críticos en artefactos trazables: `OrcaRunResult`.
|
||||
- Usa tokens como hechos semánticos per-run, que es la parte diferencial.
|
||||
|
||||
Pero ahora mismo el README mezcla kernel y futuro. Para que sea implementable, v0 debería recortarse así:
|
||||
|
||||
```txt
|
||||
entra en v0:
|
||||
setupOrca
|
||||
createEngineOrca
|
||||
App.Orchestration inerte
|
||||
onEvent
|
||||
stages
|
||||
queue/drop
|
||||
tokens per-run flag
|
||||
after/provides
|
||||
OrcaResult success/skipped/error/fatal/interrupted
|
||||
OrcaRunResult
|
||||
validate()
|
||||
diagnostics
|
||||
|
||||
sale de v0:
|
||||
commit()
|
||||
replace
|
||||
timeouts
|
||||
transaction
|
||||
compensate
|
||||
parallel
|
||||
abortOn/unless avanzados si complican
|
||||
tokens con payload
|
||||
fan-in
|
||||
```
|
||||
|
||||
La pregunta decisiva no es “¿está bien diseñado?”. Sí, lo está.
|
||||
|
||||
La pregunta correcta es: **¿cuál es el núcleo mínimo que demuestra que `orca` merece existir?**
|
||||
|
||||
Respuesta: `event -> staged actions -> tokens -> result trace`.
|
||||
|
||||
Si eso funciona, lo demás se puede crecer con calma.
|
||||
|
||||
|
||||
como evitamos la reentrada en orca , si no establecemos en el payload del evento los eventos que le preceden en el stack ? al disparar el componente verifica si esta gestionando ya un evento de ese stack y lo omitiria , como lo suelen hacer el evitar loops infinitos ?
|
||||
|
||||
|
||||
La forma sana no es meter “eventos precedentes” en el payload de negocio. Eso contaminaría todos los eventos con metadata de orquestación y acabaría mezclando dominio con runtime.
|
||||
|
||||
Lo correcto es que `orca` envuelva cada evento en un **OrcaEnvelope** interno:
|
||||
|
||||
```ts
|
||||
interface OrcaEnvelope<TPayload = unknown> {
|
||||
readonly event: string;
|
||||
readonly payload: TPayload;
|
||||
readonly meta: {
|
||||
readonly eventId: string;
|
||||
readonly parentEventId?: string;
|
||||
readonly runId?: string;
|
||||
readonly traceId: string;
|
||||
readonly depth: number;
|
||||
readonly stack: readonly string[];
|
||||
readonly emittedBy?: OrcaActionId;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
El payload sigue limpio:
|
||||
|
||||
```ts
|
||||
{ previousActorId, nextActorId }
|
||||
```
|
||||
|
||||
Y `orca` conserva el contexto:
|
||||
|
||||
```ts
|
||||
{
|
||||
event: APP_EVENT_CACHE_CLEARED,
|
||||
meta: {
|
||||
parentEventId: 'evt-1',
|
||||
traceId: 'trace-7',
|
||||
depth: 2,
|
||||
stack: [
|
||||
APP_EVENT_USER_IDENTITY_CHANGED,
|
||||
APP_EVENT_CACHE_CLEARED
|
||||
],
|
||||
emittedBy: ORCA_ACTION_CLEAR_PRIVATE_CACHE
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Entonces la prevención de loops vive en `orca`, no en los módulos.
|
||||
|
||||
Reglas típicas para evitar loops infinitos:
|
||||
|
||||
```txt
|
||||
1. No ejecutar inline eventos emitidos durante un run.
|
||||
2. Encolarlos con traceId y parentEventId.
|
||||
3. Mantener stack interno por trace.
|
||||
4. Rechazar o saltar eventos si superan maxDepth.
|
||||
5. Rechazar o saltar si el mismo evento aparece demasiado en el mismo trace.
|
||||
6. Aplicar dedupeKey opcional.
|
||||
7. Tener maxEventsPerTrace.
|
||||
8. Tener diagnostics cuando se corta el loop.
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```ts
|
||||
const Orca = createEngineOrca({
|
||||
bus,
|
||||
timers,
|
||||
logger,
|
||||
reentry: {
|
||||
maxDepth: 16,
|
||||
maxEventsPerTrace: 128,
|
||||
repeatedEventPolicy: ORCA_REENTRY_SKIP,
|
||||
repeatedEventLimit: 2
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Si ocurre esto:
|
||||
|
||||
```txt
|
||||
A -> action publica B
|
||||
B -> action publica A
|
||||
A -> action publica B
|
||||
...
|
||||
```
|
||||
|
||||
`orca` ve el trace:
|
||||
|
||||
```txt
|
||||
A, B, A, B, A
|
||||
```
|
||||
|
||||
Y puede cortar:
|
||||
|
||||
```ts
|
||||
ORCA_RESULT_INTERRUPTED
|
||||
ORCA_REENTRY_REASON_REPEATED_EVENT
|
||||
```
|
||||
|
||||
o marcar el run como:
|
||||
|
||||
```ts
|
||||
ORCA_RUN_ABORTED
|
||||
```
|
||||
|
||||
Los patrones habituales en otros sistemas:
|
||||
|
||||
- Redux middleware suele evitar loops por convención y con guards manuales.
|
||||
- Redux-Saga usa `takeLatest`, cancelación y control de tasks.
|
||||
- EventEmitter no te protege: si emites recursivo, te comes el loop.
|
||||
- State machines evitan loops por transición/estado: si ya estás en cierto estado, el evento no transiciona.
|
||||
- Sistemas workflow usan `trace/correlationId`, depth limits, idempotency keys y dedupe.
|
||||
|
||||
Para `orca`, yo fijaría esto:
|
||||
|
||||
```txt
|
||||
payload = dominio, limpio
|
||||
envelope/meta = runtime, trace, stack, depth
|
||||
tokens = coordinación intra-run
|
||||
bus events = comunicación inter-run
|
||||
```
|
||||
|
||||
Y añadiría al contrato v0:
|
||||
|
||||
```ts
|
||||
interface OrcaReentryOptions {
|
||||
readonly maxDepth?: number; // default 16
|
||||
readonly maxEventsPerTrace?: number; // default 128
|
||||
readonly repeatedEventLimit?: number; // default 1 o 2
|
||||
readonly repeatedEventPolicy?:
|
||||
| typeof ORCA_REENTRY_SKIP
|
||||
| typeof ORCA_REENTRY_ABORT_TRACE
|
||||
| typeof ORCA_REENTRY_ERROR;
|
||||
}
|
||||
```
|
||||
|
||||
Así evitas reentrada sin ensuciar los payloads y sin obligar a cada módulo/componente a recordar “quién vino antes”.
|
||||
|
||||
|
||||
cual estrategia es mas arquitectonicamente mejor ?
|
||||
|
||||
|
||||
Arquitectónicamente, la mejor estrategia es:
|
||||
|
||||
```txt
|
||||
payload limpio + envelope runtime interno + cola de orca + límites de reentrada
|
||||
```
|
||||
|
||||
No metería el stack en el payload del evento.
|
||||
|
||||
Por qué:
|
||||
|
||||
- El payload pertenece al dominio: `userId`, `tenantId`, `orderId`, etc.
|
||||
- El stack pertenece al runtime de orquestación.
|
||||
- Si mezclas ambos, todos los módulos empiezan a conocer detalles de `orca`.
|
||||
- Si mañana cambias cómo trazas reentrada, rompes contratos de eventos.
|
||||
- Los eventos pueden venir de `connection`, `auth`, `sess`, `http`, etc.; no todos deberían cargar metadata de runtime.
|
||||
|
||||
La arquitectura correcta sería:
|
||||
|
||||
```txt
|
||||
buss event:
|
||||
type + payload limpio
|
||||
|
||||
orca internal envelope:
|
||||
eventId
|
||||
traceId
|
||||
parentEventId
|
||||
runId
|
||||
depth
|
||||
stack
|
||||
emittedByAction
|
||||
dedupeKey
|
||||
```
|
||||
|
||||
Flujo:
|
||||
|
||||
```txt
|
||||
1. buss publica EVENT_A(payload)
|
||||
2. orca crea envelope A con traceId
|
||||
3. action de A publica EVENT_B
|
||||
4. orca no ejecuta B inline
|
||||
5. orca crea envelope B con parent=A, mismo traceId, depth+1
|
||||
6. orca comprueba stack/repeated/depth
|
||||
7. si es seguro, encola B
|
||||
8. si detecta loop, corta con diagnostic
|
||||
```
|
||||
|
||||
Esto es más limpio que “cada componente mira si ya está gestionando un evento de ese stack”, porque esa opción reparte lógica de orquestación por toda la app. Es justo lo que `orca` intenta evitar.
|
||||
|
||||
Estrategia recomendada para v0:
|
||||
|
||||
```ts
|
||||
createEngineOrca({
|
||||
bus,
|
||||
logger,
|
||||
reentry: {
|
||||
maxDepth: 16,
|
||||
maxEventsPerTrace: 128,
|
||||
repeatedEventLimit: 2,
|
||||
repeatedEventPolicy: ORCA_REENTRY_SKIP
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Reglas:
|
||||
|
||||
- Eventos publicados durante un run se encolan, no se ejecutan inline.
|
||||
- El stack es interno de `orca`.
|
||||
- Si el mismo evento aparece demasiadas veces en el mismo trace, se salta o aborta.
|
||||
- Si depth supera límite, aborta el trace.
|
||||
- Si eventsPerTrace supera límite, aborta el trace.
|
||||
- Se emite diagnostic estructurado.
|
||||
- El `OrcaRunResult` registra `traceId`, `parentEventId`, `depth`, `interruptedReason`.
|
||||
|
||||
Mi voto: **envelope interno + queue + trace guards**. Es la opción más limpia, menos acoplada y más mantenible.
|
||||
|
||||
|
||||
|
||||
me refiero a los otros sistemas de referencia
|
||||
|
||||
|
||||
|
||||
En los sistemas de referencia, la estrategia arquitectónicamente mejor **no es meter el stack en el payload**. Casi todos los sistemas buenos separan:
|
||||
|
||||
```txt
|
||||
payload = datos de negocio
|
||||
runtime context = control, cancelación, stack, tracing, concurrencia
|
||||
```
|
||||
|
||||
**Comparativa**
|
||||
| Sistema | Cómo evita loops/reentrada | Lectura para `orca` |
|
||||
|---|---|---|
|
||||
| RTK Listener Middleware | Usa runtime API: `signal`, `cancelActiveListeners()`, `cancel()`, `take`, `condition`, `delay`, `pause`, `fork`. No mete stack en la action. | Muy buen referente. `orca` debería pasar `signal`, `traceId`, `runId` y control de cancelación en `OrcaActionContext`. Fuente: [RTK listener middleware](https://redux-toolkit.js.org/api/createListenerMiddleware). |
|
||||
| Redux-Saga | Usa tareas y cancelación: `takeLatest` cancela la tarea anterior; `takeEvery` permite concurrencia; `race`, `cancel`, `fork`, `join`. | Bueno para políticas `queue/drop/replace/parallel`, pero no copiaría generators. Fuente: [Redux-Saga concurrency](https://redux-saga.js.org/docs/advanced/Concurrency/). |
|
||||
| XState | Evita loops mediante estado, guards y transiciones. Si ya estás en un estado, un evento puede no transicionar. El control vive en la máquina, no en el payload. | Útil para flows cerrados; para `orca`, copiaría `setup()` tipado y la idea de runtime/state como dueño del control. Fuente: [XState setup](https://stately.ai/docs/setup). |
|
||||
| Effect Workflow | Distingue error/cause/interruption y tiene compensación estructurada. El runtime sabe qué falló y cómo compensar; no lo mete en payload. | Muy buen referente para `OrcaInterrupted`, compensaciones futuras y `Cause`. Fuente: [Effect Workflow compensation](https://effect-ts.github.io/effect/workflow/Workflow.ts.html). |
|
||||
| EventEmitter simple | No evita loops. Si un listener emite el mismo evento recursivamente, problema del usuario. | Esto es justo lo que `orca` no debe ser. |
|
||||
|
||||
**Veredicto**
|
||||
La mejor estrategia para `orca` es una mezcla de RTK Listener + Saga + XState:
|
||||
|
||||
```txt
|
||||
payload limpio
|
||||
OrcaEnvelope interno
|
||||
cola propia de orca
|
||||
traceId / parentEventId / runId
|
||||
AbortSignal en OrcaActionContext
|
||||
política de concurrencia por evento
|
||||
límites maxDepth / maxEventsPerTrace
|
||||
diagnostics cuando corta loops
|
||||
```
|
||||
|
||||
No pondría el stack en el payload. Eso es arquitectura más débil porque obliga a todos los módulos a conocer el runtime de `orca`.
|
||||
|
||||
La estrategia superior es:
|
||||
|
||||
```ts
|
||||
interface OrcaActionContext {
|
||||
readonly runId: OrcaRunId;
|
||||
readonly eventId: OrcaEventId;
|
||||
readonly traceId: OrcaTraceId;
|
||||
readonly parentEventId?: OrcaEventId;
|
||||
readonly depth: number;
|
||||
readonly signal: AbortSignal;
|
||||
}
|
||||
```
|
||||
|
||||
Y el envelope interno:
|
||||
|
||||
```ts
|
||||
interface OrcaEnvelope<TPayload> {
|
||||
readonly event: string;
|
||||
readonly payload: TPayload;
|
||||
readonly meta: OrcaEventMeta;
|
||||
}
|
||||
```
|
||||
|
||||
Así `orca` se parece a los sistemas buenos: el **runtime** controla reentrada, cancelación y concurrencia; el **payload** sigue siendo dominio limpio.
|
||||
|
||||
|
||||
ahora mismo no se parece a eso en nada ?
|
||||
|
||||
|
||||
|
||||
|
||||
Correcto: **el diseño actual apunta en esa dirección, pero todavía no se parece lo suficiente en contrato concreto**.
|
||||
|
||||
Ahora mismo el README ya dice cosas buenas:
|
||||
|
||||
```txt
|
||||
payload limpio
|
||||
no ejecutar inline
|
||||
queue/drop/replace
|
||||
tokens per-run
|
||||
RunResult
|
||||
App.Orchestration inerte
|
||||
```
|
||||
|
||||
Pero faltan las piezas que lo harían parecerse de verdad a RTK/Saga/XState/Effect en lo importante:
|
||||
|
||||
```txt
|
||||
OrcaEnvelope interno
|
||||
OrcaActionContext con traceId/runId/eventId/depth
|
||||
AbortSignal en acciones
|
||||
cola interna explícita de orca
|
||||
políticas de reentrada: maxDepth, maxEventsPerTrace
|
||||
reason/result para reentry-skip / interrupted
|
||||
relación formal buss -> orca listener -> queue
|
||||
```
|
||||
|
||||
Así que mi diagnóstico es:
|
||||
|
||||
```txt
|
||||
Conceptualmente: sí está en el camino correcto.
|
||||
Contractualmente: todavía no está cerrado.
|
||||
Implementablemente: aún tendría ambigüedad peligrosa.
|
||||
```
|
||||
|
||||
La sección que habría que añadir al README antes de implementar es algo como:
|
||||
|
||||
```ts
|
||||
interface OrcaEnvelope<TPayload = unknown> {
|
||||
readonly event: string;
|
||||
readonly payload: TPayload;
|
||||
readonly meta: OrcaEventMeta;
|
||||
}
|
||||
|
||||
interface OrcaEventMeta {
|
||||
readonly eventId: OrcaEventId;
|
||||
readonly traceId: OrcaTraceId;
|
||||
readonly parentEventId?: OrcaEventId;
|
||||
readonly parentRunId?: OrcaRunId;
|
||||
readonly emittedByAction?: OrcaActionId;
|
||||
readonly depth: number;
|
||||
readonly stack: readonly string[];
|
||||
}
|
||||
|
||||
interface OrcaActionContext {
|
||||
readonly runId: OrcaRunId;
|
||||
readonly event: string;
|
||||
readonly eventId: OrcaEventId;
|
||||
readonly traceId: OrcaTraceId;
|
||||
readonly depth: number;
|
||||
readonly signal: AbortSignal;
|
||||
readonly emit: <TPayload>(event: string, payload: TPayload) => void;
|
||||
}
|
||||
```
|
||||
|
||||
Y reglas:
|
||||
|
||||
```txt
|
||||
1. Las acciones no llaman bus.publish directamente si quieren preservar trace.
|
||||
2. Usan ctx.emit() para eventos derivados.
|
||||
3. ctx.emit() crea envelope hijo.
|
||||
4. Eventos derivados se encolan, no inline.
|
||||
5. Orca corta loops con maxDepth/maxEventsPerTrace/repeatedEventLimit.
|
||||
6. Si una acción llama App.Bus directamente, orca lo trata como evento raíz nuevo.
|
||||
```
|
||||
|
||||
Esto último es importante: **si queremos trazabilidad real, las acciones deben emitir eventos derivados vía `ctx.emit()`**, no vía `App.Bus.publish()` directamente. Ahí está la diferencia entre “tenemos bus” y “tenemos runtime de orquestación”.
|
||||
|
||||
Así que sí: ahora mismo aún no está del todo. Tiene el esqueleto filosófico, pero le falta el contrato de runtime/envelope/context para ser una pieza robusta.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ctx deberiamos definirlo, seria otra capa mas ?
|
||||
|
||||
|
||||
|
||||
|
||||
Sí, `ctx` hay que definirlo. Pero no lo vería como “otra capa” al nivel de `buss`/`orca`/`timr`; lo vería como **el runtime context de una ejecución de `orca`**.
|
||||
|
||||
No es un artefacto nuevo. Es parte del contrato de `orca`.
|
||||
|
||||
```txt
|
||||
orca = engine
|
||||
run = ejecución de un evento
|
||||
ctx = contexto que orca entrega a cada action dentro de ese run
|
||||
```
|
||||
|
||||
El `ctx` es donde ponemos todo lo que no debe ir en el payload:
|
||||
|
||||
```ts
|
||||
interface OrcaActionContext {
|
||||
readonly runId: OrcaRunId;
|
||||
readonly eventId: OrcaEventId;
|
||||
readonly traceId: OrcaTraceId;
|
||||
readonly parentEventId?: OrcaEventId;
|
||||
readonly depth: number;
|
||||
|
||||
readonly signal: AbortSignal;
|
||||
|
||||
emit<TPayload>(
|
||||
event: string,
|
||||
payload: TPayload,
|
||||
options?: OrcaEmitOptions
|
||||
): void;
|
||||
|
||||
token(token: OrcaToken): void;
|
||||
|
||||
hasToken(token: OrcaToken): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
La separación queda:
|
||||
|
||||
```txt
|
||||
payload
|
||||
Datos del evento.
|
||||
Pertenece al dominio.
|
||||
|
||||
ctx
|
||||
Datos/control de ejecución.
|
||||
Pertenece a orca.
|
||||
|
||||
App modules
|
||||
Servicios reales.
|
||||
La action los cierra por closure.
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```ts
|
||||
App.Orchestration.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
|
||||
id: ORCA_ACTION_CLEAR_PRIVATE_CACHE,
|
||||
stage: ORCA_STAGE_MAIN,
|
||||
provides: [ORCA_TOKEN_CACHE_OK, ORCA_TOKEN_CACHE_ERROR],
|
||||
|
||||
action: async (payload, ctx) => {
|
||||
if (ctx.signal.aborted) {
|
||||
return orcaInterrupted('run aborted');
|
||||
}
|
||||
|
||||
const result = await App.Cache.clearActorScope(payload.previousActorId);
|
||||
|
||||
if (!result.ok) {
|
||||
return orcaError(result.error, {
|
||||
emits: [ORCA_TOKEN_CACHE_ERROR]
|
||||
});
|
||||
}
|
||||
|
||||
ctx.emit(APP_EVENT_PRIVATE_CACHE_CLEARED, {
|
||||
actorId: payload.previousActorId
|
||||
});
|
||||
|
||||
return orcaSuccess({
|
||||
emits: [ORCA_TOKEN_CACHE_OK]
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`ctx.emit()` es importante porque preserva traza:
|
||||
|
||||
```txt
|
||||
run A
|
||||
action clear-cache
|
||||
ctx.emit(B)
|
||||
|
||||
orca crea envelope B:
|
||||
traceId = A.traceId
|
||||
parentEventId = A.eventId
|
||||
emittedByAction = clear-cache
|
||||
depth = A.depth + 1
|
||||
```
|
||||
|
||||
Si la action llama directamente:
|
||||
|
||||
```ts
|
||||
App.Bus.publish(B, payload)
|
||||
```
|
||||
|
||||
entonces `orca` no sabe que B viene de A salvo que intercepte todo, lo cual es más frágil. Por eso yo pondría esta regla:
|
||||
|
||||
```txt
|
||||
Dentro de acciones orca, usa ctx.emit() para eventos derivados.
|
||||
Usa App.Bus.publish() solo para eventos raíz externos al run.
|
||||
```
|
||||
|
||||
Entonces no es una capa adicional. Es el equivalente a:
|
||||
|
||||
- `listenerApi` en RTK listener middleware.
|
||||
- `saga effects` en redux-saga.
|
||||
- `context/event` en XState.
|
||||
- `workflow context/cause` en Effect/Temporal.
|
||||
|
||||
Mi recomendación: sí, definir `OrcaActionContext` como contrato central de v0. Sin `ctx`, `orca` será solo un callback runner. Con `ctx`, empieza a ser runtime de orquestación.
|
||||
|
||||
|
||||
|
||||
y como sabe una accion de un modulo a la hora de emitir un evento donde lo debe hacer, por ejemplo
|
||||
|
||||
auth.onChangeUser (newUser) {
|
||||
bus.publish(AUTH_CHANGE_EVENT,newUser)
|
||||
o
|
||||
ctx.publish(AUTH_CHANGE_EVENT,newUser)
|
||||
}
|
||||
|
||||
|
||||
|
||||
La regla tiene que ser muy clara:
|
||||
|
||||
```txt
|
||||
Un módulo nunca usa ctx.
|
||||
Una acción de orca sí puede usar ctx.
|
||||
```
|
||||
|
||||
Tu ejemplo:
|
||||
|
||||
```ts
|
||||
auth.onChangeUser((newUser) => {
|
||||
bus.publish(AUTH_CHANGE_EVENT, newUser);
|
||||
});
|
||||
```
|
||||
|
||||
Eso está bien si ocurre **dentro del módulo auth**. `auth` no debe saber que existe `orca`, por tanto no puede recibir ni usar `ctx`.
|
||||
|
||||
En cambio esto:
|
||||
|
||||
```ts
|
||||
auth.onChangeUser((newUser) => {
|
||||
ctx.publish(AUTH_CHANGE_EVENT, newUser);
|
||||
});
|
||||
```
|
||||
|
||||
Eso estaría mal si `auth.onChangeUser` es código del módulo. Meterías `orca` dentro de `auth`.
|
||||
|
||||
La separación correcta:
|
||||
|
||||
```txt
|
||||
Módulo -> Bus
|
||||
Orca action -> Ctx
|
||||
```
|
||||
|
||||
Ejemplo completo:
|
||||
|
||||
```ts
|
||||
// Dentro de auth
|
||||
function setUser(nextUser: User) {
|
||||
currentUser = nextUser;
|
||||
|
||||
bus.publish(AUTH_EVENT_USER_CHANGED, {
|
||||
userId: nextUser.id
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`auth` publica en `bus`. Punto. No conoce `orca`.
|
||||
|
||||
Ahora una acción de `orca`:
|
||||
|
||||
```ts
|
||||
App.Orchestration.onEvent(AUTH_EVENT_USER_CHANGED, {
|
||||
id: ORCA_ACTION_CLEAR_PRIVATE_STATE,
|
||||
stage: ORCA_STAGE_MAIN,
|
||||
|
||||
action: async (payload, ctx) => {
|
||||
await App.Cache.clearActorScope(payload.userId);
|
||||
|
||||
ctx.emit(APP_EVENT_PRIVATE_STATE_CLEARED, {
|
||||
userId: payload.userId
|
||||
});
|
||||
|
||||
return orcaSuccess({
|
||||
emits: [ORCA_TOKEN_CACHE_OK]
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Aquí sí usamos `ctx.emit()` porque estamos dentro de una action y queremos preservar trace.
|
||||
|
||||
Entonces hay dos tipos de emisión:
|
||||
|
||||
```txt
|
||||
bus.publish()
|
||||
Para módulos, componentes, servidor, código externo a orca.
|
||||
Crea evento raíz desde la perspectiva de orca.
|
||||
|
||||
ctx.emit()
|
||||
Solo dentro de una OrcaAction.
|
||||
Crea evento hijo dentro del trace actual.
|
||||
```
|
||||
|
||||
Si un módulo emite durante una acción, por ejemplo:
|
||||
|
||||
```ts
|
||||
action: async () => {
|
||||
await App.Auth.changeUser(nextUser); // Auth internamente hace bus.publish
|
||||
}
|
||||
```
|
||||
|
||||
`orca` lo verá como evento publicado durante un run, pero no sabrá con tanta precisión qué acción lo emitió salvo que tenga un contexto activo global interno. Hay dos opciones:
|
||||
|
||||
**Opción A, simple y limpia v0:**
|
||||
|
||||
```txt
|
||||
Módulo publica por bus.
|
||||
Orca detecta que hay run activo y lo encola como child event genérico.
|
||||
parentRunId sí, emittedByAction quizá no.
|
||||
```
|
||||
|
||||
**Opción B, más precisa:**
|
||||
|
||||
`orca` ejecuta cada action dentro de un “current action scope” interno. Si durante esa ejecución algún módulo hace `bus.publish`, `orca` intercepta el publish y lo marca como:
|
||||
|
||||
```ts
|
||||
emittedByAction: ORCA_ACTION_CLEAR_PRIVATE_STATE
|
||||
```
|
||||
|
||||
Eso requiere integración fuerte entre `buss` y `orca`.
|
||||
|
||||
Mi recomendación:
|
||||
|
||||
```txt
|
||||
v0:
|
||||
módulos siempre bus.publish
|
||||
actions pueden ctx.emit
|
||||
si un módulo publica durante un run, orca lo trata como evento derivado del run si puede detectarlo, pero no exigimos emittedByAction perfecto
|
||||
|
||||
v1:
|
||||
integración bus/orca con current action scope para trazabilidad completa
|
||||
```
|
||||
|
||||
Regla para desarrolladores:
|
||||
|
||||
```txt
|
||||
Si estás escribiendo código de módulo: usa bus.publish.
|
||||
Si estás escribiendo una OrcaAction: usa ctx.emit para eventos derivados del flujo.
|
||||
```
|
||||
|
||||
Así evitamos acoplar módulos a `orca`, pero damos a las acciones una vía correcta para preservar trazabilidad.
|
||||
|
||||
|
||||
@ -1,124 +0,0 @@
|
||||
<!--
|
||||
Bottom-tab navigation for the product surface (Discover, Matches,
|
||||
Profile, Safety, plus a logout). Pages opt-in by mounting it; the
|
||||
operator-style header is rendered by the layout only on
|
||||
admin/devtools, so this is the regular user's only cross-section
|
||||
nav.
|
||||
|
||||
Bottom-fixed on mobile, sticky on desktop. Tab labels stay short
|
||||
so the bar is finger-sized on phones.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { getNexoContext } from '../_lib/context';
|
||||
|
||||
const nexo = getNexoContext();
|
||||
|
||||
const items = [
|
||||
{ href: '/dating/discover', label: 'Descubre', icon: '✦' },
|
||||
{ href: '/dating/matches', label: 'Matches', icon: '♡' },
|
||||
{ href: '/dating/profile', label: 'Perfil', icon: '◔' },
|
||||
{ href: '/dating/safety', label: 'Seguridad', icon: '⚐' }
|
||||
] as const;
|
||||
|
||||
function isCurrent(href: string): boolean {
|
||||
return page.url.pathname === href || page.url.pathname.startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await nexo.api.logout();
|
||||
} finally {
|
||||
nexo.setSession({ authenticated: false });
|
||||
await goto('/dating/login', { replaceState: true });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="appnav" aria-label="Nexo">
|
||||
<ul>
|
||||
{#each items as item (item.href)}
|
||||
<li>
|
||||
<a href={item.href} aria-current={isCurrent(item.href) ? 'page' : undefined}>
|
||||
<span class="icon" aria-hidden="true">{item.icon}</span>
|
||||
<span class="label">{item.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
<li class="spacer" aria-hidden="true"></li>
|
||||
<li>
|
||||
<button type="button" class="logout" onclick={logout}>
|
||||
<span class="icon" aria-hidden="true">⏻</span>
|
||||
<span class="label">Salir</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.appnav {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 20;
|
||||
margin: 1.5rem -1rem -1rem;
|
||||
background: color-mix(in srgb, canvas 92%, currentColor 8%);
|
||||
border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent);
|
||||
}
|
||||
.appnav ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0.4rem 0.6rem;
|
||||
display: flex;
|
||||
gap: 0.15rem;
|
||||
max-width: 40rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.appnav a,
|
||||
.appnav button {
|
||||
font: inherit;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.1rem;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.78rem;
|
||||
min-width: 3.6rem;
|
||||
}
|
||||
.appnav a[aria-current='page'] {
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.appnav .icon {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.appnav .logout {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.appnav .logout:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@media (min-width: 800px) {
|
||||
.appnav {
|
||||
position: static;
|
||||
margin: 1.5rem 0 0;
|
||||
border-top: none;
|
||||
border: 1px solid color-mix(in srgb, currentColor 10%, transparent);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, canvas 96%, currentColor 4%);
|
||||
}
|
||||
.appnav ul {
|
||||
padding: 0.4rem 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,103 +0,0 @@
|
||||
<!--
|
||||
Shared auth shell. Centers a card on the page, hosts a brand line on
|
||||
top, and renders the consumer's form via children. Used by login,
|
||||
register, reset and mfa so visual rhythm stays consistent and the
|
||||
pages stay focused on their own form/state.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: Snippet;
|
||||
footer?: Snippet;
|
||||
}
|
||||
let { title, subtitle, children, footer }: Props = $props();
|
||||
</script>
|
||||
|
||||
<section class="auth-frame">
|
||||
<div class="auth-card">
|
||||
<header>
|
||||
<p class="brand">Nexo</p>
|
||||
<h1>{title}</h1>
|
||||
{#if subtitle}<p class="subtitle">{subtitle}</p>{/if}
|
||||
</header>
|
||||
<div class="body">
|
||||
{@render children()}
|
||||
</div>
|
||||
{#if footer}
|
||||
<footer>
|
||||
{@render footer()}
|
||||
</footer>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.auth-frame {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 2rem 1rem 4rem;
|
||||
}
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 26rem;
|
||||
background: color-mix(in srgb, canvas 96%, currentColor 4%);
|
||||
border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem 1.5rem 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
box-shadow: 0 1px 0 color-mix(in srgb, currentColor 6%, transparent);
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.brand {
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.6;
|
||||
margin: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
margin: 0;
|
||||
}
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
opacity: 0.75;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
footer {
|
||||
font-size: 0.88rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent);
|
||||
}
|
||||
footer :global(a) {
|
||||
color: #c44a78;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
footer :global(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
footer :global(span) {
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
@ -1,72 +0,0 @@
|
||||
<!--
|
||||
Form field primitive. Wraps a `<label>` + control + per-field error
|
||||
in a single block so every form across the demo has the same focus
|
||||
ring, spacing and error wiring without each page re-implementing it.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
interface Props {
|
||||
id: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
error?: string;
|
||||
children: Snippet;
|
||||
}
|
||||
let { id, label, hint, error, children }: Props = $props();
|
||||
const errorId = $derived(error ? `${id}-error` : undefined);
|
||||
</script>
|
||||
|
||||
<div class="field" class:has-error={Boolean(error)}>
|
||||
<label for={id}>{label}</label>
|
||||
{@render children()}
|
||||
{#if error}
|
||||
<p id={errorId} class="error" role="alert">{error}</p>
|
||||
{:else if hint}
|
||||
<p class="hint">{hint}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
:global(.field input),
|
||||
:global(.field textarea),
|
||||
:global(.field select) {
|
||||
font: inherit;
|
||||
font-size: 0.95rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid color-mix(in srgb, currentColor 38%, transparent);
|
||||
background: canvas;
|
||||
color: inherit;
|
||||
}
|
||||
:global(.field input:focus-visible),
|
||||
:global(.field textarea:focus-visible),
|
||||
:global(.field select:focus-visible) {
|
||||
outline: 2px solid color-mix(in srgb, #0366d6 70%, transparent);
|
||||
outline-offset: 1px;
|
||||
border-color: transparent;
|
||||
}
|
||||
.has-error :global(input),
|
||||
.has-error :global(textarea),
|
||||
.has-error :global(select) {
|
||||
border-color: color-mix(in srgb, #d73a49 50%, transparent);
|
||||
}
|
||||
.hint {
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.7;
|
||||
margin: 0;
|
||||
}
|
||||
.error {
|
||||
font-size: 0.82rem;
|
||||
color: color-mix(in srgb, #d73a49 70%, currentColor);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@ -1,157 +0,0 @@
|
||||
<!--
|
||||
Tarjeta canónica de perfil para Discover. Muestra foto principal,
|
||||
nombre+edad, intent, ubicación aproximada, intereses y bio. Es
|
||||
intencionalmente "dating-specific": vivirá bajo `_components/`
|
||||
hasta que el plan-implementacion la promocione a `uix`.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { DatingProfile } from '../_lib/types';
|
||||
|
||||
interface Props {
|
||||
profile: DatingProfile;
|
||||
}
|
||||
let { profile }: Props = $props();
|
||||
|
||||
const main = $derived(
|
||||
profile.photoUrls.find((entry) => entry.filename === profile.primaryPhoto) ??
|
||||
profile.photoUrls[0]
|
||||
);
|
||||
|
||||
const intentLabel = $derived(
|
||||
({
|
||||
dating: 'Conocer gente',
|
||||
long_term: 'Algo serio',
|
||||
casual: 'Algo casual',
|
||||
friends: 'Amistad',
|
||||
unsure: 'Sin etiqueta'
|
||||
} as const)[profile.intent]
|
||||
);
|
||||
</script>
|
||||
|
||||
<article class="card" data-intent={profile.intent}>
|
||||
<div class="photo">
|
||||
{#if main}
|
||||
<img src={main.card} alt={`Foto de ${profile.displayName}`} loading="lazy" />
|
||||
{:else}
|
||||
<div class="photo-placeholder" aria-hidden="true">
|
||||
<span>{profile.displayName.slice(0, 1).toUpperCase()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="intent-pill">{intentLabel}</span>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<header>
|
||||
<h2>
|
||||
<span class="name">{profile.displayName}</span>
|
||||
<span class="age">{profile.age}</span>
|
||||
</h2>
|
||||
{#if profile.approxLocation !== ''}
|
||||
<p class="location">{profile.approxLocation}</p>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if profile.bio !== ''}
|
||||
<p class="bio">{profile.bio}</p>
|
||||
{/if}
|
||||
|
||||
{#if profile.interests.length > 0}
|
||||
<ul class="chips" aria-label="Intereses">
|
||||
{#each profile.interests.slice(0, 8) as interest (interest)}
|
||||
<li>{interest}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
|
||||
background: color-mix(in srgb, canvas 96%, currentColor 4%);
|
||||
box-shadow: 0 1px 0 color-mix(in srgb, currentColor 6%, transparent);
|
||||
max-width: 24rem;
|
||||
width: 100%;
|
||||
}
|
||||
.photo {
|
||||
position: relative;
|
||||
aspect-ratio: 4 / 5;
|
||||
background: color-mix(in srgb, currentColor 6%, transparent);
|
||||
}
|
||||
.photo img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.photo-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 4rem;
|
||||
font-weight: 600;
|
||||
opacity: 0.4;
|
||||
}
|
||||
.intent-pill {
|
||||
position: absolute;
|
||||
bottom: 0.6rem;
|
||||
left: 0.6rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, canvas 80%, transparent);
|
||||
backdrop-filter: blur(6px);
|
||||
font-weight: 600;
|
||||
}
|
||||
.body {
|
||||
padding: 1rem 1.1rem 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
header h2 {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
header .age {
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.location {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.bio {
|
||||
margin: 0;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.45;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chips {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.chips li {
|
||||
font-size: 0.78rem;
|
||||
padding: 0.18rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, currentColor 8%, transparent);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue