diff --git a/.gitignore b/.gitignore index 50952bf..86b9fed 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ vite.config.ts.timestamp-* # Local docs/dev logs tmp-active-docs-*.log +demos/dating/server/*.log +demos/dating/server/tmp/ # IDE / tool state (personal, not shared) .claude/ diff --git a/audit-codex.md b/audit-codex.md deleted file mode 100644 index 60f52be..0000000 --- a/audit-codex.md +++ /dev/null @@ -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. diff --git a/auditoria- ecosistema-codex.md b/auditoria- ecosistema-codex.md deleted file mode 100644 index 76431be..0000000 --- a/auditoria- ecosistema-codex.md +++ /dev/null @@ -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` 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. diff --git a/demos/dating/README.md b/demos/dating/README.md new file mode 100644 index 0000000..981e84d --- /dev/null +++ b/demos/dating/README.md @@ -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. diff --git a/demos/dating/docs/arquitectura-ecosistema.md b/demos/dating/docs/arquitectura-ecosistema.md new file mode 100644 index 0000000..408a51d --- /dev/null +++ b/demos/dating/docs/arquitectura-ecosistema.md @@ -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. diff --git a/demos/dating/docs/auth-y-fotos.md b/demos/dating/docs/auth-y-fotos.md new file mode 100644 index 0000000..3b6b744 --- /dev/null +++ b/demos/dating/docs/auth-y-fotos.md @@ -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. diff --git a/src/web/routes/dating/design-brief.md b/demos/dating/docs/design-brief.md similarity index 100% rename from src/web/routes/dating/design-brief.md rename to demos/dating/docs/design-brief.md diff --git a/demos/dating/docs/diseno-producto.md b/demos/dating/docs/diseno-producto.md new file mode 100644 index 0000000..9b5ed7e --- /dev/null +++ b/demos/dating/docs/diseno-producto.md @@ -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. diff --git a/demos/dating/docs/matriz-tests.md b/demos/dating/docs/matriz-tests.md new file mode 100644 index 0000000..7046cf0 --- /dev/null +++ b/demos/dating/docs/matriz-tests.md @@ -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. diff --git a/demos/dating/docs/objetivos.md b/demos/dating/docs/objetivos.md new file mode 100644 index 0000000..255f9d0 --- /dev/null +++ b/demos/dating/docs/objetivos.md @@ -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. diff --git a/demos/dating/docs/plan-implementacion.md b/demos/dating/docs/plan-implementacion.md new file mode 100644 index 0000000..355a08e --- /dev/null +++ b/demos/dating/docs/plan-implementacion.md @@ -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`. diff --git a/demos/dating/docs/requisitos.md b/demos/dating/docs/requisitos.md new file mode 100644 index 0000000..36c3907 --- /dev/null +++ b/demos/dating/docs/requisitos.md @@ -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. diff --git a/demos/dating/server/README.md b/demos/dating/server/README.md new file mode 100644 index 0000000..87a5a92 --- /dev/null +++ b/demos/dating/server/README.md @@ -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 ` 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. diff --git a/demos/dating/server/domain.mjs b/demos/dating/server/domain.mjs new file mode 100644 index 0000000..ea879a0 --- /dev/null +++ b/demos/dating/server/domain.mjs @@ -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; +} diff --git a/demos/dating/server/env.mjs b/demos/dating/server/env.mjs new file mode 100644 index 0000000..55cf70d --- /dev/null +++ b/demos/dating/server/env.mjs @@ -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 `/demos/dating/server/`, so the repo +// root is three levels up — not two as it was when the server lived +// at `/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; +} diff --git a/demos/dating/server/http.mjs b/demos/dating/server/http.mjs new file mode 100644 index 0000000..d4b4027 --- /dev/null +++ b/demos/dating/server/http.mjs @@ -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}`; +} diff --git a/demos/dating/server/pocketbase.mjs b/demos/dating/server/pocketbase.mjs new file mode 100644 index 0000000..3706005 --- /dev/null +++ b/demos/dating/server/pocketbase.mjs @@ -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; + } +} diff --git a/servers/dating/realtime.mjs b/demos/dating/server/realtime.mjs similarity index 100% rename from servers/dating/realtime.mjs rename to demos/dating/server/realtime.mjs diff --git a/servers/dating/routes.mjs b/demos/dating/server/routes.mjs similarity index 100% rename from servers/dating/routes.mjs rename to demos/dating/server/routes.mjs diff --git a/servers/dating/server.mjs b/demos/dating/server/server.mjs similarity index 100% rename from servers/dating/server.mjs rename to demos/dating/server/server.mjs diff --git a/demos/dating/web/_components/AppNav.svelte b/demos/dating/web/_components/AppNav.svelte new file mode 100644 index 0000000..2ed2ce2 --- /dev/null +++ b/demos/dating/web/_components/AppNav.svelte @@ -0,0 +1,78 @@ + + + + + + diff --git a/demos/dating/web/_components/AuthLayout.svelte b/demos/dating/web/_components/AuthLayout.svelte new file mode 100644 index 0000000..f0387a5 --- /dev/null +++ b/demos/dating/web/_components/AuthLayout.svelte @@ -0,0 +1,182 @@ + + + +
+
+
+ + + Nexo + +
+

{title}

+ {#if subtitle}

{subtitle}

{/if} +
+
+ {@render children()} +
+ {#if footer} +
+ {@render footer()} +
+ {/if} +
+
+ + +
+ + diff --git a/demos/dating/web/_components/Field.svelte b/demos/dating/web/_components/Field.svelte new file mode 100644 index 0000000..182eba8 --- /dev/null +++ b/demos/dating/web/_components/Field.svelte @@ -0,0 +1,31 @@ + + + +
+ + {@render children()} + {#if error} + + {:else if hint} +

{hint}

+ {/if} +
diff --git a/demos/dating/web/_components/Icon.svelte b/demos/dating/web/_components/Icon.svelte new file mode 100644 index 0000000..c48d01d --- /dev/null +++ b/demos/dating/web/_components/Icon.svelte @@ -0,0 +1,181 @@ + + + + + + + {#if title}{title}{/if} + {#each entry.paths as d (d)} + + {/each} + diff --git a/demos/dating/web/_components/MessageToast.svelte b/demos/dating/web/_components/MessageToast.svelte new file mode 100644 index 0000000..0b60b30 --- /dev/null +++ b/demos/dating/web/_components/MessageToast.svelte @@ -0,0 +1,69 @@ + + + +{#if toasts.length > 0} +
+ {#each toasts as toast (toast.id)} + +
open(toast)} + onkeydown={(event) => onKey(event, toast)} + > + + +
{toast.title}
+
{toast.body}
+
+ +
+ {/each} +
+{/if} diff --git a/demos/dating/web/_components/ProfileCard.svelte b/demos/dating/web/_components/ProfileCard.svelte new file mode 100644 index 0000000..8c45514 --- /dev/null +++ b/demos/dating/web/_components/ProfileCard.svelte @@ -0,0 +1,75 @@ + + + +
+
+ {#if main} + {`Foto + {:else} + + {/if} +
+ {intentLabel} +
+ {profile.displayName} + {profile.age} +
+ {#if profile.approxLocation !== ''} +
+ + {profile.approxLocation} +
+ {/if} +
+
+ + {#if profile.bio !== ''} +
+

Sobre mí

+

{profile.bio}

+
+ {/if} + + {#if profile.interests.length > 0} +
    + {#each profile.interests.slice(0, 8) as interest (interest)} +
  • {interest}
  • + {/each} +
+ {/if} +
+ + diff --git a/demos/dating/web/_design/primitives.css b/demos/dating/web/_design/primitives.css new file mode 100644 index 0000000..293e280 --- /dev/null +++ b/demos/dating/web/_design/primitives.css @@ -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; +} diff --git a/demos/dating/web/_design/tokens.css b/demos/dating/web/_design/tokens.css new file mode 100644 index 0000000..a5638f5 --- /dev/null +++ b/demos/dating/web/_design/tokens.css @@ -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; + } +} diff --git a/src/web/routes/dating/_lib/api.ts b/demos/dating/web/_lib/api.ts similarity index 100% rename from src/web/routes/dating/_lib/api.ts rename to demos/dating/web/_lib/api.ts diff --git a/src/web/routes/dating/_lib/app.ts b/demos/dating/web/_lib/app.ts similarity index 100% rename from src/web/routes/dating/_lib/app.ts rename to demos/dating/web/_lib/app.ts diff --git a/src/web/routes/dating/_lib/context.ts b/demos/dating/web/_lib/context.ts similarity index 100% rename from src/web/routes/dating/_lib/context.ts rename to demos/dating/web/_lib/context.ts diff --git a/src/web/routes/dating/_lib/realtime.ts b/demos/dating/web/_lib/realtime.ts similarity index 100% rename from src/web/routes/dating/_lib/realtime.ts rename to demos/dating/web/_lib/realtime.ts diff --git a/src/web/routes/dating/_lib/types.ts b/demos/dating/web/_lib/types.ts similarity index 100% rename from src/web/routes/dating/_lib/types.ts rename to demos/dating/web/_lib/types.ts diff --git a/docs/AUDIT_KIMI.md b/docs/AUDIT_KIMI.md deleted file mode 100644 index a1c6704..0000000 --- a/docs/AUDIT_KIMI.md +++ /dev/null @@ -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 - - - - -``` - -**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 - - - -
  • - onToggle?.(id)} - class="checkbox" - /> - {text} - -
  • -``` - -**✅ 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 - - -``` - -**⚠️ Patrón a Mejorar - Estado Global:** - -```typescript -// stores/todoStore.ts -// ✅ Svelte 5 runes store (moderno) - -function createTodoStore() { - let todos = $state([]); - 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 - - - - -``` - ---- - -## 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 - - - -{#await HeavyChart then { default: Chart }} - -{/await} - - - - - -
    {items[index].text}
    -
    -``` - -### 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 - -{#each todos as todo (todo.id)} - - -{/each} - - - - - -``` - ---- - -## 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 - - - - - - - - - - -
    - - - {#if errors.email} - - {errors.email} - - {/if} -
    - - -
    - {announcement} -
    -``` - -### 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 - - - -
    - - - - - {#if error} - - {error} - - {/if} -
    -``` - -### 10.2 Hook Personalizado: useAsync - -```typescript -// hooks/useAsync.ts -import { $state, $derived } from 'svelte'; - -interface AsyncState { - data: T | null; - loading: boolean; - error: Error | null; -} - -export function useAsync( - asyncFn: () => Promise, - immediate = true -) { - let state = $state>({ - 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* diff --git a/docs/AUDIT_OPENCODE.md b/docs/AUDIT_OPENCODE.md deleted file mode 100644 index 0095e95..0000000 --- a/docs/AUDIT_OPENCODE.md +++ /dev/null @@ -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 ``, 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` pero lo castea `as unknown as ActiveLang`. 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` 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` 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` 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(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`. - -### 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 diff --git a/docs/AUDIT_claude.md b/docs/AUDIT_claude.md deleted file mode 100644 index ac926f2..0000000 --- a/docs/AUDIT_claude.md +++ /dev/null @@ -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`, 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` 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`. 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>>` - 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` 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. diff --git a/BRAND.md b/docs/BRAND.md similarity index 100% rename from BRAND.md rename to docs/BRAND.md diff --git a/docs/NEXT_STEPS.md b/docs/NEXT_STEPS.md deleted file mode 100644 index 694cdcf..0000000 --- a/docs/NEXT_STEPS.md +++ /dev/null @@ -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. diff --git a/SECURITY.md b/docs/SECURITY.md similarity index 100% rename from SECURITY.md rename to docs/SECURITY.md diff --git a/docs/active-app-refactorizacion.md b/docs/active-app-refactorizacion.md deleted file mode 100644 index 1a60879..0000000 --- a/docs/active-app-refactorizacion.md +++ /dev/null @@ -1,2467 +0,0 @@ -# Refactorización de `arts/active-app` — eliminación de eventos parche y modelo de servicios - -## Estado de este documento - -**ARCHIVADO — refactor completado 2026-05-04.** El documento captura el -análisis y plan de las fases 1-4 (eliminación de `APP_EVENT_*`, modelo de -servicios declarativo, integración con `orca`). Todas las fases se -ejecutaron; el contrato vivo es `src/arts/active-app/README.md` y el código -mismo. Esta página se mantiene como registro histórico del razonamiento que -guió el big-bang, no como guía operativa. - -`orca` arrancó como v0-kernel durante este refactor y se completó al 100% -en sesiones posteriores; ver `src/arts/orca/README.md` para el estado -actual. - ---- - -## Resumen ejecutivo - -Tres problemas convergentes: - -1. `arts/cache`, `arts/perm`, `arts/connection` se **suscriben internamente al - bus** para reaccionar a `APP_EVENT_*`. Eso filtra vocabulario de App - (tenant, refresh, identity) a piezas que deberían ser runtime puro. -2. Los `APP_EVENT_*` son **deuda técnica disfrazada**: la mayoría son - *republicaciones* de eventos cuyo dueño real es otro módulo (sesión publica - identidad, conexión publica connectivity), o son **comandos** disfrazados de - eventos (`PERMISSIONS_REFRESH_REQUESTED`, `CACHE_INVALIDATE_REQUESTED`). -3. `arts/active-app` esconde *side-effects* de orquestación —violando una - invariante explícita de `arts/orca`— porque hoy no existe `orca` como pieza - de orquestación dedicada. - -**Decisión arquitectónica:** - -- Eliminar todos los `APP_EVENT_*` salvo los que `aapp` dueña realmente - (`DISPOSE_STARTING`). -- Eliminar el `session-translator` y las suscripciones internas en - `cache`/`perm`/`connection`. -- Exponer **API imperativa pública** en cada artefacto (`invalidate`, - `refresh`, `cancelPrivateRequests`, `reauthenticateAll`). -- Reescribir `aapp` como **compositor + factory + lifecycle** sobre un - esquema declarativo de servicios (`AppServiceSchema`). -- Toda orquestación inter-modular se mueve a `orca` (cuando exista) o a - *bridge code* explícito en `arts/active-app` mientras tanto. - ---- - -## 1. Problema raíz - -### 1.1 Acoplamiento concreto observado - -| Artefacto | Línea | Suscripciones internas | -|---|---|---| -| `arts/cache/active-cache.svelte.ts` | 211–219 | `USER_IDENTITY_CHANGED`, `TENANT_SWITCHED` | -| `arts/perm/active-permissions.svelte.ts` | 151–165 | `USER_IDENTITY_CHANGED`, `PERMISSIONS_REFRESH_REQUESTED`, `TENANT_SWITCHED` | -| `arts/connection/bus-session-source.ts` | 10 | `USER_IDENTITY_CHANGED` (vía `createBusSessionSource`) | - -Cada uno hace `bus.on(APP_EVENT_X, () => método-interno())` para -auto-reaccionar. La consecuencia: - -- `arts/cache` **conoce el concepto "tenant"**. -- `arts/perm` **conoce el concepto "refresh"**. -- `arts/connection` **conoce el concepto "identity"**. - -Si alguien intentara usar `arts/cache` fuera de Active framework, está obligado -a entender qué es un tenant o a inyectar un bus que falsifique eventos `app.*`. -Eso rompe la promesa "los arts son piezas runtime reusables". - -### 1.2 Por qué los `APP_EVENT_*` son un parche - -| Evento | Dueño real | Naturaleza | Veredicto | -|---|---|---|---| -| `USER_IDENTITY_CHANGED` | `session` | Republicación de `SESSION_EVENT_LIFECYCLE_*` | **Eliminar.** Consumidores escuchan `SESSION_EVENT_*`. | -| `PERMISSIONS_REFRESH_REQUESTED` | nadie | **Comando**, no hecho | **Eliminar.** Reemplazar por llamada imperativa `App.perms.refresh()`. | -| `CACHE_INVALIDATE_REQUESTED` | nadie | **Comando**, no hecho | **Eliminar.** Reemplazar por `App.cache.invalidate(...)`. | -| `TENANT_SWITCHED` | App-state | Hecho de App-level | **Mantener provisional.** Si tenant pasa a un módulo dueño en el futuro, eliminar. | -| `CONNECTIVITY_CHANGED` | navegador / `connection` | Estado del navegador | **Mover a `connection`** como `CONNECTION_EVENT_ONLINE/OFFLINE`. | -| `DISPOSE_STARTING` | App | Lifecycle de aapp | **Mantener.** Único evento donde App es realmente dueña. | - -La regla: - -> Un evento existe en App **solo si App es el dueño del hecho**. Comandos no son -> eventos. Las republicaciones no son eventos: son indirección. - ---- - -## 2. Restricción arquitectónica: `orca` - -`arts/orca/README.md` define el motor de orquestación que va a reemplazar las -suscripciones internas. Documento extenso ya cerrado en su contrato. - -### 2.1 Invariantes de orca relevantes para este refactor - -Citas literales (ver `arts/orca/README.md`): - -> - `orca` no importa artefactos concretos salvo contratos comunes. -> - **los artefactos no consumen `orca`**; solo publican eventos en `buss`. -> - **la aplicacion registra acciones en `orca`**. -> - `sess`, `cach`, `perm`, `connection`, `auth` o `http` **no deben depender -> de `orca`** para sus flujos internos. -> - `aapp` puede crear `Bus`, `Timers`, `Logger` y `Orchestration`, pero **no -> debe esconder la politica de orquestacion**. - -Y de "Que problema resuelve": - -> Sin `orca`, las reacciones inter-modulo tienden a acabar repartidas: -> -> ``` -> sess conoce cach -> cach conoce perm -> connection conoce sess -> aapp conoce todo -> ``` -> -> Eso escala mal. - -### 2.2 Implicación: este refactor es pre-requisito de orca v0 - -Si `orca` registra una action en `SESSION_EVENT_LIFECYCLE_REVOKED` que invalida -cache, y `arts/cache` **también** se suscribe internamente al mismo evento, hay -**doble reacción**: race condition, double-invalidation, estado corrupto. - -Por tanto el refactor de eliminar suscripciones internas en `cache`/`perm`/ -`connection` **no es opcional** para que `orca` exista. O se hace ahora, o se -hace como primer paso del proyecto orca. Conviene hacerlo ahora porque: - -- El acoplamiento conceptual es ruido en el código actual incluso sin orca. -- Permite simplificar `libs/active-app/events.ts` drásticamente. -- Permite mover `libs/active-app/` → `arts/active-app/` (la única razón de - vivir en libs eran los consumidores externos cache/perm/conn que dejarán - de existir). - ---- - -## 3. Diseño emergente: `aapp` como compositor + servicios - -`aapp` deja de tener lógica reaccional propia. Pasa a ser **compositor -explícito** con dos secciones: **núcleo** (siempre presente, parte de su -runtime) y **servicios** (declarados explícitamente por el desarrollador en un -`AppServiceSchema`). - -### 3.1 Distinción núcleo vs servicios - -**Núcleo** (siempre presente, sin opt-in): - -| Servicio | Responsabilidad | -|---|---| -| `logger` | logger compartido | -| `lang` | i18n | -| `storage` | almacenamiento sync (con adapters) | -| `format` | formateadores localizados | -| `dom` | reactividad DOM | -| `frontend` | preferencias de UI | -| `bus` | event bus | -| `timers` | scheduler de timers | -| `orchestration` | `orca` (siempre presente, inerte hasta que se registren acciones) | - -Configurables vía las opciones que ya existen hoy en `createActiveApp()`. La -configuración del núcleo no entra en `services:` — entra en la raíz del options -object. Mantenemos los contratos ya definidos. - -**Servicios opcionales** (opt-in): - -| Servicio | Hoy es | -|---|---| -| `sium` | factory lazy `App.createSiumEngine()` | -| `session` | factory lazy `App.createActiveSession<...>()` | -| `cache` | factory lazy `App.createActiveCache(...)` | -| `perm` | factory lazy `App.createActivePerms(...)` | -| `http` | factory lazy `App.createEngineHttp(...)` | -| `auth` | factory lazy `App.createActiveAuth(...)` | -| `connection` | factory lazy `App.createActiveConnections(...)` | - -Pasan a declararse en el schema. Si no se declaran, no existen en la app y el -acceso (`App.cache`) es **error de tipos**. - -### 3.2 Esquema base - -```ts -const App = createActiveApp({ - // Núcleo (configurable; siempre presente) - lang: { schema: appLang, defaultLocale: 'es' }, - storage: { adapter: localStorageAdapter() }, - frontend: { theme: 'system' }, - - // Servicios (opt-in declarativos) - services: { - cache: defineActiveCache({ adapter: 'memory' }), - session: defineActiveSession({ refresh: refreshFn }), - http: defineEngineHttp({ baseUrl: '/api' }), - } -}); - -App.bus // núcleo: tipo EngineBus -App.cache // OK: declarado en services -App.session // OK: declarado en services -App.perm // ❌ TS error: no está en services -App.connections // ❌ TS error: no está en services -``` - -### 3.3 Contrato base de un servicio - -```ts -type ServiceState = - | 'pending' // declarado, aún no construido - | 'initializing' // construyéndose en este momento - | 'running' // operativo - | 'failed' // crash al iniciar - | 'disposing' // dispose en curso - | 'disposed'; // ya destruido - -type ServiceInitMode = - | 'immediate' // se construye en commit() de la app - | 'lazy'; // se construye en el primer acceso (App.cache → trigger init) - -interface AppService { - readonly serviceName: TName; - readonly initMode: ServiceInitMode; - readonly dependencies: readonly string[]; // claves de núcleo o de otros servicios - readonly state: ServiceState; // observable público - readonly instance: TInstance; // la instancia construida - readonly dispose: () => void | Promise; -} - -// Cada artefacto exporta un define*() que produce una factory tipada -interface AppServiceFactory { - readonly name: TName; - readonly initMode: ServiceInitMode; - readonly dependencies: readonly (keyof TDeps & string)[]; - create(deps: TDeps): TInstance; - dispose?(instance: TInstance): void | Promise; -} -``` - -Cada art expone su factory: - -```ts -// arts/cache/index.ts -export function defineActiveCache(options: ActiveCacheOptions) { - return { - name: 'cache' as const, - initMode: 'lazy' as const, - dependencies: ['bus', 'logger', 'timers'] as const, - create(deps: { bus: EngineBus; logger: EngineLogger; timers: TimerScheduler }) { - return createActiveCache({ ...options, ...deps }); - }, - dispose(instance: ActiveCache) { - instance.dispose(); - } - }; -} -``` - -### 3.4 Inicialización: `lazy` vs `immediate` - -| Modo | Cuándo se construye | Caso de uso | -|---|---|---| -| `immediate` | Al hacer `commit()` (después de `createActiveApp(...)`) | Servicios que la app necesita de salida (sesión inicial, http base) | -| `lazy` | En el primer acceso `App.cache` | Servicios que pueden no usarse en algunos flujos (cache cuando solo hay rutas estáticas) | - -Default por servicio: lo decide el `define*()` del art. La app puede sobrescribirlo: - -```ts -services: { - cache: defineActiveCache(options).withInitMode('immediate') -} -``` - -### 3.5 Validación del schema - -Estática (al construir): - -- **Servicios declarados que no existen como factory** → error. -- **Dependencias declaradas que apuntan a un servicio no presente en el schema - ni en el núcleo** → error en `createActiveApp()` (compile-time vía tipos - cuando sea posible; runtime al `commit()` si los tipos no llegan). -- **Ciclos de dependencias** → error en `commit()`. - -Runtime: - -- Servicios `immediate` se construyen en orden topológico al `commit()`. -- Si la construcción de uno falla, su `state` queda en `'failed'` y se aborta - el `commit()` con error agregado. -- Servicios `lazy` se construyen en el primer acceso; el error queda en su - `state`. - -### 3.6 Type-safety - -`ActiveApp` se vuelve genérico sobre el schema: - -```ts -type ActiveApp>> = - CoreApp & { - [K in keyof TServices]: TServices[K]['instance']; - } & { - // Acceso a metadata - services: { - [K in keyof TServices]: AppService; - }; - }; -``` - -Resultado: si declaras `cache` y `session`, `App.cache` y `App.session` existen -con su tipo correcto y `App.perm` falla en compile-time. - -### 3.7 Orquestación - -La orquestación **no entra en el schema de servicios**. Va por `orca`: - -```ts -// El developer registra acciones en App.orchestration, no en el schema -App.orchestration.onEvent(SESSION_EVENT_LIFECYCLE_REVOKED, { - id: ORCA_ACTION_INVALIDATE_CACHE, - stage: ORCA_STAGE_MAIN, - action: async () => { - await App.cache.invalidate({ on: 'userIdentityChange' }); - return orcaSuccess(); - } -}); -``` - -Razón: orca tiene su propio sistema (stages, tokens, policies) que no -pertenece al schema declarativo de servicios. Mezclarlos hace que el schema -crezca a un DSL paralelo de orca, redundante. - -**Mientras `orca v0` no exista**, `aapp` puede aceptar un slot opcional -`bridges:` con suscripciones provisionales del estilo `bus.on(EVENT, () => -App.X.method())`. Comentado claramente como código transitorio. Cuando `orca` -llegue, esas líneas se reemplazan por `Orca.onEvent()`. - ---- - -## 4. Decisiones cerradas - -1. **Modelo:** núcleo (siempre presente) + servicios (opt-in declarado). -2. **Naming:** `createActiveApp(options)` (no `new ActiveApp(...)`); helper de - servicio `defineActive*` / `defineEngine*`. La interfaz base se llama - `AppService` (sin "Active" — el "Active" del framework significa - `$state`-reactivo y no aplica a todos los servicios). -3. **Tipo del schema:** `services: { [K in TName]: AppServiceFactory<...> }` - tipado, con `K` literal para inferencia. -4. **Servicios declarados que no existen como factory:** error. -5. **Dependencias faltantes:** error en compile-time (cuando los tipos - alcanzan) y en `commit()` runtime como respaldo. -6. **Init mode por servicio:** `lazy` por defecto en factories de servicios - opcionales; `immediate` solo si el `define*()` lo declara así. Override en - la declaración del schema permitido. -7. **Núcleo configurable** vía opciones existentes (`lang`, `storage`, - `frontend`, …) en la raíz del options object. -8. **Orquestación**: vive en `orca`, no en el schema de servicios. -9. **`AppEventBus`, `APP_EVENT_*`**: sobreviven solo `DISPOSE_STARTING`. El - resto se elimina. `libs/active-app/events.ts` se reduce a este único - evento (o desaparece, ver §6). -10. **`session-translator`**: se elimina. `orca` (o el bridge provisional) - escucha `SESSION_EVENT_LIFECYCLE_*` directamente. - ---- - -## 5. Decisiones abiertas - -1. **Ubicación final de `libs/active-app/`:** una vez vaciado, ¿se mueve todo - a `arts/active-app/` (sin libs) o se mantiene `libs/active-app/` con - solo `consts.ts` y `errors.ts`? **Recomendación**: mover todo a - `arts/active-app/`. Solo el código del núcleo y los servicios es runtime; - no hay contrato puro reusable que justifique una capa abstracta. -2. **Tenant**: ¿quién dueña el cambio de tenant? Si se confirma que es App, - `APP_EVENT_TENANT_SWITCHED` sobrevive. Si pasa a un módulo (futuro - `tenant`), se elimina. **Acción**: investigar consumidores reales y - decidir. -3. **Connectivity**: ¿`arts/connection` ya publica `CONNECTION_EVENT_ONLINE/ - OFFLINE`? Si sí, `APP_EVENT_CONNECTIVITY_CHANGED` se elimina y los - listeners migran. **Acción**: verificar antes de Fase 1. -4. **`bridges:` provisional vs forzar `orca` desde día uno**: ¿conviene - meter las suscripciones provisionales en `aapp` con un slot dedicado o - esperar a `orca`? -5. **Eager construction en `commit()`**: ¿el orden topológico se calcula - automáticamente o se exige al desarrollador declararlo? **Recomendación**: - automático con detección de ciclos. - ---- - -## 6. Plan de refactor por fases - -### Fase 1 — Eliminar eventos parche (ejecutable inmediatamente) - -**Objetivo**: dejar `arts/cache`, `arts/perm`, `arts/connection` sin -suscripciones internas a eventos `APP_EVENT_*`. Eliminar el `session- -translator`. Reducir `libs/active-app/events.ts` a `DISPOSE_STARTING` (y -posiblemente `TENANT_SWITCHED` si decidimos mantenerlo). - -**Riesgo**: alto. Cambia el comportamiento "auto-invalidate" que hoy hacen -los arts. Tests que asumen ese comportamiento se rompen. - -**Subpasos**: - -1. **1A — Verificar dueños reales**: - - Confirmar que `connection` publica `CONNECTION_EVENT_ONLINE/OFFLINE`. Si - no, considerar que `connection` lo añada antes de eliminar - `APP_EVENT_CONNECTIVITY_CHANGED`. - - Confirmar que `tenant` no tiene un módulo dueño y que mantenerlo en App - es la opción correcta. -2. **1B — Exponer API imperativa pública** en `arts/cache`, `arts/perm`, - `arts/connection` para los efectos hoy automáticos: - - `cache.invalidate({ on: 'userIdentityChange' | 'tenantSwitched' | ... })` - - `perm.invalidate()`, `perm.refresh({ cause? })` - - `connections.adoptIdentity(id)`, `connections.reauthenticateAll()` -3. **1C — Eliminar suscripciones internas** en los `active-*.svelte.ts` de - los tres arts. Conservar (de momento) el option `bus?: AppEventBus` para - no romper firmas de creación. -4. **1D — Eliminar `session-translator.ts`** y sus tests. -5. **1E — Mover suscripciones provisionales a `aapp`**: - - `arts/active-app/active-app.svelte.ts` registra `bus.on(SESSION_EVENT_*, - () => app.cache?.invalidate(...))` como código transitorio. - - Comentado: `// PROVISIONAL: when orca v0 lands, replace with - App.orchestration.onEvent(...)` -6. **1F — Eliminar publishers/payloads/helpers de - `libs/active-app/events.ts`** salvo `DISPOSE_STARTING`. Tests que - publicaban `publishAppUserIdentityChanged(...)` migran a publicar - `SESSION_EVENT_LIFECYCLE_*` directamente o a llamar la API imperativa. -7. **1G — Limpiar imports** en `arts/cache`, `arts/perm`, `arts/connection` - que ya no apunten a `$libs/active-app/events`. -8. **1H — Actualizar `artifact-docs.ts`** para reflejar la realidad. -9. **1I — Tests + commit**. - -### Fase 2 — Exposición sólida de API imperativa - -Si Fase 1 deja la API imperativa "como mejor se pudo", Fase 2 audita y -estabiliza: - -- Firma uniforme `cancelable(): { dispose(): void }` para suscripciones - externas si las hay. -- Documentar contractualmente qué métodos cada art expone para ser invocados - desde `orca`. -- Consolidar nombres (`invalidate` vs `clear`, `refresh` vs `reload`). - -### Fase 3 — Modelo de servicios - -Implementar `AppServiceSchema`, `AppService`, `AppServiceFactory`, -`createActiveApp({ services: { … } })` con type-safety, init modes, -validación. - -Subpasos: - -1. Definir tipos en `arts/active-app/services.ts`. -2. Cada art expone su `defineActive*()` / `defineEngine*()` factory. -3. Reescribir `createActiveApp()` para construirse desde el schema. -4. Migrar las apps cliente (web/routes) a la nueva API. -5. Eliminar las firmas legacy `App.createActive*()`. - -### Fase 4 — Integración con orca v0 - -Cuando `arts/orca/` esté implementado: - -1. `App.orchestration` (orca) reemplaza al *bridge code* provisional de - Fase 1E. -2. Los registros provisionales `bus.on(SESSION_EVENT_*, ...)` se reescriben - como `App.orchestration.onEvent(SESSION_EVENT_*, { id, stage, action })`. -3. `aapp` deja de tener cualquier `bus.on()` directo. Solo compose + - factory + lifecycle. - ---- - -## 7. Notas para evaluadores - -### 7.1 Qué validar antes de mergear este diseño - -- ¿La distinción núcleo/servicios cubre todos los casos? Por ejemplo, `dom` - está en núcleo pero solo tiene sentido en cliente. Quizás `dom` y - `frontend` deberían ser servicios opcionales con default `present` en - cliente y `absent` en server. -- ¿`orchestration` (orca) en el núcleo es lo correcto, o debería ser un - servicio opcional para apps que no orquestan nada? -- ¿La separación `libs/X` (contrato) vs `arts/X` (runtime) sigue - justificándose para todos los módulos, o solo cuando hay consumidores en - capa abstracta? - -### 7.2 Riesgos no resueltos - -- **API imperativa actualmente parcial**: `cache.invalidate({ on: ... })` - hoy sí existe; `perm.refresh()` también. Pero se pasaba el flag `on:` a - un option que el art interpretaba; con la nueva API, debe pasarse - explícitamente desde la action de orca o desde el bridge provisional. -- **Tests de ecosistema** asumen comportamiento auto-reactivo. Migrar test - por test es trabajo manual no automatizable. -- **`bus-session-source`** en `arts/connection` es un caso de adapter de App - a Connection. Considerar moverlo a `arts/active-app/integrations/` - para no contaminar `connection` con vocabulario de App. - -### 7.3 Coherencia con memoria del usuario - -Este diseño respeta: - -- **"arts/* boundaries — runtime only"**: arts dejan de filtrar vocabulario - de App. -- **"Define dependencies in the API, not via silent fallbacks"**: el schema - hace explícitas las dependencias entre servicios. -- **"Minimal API surface"**: factories `define*()` no añaden helpers - redundantes; reusan los `createActive*()` ya existentes. -- **"professional-grade design preferences"**: sin acoplamientos ocultos, - sin runtime arg-shape magic, type-safety completa. - ---- - -## Apéndice A — Mapa de eventos `APP_EVENT_*` - -``` -APP_EVENT_USER_IDENTITY_CHANGED → eliminar (republicación de SESSION_EVENT_LIFECYCLE_*) -APP_EVENT_PERMISSIONS_REFRESH_REQUESTED → eliminar (comando → App.perm.refresh()) -APP_EVENT_CACHE_INVALIDATE_REQUESTED → eliminar (comando → App.cache.invalidate(...)) -APP_EVENT_CONNECTIVITY_CHANGED → mover a arts/connection (CONNECTION_EVENT_*) -APP_EVENT_TENANT_SWITCHED → mantener provisional (App es dueña actualmente) -APP_EVENT_DISPOSE_STARTING → mantener (App es dueña real del lifecycle) -``` - -## Apéndice B — Tipos preliminares - -```ts -// arts/active-app/services.ts (a crear en Fase 3) - -export type ServiceState = - | 'pending' - | 'initializing' - | 'running' - | 'failed' - | 'disposing' - | 'disposed'; - -export type ServiceInitMode = 'immediate' | 'lazy'; - -export interface AppServiceFactory< - TName extends string, - TDeps extends Record, - TInstance -> { - readonly name: TName; - readonly initMode: ServiceInitMode; - readonly dependencies: readonly (keyof TDeps & string)[]; - create(deps: TDeps): TInstance; - dispose?(instance: TInstance): void | Promise; -} - -export interface AppService { - readonly serviceName: TName; - readonly initMode: ServiceInitMode; - readonly dependencies: readonly string[]; - readonly state: ServiceState; - readonly instance: TInstance; - readonly dispose: () => void | Promise; -} - -export type AppServiceSchema = Record>; - -export type ResolveAppInstances = { - [K in keyof S]: S[K] extends AppServiceFactory - ? I - : never; -}; -``` - -## Apéndice C — Ejemplo de uso final - -```ts -import { createActiveApp } from '$active-app'; -import { defineActiveCache } from '$cache'; -import { defineActiveSession } from '$session'; -import { defineEngineHttp } from '$http'; -import { SESSION_EVENT_LIFECYCLE_REVOKED } from '$session'; -import { ORCA_STAGE_MAIN, orcaSuccess } from '$orca'; - -const App = createActiveApp({ - // Núcleo configurable - lang: { schema: appLang, defaultLocale: 'es' }, - storage: { adapter: localStorageAdapter() }, - frontend: { theme: 'system' }, - - // Servicios opt-in (opcionales, declarados) - services: { - cache: defineActiveCache({ adapter: 'memory' }), - session: defineActiveSession({ - refresh: refreshFn - }).withInitMode('immediate'), - http: defineEngineHttp({ baseUrl: '/api' }), - } -}); - -// Orquestación explícita vía orca (no escondida en services:) -App.orchestration.onEvent(SESSION_EVENT_LIFECYCLE_REVOKED, { - id: 'app.invalidate-on-revoke', - stage: ORCA_STAGE_MAIN, - action: async () => { - await App.cache.invalidate({ on: 'userIdentityChange' }); - return orcaSuccess(); - } -}); - -// Type-safety: -App.cache // ✅ ActiveCache -App.session // ✅ ActiveSession -App.http // ✅ EngineHttp -App.perm // ❌ TS error: 'perm' is not in services schema -App.connections // ❌ TS error -``` - ---- - ---- - -## 8. Revisión arquitectónica recibida — round 1 - -Llegó un análisis externo del código actual con tres observaciones que el -diseño inicial no había nombrado. - -### 8.1 Tres mecanismos de orquestación compitiendo - -`active-app.svelte.ts` no tiene un solo problema de acoplamiento, tiene tres -mecanismos paralelos, cada uno reinventando orquestación con menos rigor que -el anterior: - -1. **Suscripciones internas en los servicios** (`Cache`, `Perms`, `Connection`) - — el problema diagnosticado en §1. -2. **`wireSessionTranslator`** — republica `SESSION_EVENT_LIFECYCLE_*` como - `APP_EVENT_USER_IDENTITY_CHANGED`. Combinado con los presets - `APP_ORCHESTRATION_TRANSLATOR_*` declarados en `consts.ts`, esto es **un - orca embrionario sin tokens, sin stages, sin políticas, enterrado en - aapp**. -3. **`createAuthCacheInvalidator`** — orquestación implícita auth ↔ cache ↔ - perms con 0 trazabilidad. - -Conclusión arquitectónica: ya estás haciendo orca, solo que sin formalizar. -Cuando llegue, no añade complejidad: la **reemplaza**. - -### 8.2 Asimetría singleton vs multi-create - -Hoy: - -- `createActiveSession`, `createActivePerms`, `createActiveAuth` lanzan - `*AlreadyCreatedError` si se llaman dos veces. -- `createActiveConnections` mantiene un `Set` y permite - múltiples instancias. - -El modelo de servicios borra esta asimetría: **una instancia por servicio -declarado**. `connections` pasa a singleton. El caso raro de "varios -registries" se resuelve en código de aplicación, no en infraestructura. - -### 8.3 La distinción "siempre presente" vs "factory" es ruido - -`Cache` se construye siempre que se construye App. `Sess`, `Perms`, `Auth` -son factories invocadas a demanda. La razón histórica es bundle (cache es -"casi siempre necesario") no principio de diseño. Con servicios declarativos, -la distinción desaparece: **todos los servicios opcionales se declaran o no -se declaran**. Cero asimetría. - -### 8.4 Código propuesto (round 1) - -#### `arts/active-app/services.ts` - -```ts -import type { EngineBus } from '$bus'; -import type { ActiveTimers } from '$timer'; -import type { EngineLogger } from '$logger'; - -/** - * El núcleo siempre presente. Todos los servicios pueden depender de - * cualquier subset de estas piezas. - */ -export interface CoreServices { - readonly logger: EngineLogger; - readonly bus: EngineBus; - readonly timers: ActiveTimers; - // readonly orca: EngineOrca; // cuando v0.0 aterrice -} - -export type CoreServiceKey = keyof CoreServices; - -export type ServiceInitMode = 'immediate' | 'lazy'; - -export type ServiceStatus = 'absent' | 'present' | 'failed'; - -export interface AppServiceFactory< - TName extends string = string, - TCoreDeps extends readonly CoreServiceKey[] = readonly CoreServiceKey[], - TServiceDeps extends readonly string[] = readonly string[], - TInstance = unknown -> { - readonly name: TName; - readonly coreDependencies: TCoreDeps; - readonly serviceDependencies?: TServiceDeps; - readonly initMode?: ServiceInitMode; - - create(deps: { - core: Pick; - services: Partial>; - }): TInstance; - - dispose?(instance: TInstance): void | Promise; -} - -export type AppServiceSchema = Record; - -export type ResolveServiceInstances = { - [K in keyof TSchema]: TSchema[K] extends AppServiceFactory< - string, - readonly CoreServiceKey[], - readonly string[], - infer I - > - ? I - : never; -}; -``` - -#### `arts/active-app/active-app.svelte.ts` (refactor) - -Más corto que el actual. Toda la lógica "factory por artefacto" desaparece; -la responsabilidad pasa al schema. - -```ts -import { createActiveTimers } from '$timer/active-timers.svelte'; -import { createSvelteEngineBus } from '$bus'; -import { createEngineLogger } from '$logger/engine-logger'; -// import { createEngineOrca } from '$orca'; // cuando v0.0 aterrice - -import type { - ActiveApp, - ActiveAppOptions, - ActiveAppBusEvents -} from './types.ts'; -import type { - AppServiceSchema, - CoreServices, - ServiceStatus -} from './services.ts'; - -export function createActiveApp( - options: ActiveAppOptions = {} as ActiveAppOptions -): ActiveApp { - // Núcleo - const Logger = createEngineLogger(options.logger); - const Timers = createActiveTimers({ ...options.timers, logger: Logger }); - const Bus = createSvelteEngineBus({ - ...options.bus, - logger: Logger, - clock: Timers.clock - }); - // const Orca = createEngineOrca({ bus: Bus, timers: Timers, logger: Logger }); - - const core: CoreServices = { logger: Logger, bus: Bus, timers: Timers }; - - // Servicios declarados - const schema = options.services ?? ({} as TSchema); - const builders = buildServiceBuilders(schema, core); - - let disposed = false; - - return { - Logger, - Bus, - Timers, - // Orca, - ...builders.proxies, - - get services() { - return builders.statusMap(); - }, - - dispose() { - if (disposed) return; - disposed = true; - builders.disposeAll(); // servicios primero - Bus.dispose(); // núcleo después - Timers.dispose(); - Logger.dispose(); - } - } as ActiveApp; -} - -interface ServiceBuilders { - readonly proxies: Record; - readonly statusMap: () => Record; - readonly disposeAll: () => void; -} - -function buildServiceBuilders( - schema: AppServiceSchema, - core: CoreServices -): ServiceBuilders { - validateSchema(schema); - const order = topologicalOrder(schema); - - const instances = new Map(); - const status = new Map(); - const failures = new Map(); - - for (const name of order) { - status.set(name, 'absent'); - const factory = schema[name]; - if (factory.initMode === 'immediate') construct(name); - } - - function construct(name: string): unknown { - if (instances.has(name)) return instances.get(name); - - const factory = schema[name]; - const coreSubset = pick(core, factory.coreDependencies); - const serviceSubset: Record = {}; - for (const dep of factory.serviceDependencies ?? []) { - if (schema[dep]) serviceSubset[dep] = construct(dep); - } - - try { - const instance = factory.create({ core: coreSubset, services: serviceSubset }); - instances.set(name, instance); - status.set(name, 'present'); - return instance; - } catch (error) { - status.set(name, 'failed'); - failures.set(name, error); - throw error; - } - } - - const proxies: Record = {}; - for (const name of Object.keys(schema)) { - Object.defineProperty(proxies, name, { - get: () => construct(name), - enumerable: true - }); - } - - function disposeAll() { - const built = order.filter((n) => instances.has(n)).reverse(); - for (const name of built) { - try { - schema[name].dispose?.(instances.get(name)); - } catch { /* dispose errors do not propagate */ } - } - instances.clear(); - } - - return { - proxies, - statusMap: () => Object.fromEntries(status), - disposeAll - }; -} - -function pick(obj: T, keys: readonly K[]): Pick { - const result = {} as Pick; - for (const k of keys) result[k] = obj[k]; - return result; -} - -function validateSchema(schema: AppServiceSchema): void { - for (const [key, factory] of Object.entries(schema)) { - if (factory.name !== key) { - throw new Error( - `[active-app] service factory name "${factory.name}" must match schema key "${key}"` - ); - } - } -} - -function topologicalOrder(schema: AppServiceSchema): string[] { - const visited = new Set(); - const visiting = new Set(); - const order: string[] = []; - - function visit(name: string) { - if (visited.has(name)) return; - if (visiting.has(name)) { - throw new Error(`[active-app] dependency cycle detected at "${name}"`); - } - visiting.add(name); - const factory = schema[name]; - if (factory) { - for (const dep of factory.serviceDependencies ?? []) { - if (schema[dep]) visit(dep); - } - } - visiting.delete(name); - visited.add(name); - order.push(name); - } - - for (const name of Object.keys(schema)) visit(name); - return order; -} -``` - -#### `arts/active-app/types.ts` (refactor) - -```ts -import type { EngineBus, EngineBusOptions } from '$bus'; -import type { EngineLogger, LoggerOptions } from '$logger'; -import type { ActiveTimers, EngineTimersOptions } from '$timer'; -// import type { EngineOrca } from '$orca'; // cuando v0.0 aterrice -import type { ActiveAppBusEvents } from './bus-events'; -import type { - AppServiceSchema, - ResolveServiceInstances, - ServiceStatus -} from './services.ts'; - -export interface ActiveAppOptions { - logger?: LoggerOptions; - timers?: Omit; - bus?: Omit; - services?: TSchema; -} - -export interface ActiveAppCore { - readonly Logger: EngineLogger; - readonly Bus: EngineBus; - readonly Timers: ActiveTimers; - // readonly Orca: EngineOrca; -} - -export type ActiveApp = ActiveAppCore & - ResolveServiceInstances & { - readonly services: Record; - dispose(): void; - }; -``` - -### 8.5 Decisiones a las preguntas abiertas (round 1) - -| Pregunta | Decisión | -|---|---| -| ¿Bus/Timers/Logger siempre construidos? | **SÍ.** Coste despreciable. SSR mínimo paga ~50ns y gana coherencia. | -| ¿Big-bang vs coexistencia de APIs? | **Big-bang en rama dedicada.** Coexistencia genera dos APIs vivas que confunden. | -| ¿`services: TSchema` opcional? | **SÍ, default `{}`.** `createActiveApp()` zero-arg sigue siendo válido para tests y SSR mínimo. | - -### 8.6 Lo que desaparece del código actual con el refactor (round 1) - -- `wireSessionTranslator` — pasa a preset. -- `loadPersistedFrontendPreferences` y `bindFrontendStorage` — se mueven al - factory `defineActiveFrontend`. Viven con el artefacto, no en aapp. -- `createAuthCacheInvalidator` — pasa a preset opt-in. -- Flags `Sess !== undefined` y errores `APP_ERROR_ALREADY_CREATED_*` — el - modelo declarativo garantiza singleton por construcción. -- Sistema de presets `APP_ORCHESTRATION_STANDARD/SILENT/TRANSLATORS` — se - vuelven funciones explícitas opt-in (§9). - ---- - -## 9. Revisión arquitectónica recibida — round 2 - -Después del round 1 emergen dos refinamientos críticos. El primero cambia -**cuándo** se hace el refactor; el segundo cambia **dónde** vive cada pieza. - -### 9.1 Construir orca v0.0 ANTES del refactor de aapp - -Argumento decisivo: si refactorizas aapp sin orca, los presets se escriben -con `Bus.on()` directo. Cuando llegue orca, hay que reescribirlos como -acciones registradas con stages, tokens, políticas. Esa segunda iteración -**no es migración trivial** — es repensar cada flujo crítico (qué stage, -qué tokens, qué política de fallo). Se toman las mismas decisiones de -diseño dos veces, divergen, y los presets `Bus.on` quedan como código -heredado que "funciona" hasta que alguien tenga tiempo de migrarlos. -Spoiler: ese tiempo no llega. - -**Construir orca v0.0 ahora —aunque con motor mínimo— fija el modelo -mental antes de escribir un solo preset.** Cuando avances a v0.1 con -tokens activos, los presets ya tienen la forma correcta y solo ganan -capacidades. - -### 9.2 Orca v0.0: superficie completa, motor mínimo - -La regla operativa: - -> Toda la API pública del orca futuro existe en v0.0. Internamente, las -> características avanzadas son no-ops o se reducen al caso simple. - -**Entra en v0.0:** - -- `createEngineOrca({ bus, timers, logger })` — constructor real. -- `onEvent(event, action)` — registro funcional con detach. -- Action interface completo (`id`, `stage`, `action`, `onError`, `after`, - `unless`, `abortOn`, `provides`, `actionTimeoutMs`, `compensate`). -- Result types: `orcaSuccess`, `orcaSkipped`, `orcaError` (los tres - mínimos producidos; `orcaTimeout` y `orcaFatal` definidos pero no - producidos). -- `OrcaRunResult` con trace básico. -- `dispose()` idempotente. -- Diagnostics catalogados via `logr` (RUN_STARTED, RUN_COMPLETED, - ACTION_STARTED, ACTION_FAILED, ACTION_COMPLETED, ACTION_SKIPPED, - RUN_ABORTED, CONFIGURATION_INVALID). - -**Reducido a versión simple en v0.0:** - -- **Stages**: existen como concepto, motor ejecuta en orden canónico, y - dentro de cada stage en orden de registro. Sin paralelismo, sin priority. -- **Tokens (`after`/`provides`/`unless`/`abortOn`)**: aceptados en el - action interface pero **ignorados por el motor**. Los presets pueden - declararlos correctamente para forward-compat. -- **Políticas de error**: solo `CONTINUE` y `ABORT_RUN` distinguen. Las - demás se aceptan en el tipo y se tratan como `CONTINUE`. -- **Timeouts**: aceptados en el action interface, ignorados por el motor. - Acciones que cuelgan, cuelgan. Documentado como limitación temporal. -- **Concurrencia entre runs**: solo modo `QUEUE` implícito. Eventos - recibidos durante un run se encolan FIFO. -- **Compensación**: campo `compensate` aceptado, motor no lo invoca. -- **Validación estática del grafo**: solo IDs duplicados. Ciclos de - tokens, deadlocks, etc., para v0.1. - -**No entra en v0.0:** - -- `ActiveOrca` (capa reactiva). -- `/test/orca` con visualización del DAG. -- Replay determinista. -- Modos `REPLACE` / `DROP` / `PARALLEL`. -- Helper `setupOrca({ tokens, events, actions })` con tipado estricto. - -Resultado: motor de ~400-600 líneas con **contrato exterior indistinguible -del orca completo**. Las acciones que se escriban hoy funcionan el día que -orca esté completo, sin cambios. - -### 9.3 Cambio crítico: presets y factories viven en `arts/active-app/` - -**Esto corrige una recomendación inicial errónea de §3.** - -La idea inicial fue "los presets viven cerca del owner semántico" — por -ejemplo `arts/cache/presets/invalidate-on-identity-change.ts`. Eso **es -incorrecto** porque reintroduce el acoplamiento que estamos eliminando: si -el preset vive en `arts/cache/`, entonces `arts/cache/` importa -`SESSION_EVENT_LIFECYCLE_*` desde `$session` y constantes de orca desde -`$orca`. **Acoplamiento de art a art** vuelve por la puerta de atrás justo -cuando lo estamos sacando por la principal. - -La regla correcta: - -> **Las artes no conocen a otras artes.** No importan tipos, ni eventos, ni -> constantes de otras artes. Solo dependen del núcleo (`logger`, `bus`, -> `timers`, `orca`) y de utilidades en `libs/`. -> -> **Los presets de orquestación viven en `arts/active-app/presets/`** porque -> por definición conocen múltiples artes. Son código de composición, no de -> artefacto. -> -> **Los `define*()` factories viven en `arts/active-app/service-factories/`** -> por la misma razón: importan de las artes (`createActiveCache` desde -> `$cache`) y del active-app (`AppServiceFactory` desde `./services`). Si -> vivieran en `arts/cache/`, ese módulo importaría `AppServiceFactory` desde -> `$active-app` y se rompería la inversión de dependencias. - -Consecuencia: los arts (`arts/cache/index.ts`, `arts/session/index.ts`, -etc.) quedan **puros**. Solo exportan motor + Active reactivo + tipos -propios. Cero conocimiento de composición. Cero conocimiento de App. - -### 9.4 `App.Orca` (no `App.Orchestration`) - -Coherente con: - -- El namespace del núcleo: `Logger`, `Bus`, `Timers`, `Orca`. Cadencia - visual mantenida. -- El alias del import: `$orca`. Que el campo de App se llame distinto al - artefacto sería ruido innecesario. -- El propio `arts/orca/README.md` que documenta el artefacto como "Orca". - -La objeción "Orca no es autoexplicativo para alguien que no conoce el -sistema" no aplica: nadie sabe qué es `Bus` o `Format` sin leer la -documentación. La consistencia interna sí es un objetivo realista; la -autoexplicación al primer vistazo no. - -### 9.5 La matización sobre paquetes externos - -Si en el futuro algún art se distribuye como paquete npm reusable, querría -exportar sus propios presets. Para v0 esto **no aplica**: todos los presets -en `arts/active-app/presets/`. Si llega ese caso, decidiremos entonces. No -premature factoring. - ---- - -## 10. Plan revisado de implementación - -| Paso | Trabajo | Tiempo estimado | -|---|---|---| -| **1** | Implementar `arts/orca/` v0.0 (motor + tests). | ~1 semana | -| **2** | Refactor `aapp` con modelo de servicios + `App.Orca` siempre presente. | ~3 días | -| **3** | Reescribir traductores actuales como presets en `arts/active-app/presets/`. Eliminar `wireSessionTranslator`, `createAuthCacheInvalidator`, `APP_ORCHESTRATION_*`. | ~1 semana | -| **4** | Migrar páginas (`web/routes/*`) al schema declarativo. Iterativo. | variable | - -El paso 1 es lo que destraba todo. Sin él, el paso 3 no tiene un sitio donde -aterrizar (los presets no pueden escribirse sobre la API final). - ---- - -## 11. Decisiones cerradas (consolidado) - -1. **Modelo:** núcleo (siempre presente: Logger/Bus/Timers/Orca) + servicios - (opt-in declarado). -2. **Naming:** `createActiveApp(options)`. `defineActiveX` / `defineEngineX` - como helpers. `App.Orca` como campo del núcleo. -3. **Tipo del schema:** `services: { [K in TName]: AppServiceFactory<...> }` - con K literal para inferencia. -4. **Servicios declarados que no existen como factory:** error. -5. **Dependencias faltantes:** error en compile-time (cuando los tipos - alcanzan) y en `commit()` runtime como respaldo. -6. **Init mode por servicio:** `lazy` por defecto. `immediate` solo si el - `define*()` lo declara así o el schema lo override. -7. **Núcleo configurable** vía opciones existentes (`lang`, `storage`, - `frontend`, …) en la raíz del options object. -8. **Orquestación**: vive en orca. Los presets reusables en - `arts/active-app/presets/`. -9. **Eventos APP_***: sobreviven solo `DISPOSE_STARTING`. El resto se - elimina. -10. **`session-translator`**: se elimina. Pasa a preset - `applySessionRepublishIdentity`. -11. **Presets viven en `arts/active-app/presets/`**, no en cada art. -12. **`define*()` factories viven en `arts/active-app/service-factories/`**. -13. **Bus, Timers, Logger, Orca siempre construidos** — núcleo, no - opcional. -14. **Big-bang en rama dedicada**, no coexistencia de APIs. -15. **`services: TSchema` opcional con default `{}`**. -16. **Orca v0.0 antes del refactor de aapp**. Pre-requisito. -17. **Singleton uniforme** para todos los servicios. `connections` pasa a - singleton (rompiendo la asimetría actual con `Set`). - ---- - -## Apéndice D — Estructura final del directorio - -### `arts/` - -``` -src/arts/ -├── orca/ ← motor de orquestación (artefacto puro) -│ ├── README.md -│ ├── consts.ts -│ ├── errors.ts -│ ├── types.ts -│ ├── result.ts -│ ├── engine-orca.ts -│ ├── diagnostics.ts -│ ├── index.ts -│ └── test/ -├── cache/ ← motor puro, no conoce a nadie -├── session/ ← motor puro -├── auth/ ← motor puro -├── perm/ ← motor puro -├── connection/ ← motor puro -├── lang/, logger/, timer/, format/, frontend/, dom/, sium/, storage/, http/, bus/ -└── active-app/ ← composición (conoce a todos) - ├── README.md - ├── refactorizacion.md - ├── consts.ts - ├── errors.ts - ├── types.ts - ├── services.ts ← AppServiceFactory, CoreServices - ├── service-builder.ts ← topología, lazy proxies, dispose - ├── active-app.svelte.ts ← createActiveApp() - ├── bus-context.svelte.ts - ├── integrations/ ← frontend-storage, etc. - ├── service-factories/ ← define*() para cada art - │ ├── index.ts - │ ├── lang.ts - │ ├── storage.ts - │ ├── format.ts - │ ├── frontend.ts - │ ├── dom.ts - │ ├── http.ts - │ ├── cache.ts - │ ├── session.ts - │ ├── perm.ts - │ ├── auth.ts - │ └── connections.ts - └── presets/ ← orquestación reusable opt-in - ├── index.ts ← applyStandardOrca + named exports - ├── session-republish-identity.ts - ├── cache-invalidate-on-identity-change.ts - ├── perm-invalidate-on-identity-change.ts - ├── auth-invalidate-cache-on-revoke.ts - ├── connection-republish-connectivity.ts - └── _shared/ - └── tokens.ts -``` - -### Cómo queda un art puro (ejemplo `arts/cache/index.ts`) - -Después del refactor, **cero conocimiento de App**: - -```ts -export { - createEngineCache, - createActiveCache -} from './active-cache.svelte'; - -export type { - EngineCache, - ActiveCache, - ActiveCacheOptions, - CacheSnapshot, - CacheEntry, - CacheError -} from './types'; - -export { CACHE_DIAGNOSTIC_EVENTS } from './consts'; -export { - CacheDisposedError, - CacheInvalidScopeError, - isCacheDisposedError -} from './errors'; -``` - -### Cómo queda un factory (ejemplo `arts/active-app/service-factories/cache.ts`) - -```ts -import { createActiveCache, type ActiveCache, type ActiveCacheOptions } from '$cache'; -import type { AppServiceFactory } from '../services'; - -export function defineActiveCache( - options: Omit = {} -): AppServiceFactory<'cache', ['logger', 'bus'], [], ActiveCache> { - return { - name: 'cache', - coreDependencies: ['logger', 'bus'], - initMode: 'lazy', - create({ core }) { - return createActiveCache({ - ...options, - logger: core.logger, - bus: core.bus - }); - }, - dispose(instance) { - instance.dispose(); - } - }; -} -``` - -### Cómo queda un preset (ejemplo `arts/active-app/presets/cache-invalidate-on-identity-change.ts`) - -```ts -import { SESSION_EVENT_LIFECYCLE_REVOKED, SESSION_EVENT_LIFECYCLE_ADOPTED } from '$session'; -import { ORCA_STAGE_MAIN, ORCA_ON_ERROR_CONTINUE, orcaSuccess, orcaError } from '$orca'; -import type { ActiveApp } from '../types'; - -const ACTION_ID = 'cache.invalidate-on-identity-change'; -const TOKEN_INVALIDATED = 'cache:invalidated-on-identity'; - -export function applyCacheInvalidateOnIdentityChange( - App: ActiveApp & { cache: { invalidate(opts: { scope?: string }): Promise } } -): () => void { - const detach1 = App.Orca.onEvent(SESSION_EVENT_LIFECYCLE_REVOKED, { - id: `${ACTION_ID}.revoked`, - stage: ORCA_STAGE_MAIN, - provides: [TOKEN_INVALIDATED], - onError: ORCA_ON_ERROR_CONTINUE, - action: async (payload) => { - try { - await App.cache.invalidate({ scope: payload.previousActorId }); - return orcaSuccess({ emits: [TOKEN_INVALIDATED] }); - } catch (error) { - return orcaError(error); - } - } - }); - - const detach2 = App.Orca.onEvent(SESSION_EVENT_LIFECYCLE_ADOPTED, { - id: `${ACTION_ID}.adopted`, - stage: ORCA_STAGE_MAIN, - provides: [TOKEN_INVALIDATED], - onError: ORCA_ON_ERROR_CONTINUE, - action: async (payload) => { - if (!payload.previousActorId) return orcaSuccess(); - try { - await App.cache.invalidate({ scope: payload.previousActorId }); - return orcaSuccess({ emits: [TOKEN_INVALIDATED] }); - } catch (error) { - return orcaError(error); - } - } - }); - - return () => { - detach1(); - detach2(); - }; -} -``` - -### Aggregator estándar (ejemplo `arts/active-app/presets/index.ts`) - -```ts -import type { ActiveApp } from '../types'; -import { applySessionRepublishIdentity } from './session-republish-identity'; -import { applyCacheInvalidateOnIdentityChange } from './cache-invalidate-on-identity-change'; -import { applyPermInvalidateOnIdentityChange } from './perm-invalidate-on-identity-change'; -import { applyAuthInvalidateCacheOnRevoke } from './auth-invalidate-cache-on-revoke'; -import { applyConnectionRepublishConnectivity } from './connection-republish-connectivity'; - -export { - applySessionRepublishIdentity, - applyCacheInvalidateOnIdentityChange, - applyPermInvalidateOnIdentityChange, - applyAuthInvalidateCacheOnRevoke, - applyConnectionRepublishConnectivity -}; - -export function applyStandardOrca(App: ActiveApp): () => void { - const detachers: Array<() => void> = []; - - if ('session' in App) detachers.push(applySessionRepublishIdentity(App as never)); - if ('cache' in App) detachers.push(applyCacheInvalidateOnIdentityChange(App as never)); - if ('perm' in App) detachers.push(applyPermInvalidateOnIdentityChange(App as never)); - if ('auth' in App && 'cache' in App) detachers.push(applyAuthInvalidateCacheOnRevoke(App as never)); - if ('connections' in App) detachers.push(applyConnectionRepublishConnectivity(App as never)); - - return () => { - for (const detach of detachers.reverse()) detach(); - }; -} -``` - -### Uso final desde la app - -```ts -import { createActiveApp } from '$active-app'; -import { - defineActiveLang, - defineActiveCache, - defineActiveSession, - defineEngineHttp -} from '$active-app/service-factories'; -import { applyStandardOrca } from '$active-app/presets'; - -export const App = createActiveApp({ - logger: { level: LogLevel.INFO }, - services: { - lang: defineActiveLang({ schema, defaultLocale: 'es' }), - cache: defineActiveCache({ adapter: 'memory' }), - session: defineActiveSession({ onRefresh, onRevoke }), - http: defineEngineHttp({ baseUrl: '/api' }) - } -}); - -applyStandardOrca(App); -``` - -La aplicación importa todo de `$active-app/*`. Los arts no aparecen en sus -imports. - ---- - -## Apéndice E — Contrato de `EngineOrca` v0.0 - -Versión completa del contrato y la implementación mínima del motor. La -disciplina aplicada es **superficie completa, motor mínimo**: campos -`@v0.0` se honran, campos `@v0.1+` se aceptan en los tipos pero el motor -no actúa sobre ellos. - -### E.1 `arts/orca/consts.ts` - -```ts -export const ORCA_MODULE = 'orca' as const; - -// ── Stages ──────────────────────────────────────────────────────── - -export const ORCA_STAGE_GUARD = 'guard' as const; -export const ORCA_STAGE_PRE = 'pre' as const; -export const ORCA_STAGE_MAIN = 'main' as const; -export const ORCA_STAGE_POST = 'post' as const; -export const ORCA_STAGE_CLEANUP = 'cleanup' as const; -export const ORCA_STAGE_FINALLY = 'finally' as const; - -export const ORCA_STAGES_CANONICAL_ORDER = [ - ORCA_STAGE_GUARD, - ORCA_STAGE_PRE, - ORCA_STAGE_MAIN, - ORCA_STAGE_POST, - ORCA_STAGE_CLEANUP, - ORCA_STAGE_FINALLY -] as const; - -// ── Result statuses ─────────────────────────────────────────────── - -export const ORCA_RESULT_SUCCESS = 'success' as const; -export const ORCA_RESULT_SKIPPED = 'skipped' as const; -export const ORCA_RESULT_ERROR = 'error' as const; -export const ORCA_RESULT_TIMEOUT = 'timeout' as const; // aceptado, no producido en v0.0 -export const ORCA_RESULT_FATAL = 'fatal' as const; // aceptado, no producido en v0.0 - -// ── Action statuses (en el run trace) ───────────────────────────── - -export const ORCA_ACTION_STATUS_SUCCESS = 'success' as const; -export const ORCA_ACTION_STATUS_SKIPPED = 'skipped' as const; -export const ORCA_ACTION_STATUS_BLOCKED = 'blocked' as const; -export const ORCA_ACTION_STATUS_ERROR = 'error' as const; -export const ORCA_ACTION_STATUS_TIMEOUT = 'timeout' as const; -export const ORCA_ACTION_STATUS_FATAL = 'fatal' as const; - -// ── Run statuses ────────────────────────────────────────────────── - -export const ORCA_RUN_SUCCESS = 'success' as const; -export const ORCA_RUN_PARTIAL = 'partial' as const; -export const ORCA_RUN_ABORTED = 'aborted' as const; -export const ORCA_RUN_FATAL = 'fatal' as const; -export const ORCA_RUN_TIMEOUT = 'timeout' as const; - -// ── Error policies ──────────────────────────────────────────────── -// v0.0 implementa CONTINUE y ABORT_RUN. El resto se acepta y se trata -// como CONTINUE. - -export const ORCA_ON_ERROR_CONTINUE = 'continue' as const; -export const ORCA_ON_ERROR_ABORT_ACTION = 'abort-action' as const; -export const ORCA_ON_ERROR_ABORT_STAGE = 'abort-stage' as const; -export const ORCA_ON_ERROR_ABORT_RUN = 'abort-run' as const; - -// ── Diagnostic event names ──────────────────────────────────────── - -export const ORCA_DIAGNOSTIC_EVENTS = { - RUN_STARTED: 'orca.run.started', - RUN_COMPLETED: 'orca.run.completed', - RUN_ABORTED: 'orca.run.aborted', - ACTION_STARTED: 'orca.action.started', - ACTION_COMPLETED: 'orca.action.completed', - ACTION_FAILED: 'orca.action.failed', - ACTION_SKIPPED: 'orca.action.skipped', - CONFIGURATION_INVALID: 'orca.configuration.invalid' -} as const; - -export const LOGGER_CATEGORY = 'orca' as const; -``` - -### E.2 `arts/orca/errors.ts` - -```ts -import { CodeError, errCode, type ErrCode, type ErrorMessages } from '$libs/errs'; - -const ORCA_ERR = 'orca' as const; - -export const ORCA_ERR_DISPOSED: ErrCode = errCode(ORCA_ERR, 'disposed'); -export const ORCA_ERR_DUPLICATE_ACTION_ID: ErrCode = errCode(ORCA_ERR, 'duplicate_action_id'); -export const ORCA_ERR_INVALID_STAGE: ErrCode = errCode(ORCA_ERR, 'invalid_stage'); -export const ORCA_ERR_INVALID_ACTION: ErrCode = errCode(ORCA_ERR, 'invalid_action'); - -export const ORCA_ERROR_MESSAGES: ErrorMessages = { - [ORCA_ERR_DISPOSED]: 'Orca engine has been disposed.', - [ORCA_ERR_DUPLICATE_ACTION_ID]: 'Action id already registered for this event.', - [ORCA_ERR_INVALID_STAGE]: 'Action declared with unknown stage.', - [ORCA_ERR_INVALID_ACTION]: 'Action definition is missing required fields.' -}; - -export class OrcaDisposedError extends CodeError { - constructor(message?: string) { - super(ORCA_ERR_DISPOSED, { message: message ?? ORCA_ERROR_MESSAGES[ORCA_ERR_DISPOSED] }); - } -} - -export class OrcaDuplicateActionIdError extends CodeError { - constructor(event: string, actionId: string) { - super(ORCA_ERR_DUPLICATE_ACTION_ID, { - message: `Action "${actionId}" already registered for event "${event}".` - }); - } -} - -export class OrcaInvalidStageError extends CodeError { - constructor(stage: string) { - super(ORCA_ERR_INVALID_STAGE, { - message: `Unknown stage "${stage}". Valid stages: guard, pre, main, post, cleanup, finally.` - }); - } -} - -export class OrcaInvalidActionError extends CodeError { - constructor(reason: string) { - super(ORCA_ERR_INVALID_ACTION, { - message: `Invalid action: ${reason}` - }); - } -} - -export function isOrcaDisposedError(error: unknown): error is OrcaDisposedError { - return error instanceof OrcaDisposedError; -} -``` - -### E.3 `arts/orca/types.ts` - -Contrato exterior completo. **Marcas `@v0.0` y `@v0.1+`** indican qué se -honra y qué se acepta-pero-ignora. - -```ts -import type { EngineBus } from '$bus'; -import type { ActiveTimers } from '$timer'; -import type { Logger } from '$libs/logr'; -import type { - ORCA_STAGE_GUARD, - ORCA_STAGE_PRE, - ORCA_STAGE_MAIN, - ORCA_STAGE_POST, - ORCA_STAGE_CLEANUP, - ORCA_STAGE_FINALLY, - ORCA_RESULT_SUCCESS, - ORCA_RESULT_SKIPPED, - ORCA_RESULT_ERROR, - ORCA_RESULT_TIMEOUT, - ORCA_RESULT_FATAL, - ORCA_ACTION_STATUS_SUCCESS, - ORCA_ACTION_STATUS_SKIPPED, - ORCA_ACTION_STATUS_BLOCKED, - ORCA_ACTION_STATUS_ERROR, - ORCA_ACTION_STATUS_TIMEOUT, - ORCA_ACTION_STATUS_FATAL, - ORCA_RUN_SUCCESS, - ORCA_RUN_PARTIAL, - ORCA_RUN_ABORTED, - ORCA_RUN_FATAL, - ORCA_RUN_TIMEOUT, - ORCA_ON_ERROR_CONTINUE, - ORCA_ON_ERROR_ABORT_ACTION, - ORCA_ON_ERROR_ABORT_STAGE, - ORCA_ON_ERROR_ABORT_RUN -} from './consts'; - -// ── Identifiers ─────────────────────────────────────────────────── - -export type OrcaStage = - | typeof ORCA_STAGE_GUARD - | typeof ORCA_STAGE_PRE - | typeof ORCA_STAGE_MAIN - | typeof ORCA_STAGE_POST - | typeof ORCA_STAGE_CLEANUP - | typeof ORCA_STAGE_FINALLY; - -export type OrcaToken = string; -export type OrcaActionId = string; -export type OrcaRunId = string; - -export type OrcaErrorPolicy = - | typeof ORCA_ON_ERROR_CONTINUE - | typeof ORCA_ON_ERROR_ABORT_ACTION - | typeof ORCA_ON_ERROR_ABORT_STAGE - | typeof ORCA_ON_ERROR_ABORT_RUN; - -// ── Result types ────────────────────────────────────────────────── - -export interface OrcaSuccess { - readonly ok: true; - readonly status: typeof ORCA_RESULT_SUCCESS; - readonly value?: TValue; - readonly emits?: readonly OrcaToken[]; -} - -export interface OrcaSkipped { - readonly ok: true; - readonly status: typeof ORCA_RESULT_SKIPPED; - readonly reason?: string; - readonly emits?: readonly OrcaToken[]; -} - -export interface OrcaError { - readonly ok: false; - readonly status: typeof ORCA_RESULT_ERROR; - readonly error: unknown; - readonly recoverable?: boolean; - readonly emits?: readonly OrcaToken[]; -} - -/** - * @v0.1+ Returned by the engine when an action exceeds `actionTimeoutMs`. - * @v0.0 The shape exists so action authors can type results, but the engine - * never produces one (timeouts are not enforced). - */ -export interface OrcaTimeout { - readonly ok: false; - readonly status: typeof ORCA_RESULT_TIMEOUT; - readonly timeoutMs: number; - readonly emits?: readonly OrcaToken[]; -} - -/** - * @v0.1+ Distinguished from `OrcaError` for run-aborting failures. - * @v0.0 Engine treats fatal as error. - */ -export interface OrcaFatal { - readonly ok: false; - readonly status: typeof ORCA_RESULT_FATAL; - readonly error: unknown; - readonly emits?: readonly OrcaToken[]; -} - -export type OrcaResult = - | OrcaSuccess - | OrcaSkipped - | OrcaError - | OrcaTimeout - | OrcaFatal; - -// ── Action context ──────────────────────────────────────────────── - -export interface OrcaActionContext { - readonly runId: OrcaRunId; - readonly event: string; - readonly stage: OrcaStage; - /** - * Tokens already emitted in the current run by previous actions. - * @v0.0 The set is populated correctly, but not consumed by the engine - * (after/unless/abortOn are ignored). Action authors MAY read it. - */ - readonly tokens: ReadonlySet; - /** - * Abort signal for the current action. - */ - readonly signal: AbortSignal; - /** - * Logger for ad-hoc diagnostics inside the action. - */ - readonly logger: Logger; -} - -// ── Action definition ───────────────────────────────────────────── - -export type OrcaActionFn = ( - payload: TPayload, - context: OrcaActionContext -) => OrcaResult | Promise>; - -export interface OrcaAction { - readonly id: OrcaActionId; - readonly stage: OrcaStage; - - /** @v0.1+ accepted, ignored in v0.0 */ - readonly after?: readonly OrcaToken[]; - /** @v0.1+ accepted, ignored in v0.0 */ - readonly unless?: readonly OrcaToken[]; - /** @v0.1+ accepted, ignored in v0.0 */ - readonly abortOn?: readonly OrcaToken[]; - - /** Documented for consumers; engine uses emits in run context regardless. */ - readonly provides?: readonly OrcaToken[]; - - /** @v0.1+ accepted, ignored. Long actions hang in v0.0. */ - readonly actionTimeoutMs?: number; - - /** @v0.0 Honored. Only CONTINUE and ABORT_RUN distinct. */ - readonly onError?: OrcaErrorPolicy; - - /** @v1+ accepted, never invoked in v0.0. */ - readonly compensate?: OrcaActionFn; - - readonly action: OrcaActionFn; -} - -// ── Run trace ───────────────────────────────────────────────────── - -export interface OrcaActionRun { - readonly id: OrcaActionId; - readonly stage: OrcaStage; - readonly status: - | typeof ORCA_ACTION_STATUS_SUCCESS - | typeof ORCA_ACTION_STATUS_SKIPPED - | typeof ORCA_ACTION_STATUS_BLOCKED - | typeof ORCA_ACTION_STATUS_ERROR - | typeof ORCA_ACTION_STATUS_TIMEOUT - | typeof ORCA_ACTION_STATUS_FATAL; - readonly startedAt?: number; - readonly endedAt?: number; - readonly durationMs?: number; - readonly emitted: readonly OrcaToken[]; - readonly error?: unknown; -} - -export interface OrcaRunResult { - readonly id: OrcaRunId; - readonly event: string; - readonly status: - | typeof ORCA_RUN_SUCCESS - | typeof ORCA_RUN_PARTIAL - | typeof ORCA_RUN_ABORTED - | typeof ORCA_RUN_FATAL - | typeof ORCA_RUN_TIMEOUT; - readonly startedAt: number; - readonly endedAt: number; - readonly durationMs: number; - readonly tokens: readonly OrcaToken[]; - readonly actions: readonly OrcaActionRun[]; -} - -// ── Engine ──────────────────────────────────────────────────────── - -export interface EngineOrcaOptions { - readonly bus: EngineBus; - readonly timers: ActiveTimers; - readonly logger?: Logger; - /** @default 256 */ - readonly maxRuns?: number; -} - -export interface EngineOrca { - onEvent( - event: string, - action: OrcaAction - ): () => void; - - actionCount(event: string): number; - recentRuns(): readonly OrcaRunResult[]; - readonly running: boolean; - readonly disposed: boolean; - dispose(): void; -} -``` - -### E.4 `arts/orca/result.ts` - -Helpers para construir results. Todo el ecosistema los usa en lugar de -literales. - -```ts -import { - ORCA_RESULT_SUCCESS, - ORCA_RESULT_SKIPPED, - ORCA_RESULT_ERROR, - ORCA_RESULT_TIMEOUT, - ORCA_RESULT_FATAL -} from './consts'; -import type { - OrcaSuccess, - OrcaSkipped, - OrcaError, - OrcaTimeout, - OrcaFatal, - OrcaToken -} from './types'; - -export function orcaSuccess( - options: { value?: TValue; emits?: readonly OrcaToken[] } = {} -): OrcaSuccess { - return { - ok: true, - status: ORCA_RESULT_SUCCESS, - value: options.value, - emits: options.emits - }; -} - -export function orcaSkipped( - reason?: string, - options: { emits?: readonly OrcaToken[] } = {} -): OrcaSkipped { - return { - ok: true, - status: ORCA_RESULT_SKIPPED, - reason, - emits: options.emits - }; -} - -export function orcaError( - error: unknown, - options: { emits?: readonly OrcaToken[]; recoverable?: boolean } = {} -): OrcaError { - return { - ok: false, - status: ORCA_RESULT_ERROR, - error, - recoverable: options.recoverable, - emits: options.emits - }; -} - -/** @v0.1+ */ -export function orcaTimeout( - timeoutMs: number, - options: { emits?: readonly OrcaToken[] } = {} -): OrcaTimeout { - return { - ok: false, - status: ORCA_RESULT_TIMEOUT, - timeoutMs, - emits: options.emits - }; -} - -/** @v0.1+ */ -export function orcaFatal( - error: unknown, - options: { emits?: readonly OrcaToken[] } = {} -): OrcaFatal { - return { - ok: false, - status: ORCA_RESULT_FATAL, - error, - emits: options.emits - }; -} -``` - -### E.5 `arts/orca/engine-orca.ts` — motor v0.0 - -```ts -import { - ORCA_STAGES_CANONICAL_ORDER, - ORCA_STAGE_FINALLY, - ORCA_ON_ERROR_CONTINUE, - ORCA_ON_ERROR_ABORT_RUN, - ORCA_RESULT_SUCCESS, - ORCA_RESULT_SKIPPED, - ORCA_RESULT_ERROR, - ORCA_RESULT_FATAL, - ORCA_ACTION_STATUS_SUCCESS, - ORCA_ACTION_STATUS_SKIPPED, - ORCA_ACTION_STATUS_ERROR, - ORCA_ACTION_STATUS_BLOCKED, - ORCA_RUN_SUCCESS, - ORCA_RUN_PARTIAL, - ORCA_RUN_ABORTED, - ORCA_DIAGNOSTIC_EVENTS, - LOGGER_CATEGORY -} from './consts'; -import { - OrcaDisposedError, - OrcaDuplicateActionIdError, - OrcaInvalidActionError, - OrcaInvalidStageError -} from './errors'; -import type { - EngineOrca, - EngineOrcaOptions, - OrcaAction, - OrcaActionContext, - OrcaActionRun, - OrcaResult, - OrcaRunId, - OrcaRunResult, - OrcaStage, - OrcaToken -} from './types'; - -const DEFAULT_MAX_RUNS = 256; - -export function createEngineOrca(options: EngineOrcaOptions): EngineOrca { - const { bus, timers, logger } = options; - const maxRuns = options.maxRuns ?? DEFAULT_MAX_RUNS; - - const actionsByEvent = new Map(); - const busDetachers = new Map void>(); - const recentRuns: OrcaRunResult[] = []; - const runQueue: Array<() => Promise> = []; - - let disposed = false; - let running = false; - let activeRunController: AbortController | null = null; - - function ensureNotDisposed() { - if (disposed) throw new OrcaDisposedError(); - } - - function validateAction(action: OrcaAction): void { - if (!action || typeof action !== 'object') { - throw new OrcaInvalidActionError('action must be an object'); - } - if (typeof action.id !== 'string' || action.id.length === 0) { - throw new OrcaInvalidActionError('action.id must be a non-empty string'); - } - if (typeof action.action !== 'function') { - throw new OrcaInvalidActionError('action.action must be a function'); - } - if (!ORCA_STAGES_CANONICAL_ORDER.includes(action.stage)) { - throw new OrcaInvalidStageError(action.stage); - } - } - - function onEvent( - event: string, - action: OrcaAction - ): () => void { - ensureNotDisposed(); - validateAction(action); - - const list = actionsByEvent.get(event) ?? []; - - if (list.some((a) => a.id === action.id)) { - throw new OrcaDuplicateActionIdError(event, action.id); - } - - list.push(action as OrcaAction); - actionsByEvent.set(event, list); - - // Suscripción lazy al bus: solo cuando se registra la primera acción. - if (!busDetachers.has(event)) { - const detach = bus.on(event, (payload: unknown) => { - enqueueRun(event, payload); - }); - busDetachers.set(event, detach); - } - - return () => { - const current = actionsByEvent.get(event); - if (!current) return; - const filtered = current.filter((a) => a.id !== action.id); - if (filtered.length === 0) { - actionsByEvent.delete(event); - busDetachers.get(event)?.(); - busDetachers.delete(event); - } else { - actionsByEvent.set(event, filtered); - } - }; - } - - function enqueueRun(event: string, payload: unknown): void { - if (disposed) return; - const actions = actionsByEvent.get(event); - if (!actions || actions.length === 0) return; - - // Snapshot: acciones registradas durante un run no participan en él. - const snapshot = actions.slice(); - - runQueue.push(() => executeRun(event, payload, snapshot)); - drainQueue(); - } - - async function drainQueue(): Promise { - if (running || disposed) return; - const next = runQueue.shift(); - if (!next) return; - - running = true; - try { - await next(); - } finally { - running = false; - if (!disposed && runQueue.length > 0) { - queueMicrotask(() => drainQueue()); - } - } - } - - async function executeRun( - event: string, - payload: unknown, - actions: OrcaAction[] - ): Promise { - const runId = generateRunId(); - const startedAt = timers.clock.now(); - const tokens = new Set(); - const actionRuns: OrcaActionRun[] = []; - const controller = new AbortController(); - activeRunController = controller; - - emitDiagnostic(ORCA_DIAGNOSTIC_EVENTS.RUN_STARTED, { runId, event }); - - let aborted = false; - const byStage = groupByStage(actions); - - for (const stage of ORCA_STAGES_CANONICAL_ORDER) { - const stageActions = byStage.get(stage); - if (!stageActions || stageActions.length === 0) continue; - - // FINALLY siempre se ejecuta, incluso tras abort. - if (aborted && stage !== ORCA_STAGE_FINALLY) continue; - - for (const action of stageActions) { - if (controller.signal.aborted) break; - - const actionRun = await runAction({ - action, - payload, - context: { - runId, - event, - stage, - tokens, - signal: controller.signal, - logger: logger ?? noopLogger() - }, - tokens - }); - - actionRuns.push(actionRun); - - if (actionRun.status === ORCA_ACTION_STATUS_ERROR) { - const policy = action.onError ?? ORCA_ON_ERROR_CONTINUE; - if (policy === ORCA_ON_ERROR_ABORT_RUN) { - aborted = true; - controller.abort(); - emitDiagnostic(ORCA_DIAGNOSTIC_EVENTS.RUN_ABORTED, { - runId, - event, - cause: actionRun.error - }); - break; - } - } - } - } - - const endedAt = timers.clock.now(); - const status = computeRunStatus(aborted, actionRuns); - - const runResult: OrcaRunResult = { - id: runId, - event, - status, - startedAt, - endedAt, - durationMs: endedAt - startedAt, - tokens: Array.from(tokens), - actions: actionRuns - }; - - recordRun(runResult); - activeRunController = null; - - emitDiagnostic(ORCA_DIAGNOSTIC_EVENTS.RUN_COMPLETED, { - runId, - event, - status, - durationMs: runResult.durationMs, - actionCount: actionRuns.length - }); - } - - async function runAction(opts: { - action: OrcaAction; - payload: unknown; - context: OrcaActionContext; - tokens: Set; - }): Promise { - const { action, payload, context, tokens } = opts; - const startedAt = timers.clock.now(); - - emitDiagnostic(ORCA_DIAGNOSTIC_EVENTS.ACTION_STARTED, { - runId: context.runId, - actionId: action.id, - stage: action.stage - }); - - if (context.signal.aborted) { - return { - id: action.id, - stage: action.stage, - status: ORCA_ACTION_STATUS_BLOCKED, - startedAt, - endedAt: startedAt, - durationMs: 0, - emitted: [] - }; - } - - try { - const result = await action.action(payload, context); - const endedAt = timers.clock.now(); - const emitted = result.emits ?? []; - - for (const token of emitted) tokens.add(token); - - const status = mapResultToActionStatus(result); - - if (status === ORCA_ACTION_STATUS_ERROR) { - emitDiagnostic(ORCA_DIAGNOSTIC_EVENTS.ACTION_FAILED, { - runId: context.runId, - actionId: action.id, - error: (result as { error?: unknown }).error - }); - } else if (status === ORCA_ACTION_STATUS_SKIPPED) { - emitDiagnostic(ORCA_DIAGNOSTIC_EVENTS.ACTION_SKIPPED, { - runId: context.runId, - actionId: action.id, - reason: (result as { reason?: string }).reason - }); - } else { - emitDiagnostic(ORCA_DIAGNOSTIC_EVENTS.ACTION_COMPLETED, { - runId: context.runId, - actionId: action.id, - durationMs: endedAt - startedAt - }); - } - - return { - id: action.id, - stage: action.stage, - status, - startedAt, - endedAt, - durationMs: endedAt - startedAt, - emitted: Array.from(emitted), - error: status === ORCA_ACTION_STATUS_ERROR - ? (result as { error?: unknown }).error - : undefined - }; - } catch (thrown) { - // Excepciones no capturadas se convierten en error. - const endedAt = timers.clock.now(); - emitDiagnostic(ORCA_DIAGNOSTIC_EVENTS.ACTION_FAILED, { - runId: context.runId, - actionId: action.id, - error: thrown, - thrown: true - }); - return { - id: action.id, - stage: action.stage, - status: ORCA_ACTION_STATUS_ERROR, - startedAt, - endedAt, - durationMs: endedAt - startedAt, - emitted: [], - error: thrown - }; - } - } - - function recordRun(run: OrcaRunResult): void { - recentRuns.push(run); - while (recentRuns.length > maxRuns) recentRuns.shift(); - } - - function emitDiagnostic(event: string, data: Record): void { - if (!logger) return; - logger.debug?.({ category: LOGGER_CATEGORY, message: event, data }); - } - - function dispose(): void { - if (disposed) return; - disposed = true; - activeRunController?.abort(); - for (const detach of busDetachers.values()) detach(); - busDetachers.clear(); - actionsByEvent.clear(); - runQueue.length = 0; - } - - return { - onEvent, - actionCount(event: string) { - return actionsByEvent.get(event)?.length ?? 0; - }, - recentRuns() { - return recentRuns.slice(); - }, - get running() { - return running; - }, - get disposed() { - return disposed; - }, - dispose - }; -} - -// ── Helpers ─────────────────────────────────────────────────────── - -function groupByStage(actions: OrcaAction[]): Map { - const result = new Map(); - for (const action of actions) { - const list = result.get(action.stage) ?? []; - list.push(action); - result.set(action.stage, list); - } - return result; -} - -function mapResultToActionStatus(result: OrcaResult) { - switch (result.status) { - case ORCA_RESULT_SUCCESS: - return ORCA_ACTION_STATUS_SUCCESS; - case ORCA_RESULT_SKIPPED: - return ORCA_ACTION_STATUS_SKIPPED; - case ORCA_RESULT_ERROR: - return ORCA_ACTION_STATUS_ERROR; - case ORCA_RESULT_FATAL: - return ORCA_ACTION_STATUS_ERROR; // v0.0 trata fatal como error - default: - return ORCA_ACTION_STATUS_ERROR; // timeout no se produce en v0.0 - } -} - -function computeRunStatus(aborted: boolean, actions: OrcaActionRun[]) { - if (aborted) return ORCA_RUN_ABORTED; - const anyError = actions.some( - (a) => a.status === 'error' || a.status === 'fatal' || a.status === 'timeout' - ); - if (anyError) return ORCA_RUN_PARTIAL; - return ORCA_RUN_SUCCESS; -} - -function generateRunId(): OrcaRunId { - return `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; -} - -function noopLogger() { - const noop = () => {}; - return { - trace: noop, - debug: noop, - info: noop, - warn: noop, - error: noop, - fatal: noop - }; -} -``` - -### E.6 `arts/orca/index.ts` (barrel) - -```ts -export { createEngineOrca } from './engine-orca'; -export { - orcaSuccess, - orcaSkipped, - orcaError, - orcaTimeout, - orcaFatal -} from './result'; - -export { - ORCA_MODULE, - ORCA_STAGE_GUARD, - ORCA_STAGE_PRE, - ORCA_STAGE_MAIN, - ORCA_STAGE_POST, - ORCA_STAGE_CLEANUP, - ORCA_STAGE_FINALLY, - ORCA_STAGES_CANONICAL_ORDER, - ORCA_RESULT_SUCCESS, - ORCA_RESULT_SKIPPED, - ORCA_RESULT_ERROR, - ORCA_RESULT_TIMEOUT, - ORCA_RESULT_FATAL, - ORCA_RUN_SUCCESS, - ORCA_RUN_PARTIAL, - ORCA_RUN_ABORTED, - ORCA_RUN_FATAL, - ORCA_RUN_TIMEOUT, - ORCA_ON_ERROR_CONTINUE, - ORCA_ON_ERROR_ABORT_ACTION, - ORCA_ON_ERROR_ABORT_STAGE, - ORCA_ON_ERROR_ABORT_RUN, - ORCA_DIAGNOSTIC_EVENTS, - LOGGER_CATEGORY -} from './consts'; - -export { - OrcaDisposedError, - OrcaDuplicateActionIdError, - OrcaInvalidStageError, - OrcaInvalidActionError, - isOrcaDisposedError -} from './errors'; - -export type { - EngineOrca, - EngineOrcaOptions, - OrcaAction, - OrcaActionFn, - OrcaActionContext, - OrcaActionId, - OrcaActionRun, - OrcaError, - OrcaErrorPolicy, - OrcaFatal, - OrcaResult, - OrcaRunId, - OrcaRunResult, - OrcaSkipped, - OrcaStage, - OrcaSuccess, - OrcaTimeout, - OrcaToken -} from './types'; -``` - -### E.7 Matriz de tests para v0.0 - -```ts -describe('EngineOrca v0.0', () => { - describe('registration', () => { - it('registra y elimina acciones por evento'); - it('lanza OrcaDuplicateActionIdError si se registra la misma id dos veces'); - it('lanza OrcaInvalidStageError si el stage es inválido'); - it('lanza OrcaInvalidActionError si falta id o action'); - it('se suscribe al bus al registrar la primera acción del evento'); - it('se desuscribe del bus al eliminar la última acción del evento'); - }); - - describe('execution', () => { - it('ejecuta acciones en orden canónico de stages'); - it('ejecuta acciones del mismo stage en orden de registro'); - it('captura excepciones de acciones como error'); - it('agrega tokens emitidos al run context (aunque no los consume)'); - it('ejecuta finally aunque el run haya sido abortado'); - it('no ejecuta acciones registradas durante un run en ese mismo run'); - }); - - describe('error policies', () => { - it('continúa con onError: CONTINUE'); - it('aborta el run con onError: ABORT_RUN'); - it('trata ABORT_ACTION y ABORT_STAGE como CONTINUE en v0.0'); - }); - - describe('concurrency', () => { - it('encola eventos del mismo tipo durante un run en vuelo'); - it('procesa eventos encolados en orden FIFO'); - }); - - describe('run trace', () => { - it('produce OrcaRunResult con startedAt/endedAt/durationMs'); - it('lista todas las acciones ejecutadas con su status'); - it('respeta maxRuns en recentRuns()'); - }); - - describe('disposal', () => { - it('dispose() es idempotente'); - it('aborta el run en vuelo al disponer'); - it('lanza OrcaDisposedError al registrar tras dispose'); - it('eventos del bus tras dispose no ejecutan acciones'); - }); - - describe('v0.1+ accepted-but-ignored fields', () => { - it('acepta after sin esperar tokens'); - it('acepta unless sin saltar acciones'); - it('acepta abortOn sin bloquear'); - it('acepta actionTimeoutMs sin enforcer timeout'); - it('acepta compensate sin invocarlo'); - }); -}); -``` - -### E.8 Lo que el motor v0.0 NO hace (resumen claro) - -Para que el README de orca pueda referenciarlo: - -- No espera tokens (`after` ignorado). -- No salta acciones por tokens presentes (`unless` ignorado). -- No bloquea acciones por tokens (`abortOn` ignorado). -- No respeta timeouts (`actionTimeoutMs` ignorado). -- No invoca compensaciones (`compensate` ignorado). -- No detecta deadlocks de tokens. -- No expone `ActiveOrca` (la capa reactiva). -- No tiene modos de concurrencia configurables — siempre QUEUE. -- No distingue ABORT_ACTION ni ABORT_STAGE — todos son CONTINUE excepto - ABORT_RUN. - -Lo que sí garantiza: **un preset bien escrito en v0.0 sigue funcionando -correctamente en v0.1+, ganando capacidades sin reescritura**. Esa es la -propiedad de diseño que justifica este enfoque. - ---- - -## Cambios aplicados durante esta sesión - -A medida que se ejecutan las fases, esta sección se actualiza: - -- **2026-05-02 (sesión de diseño)**: - - Documento creado (§1–§7): análisis inicial, modelo de servicios, - decisiones, plan. - - §8: revisión round 1 con código concreto. - - §9: revisión round 2 — orca v0.0 primero, presets y factories en - `arts/active-app/`. - - §10–§11: plan revisado, decisiones consolidadas (17 items). - - Apéndices D y E: estructura final + contrato completo de - `EngineOrca` v0.0. - -- **2026-05-02 (sesión larga de implementación)**: - - **Paso 1 (commit `0eddd2a`)** — `arts/orca/` v0.0 implementado. - 9 archivos, +37 tests. - - **Paso 2A (commit `d528652`)** — Contratos `services.ts` + - `service-builder.ts` + 8 factories puros (lang, storage, format, - dom, frontend, http, sium, auth). +23 tests. - - **Paso 2B (commit `a04fa67`)** — 4 factories restantes (cache, - perm, session, connections) + 4 presets de orca + agregador - `applyStandardOrca`. +8 tests. - - **Paso 3A (commit `fb2e3ac`)** — `App.Orca` añadido al núcleo de - `createActiveApp()`. - - **Paso 3B (commit `7148a5d`)** — `services: TSchema` aceptado en - `createActiveApp()`. `App.cache`, `App.session`, etc. (lowercase) - accesibles. +6 tests. - - **Paso 3C (commit `60e130b`)** — APIs legacy marcadas - `@deprecated` con guía de migración (cache/perm options, - `App.createActiveX()`, `App.Sess/Perms/Auth`, - `ActiveAppOptions.{connections, permissions, auth, orchestration}`, - publishers de APP_EVENT_*). - -- **Suite total: 1408 tests pasan.** Sin regresiones. - -## Estado actual: BIG-BANG COMPLETADO (commits `01a85ad` + `64ab1f0`) - -El refactor está cerrado. La API legacy ha sido eliminada completamente. - -**Modelo único soportado en `master`:** - -```ts -const App = createActiveApp({ - logger: { ... }, - services: { - cache: defineActiveCache(), - session: defineActiveSession({ onRefresh, onRevoke }), - http: defineEngineHttp({ baseUrl: '/api' }) - } -}); -applyStandardOrca(App); -``` - -`App.Orca` siempre presente. Servicios construyen lazy. Acceso vía -propiedades lowercase (`App.cache`, `App.session`). - -## Eliminaciones aplicadas en el big-bang - -### Tests legacy (commit `01a85ad`) - -- `ecosystem.integration.test.ts` (9 monolithic, ~1900 líneas). -- `session-translator.test.ts`. -- `active-app.test.ts` reescrito de 1072 → ~200 líneas con tests - enfocados en composición del núcleo. - -### Código legacy (commit `64ab1f0`) - -- `arts/active-app/integrations/session-translator.ts` y `auth-cache.ts`. -- `arts/connection/bus-session-source.ts`. -- `libs/active-app/` directorio entero. Su contenido se consolidó en - `arts/active-app/{consts,errors,events}.ts`. -- `arts/cache`: `bus` y `autoInvalidateOn` options, `wireAutoInvalidation`, - `CACHE_AUTO_INVALIDATE_*` constantes y types. -- `arts/perm`: `bus` y `autoInvalidateOn`, `wireAutoInvalidation`, - `PERM_AUTO_INVALIDATE_*`. -- `arts/connection`: `bus` option en `EngineConnectionsOptions`, - `shouldWireBusSessionSource` helper. -- `arts/active-app/active-app.svelte.ts`: - - `createSiumEngine()`, `createActiveSession()`, `createActiveConnections()`, - `createActivePerms()`, `createActiveAuth()` factory methods. - - `App.Sess`/`App.Perms`/`App.Auth` getters y singleton guards. - - Sistema `APP_ORCHESTRATION_*` entero (presets, translators, - `resolveActiveAppOrchestration`, `STANDARD_ORCHESTRATION_TRANSLATORS`). - - `APP_ERROR_ALREADY_CREATED_*` y `APP_ERROR_CREATE_PERM_ENDPOINT_REQUIRED` - constantes. -- `APP_EVENT_USER_IDENTITY_CHANGED`, `TENANT_SWITCHED`, - `PERMISSIONS_REFRESH_REQUESTED`, `CACHE_INVALIDATE_REQUESTED`, - `CONNECTIVITY_CHANGED`. Solo sobrevive `APP_EVENT_DISPOSE_STARTING`. -- Sus payloads y `APP_USER_IDENTITY_CAUSE_*`. -- `publishApp{UserIdentityChanged, PermsRefreshRequested, - CacheInvalidateRequested, TenantSwitched, ConnectivityChanged}`. - Sobreviven `publishAppDisposeStarting` y el nuevo `onAppDisposeStarting`. - -### Forma final de `arts/active-app/` - -``` -arts/active-app/ -├── README.md -├── refactorizacion.md (este documento) -├── active-app.svelte.ts — createActiveApp() compositor + schema -├── bus-context.svelte.ts — getBus / setBus para Svelte component context -├── consts.ts — APP_MODULE, APP_BUS_CONTEXT_KEY, runtime gates -├── errors.ts — todo el infra de errores (ya no en libs/) -├── events.ts — APP_EVENT_DISPOSE_STARTING + helpers -├── types.ts — ActiveApp, ActiveAppOptions, etc. -├── services.ts — AppServiceFactory, CoreServices, schema types -├── service-builder.ts — topología, lazy proxies, dispose -├── service-factories/ — define*() para cada art (12 archivos) -├── presets/ — orca actions opt-in + applyStandardOrca -├── integrations/ — frontend-storage (lo único que queda) -├── testing/ — createTestApp helper -└── test/ — composition + schema + presets + builder + factories -``` - -`libs/active-app/` ya no existe. - -## Suite de tests - -**1374 tests pasan.** La diferencia respecto al pico de 1408 son los -tests legacy eliminados; la cobertura del modelo nuevo es comprehensiva -(`schema-declarative.test.ts`, `service-builder.test.ts`, -`service-factories.test.ts`, `presets.test.ts`, `active-app.test.ts` -core, más las suites por art). diff --git a/docs/audit-1-5.md b/docs/audit-1-5.md deleted file mode 100644 index cabb4ac..0000000 --- a/docs/audit-1-5.md +++ /dev/null @@ -1,259 +0,0 @@ -# Active framework — ecosystem audit (arts / libs / svrs) - -Scope: `src/arts/`, `src/libs/`, `src/svrs/`. The `src/web/` folder is -explicitly excluded. Findings are grouped by audit axis. Severity tags: - -- **bug** — incorrect runtime behaviour -- **violation** — breaks an established framework rule -- **drift** — code or docs out of sync with the rest -- **opportunity** — refactor / clean-up that is not a bug today - -Each item includes file paths and line ranges so the fix can be applied -without re-searching. Items are ordered roughly by priority within each -section. - ---- - -## 1. Layer-boundary violations - -Rule: `arts/` may import from `$libs/*` and from its own internal -files; cross-artifact imports (`$`) are reserved for the -composition root `arts/aapp`. `libs/*` may not import from any -`$` alias. `svrs/*` is similarly restricted. - -### Real coupling (runtime, not test) — **violation** - -- `src/arts/fend/active-frontend.svelte.ts:2` — - `import { createActiveDom, type ActiveDom } from '$adom';` - `arts/fend` reaches into `arts/adom`. The DOM port should live in - `libs/dom` and `fend` should accept an interface, leaving the DOM - engine wiring to `aapp`. -- `src/arts/sium/engine-resolver.ts:1-2` — - `import { ID_FALLBACK_SEPARATOR, parseLangRef } from '$lang';` - `arts/sium` imports runtime symbols from `arts/lang`. These two - values are pure helpers and belong in `libs/lang` so `sium` can - consume them without crossing artifacts. -- `src/arts/conn/engine-connections.ts:1` — - `import { createEngineTimers } from '$timr';` - `arts/conn` constructs its own timer engine instead of accepting a - `TimerScheduler` through options. The pattern elsewhere (sess, cach, - perm) is "App injects Timers"; conn should follow it. - -### Type-only cross-artifact imports — **opportunity** - -These compile away but still couple the two artifacts at the type -level. They should be promoted to `libs//types.ts` so consumers -import the contract without naming the engine. - -- `src/arts/auth/client.ts:2` — `import type { EngineHttp, HttpBodyInit, HttpResult } from '$http';` -- `src/arts/sess/types.ts:33` — `import type { SyncStorageAdapter } from '$stor';` -- `src/arts/sium/engine-resolver.ts:1` — `import type { EngineLang, LangParams, SupportedLocale } from '$lang';` -- `src/arts/conn/types.ts` and related — `import type { TimerScheduler, ... } from '$timr';` - -### `svrs/*` and `aapp` — clean - -`src/svrs/*` does not import any `$` alias. `arts/aapp` is -the composition root and is allowed to import every engine; the -imports there are intentional. - ---- - -## 2. Naming convention drift - -Rule: every module-event / method / diagnostic value must be a -**module-scoped lowercase string** (`'sess.lifecycle.adopted'`, -`'cach.delete'`, `'conn.connected'`, `'buss.event.published'`). Bare -names (`'delete'`, `'hit'`, `'auth_failed'`) collide across artifacts -once aggregated in logs or wired through the bus. - -### Unscoped diagnostic event values — **violation** - -- `src/arts/conn/consts.ts:4-20` — `CONNECTION_DIAGNOSTIC_EVENTS` has - 15 bare values: `'auth_failed'`, `'browser_reconnect'`, - `'connect_failed'`, `'frame_decode_failed'`, `'frame_encode_failed'`, - `'heartbeat_timeout'`, `'listener_threw'`, `'reauth_failed'`, - `'reconnect_exhausted'`, `'send_failed'`, `'session_changed'`, - `'session_expired'`, `'session_refreshed'`, `'session_revoked'`, - `'transport_error'`. All should carry the `'conn.'` prefix. -- `src/arts/sess/consts.ts:4-20` — `SESSION_DIAGNOSTIC_EVENTS` has 15 - bare values: `'adopt_server_invalid'`, `'disposed_access'`, - `'listener_threw'`, `'refresh_*'` (5×), `'revoke_global_*'` (3×), - `'storage_*'` (3×). All need `'sess.'`. -- `src/arts/perm/consts.ts:41-45` — - `PERMISSION_CLIENT_DIAGNOSTIC_EVENTS` has `'remote_batch_failed'`, - `'remote_check_failed'`, `'remote_what_failed'`. All need `'perm.'`. -- `src/svrs/perm/consts.ts:3-7` — `PERMISSION_DIAGNOSTIC_EVENTS` has - `'decision'`, `'denied'`, `'indeterminate'`. All need a `'perm.'` - prefix (or a server-specific scope, e.g. `'perm.server.decision'`). - -### Unscoped method constants — **violation** - -- `src/svrs/perm/consts.ts:19-25` — `PERMISSION_METHOD_*` constants - hold bare names (`'check'`, `'can'`, `'assert'`, `'explain'`, - `'what'`, `'who'`, `'filter'`). The client-side counterpart in - `arts/perm` already uses `'perm.check'`, `'perm.can'`, … — server - must align. - -### Why this matters - -`Logger.warn(category, message)` aggregates across artifacts. With -unscoped diagnostic strings, a value like `'listener_threw'` appears -under both `category: 'conn'` and `category: 'sess'` and is -indistinguishable in any flat log search. - ---- - -## 3. Error-class and dispose-pattern consistency - -### Hard-coded error `name` strings — **violation** - -The convention is `this.name = _ERROR_NAME_*` (read from -`consts.ts`). - -- `src/arts/sium/core/types.ts:302` — `SiumValidationError` sets - `this.name = 'SiumValidationError'` literally. -- `src/arts/sium/core/types.ts:322` — `SiumAsyncSchemaError` same - pattern. - -### Error classes without matching `is*Error` guards — **violation** - -The pattern is "one class, one guard". Missing guards make `instanceof` -checks leak into call sites. - -- `src/arts/conn/errors.ts:49` — `ConnChannelAlreadyExistsError` no - `isConnChannelAlreadyExistsError`. -- `src/arts/conn/errors.ts:58` — `ConnChannelNotFoundError` no - `isConnChannelNotFoundError`. -- `src/libs/auth/errors.ts:24-66` — 10 auth error classes have no - guards: `AuthAccountNotLinkedError`, `AuthSessionRevokedError`, - `AuthAssuranceRequiredError`, `AuthRateLimitedError`, - `AuthTenantBoundaryError`, `AuthOAuthStateInvalidError`, - `AuthOAuthProviderError`, `AuthOtpInvalidError`, - `AuthMfaRequiredError`, `AuthWebAuthnError`. - -### `dispose()` without idempotency guard — **bug** - -The convention is `if (disposed) return; disposed = true; …`. Calling -`dispose()` twice should be a no-op. - -- `src/arts/conn/active-connections.svelte.ts:118` — second call would - re-traverse `detachers` (empty by then) and call `engine.dispose()` - again. -- `src/arts/fend/active-frontend.svelte.ts:185` — second call would - re-invoke `ownedDom?.dispose()` if the inner DOM was created here. - -### `Logger` option not defaulting to `SILENT_LOGGER` — **violation** - -Pattern in the codebase: `const logger = options.logger ?? SILENT_LOGGER;` -(see `arts/buss/engine-bus.ts:54`). - -- `src/arts/stor/engine-storage.ts` — `createStorageDiagnostics(options.logger)` - passes the `undefined` straight through. -- `src/arts/timr/engine-timers.ts` — same issue with - `createTimerDiagnostics`. -- `src/arts/http/engine-options.ts` — same issue with - `createHttpDiagnostics`. - -If diagnostics ever calls a method on the logger without guarding for -`undefined`, these three engines crash when used without a logger -(e.g. in unit tests or smoke harnesses). Verify each diagnostics -constructor; if it already null-checks internally, demote to -**opportunity** for consistency only. - -### Generic `throw new Error(...)` in runtime code — **opportunity** - -Typed error classes carry the discriminating `name` plus optional -fields (cause, code). Generic throws lose that. - -- `src/arts/lang/engine-lang.ts:124, 133` — circular-reference message - thrown as plain `Error`. -- `src/libs/perm/runtime.ts:154, 181` — throws - `Error(PERMISSION_ERROR_MSG_FILTER_REQUIRES_ACTOR)` where a - `PermissionInvalidFilterError` would fit. - ---- - -## 4. Dead code, casts, and `any` - -Codebase is largely clean. Specific findings worth acting on: - -- `src/svrs/auth/adapters/db.ts:40, 64, 72, 79, 111, 134` — six - repetitions of `as unknown as Readonly>`. - Consolidate into a `DbRow` type alias or a `coerceRow()` helper. - **opportunity**. -- `src/libs/buss/silent-bus.ts:137` — `} as unknown as EngineBus;`. - Acceptable because the no-op surface is intentionally generic, but - worth replacing with proper generic constraints when the object - literal grows. **opportunity**. -- `src/arts/lang/types.ts:83` — `HasParams` uses `any` deliberately - (contravariant variance). Comment is in place. **no action**. -- `src/libs/days/_vendor/**` — multiple `@ts-ignore` and `TODO` - comments. Vendor code, leave as-is. **no action**. - -No dead exports were found in the spot checks of `libs/buss`, -`libs/auth`, `arts/conn`, `libs/aapp/events.ts`. No duplicate type -definitions detected across the audited modules. - ---- - -## 5. Documentation drift - -Only the high-traffic READMEs were sampled. - -### `arts/buss/README.md` - -- The session-translator example (around line 563) hardcodes `cause: - APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED` for every event. The actual - translator (`arts/aapp/integrations/session-translator.ts:37`) calls - `resolveAppIdentityCause(payload.event)` to map each `EVENT_*` to - the correct cause. The example should mirror that mapping or it - teaches the wrong pattern. **drift**. -- The README was already partially refreshed earlier in this session; - re-check the "Implementation gaps → Done" list now that - `subscribe()`, `publishCausedBy()`, `invokeListener`, the depth - guard, the payload-cloneable check, the Svelte adapter, the - `createBusRecent` wrapper, the `bus-context.svelte.ts`, and the - `APP_EVENT_RUNTIMES` table all landed. - -### `arts/sess/README.md` - -- The README still references a `publishSessIdentityChanged` helper. - Actual export is `publishSessLifecycleEvent` in - `arts/sess/bus-helpers.ts`. Either rename in code or update the - README. **drift**. - -### `arts/cach/README.md` - -- README written in Spanish (a deliberate choice; not flagged as a - rule break). At lines 11–12 it suggests importing - `createEngineCache` from `$svrs/cach`, but the artifact's barrel - (`arts/cach/index.ts`) only exports `createActiveCache`. Add a one- - line note clarifying the engine vs active split. **drift**. - -### Other artifacts (aapp, adom, auth, conn, fend, fmts, http, lang, logr, perm, sium, stor, timr) - -Spot checks did not surface drift — claims match exports. - ---- - -## Suggested fix order - -1. **Coupling fixes** (Section 1, runtime violations). They constrain - every other refactor: until `fend → adom`, `sium → lang`, and - `conn → timr` are decoupled, those artifacts cannot be tested in - isolation. -2. **Naming convention pass** (Section 2). Mechanical, scoped to four - `consts.ts` files, and unblocks coherent log aggregation. Update - any test that hard-codes the literal values. -3. **Error guards + idempotent `dispose`** (Section 3). Low-risk and - localised; prevents subtle bugs if any consumer ever calls - `dispose()` twice or tries to discriminate auth errors. -4. **Logger defaults** (Section 3). Verify the three diagnostics - constructors and add `?? SILENT_LOGGER` where missing. -5. **Documentation refresh** (Section 5). Easiest after the code - changes above so the README reflects the final shape. -6. **Cast consolidation** (Section 4). Pure refactor, ship it whenever - convenient. - -Items in Section 4 (vendor code, justified `any`, intentional cast) -require **no action**. diff --git a/docs/before_0_1.md b/docs/before_0_1.md deleted file mode 100644 index e0e702c..0000000 --- a/docs/before_0_1.md +++ /dev/null @@ -1,309 +0,0 @@ -# before_0_1 - -> Lo que hay que **cerrar, incluir o verificar** antes de etiquetar -> `0.1.0`. Construido sobre las evidencias del repo a fecha de hoy y los -> hallazgos del audit. Un par de bloqueantes son políticas / hygiene que -> pesan más que cualquier hallazgo técnico individual. - ---- - -## 1. Qué significa `0.1` aquí (definición operativa) - -`0.1` no es _production-ready_. Es la versión que **un equipo externo -puede probar sin sorpresas grandes**, sabiendo que la API pública no -romperá silenciosamente durante la línea `0.1.x`. Esto fija el listón: - -- API pública de los 9 roots always-present **congelada** durante `0.1.x`. -- Las 5 factorías scoped (`Sium`, `Session`, `Connections`, `Auth`, - `Permissions`) pueden iterar pero deben respetar deprecation policy. -- Sin bugs de seguridad conocidos en flujos hot. -- Repo aceptable para un PR externo: licencia, README, contributing, CI. -- Docs suficientes para enviar una feature sin leer el código fuente. -- Build estático funciona end-to-end (no solo el typecheck). - -Lo que **no** entra en `0.1`: MFA en producción, OAuth con todos los -proveedores, todos los módulos con docs profundas, métricas / telemetría, -ejemplos reales más allá de las páginas `/test`. - ---- - -## 2. Estado real verificado hoy - -Antes de listar gaps, lo que ya está cerrado (corroborado leyendo el -repo, los tests y los gates locales): - -- **Tests reales**: 108 archivos / 1208 tests verdes. -- **Typecheck**: `npm run check` 0/0/0. -- **Build estático**: `npm run build` verde. -- **Gate completo**: `npm run test:all` verde (`check` + `test` + - `build` + `test:static` + `test:bundle`). -- **Bundle smoke**: `createActiveApp({})` está medido con Vite/OXC en - `scripts/bundle-smoke.mjs`; baseline actual ~65 KB gzip, presupuesto - `0.1` en 70 KB gzip configurable con `ACTIVE_BUNDLE_GZIP_LIMIT_KB`. -- **Refactor wave** documentada en `NEXT_STEPS.md` ha resuelto - parcialmente varios de mis hallazgos del audit: - - **C2/C3** (Auth → Permissions/Cache invalidation) — cableado a través - de `arts/aapp/integrations/auth-cache.ts`. _A verificar_ con tests - de race cross-actor que el flujo real cierra el gap. - - **C1** (perm race cross-actor) — el cliente perm se ha partido en - `client-cache.ts` / `client-keys.ts` / `client-snapshot.ts`. _A - verificar_ que `pending` se invalida en `hydrate` / cambio de scope. - - Cobertura: `auth + sess + perm + cach` ya tienen test de - integración cruzada. -- **Brand brief** (`BRAND.md`) escrito y assets estáticos añadidos. -- **Documentación interna** (`/active`): shell + landing + Get Started, - páginas para todos los módulos bajo `docs/`, sección de capa - servidor (`$svrs`), seguridad, versionado y guía para AI agents. La - profundidad por módulo sigue siendo desigual y es trabajo pendiente. - -Lo que **sigue abierto** sale en §3. - ---- - -## 3. Bloqueantes duros para `0.1` - -Cada bloqueante lleva una _Acceptance criteria_ verificable. - -### 3.1 Repo hygiene - -Estado actual: - -| Pieza | Estado | Evidencia | -| ------------------ | ------- | --------------------------------------------------------------------------------------------- | -| `LICENSE` | Cerrado | Existe y coincide con `package.json` (`UNLICENSED`). | -| `README.md` | Cerrado | Incluye tagline, quick start, módulos, desarrollo, seguridad y link a `BRAND.md`. | -| `SECURITY.md` | Cerrado | Política de reporte y threat model resumido. | -| `CONTRIBUTING.md` | Cerrado | Setup, verificación, layout, constants-first, logging/diagnostics y deprecation policy. | -| `CHANGELOG.md` | Cerrado | Sigue Keep a Changelog y abre `0.1.0` como target. | -| `.github/` | Cerrado | CI, bug report, feature request y PR template. | -| `.gitignore` | Cerrado | Ignora `tmp-active-docs-*.log` y estado local de IDE/herramientas sin ignorar auditorías/docs. | -| `package.json` | Cerrado | `license`, `engines.node`, `repository`, `bugs` y `homepage` presentes. | - -Queda como higiene manual: no commitear directorios personales (`.claude/`, -`.opencode/`, `.idea/`) ni logs temporales ya ignorados. - -### 3.2 Seguridad - -Varios puntos del audit ya están cerrados en código y tests. Mantenerlos aquí -como evidencias evita que el checklist vuelva a arrastrar deuda antigua: - -| ID | Estado | Evidencia | -| --- | -------- | ------------------------------------------------------------------------------------------------------ | -| S1 | Cerrado | `rate-limit.test.ts`; password, recovery y OAuth llaman `enforceAuthRateLimit(...)`. | -| S2 | Cerrado | `oauth-pkce.test.ts`; `completeOAuth` entrega `codeVerifier` al provider y valida el challenge. | -| S3 | Cerrado | `engine-password.test.ts`; `verifyMfaChallenge` no está en la superficie estable de `EngineAuth`. | -| S4 | Cerrado | Auth y Cache memory adapters emiten warning productivo con tests; `svrs/perm` no tiene memory adapter. | -| S5 | Cerrado | `AUTH_COOKIE_POLICY.SAME_SITE` es `strict`; `csrf.test.ts` lo cubre. | -| S6 | Cerrado | `client-http.test.ts` cubre in-flight decisions y batch/what tras cambio de actor/scope. | - -| ID | Bloqueante | Acceptance | -| --- | ------------------------------ | ------------------------------------------------------------------------------------------------------ | -| S7 | Cerrado | `SECURITY.md` documenta cookie scopes, CSRF flow, refresh rotation, OAuth state binding, MFA actual y actor/tenant model. | - -### 3.3 Correctitud / API - -| ID | Estado | Evidencia | -| --- | ------- | -------------------------------------------------------------------------------------------------------------- | -| A1 | Cerrado | `AUTH_METHOD_*` / `PERMISSION_METHOD_*` se usan en `ensureLive`; tests verifican mensajes post-dispose. | -| A2 | Cerrado | `active-app.test.ts` incluye snapshot de superficie de roots y factories scoped actuales. | -| A4 | Cerrado | `AuthRateLimitPort` se exporta porque S1 está cableado y cubierto por tests. | -| A5 | Cerrado | Mono Lang está documentado como passthrough tipado amplio; warnings DEV pasan por `Logger` bajo `lang.mono`. | - -| ID | Bloqueante | Acceptance | -| --- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- | -| A3 | Cerrado | `CONTRIBUTING.md` describe `@deprecated`, warning runtime, `__EXPERIMENTAL_*` y ventana mínima de una minor. | - -### 3.4 Documentación mínima - -`/active` ya tiene rutas para todos los módulos, pero la profundidad sigue -siendo desigual. Para `0.1`: - -| Bloqueante | Acceptance | -| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| Los **9 roots always-present** con docs reales (no stub). | `App, Lang, Logger, Formats, Frontend, Dom, Storage, Http, Timers, Cache` tienen sección Overview / Quick start / API / Limits / Testing. | -| Las 5 factorías scoped pueden ser stub si llevan una nota "shape final en `0.2`". | El stub indica explícitamente qué partes de su API no están comprometidas. | -| `SECURITY.md` (cubre S7) **enlazado** desde sidebar de `/active`. | Item en la sección `Get Started` con ruta `/active/security`. | -| Migration / versioning policy publicada. | Página `/active/get-started/versioning` o sección dentro de Composition. | - -### 3.5 Build y tooling - -| Pieza | Estado | Evidencia | -| ---------------------- | ------- | -------------------------------------------------------------------------------------------------- | -| Build estático | Cerrado | `npm run build` está incluido en `test:all` y fue verificado localmente. | -| CI en GitHub Actions | Cerrado | `.github/workflows/ci.yml` ejecuta typecheck, tests, build, static smoke y bundle smoke. | -| Scripts npm completos | Cerrado | `test:typecheck`, `test:static`, `test:bundle` y `test:all` existen. | -| Reproducibilidad | Cerrado | `package-lock.json`, `.nvmrc` y `engines.node >=22` presentes. | -| Bundle size sanity | Cerrado | `scripts/bundle-smoke.mjs` verifica `createActiveApp({})` contra `ACTIVE_BUNDLE_GZIP_LIMIT_KB`. | - -> `npm run lint` existe, pero sigue siendo deuda global pre-`0.1`; CI no -> lo ejecuta hasta que el árbol completo quede limpio. La regla mientras -> tanto es no añadir deuda nueva en archivos tocados. - -### 3.6 Marca y assets - -Estado actual: - -| Pieza | Estado | Evidencia | -| --------------------------- | ------- | ----------------------------------------------------------------------------------------- | -| `static/favicon.svg` | Cerrado | SVG presente. | -| `favicon-16/32/48.png` | Cerrado | PNGs presentes. | -| `apple-touch-icon.png` | Cerrado | Icono 180 presente. | -| `icon-192/512.png` | Cerrado | Iconos PWA presentes. | -| `icon-maskable-512.png` | Cerrado | Icono maskable presente. | -| `og-image.png` | Cerrado | Imagen OG presente. | -| `app.html` | Cerrado | Theme color, description, OG tags, favicons, apple-touch-icon y manifest enlazados. | -| `manifest.webmanifest` | Cerrado | Manifest presente con iconos. | - -Queda como revisión manual: abrir `/active` en navegador real y comprobar -contraste/legibilidad de sidebar, code blocks y callouts en tema claro/oscuro. - ---- - -## 4. Bloqueantes blandos (pueden caer en `0.1.1`–`0.1.x`) - -Son hallazgos del audit con impacto real pero acotables a un parche: - -- M4 (refactor `validateOptionalField` en sess) — limpieza; no rompe nada. -- M5 (acoplamiento `aapp` ↔ `stor` en mensajes de log) — se puede pulir - más tarde sin tocar la superficie pública. -- M9 (body-scroll-lock con scheduling propio en vez de delegar a `timr`) — - optimización; no afecta la API. -- M10 (HTTP recompone headers en cada retry) — perf, no semántica. -- M14 (sess `BroadcastChannel` ignora el `event` recibido y vuelve a - leer storage) — documentar como diseño y seguir. -- m1–m15 (menores: docs faltantes, magic strings residuales, refactors - cosméticos). - -Estos pueden entrar como issues etiquetados `0.1.x` y resolverse de -manera incremental. - ---- - -## 5. Fuera de alcance para `0.1` (explícito) - -Para evitar que el sprint se infle, dejar fuera y comunicarlo: - -- MFA real (TOTP, WebAuthn, recovery codes). -- OAuth provider catalog (GitHub, Google, etc.) — solo el motor + un - proveedor de ejemplo. -- Métricas / telemetría / OpenTelemetry export. -- Server-side rendering completo del docs site (es estático). -- Componentes UI prefabricados (eso vive _encima_ de los artefactos). -- Internacionalización del propio docs site (English-only en `0.1`). -- Distribución por `npm publish` — `0.1` puede vivir solo como repo - template / submódulo / vendoring. Decidir si publicar es objetivo. - ---- - -## 6. Verificación pre-tag (un solo comando) - -Antes de etiquetar `0.1.0`, todo esto debe pasar en CI y local: - -```sh -# 1. Gate completo automatizado -npm run test:all # → check + tests + build + static smoke + bundle smoke - -# 2. Tipos, si se quiere aislar el paso -npm run check # → 0 errors / 0 warnings - -# 3. Suite, si se quiere aislar el paso -npm test # → 0 failed; assert >= 1208 tests - -# 4. Build estático, si se quiere aislar el paso -npm run build # → produce build/ sin errores - -# 5. Bundle smoke, si se quiere aislar el paso -npm run test:bundle # → createActiveApp({}) <= 70 KB gzip por defecto - -# 6. Static smoke, si se quiere aislar el paso tras build -npm run test:static # → rutas/docs/assets críticos existen en build/ -``` - -Y, cuando se quiera verificar el sitio servido: - -```sh -npm run preview -curl -fsS http://localhost:4173/active | grep -q "active" -curl -fsS http://localhost:4173/test/ecosystem | grep -q "ecosystem" -``` - -`npm run lint` debe ejecutarse antes del tag, pero hoy todavía representa -una limpieza global separada. No debe bloquear el gate automático hasta -que esa deuda esté cerrada. - -Y manual: - -- [ ] Abrir `/active` y `/test/ecosystem` en Chromium, Firefox, Safari - desktop. **No console errors. No layout shifts grandes.** -- [ ] `App.dispose()` se llama dos veces seguidas → no throw, no warn. -- [ ] Cambio de locale a `ar` aplica `dir="rtl"` y propaga a Formats. -- [ ] Sign-in → sign-out limpia perm cache y session bridge. -- [ ] Refresh durante revoke → no resucita la sesión. -- [ ] 404 en una ruta `/active/docs/` → muestra fallback. -- [ ] Tema oscuro: contraste suficiente en sidebar activo, code blocks, - callouts. - ---- - -## 7. Orden sugerido - -Dos sprints serios bastan si nadie se desvía. - -**Sprint A — Seguridad y correctitud** - -Cerrado para el estado actual: S1, S2, S3, S4, S5, S6, S7, A1, A2, A3, A4 y A5. - -**Sprint B — Hygiene, docs y release (1 semana)** - -1. Repo hygiene: LICENSE, README, CONTRIBUTING, SECURITY, CHANGELOG, - `.github/` con CI básica. -2. A2/A3 — snapshot tests de superficie + política de deprecación - escrita. **Cerrado para la superficie actual**; mantenerlo actualizado - con cada miembro público nuevo. -3. Completar los 9 roots always-present en `/active/docs`. -4. Marca: ejecutar al menos `favicon.svg` + `favicon-32/16` + - `og-image.png` + meta tags en `app.html`. -5. Build estático, static smoke y bundle smoke verificados en CI. -6. Tag `0.1.0` con changelog real. - ---- - -## 8. Decisiones abiertas (necesitan criterio humano antes de avanzar) - -1. **¿Se publica en npm o se distribuye como repo / template?** Cambia - la forma de empaquetar (`exports`, `files`, `sideEffects` granular). -2. **¿`AuthRateLimitPort` se cablea ahora o se posterga?** Si se - posterga, sacarlo del barrel hasta `0.2`. -3. **¿MFA en `0.1` o explicitamente en `0.2`?** Hoy está stubbed; un - stub que lanza no debe estar en la superficie pública. -4. **¿`mono-lang` es público o interno?** Si público, se documenta y - se asume el cast laxo; si interno, se quita del barrel. -5. **¿La doc de los 5 factorías scoped (`Sess`, `Auth`, `Perm`, `Conn`, - `Sium`) llega a `0.1` profunda o stub?** Mi recomendación: `Auth`, - `Sess` y `Perm` profundas (son las que un consumidor pisa en - onboarding); `Conn` y `Sium` pueden quedar como stubs marcados. -6. **¿Adapter Static es el target final?** Si hay plan de SSR (auth - server-side real), conviene cambiar a `adapter-node` antes de - `0.1` para no romper consumidores en `0.1.x`. -7. **¿La API de `App.Cache.invalidate({tags})` queda como contrato - estable?** Se merece ser parte del primer snapshot test de - superficie porque cae en el camino crítico de `Auth`. - ---- - -## 9. Métricas de "listo" - -Cuando todo lo anterior pase, el repo debería poder responder _sí_ a: - -- "¿Un dev externo clona, sigue el README y arranca `/active` en - < 5 min?" -- "¿Existe un punto de contacto claro para reportar un fallo de - seguridad?" -- "¿Hay manera de saber qué cambia entre `0.1.0` y `0.1.1` sin leer - commits?" -- "¿La superficie de los 9 roots always-present está documentada con - una garantía explícita de no-break en la línea `0.1.x`?" -- "¿Si elimino los assets de marca, queda algo identificable en el - navegador?" (Hoy: no, `static/` solo tiene `robots.txt`). - -Si las cinco respuestas son sí, etiqueta `0.1.0`. diff --git a/docs/buss.md b/docs/buss.md deleted file mode 100644 index 5d16288..0000000 --- a/docs/buss.md +++ /dev/null @@ -1,1381 +0,0 @@ -# buss — Event Bus y coordinación del ecosistema - -## Tesis - -`buss` debe ser un bus de eventos tipado, no un simple `EventEmitter` genérico -y tampoco un contenedor de reglas de negocio. Su objetivo es transportar hechos -con envelope, orden, errores y observabilidad. La coordinación del ecosistema -se construye encima: - -```txt -artifact events -> translators in aapp -> app events -> consumer reactions -``` - -Ejemplo: - -```txt -Sess emite: sess.changed -aapp traduce: app.user.identity.changed -Cach decide: si autoInvalidateOn incluye identity -> clear() -Perm decide: si autoInvalidateOn incluye identity -> invalidate() -Conn decide: si autoReauthOn + connection.session + connection.auth -> reauth/close -``` - -La regla principal: - -```txt -Los módulos no deben conocerse entre sí. -``` - -`sess` no debe importar `conn`, `cach` ni `perm`. `conn` no debe saber de -`auth`. `cach` no debe saber de `sess`. La traducción vive en `aapp`; las -reacciones viven en la factory del consumidor que las ejecuta. - -Además, los módulos no crean buses propios. El bus transversal del cliente es -`App.Bus`, creado una vez por `aapp`. Un artefacto recibe una referencia -inyectada (`EngineBus`, `AppEventBus` o `EventPublisher`) y nunca llama -`createEngineBus()` para abrir una isla privada. - -## Modelo Cerrado - -Este es el contrato que se debe implementar antes de seguir ampliando el bus. - -### 1. Bus Engine - -`arts/buss` es un artefacto raíz porque expone `createEngineBus()` y ciclo de -vida. Pero su dominio es **mecánico**, no de aplicación: - -```txt -Sí pertenece a buss: -- EngineBus -- EventPublisher -- BusEnvelope -- BusSubscription -- publish / publishAsync -- on / once / onAny -- listenerErrorMode -- diagnostics del propio bus -- BUS_EVENT_ALL - -No pertenece a buss: -- APP_EVENT_* -- SESS_EVENT_* -- AUTH_EVENT_* -- CACH_EVENT_* -- PERM_EVENT_* -- CONN_EVENT_* -- reglas de cache/permisos/conexiones -``` - -`buss` no debe conocer `sess`, `auth`, `cach`, `perm`, `conn`, `tenant`, -`actor`, `permissions`, `cache` ni `credentials`. - -### 2. Module Events - -Cada artefacto puede publicar eventos propios si recibe un `EventPublisher`. -Esos eventos pertenecen al artefacto que los emite: - -```txt -arts/sess/consts.ts -> SESS_EVENT_* -arts/auth/consts.ts -> AUTH_EVENT_* -arts/cach/consts.ts -> CACH_EVENT_* -arts/perm/consts.ts -> PERM_EVENT_* -arts/conn/consts.ts -> CONN_EVENT_* -``` - -Los module events son hechos internos del ecosistema. No son el contrato -público estable para plugins. Los consume `aapp` mediante traductores. - -Regla: - -```txt -Un artefacto puede publicar sus propios eventos. -Un artefacto no debe suscribirse a eventos privados de otro artefacto. -Un artefacto no crea su propio bus; recibe el bus central o un publisher -inyectado. -El código publica/escucha constantes de evento, no strings inline. -``` - -### 3. App Events - -Los eventos públicos y estables de aplicación no pertenecen a `buss`; pertenecen -al contrato común de aplicación. Ubicación propuesta: - -```txt -src/libs/aapp/events.ts -``` - -Ahí viven: - -```txt -APP_EVENT_USER_IDENTITY_CHANGED -APP_EVENT_TENANT_SWITCHED -APP_EVENT_PERMISSIONS_REFRESH_REQUESTED -APP_EVENT_CONNECTIVITY_CHANGED -APP_EVENT_CACHE_INVALIDATE_REQUESTED -APP_EVENT_DISPOSE_STARTING -``` - -También viven ahí sus payloads y helpers: - -```ts -publishAppUserIdentityChanged(bus, payload) -onAppUserIdentityChanged(bus, listener) -``` - -`aapp` y los consumidores importan este contrato desde `libs`, no desde otro -artefacto. - -### 4. Translators - -`aapp` no ejecuta side-effects destructivos. `aapp` solo traduce eventos de -módulo a eventos de aplicación: - -```txt -SESS_EVENT_CHANGED -> APP_EVENT_USER_IDENTITY_CHANGED -AUTH_EVENT_SIGNED_IN -> APP_EVENT_USER_IDENTITY_CHANGED -AUTH_EVENT_SIGNED_OUT -> APP_EVENT_USER_IDENTITY_CHANGED -PERM_EVENT_POLICY_DIRTY -> APP_EVENT_PERMISSIONS_REFRESH_REQUESTED -``` - -Estos traductores viven en: - -```txt -src/arts/aapp/integrations/*-translator.ts -``` - -No se llaman `*-orchestrator.ts` en el modelo final si solo traducen. La palabra -orquestador se reserva para una pieza que coordina flujo; aquí queremos piezas -pequeñas: - -```txt -module event in -> app event out -``` - -### 5. Consumer Reactions - -Las reacciones automáticas viven en el consumidor que muta su propio estado: - -```ts -createActiveCache({ - autoInvalidateOn: 'standard' -}); - -createActivePermissions({ - autoInvalidateOn: 'standard' -}); - -createActiveConnections({ - autoReauthOn: 'standard' -}); -``` - -`aapp` nunca configura `invalidateCache`, `invalidatePermissions` ni -`reauthenticateConnections` porque eso mezcla traducción con side-effects. - -### 6. Defaults - -Los defaults quedan cerrados así: - -| Capa | Default | Motivo | -| --- | --- | --- | -| `App.Bus` | siempre presente | superficie uniforme | -| Module events | se publican si el artefacto recibe bus | publicar hechos no muta otros módulos | -| `aapp` translators | `standard` por defecto | traducir a `app.*` no es destructivo | -| Consumer reactions | `none` por defecto | limpiar cache/permisos o reautenticar sockets sí es destructivo | -| `orchestration: 'silent'` | sin traductores automáticos | tests que publican `app.*` manualmente | - -### 7. Payload Safety - -Los `APP_EVENT_*` nunca contienen credenciales, tokens, passwords, headers de -authorization, secretos ni hashes sensibles. Si un consumidor necesita contexto -sensible, recibe `correlationId` y resuelve contra su propio estado o backend. - -## Frame Estratégico - -`buss` no es solo un refactor interno para quitar puentes directos. Es la -fundación del sistema de extensión del framework. - -Hoy el ecosistema tiene artefactos fijos. Mañana un módulo externo como -`feature flags`, `analytics`, `queue`, `billing` o `audit trail` debe poder: - -- escuchar hechos del runtime sin tocar el core; -- emitir hechos propios sin que el core lo conozca; -- integrarse desde `aapp` mediante translators; -- participar en trazas y diagnósticos con el mismo envelope. - -Por eso `buss` debe tratarse como API pública desde el primer commit. Los -nombres de eventos canónicos, el shape del envelope y la semántica de -`publish` son contrato externo. - -## Problema Que Resuelve - -El framework ya tiene módulos potentes, pero los casos reales no ocurren en -aislado: - -- Usuario conectado a un chat con un token. -- Cambia la identidad activa. -- Las conexiones deben reautenticarse o cerrarse. -- La cache actor-scoped o pública contaminada debe invalidarse. -- Los permisos cacheados deben vaciarse. -- Los logs deben contar qué política se ejecutó. -- El test debe verificar toda la cadena. - -Sin bus, esto acaba en puentes directos: - -```txt -sess -> conn -auth -> cach -auth -> perm -perm -> cach -``` - -Ese patrón escala mal porque convierte la composición en una malla invisible. - -Con `buss`, cada módulo publica hechos, `aapp` los traduce a un vocabulario -público de aplicación, y cada consumidor decide sus efectos opt-in. - -## Frontera De Uso - -`buss` es para hechos transversales entre artefactos. No reemplaza los eventos -privados de cada módulo. - -Se quedan internos: - -- `Session.onChange(...)` -- `Connection.onState(...)` -- `Connection.onAny(...)` -- `Cache.on(CACHE_EVENT_ALL, ...)` -- snapshots y listeners internos de `ActiveEngine` - -Van a `buss`: - -- cambio de identidad; -- sign-in/sign-out; -- sesión expirada/revocada; -- cambio de permisos; -- invalidación transversal de cache; -- reautenticación de conexiones por identidad; -- eventos públicos que extensiones externas puedan consumir. - -Regla: - -```txt -Si el evento solo interesa al módulo que lo emite, no va al bus. -Si el evento cambia política entre módulos, sí va al bus. -``` - -## Referentes - -### Node EventEmitter - -Bueno: - -- API conocida: `on`, `emit`, `off`. -- Ejecución síncrona y determinista. -- Muy probado. - -Malo: - -- Tipado débil. -- `error` tiene semántica especial. -- Listener leaks si no se controlan. -- Poca estructura para metadatos, correlación o trazabilidad. - -Qué copiar: - -- Emisión síncrona por defecto para eventos internos. -- `once`. -- Limpieza explícita. - -Qué evitar: - -- API basada en `this`. -- Strings libres repartidos. -- Eventos especiales mágicos. - -### mitt - -Bueno: - -- Muy pequeño. -- Sin dependencias. -- Funcional, sin `this`. -- Wildcard `*`. -- Tipable con mapa de eventos. - -Malo: - -- No tiene envelope. -- No tiene políticas de error. -- No tiene async orchestration. -- No tiene replay, prioridad, cola ni metadatos. - -Qué copiar: - -- Simplicidad. -- `on(type, handler)`, `off`, `emit`. -- `on('*')` para observabilidad. - -Qué mejorar: - -- Envelopes con `id`, `at`, `source`, `correlationId`. -- Constantes obligatorias. -- Integración con `Logger`. - -### Emittery - -Bueno: - -- Async-first. -- `onAny`. -- Async iterators. -- `AbortSignal`. -- Limpieza cómoda. -- Tipado fuerte. - -Malo: - -- Async por defecto puede ocultar orden y carreras. -- Más superficie de la necesaria para el core. - -Qué copiar: - -- `AbortSignal` para cancelar listeners. -- `onAny`. -- `publishAsync` separado de `publish`. -- Limpieza idempotente. - -Qué evitar: - -- Hacer que todos los eventos internos sean async por defecto. - -### NestJS Events - -Bueno: - -- Usa eventos para desacoplar módulos. -- Listeners registrados en composition root. -- Buen modelo mental para backend. - -Malo: - -- Decorators/DI pesados. -- Depende de `eventemitter2`. -- Wildcards potentes pero fáciles de abusar. - -Qué copiar: - -- Registro centralizado. -- Eventos como contrato entre módulos. - -Qué evitar: - -- Decorators. -- Dependencia externa. -- Magia de bootstrap. - -### Redux Toolkit Listener Middleware - -Bueno: - -- No es solo pub/sub: es orquestación. -- Permite efectos al reaccionar a acciones. -- Tiene cancelación, `condition`, `take`, `delay`. -- Separa evento de efecto. - -Malo: - -- Está unido a store/actions Redux. -- Puede volverse demasiado flexible. - -Qué copiar: - -- Concepto de listener/orchestrator. -- Cancelación con `AbortSignal`. -- Helpers para workflows complejos en una fase posterior. - -### Effect PubSub - -Bueno: - -- Capacidad bounded/unbounded. -- Estrategias: backpressure, dropping, sliding. -- Buen modelo para sistemas concurrentes. - -Malo: - -- Demasiado pesado para el core actual. -- Requiere adoptar modelo Effect. - -Qué copiar a futuro: - -- Cola bounded. -- Políticas de saturación. -- Replay opcional. - -### XState Actors - -Bueno: - -- Eventos como mensajes tipados. -- Estado privado. -- Comunicación explícita. - -Malo: - -- Demasiado grande para un bus común. - -Qué copiar: - -- Los eventos describen hechos, no instrucciones. - -## Diseño Propuesto - -Directorio: - -```txt -src/arts/buss/ - index.ts - consts.ts - types.ts - errors.ts - engine-bus.ts - matching.ts - diagnostics.ts - README.md - test/ -``` - -Si necesitamos capa reactiva: - -```txt -src/arts/buss/ - index.ts - active-bus.svelte.ts - types.ts - README.md - test/ -``` - -La primera versión vive en `arts/buss` aunque sea pura y no use Svelte, porque -expone `createEngineBus()` y ciclo de vida. `libs` queda reservado para -primitivas compartidas sin factory raíz. - -## Naming - -Decisión: - -```txt -Directorio: buss -Nombre público: Bus -Factory: createEngineBus() -Tipo raíz: EngineBus -``` - -Claude propone `bus/` porque `buss` puede leerse como typo. La objeción es -válida, pero la decisión recomendada para este framework es mantener `buss` -como directorio por coherencia con los artefactos de cuatro letras y exponer -siempre `Bus` en la superficie pública. - -El usuario no debería escribir `Buss` salvo al importar desde la ruta interna. - -```ts -import { createEngineBus } from '$buss'; - -const Bus = createEngineBus(); -``` - -No usar: - -```ts -createEngineBuss() -EngineBuss -``` - -Tampoco usar dentro de un módulo: - -```ts -const Bus = createEngineBus(); // mal en un artefacto normal -``` - -La creación directa de `createEngineBus()` queda para: - -```txt -- createActiveApp(), que crea App.Bus; -- tests unitarios aislados; -- servicios o herramientas que no viven dentro de una App activa. -``` - -## Contrato Base - -```ts -export type BusEventMap = object; - -export interface BusEnvelope { - readonly id: string; - readonly type: TType; - readonly payload: TPayload; - readonly at: number; - readonly source: string; - readonly correlationId?: string; - readonly causationId?: string; - readonly context?: Readonly>; - readonly tags?: readonly string[]; -} - -export interface BusListenerFailure { - readonly listenerId?: string; - readonly type: string; - readonly error: unknown; -} - -export interface BusPublishResult { - readonly envelope: BusEnvelope; - readonly errors: readonly BusListenerFailure[]; -} - -export type BusListener = ( - event: BusEnvelope, - context: BusListenerContext -) => void | Promise; - -export interface BusListenerContext { - readonly signal: AbortSignal; - readonly logger?: Logger; -} - -export interface EngineBus { - publish( - type: TType, - payload: TEvents[TType], - options?: BusPublishOptions - ): BusPublishResult; - - publishAsync( - type: TType, - payload: TEvents[TType], - options?: BusPublishOptions - ): Promise>; - - on( - type: TType, - listener: BusListener, - options?: BusListenOptions - ): BusSubscription; - - onAny(listener: BusAnyListener, options?: BusListenOptions): BusSubscription; - - once( - type: TType, - listener: BusListener, - options?: BusListenOptions - ): BusSubscription; - - listenerCount(type?: keyof TEvents & string): number; - _clearForTesting(type?: keyof TEvents & string): void; - dispose(): void; -} -``` - -### Por Qué `context` Y No `tenantId` / `actorId` - -El envelope pertenece a `arts/buss`, por tanto no debe acoplarse a un modelo -concreto de tenancy, sesión o usuario. `tenantId`, `actorId`, `locale`, -`permissionHash` y similares son claves convencionales dentro de `context`. - -Ejemplo: - -```ts -Bus.publish(APP_EVENT_USER_IDENTITY_CHANGED, payload, { - context: { - tenantId, - previousActorId, - nextActorId, - permissionHash - } -}); -``` - -## Opciones - -```ts -export interface BusPublishOptions { - readonly source?: string; - readonly correlationId?: string; - readonly causationId?: string; - readonly context?: Readonly>; - readonly tags?: readonly string[]; - readonly listenerErrorMode?: BusListenerErrorMode; -} - -export interface BusListenOptions { - readonly signal?: AbortSignal; - readonly once?: boolean; - readonly id?: string; -} - -export type BusListenerErrorMode = 'log-and-continue' | 'throw' | 'collect'; - -export interface EngineBusOptions { - readonly logger?: Logger; - readonly clock?: { now(): number }; - readonly idFactory?: () => string; - readonly maxListenersPerEvent?: number; - readonly listenerErrorMode?: BusListenerErrorMode; -} -``` - -Decisiones v0: - -- Sin `priority` en `BusListenOptions`. -- Orden por registro. -- `maxListenersPerEvent` default: `32`. -- `_clearForTesting()` existe para tests, no como API normal de producción. - -## Eventos Como Hechos - -Correcto: - -```ts -Bus.publish(SESS_EVENT_IDENTITY_CHANGED, { - previousActorId: 'actor-ada', - nextActorId: 'actor-linus' -}); -``` - -Incorrecto: - -```ts -Bus.publish('cache.clear.now', {}); -Bus.publish('connection.reauthenticate.now', {}); -``` - -Los eventos deben decir qué ha pasado, no qué otro módulo tiene que hacer. - -## Constantes - -No debe haber magic strings: - -```ts -// arts/buss/consts.ts -export const BUS_EVENT_ALL = '*'; -export const BUS_LISTENER_ERROR_MODE_COLLECT = 'collect'; - -// libs/aapp/events.ts -export const APP_EVENT_USER_IDENTITY_CHANGED = 'app.user.identity.changed'; - -// arts/sess/consts.ts -export const SESS_EVENT_CHANGED = 'sess.changed'; -export const SESS_EVENT_REVOKED = 'sess.revoked'; -``` - -Convención obligatoria: - -```txt -El símbolo exportado es UPPER_SNAKE_CASE y describe el dueño. -El valor puede ser dot-case porque es el nombre serializable del evento. -``` - -Correcto: - -```ts -Bus.publish(APP_EVENT_USER_IDENTITY_CHANGED, payload); -Bus.on(SESS_EVENT_CHANGED, listener); -``` - -Incorrecto: - -```ts -Bus.publish('app.user.identity.changed', payload); -Bus.on('sess.changed', listener); -``` - -Las constantes se centralizan por dueño, no todas dentro del bus: - -```txt -BUS_* -> contrato mecánico del bus -APP_EVENT_* -> contrato público de aplicación -SESS_EVENT_* -> contrato privado de sess -AUTH_EVENT_* -> contrato privado de auth -... -``` - -## onAny - -`onAny` es una herramienta de observabilidad, testing y devtools. - -Uso correcto: - -```ts -Bus.onAny((event) => { - App.Logger.debug('buss', event.type, { context: { envelope: event } }); -}); -``` - -Uso incorrecto: - -```ts -Bus.onAny((event) => { - if (event.type === APP_EVENT_USER_IDENTITY_CHANGED) { - App.Cache.clear(); - } -}); -``` - -La lógica de negocio debe estar en listeners concretos del consumidor o en -servicios explícitos, no en `onAny`. - -## Integración Con aapp - -`aapp` debería crear un bus por aplicación: - -```ts -const Bus = createEngineBus({ - logger: Logger, - clock: Timers.clock -}); -``` - -Y exponerlo: - -```ts -App.Bus -``` - -Los artefactos reciben el bus opcionalmente: - -```ts -createActiveSession({ logger, bus: Bus }); -createActiveConnections({ logger, bus: Bus }); -createActiveCache({ logger, bus: Bus }); -``` - -La regla es centralización: una app, un `App.Bus`. Si un módulo necesita emitir -hechos, recibe un `EventPublisher`; si necesita reaccionar, recibe un -`AppEventBus` o se configura desde su factory. - -Regla de implementación para `sess`: - -```txt -createEngineSession({ bus }) publica sess.* desde el engine puro. -createActiveSession({ bus }) publica sess.* desde el wrapper activo, después de -sincronizar current/generation con $state. -``` - -Ese matiz evita que consumidores reactivos como `conn` reciban -`app.user.identity.changed` y lean todavía el snapshot anterior de -`ActiveSession.current` dentro del mismo tick. - -Pero los módulos no deben depender de `aapp`. Solo aceptan una interfaz mínima: - -```ts -interface EventPublisher { - publish( - type: TType, - payload: TEvents[TType], - options?: BusPublishOptions - ): BusPublishResult; -} -``` - -## Modelo De Coordinación: Traducción vs Reacción - -El bus por sí mismo reduce acoplamiento porque convierte dependencias directas -entre módulos en hechos públicos. Pero un translator default demasiado -prescriptivo puede reintroducir acoplamiento desde `aapp`. - -Ejemplo peligroso: - -```txt -aapp escucha sess.changed -aapp ejecuta Permissions.invalidate() + Cache.clear() + Conn.reauthenticateAll() -``` - -Aunque `sess` no conozca `cache`, `permissions` ni `conn`, `aapp` estaría -asumiendo cómo deben comportarse todos los consumidores. Eso no escala: - -- una app puede no tener sesión; -- una app puede no crear `Permissions`; -- una app puede querer conservar cache pública; -- una app puede tener cache segmentada que no depende de identidad; -- una app puede querer cerrar conexiones en vez de reautenticarlas; -- un plugin externo puede querer reaccionar sin tocar `aapp`. - -Por tanto hay dos capas separadas: - -```txt -Capa 1: traducción - aapp traduce eventos de módulo -> app.* - -Capa 2: reacción - cada consumidor decide si escucha app.* y qué side-effect ejecuta -``` - -### Config De App: Translators - -`createActiveApp().orchestration` solo decide qué traductores automáticos -corren. No decide qué hace cache, permissions o connections. - -```ts -createActiveApp({ - orchestration: 'standard' -}); -``` - -```ts -createActiveApp({ - orchestration: 'silent' -}); -``` - -```ts -createActiveApp({ - orchestration: ['identity', 'tenant-switched'] -}); -``` - -Tipos aproximados: - -```ts -export type ActiveAppOrchestrationPreset = false | 'silent' | 'standard'; - -export type ActiveAppOrchestrationTranslator = - | 'identity' - | 'permissions-refresh' - | 'tenant-switched' - | 'connectivity' - | 'dispose'; - -export type ActiveAppOrchestrationOptions = - | ActiveAppOrchestrationPreset - | readonly ActiveAppOrchestrationTranslator[]; -``` - -`createActiveApp()` equivale a translators `standard`: publica eventos `app.*` -útiles por defecto, pero no provoca side-effects destructivos porque los -consumidores no reaccionan automáticamente salvo que se configure su propio -`auto*On`. - -### Config De Consumidores: Reacciones - -Los efectos destructivos viven en el consumidor: - -```ts -const App = createActiveApp({ - orchestration: 'standard', - cache: { - autoInvalidateOn: 'standard' - }, - permissions: { - endpoint: '/api/permissions', - autoInvalidateOn: 'standard' - }, - connections: { - autoReauthOn: 'standard' - } -}); -``` - -Y también se pueden activar por factory: - -```ts -const Permissions = App.createActivePermissions({ - endpoint: '/api/permissions', - autoInvalidateOn: ['userIdentityChange'] -}); - -const Connections = App.createActiveConnections({ - autoReauthOn: 'standard' -}); -``` - -### Modos De App - -| Modo | Traduce a `app.*` | Side-effects | Uso | -| --- | --- | --- | --- | -| `createActiveApp()` | sí, preset `standard` | ninguno por sí mismo | Apps que quieren hechos públicos sin reacciones automáticas | -| `orchestration: 'standard'` | sí, preset cerrado | ninguno por sí mismo | Apps normales, getting started | -| `orchestration: ['identity']` | solo los traductores indicados | ninguno por sí mismo | Tests/producción con control fino | -| `orchestration: 'silent'` | no | ninguno | Tests que publican `app.*` manualmente | - -### Translators `standard` - -`'standard'` debe estar documentado como lista cerrada. No puede ser magia. - -| Translator | Publica | -| --- | --- | -| `identity` | `app.user.identity.changed` desde eventos de `sess`/`auth` | -| `permissions-refresh` | `app.permissions.refresh` | -| `tenant-switched` | `app.tenant.switched` | -| `connectivity` | `app.connectivity.changed` | -| `dispose` | `app.dispose.starting` | - -### Reacciones `standard` Por Consumidor - -| Consumidor | Config | Reacciona a | -| --- | --- | --- | -| `cach` | `autoInvalidateOn: 'standard'` | `userIdentityChange`, `tenantSwitched` | -| `perm` | `autoInvalidateOn: 'standard'` | `userIdentityChange`, `permissionsRefresh`, `tenantSwitched` | -| `conn` | `autoReauthOn: 'standard'` | `userIdentityChange` | - -`conn.autoReauthOn` es opt-in porque puede cerrar o reautenticar sockets, -perder mensajes en vuelo si el transporte no bufferiza, producir un blink en -chat/realtime o disparar reconexiones masivas. - -Para `conn`, `autoReauthOn` no basta por sí solo. La registry escucha el evento -de identidad, pero cada conexión debe activar `session.enabled` y definir -`auth` para que exista una credencial nueva que enviar. Sin `auth`, el evento -queda como señal observable o debe resolverse con reconnect/disconnect manual. - -### App Events Sin Credenciales - -Los eventos de aplicación son contrato público. Un día pueden acabar en logs, -devtools, tracing, outbox o dumps de auditoría. Por tanto: - -```txt -App events nunca contienen credenciales. -``` - -Protecciones recomendadas: - -- tipos de payload cerrados: `AppUserIdentityChangePayload` solo puede contener - `previousActorId`, `nextActorId`, `tenantId`, `cause`, `generation`, etc.; -- lint/check en DEV antes de publicar: advertir si aparecen keys como `token`, - `secret`, `password`, `authorization`, o valores con forma de JWT; -- documentación explícita: si un consumidor necesita algo sensible, recibe - `correlationId` y resuelve contra `App.Sess` o contra su propio backend. - -### Decisión Adoptada Para v0.1 - -- `createActiveApp()` crea `App.Bus` siempre. -- `createActiveApp()` publica/traduce eventos de aplicación con preset - `standard`. -- `orchestration: 'silent'` desactiva traductores automáticos. -- Los side-effects destructivos son opt-in en cada consumidor. -- Los consumidores escuchan `app.*`, no eventos internos de otros módulos. -- Los payloads `app.*` no contienen credenciales. -- `APP_EVENT_*` vive fuera de `arts/buss`; `buss` no conoce eventos de dominio. - -## Orden De Bootstrap - -Para no perder eventos tempranos: - -```txt -1. crear Logger -2. crear Timers -3. crear Bus -4. crear roots always-present -5. cablear translators -6. exponer factories que pueden emitir eventos -``` - -Los translators deben estar registrados antes de que se creen sesiones, -auth clients o conexiones que puedan publicar eventos. - -## Orden De Teardown - -En `App.dispose()`: - -```txt -1. desactivar factories/active modules que puedan emitir -2. desmontar translators -3. disponer Bus -4. disponer roots always-present -``` - -Debe haber test específico que verifique que un evento emitido durante dispose -no dispara listeners contra servicios ya disposed. - -## Translators - -Los traductores viven en integraciones de `aapp`: - -```txt -src/arts/aapp/integrations/session-translator.ts -src/arts/aapp/integrations/auth-translator.ts -src/arts/aapp/integrations/permission-translator.ts -``` - -Ejemplo: - -```ts -export function wireSessionTranslator(bus: EngineBus): () => void { - const subscription = bus.on(SESS_EVENT_CHANGED, (event) => { - bus.publish(APP_EVENT_USER_IDENTITY_CHANGED, { - ...event.payload, - cause: mapSessEventToCause(event.payload.event) - }); - }); - return () => subscription.unsubscribe(); -} -``` - -Un translator debe ser fino: - -```txt -X pasó -> publico app.Y -``` - -No debe convertirse en servicio de negocio: - -```txt -X pasó -> leo cinco estados -> decido reglas de negocio -> mutaciones complejas -``` - -Si aparece lógica compleja, debe moverse a un servicio o engine específico. - -## Política De Errores - -Semántica cerrada: - -```txt -publish: - - ejecuta listeners síncronos en orden - - captura errores - - devuelve { envelope, errors } - - si listenerErrorMode === 'throw', lanza BusAggregateListenerError - -publishAsync: - - ejecuta listeners en orden - - espera listeners async - - devuelve { envelope, errors } - - si listenerErrorMode === 'throw', lanza BusAggregateListenerError -``` - -Modos: - -```ts -type BusListenerErrorMode = 'log-and-continue' | 'throw' | 'collect'; -``` - -Comportamiento: - -```txt -log-and-continue: - - loggea cada error - - continúa - - devuelve errors - -collect: - - no loggea por defecto - - continúa - - devuelve errors - -throw: - - acumula errores - - detiene según política de implementación - - lanza BusAggregateListenerError -``` - -Default recomendado: - -```txt -runtime: log-and-continue -tests: throw -``` - -`listenerErrorMode` puede sobrescribirse por publicación: - -```ts -const result = Bus.publish(SESS_EVENT_CHANGED, payload, { - listenerErrorMode: 'collect' -}); -``` - -Esto permite que el bus tenga un default operativo (`log-and-continue`) y que -tests o flujos puntuales inspeccionen errores sin producir logs ni lanzar. - -## Orden - -El orden debe ser determinista: - -```txt -1. listeners específicos en orden de registro -2. once se elimina antes de invocar -3. onAny después de listeners específicos -``` - -No hay `priority` en v0. Las prioridades numéricas son un footgun porque crean -acoplamiento invisible entre listeners. Si v1 necesita orden especial, debe -ser con fases nombradas, no con números libres. - -Ejemplo futuro aceptable: - -```ts -phase: 'before-default' | 'default' | 'after-default' -``` - -## Síncrono vs Async - -Dos métodos separados: - -```ts -Bus.publish(...); -await Bus.publishAsync(...); -``` - -`publish()` se usa para estado interno inmediato. - -`publishAsync()` se usa para workflows que pueden esperar: - -- auditoría remota; -- outbox; -- persistencia server-side; -- handlers externos; -- integraciones de plugins. - -No mezclar ambas semánticas en un único `emit`. - -## Observabilidad - -El bus debe poder producir diagnósticos: - -```txt -buss.event.published -buss.listener.started -buss.listener.completed -buss.listener.failed -buss.listener.cancelled -buss.listener.leak_warning -``` - -Con constantes full-prefix: - -```ts -export const BUS_DIAGNOSTIC_EVENTS = { - EVENT_PUBLISHED: 'buss.event.published', - LISTENER_STARTED: 'buss.listener.started', - LISTENER_COMPLETED: 'buss.listener.completed', - LISTENER_FAILED: 'buss.listener.failed', - LISTENER_CANCELLED: 'buss.listener.cancelled', - LISTENER_LEAK_WARNING: 'buss.listener.leak_warning' -} as const; -``` - -Debe usar `Logger` desde `libs/logr`, nunca acoplarse a `arts/logr`. - -El envelope debe poder serializarse 1:1 en un log: - -```ts -Logger.info(BUSS_LOG_CATEGORY, envelope.type, { - context: { envelope } -}); -``` - -## API Surface Stability - -Desde `0.1.x` se considera API pública estable: - -- `EngineBus` -- `BusEnvelope` -- `BusPublishResult` -- `BusEventMap` -- `BusListener` -- `BusSubscription` -- semántica de `publish` / `publishAsync` -- orden de ejecución documentado -- política de errores documentada - -Puede iterar sin romper: - -- diagnósticos internos; -- opciones nuevas opcionales; -- helpers de testing; -- adapters futuros; -- active wrapper. - -Los `APP_EVENT_*` tienen su propia estabilidad en `libs/aapp/events.ts`. - -Rompe compatibilidad: - -- renombrar eventos canónicos; -- quitar campos del envelope; -- cambiar el orden de ejecución; -- cambiar la semántica de errores; -- convertir `publish` síncrono en async. - -## Bundle Budget - -`buss` será always-present si lo crea `aapp`, así que su core debe ser pequeño. - -Target: - -```txt -arts/buss core <= 3 KB gzip -``` - -Este límite aplica solo al core `arts/buss`, no a translators de `aapp`, -tests, páginas demo ni documentación. Debe añadirse al smoke test de bundle -cuando se implemente. - -## Relación Con SemanticEngine / Taxis - -`SemanticEngine` y `buss` pueden parecer registry + dispatch, pero pertenecen -a capas distintas. - -| Eje | SemanticEngine | buss | -| --- | --- | --- | -| Capa | Perceptiva / UI primitives | Dominio / runtime artifacts | -| Granularidad | Componente e interacción | Hecho de aplicación | -| Canales | Visual, sound, vibra, motion | Listeners y translators | -| Bloqueo | Visual hold / transition | `publish` sync, async opcional | -| Vocabulario | Verbos perceptivos | Hechos de dominio | -| Vida del evento | Transiente | Envelope/loggable | - -No deben fusionarse. - -Sí pueden compartir ideas: - -- `id` -- `at` -- `source` -- correlación - -Pero `SemanticEngine` sirve a la experiencia perceptiva y `buss` a la -composición de artefactos. - -## Riesgos - -### God Object - -Si todo evento del runtime pasa por `buss`, deja de ser bus y se vuelve estado -central. La frontera inter/intra módulo debe aplicarse de forma estricta. - -### Translators Con Lógica De Negocio - -Los translators deben traducir hechos, no absorber decisiones de dominio. - -### Drift Entre Envelope Y Logger - -`buss` y `logr` deben alinearse. El envelope no debe inventar otra taxonomía -paralela de observabilidad. - -### onAny Como Middleware Oculto - -`onAny` solo observabilidad/devtools/testing. No policy. - -### clear Destructivo - -No exponer `clear(type?)` como API normal. Usar `_clearForTesting`. - -## Test Compuesto Objetivo - -Caso de regresión principal: - -```txt -1. Usuario Ada inicia sesión. -2. Chat abre conexión con token Ada. -3. Cache guarda presencia/proyecto para Ada. -4. Perm cachea decisión de Ada. -5. Se adopta sesión Linus. -6. Bus emite `sess.changed`. -7. Translator de App emite `app.user.identity.changed`. -8. Consumidores opt-in reaccionan: - - `perm.autoInvalidateOn` limpia decisiones - - `cach.autoInvalidateOn` limpia cache - - `conn.autoReauthOn` reautentica conexiones con `session.enabled` y `auth` -9. Chat envía auth frame con token Linus. -10. Ningún frame posterior contiene token Ada. -11. Cache refetchea para Linus. -``` - -Este test debe verificar ambas capas: traducción `sess.* -> app.*` y reacción -opt-in por consumidor. - -## Roadmap - -### Estado actual - -- `arts/buss` existe como engine mecánico. -- `App.Bus` se crea siempre en `createActiveApp()`. -- `APP_EVENT_*` vive en `src/libs/aapp/events.ts`. -- `SESS_EVENT_*` vive en `src/arts/sess/consts.ts`. -- `sess` publica `sess.*` cuando recibe bus. -- `aapp` traduce `sess.changed -> app.user.identity.changed` con - `wireSessionTranslator`. -- `cach`, `perm` y `conn` reaccionan por opt-in desde sus propias factories. -- El test compuesto de cambio de usuario/chat/cache/permisos cubre la cadena - completa y evita credenciales antiguas después del cambio de actor. - -### v0 - -- `arts/buss` -- `createEngineBus` -- `publish` -- `publishAsync` -- `on` -- `once` -- `onAny` -- `listenerCount` -- `_clearForTesting` -- `dispose` -- `AbortSignal` -- `Logger` -- `BusPublishResult` -- `BusAggregateListenerError` -- tests unitarios - -### v0.1 - -- Integración en `aapp` -- Translator `sess.* -> app.user.identity.changed` -- Reacciones opt-in en `cach`, `perm` y `conn` -- Translator `auth.* -> app.user.identity.changed` cuando auth publique eventos propios -- Mover `APP_EVENT_*` a `src/libs/aapp/events.ts` -- Mover `SESS_EVENT_*` a `arts/sess`, fuera de `arts/buss` -- Orden de bootstrap documentado y testeado -- Orden de teardown documentado y testeado -- Test de dispose: evento emitido durante teardown no dispara contra servicios ya disposed -- Test compuesto cambio de usuario/chat/cache -- Bundle smoke para `buss <= 3 KB gzip` - -### v0.2 - -- Active wrapper solo si hace falta: - - `lastEvent` - - `eventCount` - - `listenerCount` - - `onChange` -- Página `/test/buss` -- Devtools/debug panel - -### v1 - -- `waitFor` -- `condition` -- `debounce` -- `takeLatest` -- fases nombradas si hay necesidad real -- replay opcional -- bounded queue opcional -- adapter server/outbox - -## Qué No Meter Al Principio - -- No decorators. -- No dependencia externa. -- No RxJS. -- No Effect. -- No wildcards complejos tipo `**` en v0. -- No bus distribuido. -- No persistence/outbox en v0. -- No reemplazar `Logger`. -- No convertir todos los logs en eventos. -- No eventos privados intra-módulo en el bus. -- No `priority` numérico. -- No lógica de negocio compleja en translators. - -## Decisión Recomendada - -Implementar `buss` como núcleo pequeño: - -```txt -Core pequeño, eventos tipados, envelopes ricos, ejecución determinista, -translators en aapp, reacciones en consumidores y cero acoplamiento entre -artefactos. -``` - -Esto corrige el problema real del ecosistema: los módulos ya funcionan, pero -las transiciones entre módulos necesitan una gramática común, estable y -testeable. diff --git a/docs/ecosistema_v2.md b/docs/ecosistema_v2.md new file mode 100644 index 0000000..b4172dc --- /dev/null +++ b/docs/ecosistema_v2.md @@ -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. diff --git a/docs/orca_minds.txt b/docs/orca_minds.txt deleted file mode 100644 index bf9fc6c..0000000 --- a/docs/orca_minds.txt +++ /dev/null @@ -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 { - 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 { - 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 { - 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: (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( - 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. - - diff --git a/package-lock.json b/package-lock.json index 696aaf3..4eb5c86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@sveltejs/vite-plugin-svelte": "^7.0.0", "@types/node": "^22", "@vitest/browser-playwright": "^4.1.3", + "concurrently": "^9.2.1", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.17.0", @@ -1581,6 +1582,32 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/aria-query": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", @@ -1652,6 +1679,36 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1668,6 +1725,21 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1677,6 +1749,51 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1823,6 +1940,13 @@ "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/entities": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", @@ -1843,6 +1967,16 @@ "dev": true, "license": "MIT" }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2219,6 +2353,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2245,6 +2389,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -2288,6 +2442,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3185,6 +3349,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3257,6 +3431,16 @@ } } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -3326,6 +3510,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -3372,6 +3569,50 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/svelte": { "version": "5.55.4", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz", @@ -3615,6 +3856,16 @@ "node": ">=20" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -3633,8 +3884,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -4037,6 +4287,24 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", @@ -4075,6 +4343,45 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 93987ca..b74632a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "prepare": "svelte-kit sync || echo ''", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "dating:server": "node servers/dating/server.mjs", + "dating:server": "node demos/dating/server/server.mjs", + "dating:web": "vite dev", + "dating:dev": "concurrently --names \"server,web\" --prefix-colors \"magenta,cyan\" --kill-others-on-fail \"npm run dating:server\" \"npm run dating:web\"", "dev:conn-chat": "node scripts/conn-chat-server.mjs", "lint": "prettier --check . && eslint .", "format": "prettier --write .", @@ -46,6 +48,7 @@ "@sveltejs/vite-plugin-svelte": "^7.0.0", "@types/node": "^22", "@vitest/browser-playwright": "^4.1.3", + "concurrently": "^9.2.1", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.17.0", diff --git a/parte_I_revision_completa_para_claude.txt b/parte_I_revision_completa_para_claude.txt deleted file mode 100644 index ccf014a..0000000 --- a/parte_I_revision_completa_para_claude.txt +++ /dev/null @@ -1,988 +0,0 @@ -REVISIÓN COMPLETA DE LA PARTE I -Fundamentos de la semántica perceptiva de la interfaz -Versión de revisión para Claude - -OBJETIVO DEL DOCUMENTO -====================== - -Este documento corrige la revisión anterior de la Parte I. No es una lista breve de inserciones sobre Russell ni un parche aislado del Capítulo 10. Es una revisión integral de la Parte I como bloque editorial. - -El problema detectado es estructural: - -- las familias semánticas podían parecer categorías de diseño inventadas; -- los intents podían parecer una taxonomía arbitraria; -- los canales podían parecer capas técnicas; -- la crítica al semáforo podía quedar reducida a un resumen demasiado pobre; -- los fundamentos psicoperceptivos estaban presentes en la teoría, pero insuficientemente visibles en el manuscrito. - -La revisión debe hacer explícito que el libro se apoya en: - -- organización perceptiva y Gestalt; -- figura/fondo; -- affordances y acción situada; -- atención y saliencia; -- appraisal y evaluación del evento; -- Russell y el espacio valencia/activación; -- multisensorialidad; -- correspondencias crossmodales; -- esquemas sensoriomotores; -- accesibilidad como prueba de distribución semántica. - -La tesis editorial de la revisión es: - -> Este libro no inventa categorías. -> Nombra estructuras que ya participan en la forma en que los usuarios perciben, atienden, actúan, evalúan y comprenden los cambios de una interfaz. - -REGLA DE INTEGRACIÓN -==================== - -No repetir autores en cada capítulo. -No convertir cada idea en cita académica. -No escribir como paper. - -La distribución correcta es: - -1. Capítulo 3 declara el mapa completo de fundamentos. -2. Capítulos 4–8 aplican esos fundamentos al evento, gramática, interacción semántica, criterios y familias. -3. Capítulo 9 aplica appraisal, figura/fondo y affordances a la diferencia valencial/transicional. -4. Capítulo 10 ancla los intents en Russell, valencia/activación y la crítica histórica al semáforo. -5. Parte II y Parte III retoman esos fundamentos sin volver a explicarlos desde cero. - -ESTRUCTURA REVISADA DE LA PARTE I -================================= - -Introducción — La pregunta que aparece al programar interfaces -Capítulo 1 — Qué entendemos por interfaz -Capítulo 2 — Por qué preguntamos ahora -Capítulo 3 — Fundamentos psicoperceptivos de la interfaz -Capítulo 4 — El evento interactivo como unidad mínima de significado -Capítulo 5 — Por qué una gramática del evento -Capítulo 6 — Qué es una interacción semántica -Capítulo 7 — Criterios para identificar una interacción semántica -Capítulo 8 — De las clases de evento a las familias semánticas -Capítulo 9 — Semánticas valenciales y transicionales -Capítulo 10 — Intent: más allá del semáforo -Cierre de Parte I - -INTRODUCCIÓN -La pregunta que aparece al programar interfaces -=============================================== - -DIAGNÓSTICO ------------ - -La introducción funciona bien si conserva el tono humilde del programador que se pregunta por qué todo está fragmentado: componentes, estilos, estados, motion, sonido, accesibilidad y lógica viven en capas distintas, mientras el usuario percibe una experiencia unificada. - -PROBLEMA DETECTADO ------------------- - -Faltaba explicitar dos cosas: - -1. El libro usa la web como campo principal de ejemplos. -2. La teoría no se limita a la web. - -INSERCIÓN RECOMENDADA ---------------------- - -Ubicación: después de presentar la fragmentación entre componentes, estilos, motion, sonido, háptica y accesibilidad. - -Texto: - -Este libro usará principalmente la web como campo de observación y ejemplo. - -No porque la teoría pertenezca solo a la web, sino porque en la web la fragmentación se vuelve especialmente visible. El desarrollador trabaja con DOM, CSS, JavaScript, eventos técnicos, componentes, estados, media queries, Web Audio API, preferencias de usuario y accesibilidad como capas separadas. Cada una tiene su propia lógica. Cada una se documenta de forma distinta. Cada una suele resolverse con herramientas distintas. - -Pero el usuario no experimenta esas capas por separado. - -El usuario no percibe “un cambio de clase CSS”, “un listener de click”, “una transición”, “un aria-live” o “un token de color”. Percibe que algo respondió, que algo se abrió, que algo quedó guardado, que algo reclama atención, que algo se perdió, que algo sigue ocurriendo. - -La web será, por tanto, nuestro laboratorio principal. Pero no el límite de la teoría. - -Un usuario puede percibir contacto, espera, pérdida, confirmación, señal, manipulación o cambio de marco en una app nativa, en un wearable, en una interfaz espacial, en un sistema embebido o en un panel físico. Lo que cambia es la realización técnica: latencia, fluidez, calidad háptica, acceso al sonido, rendimiento, políticas de plataforma y preferencias del usuario. - -La teoría se formula para interfaces en general. -Los ejemplos partirán sobre todo de la web porque ahí la necesidad es especialmente clara. - -Principio añadido: - -> La web será el campo principal de ejemplos, no el límite de la teoría. - -CAPÍTULO 1 -Qué entendemos por interfaz -Hacia una definición operativa de la interfaz -============================================= - -DIAGNÓSTICO ------------ - -Este capítulo ya es fuerte. Redefine la interfaz como algo más que superficie visible y la formula como dimensión perceptible de un contexto operacional. Esa idea debe mantenerse. - -PROBLEMA DETECTADO ------------------- - -El capítulo contiene intuiciones psicoperceptivas, pero todavía no anuncia suficientemente que después habrá un capítulo de fundamentos. Debe preparar el Capítulo 3 sin cargarlo todavía de teoría. - -FUNCIÓN CORREGIDA DEL CAPÍTULO ------------------------------- - -Debe responder: - -- qué ha significado tradicionalmente “interfaz”; -- por qué esa definición sigue siendo útil; -- por qué se queda corta si solo pensamos en componentes; -- por qué el usuario percibe eventos, no solo objetos; -- por qué la interfaz debe entenderse como dimensión perceptible de un contexto operacional. - -INSERCIÓN 1: PUENTE HACIA FUNDAMENTOS -------------------------------------- - -Ubicación: al final de la sección “La interfaz no es solo visual”. - -Texto: - -Esta idea se desarrollará con más precisión en el capítulo de fundamentos psicoperceptivos. Allí veremos que la interfaz no solo se organiza visualmente; también se apoya en percepción de campo, affordances, atención, evaluación, movimiento, sonido, multisensorialidad y experiencia corporal. - -Por ahora basta con fijar una tesis: - -> La interfaz no es visual más algunos extras. -> Es perceptiva. - -INSERCIÓN 2: SUAVIZAR NEUROAFIRMACIONES ---------------------------------------- - -Si aparece una frase del tipo “cada canal tiene sus circuitos propios”, sustituir por: - -Cada canal tiene condiciones perceptivas propias, formas propias de comunicar significado y formas propias de fallar. No se trata de reducir la interfaz a neurofisiología, sino de reconocer que color, forma, movimiento, sonido, háptica, tiempo y profundidad no son equivalentes. - -INSERCIÓN 3: CIERRE REFORZADO ------------------------------ - -Añadir en la síntesis: - -La redefinición de interfaz como dimensión perceptible de un contexto operacional prepara el desplazamiento central del libro: del componente al evento. El componente sigue siendo necesario, pero ya no basta como unidad principal de significado. Cuando la interfaz cambia, responde, espera, interrumpe, confirma o pierde algo, el usuario no interpreta solo objetos: interpreta eventos. - -CAPÍTULO 2 -Por qué preguntamos ahora -Sedimentación, dependencia del camino y carga paradigmática -========================================================== - -DIAGNÓSTICO ------------ - -Este capítulo funciona como legitimación histórica. Explica por qué ciertas convenciones persistieron sin examen. Debe mantenerse. - -PROBLEMA DETECTADO ------------------- - -Antes mezclaba justificación histórica con fundamentos científicos. Ahora, al existir Capítulo 3, debe dejar de desarrollar los fundamentos y limitarse a anunciarlos. - -FUNCIÓN CORREGIDA ------------------ - -Debe responder: - -- por qué las convenciones actuales no son simples errores; -- cómo se sedimentan; -- cómo la dependencia del camino las estabiliza; -- cómo el paradigma dominante impide formular ciertas preguntas; -- por qué ahora sí tiene sentido revisar esas premisas. - -INSERCIÓN RECOMENDADA: CIERRE HACIA EL CAPÍTULO 3 -------------------------------------------------- - -Ubicación: final de “Por qué ahora”. - -Texto: - -Lo que ha cambiado no es que hayamos descubierto de pronto la percepción humana. La psicología de la percepción, la teoría de affordances, la atención, los modelos de evaluación afectiva, la multisensorialidad y los esquemas corporales llevan décadas ofreciendo herramientas para pensar mejor la interacción. - -Lo que ha cambiado es la necesidad de reunir esos marcos en una teoría de interfaz. - -Los sistemas de diseño han madurado. La web y las plataformas nativas tienen más canales disponibles. Las interfaces son más dinámicas, más adaptativas, más autónomas y más temporales. Las convenciones heredadas empiezan a mostrar sus límites. - -Por eso el siguiente capítulo hará una pausa antes de entrar en evento, gramática e interacción semántica. Presentará los fundamentos psicoperceptivos que sostendrán el resto del libro. - -No para convertir el diseño en psicología aplicada mecánicamente. - -Sino para evitar que la teoría semántica de la interfaz parezca una colección de intuiciones personales. - -CORRECCIÓN DE TONO ------------------- - -Evitar afirmaciones como: - -- “la neurociencia ha demostrado que…”; -- “esto es consenso absoluto…”; -- “el cerebro procesa…”. - -Preferir: - -- “este marco ayuda a explicar…”; -- “la literatura sobre percepción sugiere…”; -- “esta distinción es coherente con…”; -- “para los fines de este libro, resulta útil…”. - -CAPÍTULO 3 -Fundamentos psicoperceptivos de la interfaz -De qué está hecha la percepción interactiva -=========================================== - -DIAGNÓSTICO ------------ - -Este capítulo debe convertirse en el gran mapa científico del libro. No basta con mencionar teorías. Debe decir para qué sirve cada una dentro de la arquitectura. - -PROBLEMA DETECTADO ------------------- - -La versión anterior mencionaba fundamentos, pero no los distribuía con suficiente precisión en el sistema. Debe ser reescrito como capítulo ancla. - -FUNCIÓN CORREGIDA ------------------ - -Debe explicar: - -- qué fundamentos existen; -- qué parte de la teoría sostiene cada uno; -- qué no pretende demostrar; -- cómo se usará después. - -ESTRUCTURA REVISADA DEL CAPÍTULO --------------------------------- - -1. Por qué necesitamos fundamentos perceptivos -2. Organización perceptiva: Gestalt, agrupación y figura/fondo -3. Affordances y acción situada -4. Atención, saliencia y carga cognitiva -5. Appraisal: evaluación del evento -6. El espacio evaluativo: valencia y activación -7. Movimiento, causalidad y continuidad -8. Sonido y correspondencias crossmodales -9. Integración multisensorial -10. Esquemas sensoriomotores y cognición corporeizada -11. Accesibilidad como prueba de distribución semántica -12. Cómo usará este libro estos fundamentos - -SECCIÓN NUEVA OBLIGATORIA: EL ESPACIO EVALUATIVO ------------------------------------------------- - -Ubicación: después de Appraisal y antes de Movimiento. - -Texto: - -## El espacio evaluativo: valencia y activación - -Hasta ahora hemos descrito cómo el usuario percibe, atiende y actúa. Falta una dimensión esencial: cómo evalúa lo que ocurre. - -Cuando un evento interactivo sucede, el usuario no solo lo reconoce. También lo interpreta: - -- ¿esto es bueno o malo para mí? -- ¿requiere acción inmediata? -- ¿puedo ignorarlo? -- ¿ya ocurrió o aún puedo evitarlo? -- ¿puedo corregirlo? -- ¿se resolvió algo que estaba pendiente? - -Una forma útil de organizar estas evaluaciones es el espacio propuesto por James A. Russell, que describe los estados afectivos mediante dos dimensiones: - -- valencia: positivo / negativo; -- activación: baja / alta. - -Este modelo no pretende capturar toda la complejidad emocional. No dice todo lo que hay que decir sobre la experiencia afectiva. Pero resulta suficiente para distinguir diferencias que una interfaz necesita comunicar de forma consistente. - -Por ejemplo: - -- no es lo mismo algo negativo urgente que algo negativo ya ocurrido; -- no es lo mismo una confirmación leve que una culminación; -- no es lo mismo informar que advertir; -- no es lo mismo pedir atención que registrar una pérdida. - -Estas diferencias no son estilísticas. Son operativas. - -Este libro utilizará ese espacio como base para definir los intents, no como una teoría psicológica completa, sino como una estructura mínima para organizar la evaluación del evento. - -FIGURA OBLIGATORIA ------------------- - -## Figura — Espacio valencia/activación aplicado a intents - -Brief para Claude: - -Crear un plano cartesiano. - -Eje horizontal: -valencia negativa ←→ valencia positiva - -Eje vertical: -activación baja ↑ activación alta - -Colocar: - -- threat: negativa / alta activación -- risk: negativa / activación media -- loss: negativa / baja activación -- neutral: centro / baja activación -- affirm: positiva / baja activación -- fulfill: positiva / activación media-alta - -Pie: -Los intents no son colores. Son regiones evaluativas del evento. - -TABLA CENTRAL OBLIGATORIA -------------------------- - -## Tabla — Fundamentos y función dentro del libro - -| Fundamento | Qué explica | Qué sostiene en la teoría | -|---|---|---| -| Gestalt | agrupación, figura/fondo, campo perceptivo | presencia, profundidad, emerge, shift | -| Gibson / affordances | acción posible | contexto operacional, contact, handle | -| Atención / saliencia | orientación del foco y carga cognitiva | signal, proporción, fatiga | -| Appraisal | evaluación respecto a metas, control y consecuencia | valencial/transicional, intent | -| Russell | valencia y activación | seis intents | -| Motion / causalidad | continuidad, agencia, trayectoria | contact, handle, emerge, shift | -| Spence / crossmodalidad | correspondencias entre canales | sonido, coordinación multicanal | -| Multisensorialidad | integración de señales | canales, sincronía, accesibilidad | -| Embodiment / Johnson | esquemas corporales | handle, contact, sustain, loss | -| Accesibilidad | variabilidad perceptiva | migración semántica | - -CIERRE RECOMENDADO ------------------- - -Estos fundamentos no dictan una taxonomía cerrada. No prueban que solo existan siete familias ni seis intents. Lo que hacen es otra cosa: ofrecen criterios para que las categorías del libro no parezcan arbitrarias. - -A partir de aquí, cuando hablemos de evento, gramática, interacción semántica, familias, intent y canales, no estaremos hablando de nombres inventados desde el diseño. Estaremos nombrando formas recurrentes de percibir, actuar, atender, evaluar e integrar cambios. - -CAPÍTULO 4 -El evento interactivo como unidad mínima de significado -====================================================== - -DIAGNÓSTICO ------------ - -El capítulo es conceptualmente correcto, pero debe apoyarse explícitamente en el Capítulo 3. - -FUNCIÓN CORREGIDA ------------------ - -Debe mostrar que el evento no es una ocurrencia técnica, sino una modificación perceptible con relevancia operativa. - -INSERCIÓN RECOMENDADA ---------------------- - -Ubicación: después de la definición de evento interactivo. - -Texto: - -Esta definición se apoya en los fundamentos anteriores. - -Desde la organización perceptiva, un evento modifica el campo: algo aparece, cambia de figura, se subordina, se agrupa o se separa. - -Desde las affordances, un evento modifica lo que el usuario puede hacer o cree que puede hacer. - -Desde la atención, un evento puede orientar, reclamar, sostener o liberar foco. - -Desde el appraisal, un evento puede volverse evaluable: favorable, riesgoso, urgente, perdido o resuelto. - -Desde la multisensorialidad, un evento puede distribuirse entre varios canales y aun así percibirse como una sola unidad. - -Por eso un evento interactivo no equivale a un click, a un callback ni a un cambio interno de estado. Un evento interactivo existe cuando una modificación perceptible altera la relación operativa entre usuario y sistema. - -INSERCIÓN DE CONTRASTE ----------------------- - -Añadir una tabla breve: - -| Plano | Qué ocurre | ¿Es evento interactivo? | -|---|---|---| -| Callback interno | cambia lógica | no necesariamente | -| Cambio visual decorativo | cambia apariencia | no necesariamente | -| Press visible | registra acción | sí, si afecta agencia | -| Estado fijado | cambia consecuencia | sí | -| Alerta | orienta atención | sí | -| Progreso | sostiene espera | sí | - -CAPÍTULO 5 -Por qué una gramática del evento -================================ - -DIAGNÓSTICO ------------ - -El capítulo justifica la palabra gramática, pero debe apoyarse más en diferenciación perceptiva y carga cognitiva. - -FUNCIÓN CORREGIDA ------------------ - -Debe responder: - -- qué es una gramática; -- por qué una interfaz necesita una gramática de eventos; -- por qué no bastan efectos, componentes o patrones. - -INSERCIÓN RECOMENDADA ---------------------- - -Ubicación: después de definir gramática. - -Texto: - -La necesidad de una gramática no es solo conceptual. Es perceptiva. - -La atención humana no procesa todos los cambios con el mismo peso. La percepción agrupa, separa y jerarquiza. El usuario aprende regularidades por exposición repetida. Si el sistema usa la misma forma perceptiva para eventos distintos, aumenta la carga interpretativa. Si usa formas distintas sin regularidad, impide aprendizaje implícito. - -Una gramática del evento intenta reducir esa carga. - -Permite que ciertas diferencias se vuelvan reconocibles: - -- contacto no es consolidación; -- aparición no es señal; -- señal no es amenaza; -- amenaza no es pérdida; -- proceso no es resultado; -- manipulación no es contacto repetido; -- cambio de marco no es simple aparición. - -Sin gramática, el usuario debe reconstruir esas diferencias caso por caso. - -Con gramática, el sistema las vuelve perceptivamente disponibles. - -PRINCIPIO AÑADIDO ------------------ - -> Una gramática no añade complejidad al usuario. -> La retira del momento de interpretación y la desplaza al diseño del sistema. - -CAPÍTULO 6 -Qué es una interacción semántica -================================ - -DIAGNÓSTICO ------------ - -El capítulo ya tiene una definición fuerte. Debe conectarse mejor con fundamentos y canales. - -FUNCIÓN CORREGIDA ------------------ - -Debe definir la interacción semántica como puente entre evento y realización perceptiva. - -INSERCIÓN RECOMENDADA ---------------------- - -Ubicación: después de la definición de interacción semántica. - -Texto: - -Esta definición tiene una consecuencia importante: una interacción semántica no pertenece a un único canal. - -Puede apoyarse en organización perceptiva, si necesita cambiar figura/fondo. -Puede apoyarse en affordance, si modifica acción posible. -Puede apoyarse en atención, si reclama foco. -Puede apoyarse en appraisal, si porta evaluación. -Puede apoyarse en motion, sonido, color, forma, presencia, profundidad o háptica. - -Lo que la define no es el recurso que usa, sino la pregunta perceptiva que responde. - -Una misma semántica puede migrar de un canal a otro sin perder identidad si conserva su función. Un contacto puede ser motion, sonido o háptica. Una señal puede ser color, forma, texto o sonido. Una pérdida puede ser retirada, huella, texto o silencio grave. - -Por eso hablaremos de firma perceptiva, no de receta. - -CAPÍTULO 7 -Criterios para identificar una interacción semántica -=================================================== - -DIAGNÓSTICO ------------ - -El capítulo funciona como disciplina taxonómica. Debe explicitar que cada criterio tiene un fundamento psicoperceptivo. - -INSERCIÓN RECOMENDADA ---------------------- - -Ubicación: después de presentar los criterios. - -Texto: - -Estos criterios no son solo reglas internas de clasificación. Cada uno se apoya en una dimensión perceptiva: - -- la relevancia operativa se apoya en affordances: una diferencia importa si cambia lo que el usuario puede hacer o interpretar; -- la diferenciabilidad se apoya en organización perceptiva y atención: una categoría debe poder percibirse como distinta; -- la recurrencia se apoya en aprendizaje implícito: una semántica necesita repetirse para volverse reconocible; -- la estabilidad se apoya en memoria perceptiva: la identidad debe sobrevivir a variaciones; -- la elasticidad se apoya en adaptación de canales: la misma función puede expresarse de maneras distintas; -- la economía cognitiva se apoya en reducción de carga interpretativa; -- la multicanalidad se apoya en integración sensorial; -- la accesibilidad se apoya en variabilidad perceptiva. - -Una interacción semántica no se acepta porque tenga nombre. Se acepta porque nombra una diferencia que el usuario necesita percibir de forma recurrente. - -TABLA AÑADIDA -------------- - -| Criterio | Fundamento | -|---|---| -| Relevancia operativa | affordances | -| Diferenciabilidad | Gestalt / atención | -| Recurrencia | aprendizaje implícito | -| Estabilidad | memoria perceptiva | -| Elasticidad | variación de canales | -| Multicanalidad | integración multisensorial | -| Evaluabilidad | appraisal | -| Accesibilidad | migración semántica | - -CAPÍTULO 8 -De las clases de evento a las familias semánticas -================================================= - -DIAGNÓSTICO ------------ - -El capítulo debe seguir siendo la transición hacia las siete familias, pero debe hacer más explícito que las familias derivan de preguntas perceptivas y fundamentos. - -FUNCIÓN CORREGIDA ------------------ - -Debe explicar: - -- por qué se reduce de semánticas exploratorias a siete familias; -- qué pregunta responde cada una; -- qué fundamento perceptivo la sostiene. - -INSERCIÓN RECOMENDADA ---------------------- - -Ubicación: después de presentar las siete familias. - -Texto: - -Estas familias no son categorías de diseño. Son categorías perceptivas. - -Cada una se apoya en una forma distinta de organizar la experiencia: - -- contact se apoya en agencia, causalidad inmediata y esquema de contacto; -- commit se apoya en cierre cognitivo, consecuencia y memoria de estado; -- signal se apoya en atención, saliencia y orientación del foco; -- handle se apoya en acción corporal, control continuo y manipulación; -- emerge se apoya en figura/fondo, presencia y aparición perceptiva; -- shift se apoya en cambio de marco cognitivo y orientación contextual; -- sustain se apoya en continuidad temporal, espera y mantenimiento de confianza. - -Dicho de otro modo: - -> Las familias no salen de componentes. -> Salen de preguntas perceptivas recurrentes. - -TABLA AÑADIDA -------------- - -| Familia | Pregunta | Fundamento principal | -|---|---|---| -| contact | ¿me ha sentido? | agencia / causalidad | -| commit | ¿quedó fijado? | cierre / consecuencia | -| signal | ¿debo atender? | atención / saliencia | -| handle | ¿lo controlo? | acción corporal / manipulación | -| emerge | ¿apareció? | figura/fondo / presencia | -| shift | ¿cambió el marco? | modelo mental / orientación | -| sustain | ¿sigue ocurriendo? | continuidad temporal | - -CAPÍTULO 9 -Semánticas valenciales y transicionales -======================================= - -DIAGNÓSTICO ------------ - -Este capítulo es muy importante y debe ser reescrito como aplicación de fundamentos, no como introducción de fundamentos desde cero. - -PROBLEMA DETECTADO ------------------- - -La versión anterior introducía appraisal, Gestalt, affordances, atención, afecto y esquemas como si el lector no los conociera. Ahora ya deben haber aparecido en Capítulo 3. - -FUNCIÓN CORREGIDA ------------------ - -Debe responder: - -- qué eventos son mensaje y qué eventos son marco; -- por qué no todos aceptan intent; -- cómo se compone marco + mensaje. - -ESTRUCTURA REVISADA -------------------- - -1. El problema: colorear el contenedor -2. La distinción central: mensaje y marco -3. Aplicación de appraisal -4. Aplicación figura/fondo -5. Aplicación affordances y atención -6. Aplicación esquemas sensoriomotores -7. Mapa de familias según evaluabilidad -8. Reglas de composición -9. Antipatrones -10. Ejemplo completo -11. Síntesis - -INSERCIÓN DE APERTURA REVISADA ------------------------------- - -Texto: - -El Capítulo 3 presentó varios fundamentos que ahora convergen en una distinción central. - -Desde el appraisal, algunos eventos se evalúan respecto a metas, control, urgencia y consecuencia. Desde la Gestalt, algunos eventos funcionan como figura y otros como fondo. Desde las affordances, algunos eventos permiten o comunican acción; otros organizan el campo donde la acción ocurre. Desde la atención, algunas señales reclaman foco; otras solo reorientan el contexto. Desde los esquemas sensoriomotores, algunos eventos son agentivos; otros son estructurales. - -Todas esas distinciones apuntan a una misma idea: - -> Hay eventos que son mensaje. -> Hay eventos que son marco. - -Los primeros pueden portar intent. -Los segundos normalmente no. - -INSERCIÓN CLAVE ---------------- - -Ubicación: después de “colorear el contenedor”. - -Texto: - -El error no es usar intent. El error es aplicarlo al evento equivocado. - -Un modal puede contener una amenaza, pero el modal no es la amenaza. Un spinner puede preceder a un fallo, pero el spinner no es el fallo. Una vista puede mostrar un éxito, pero la navegación hasta esa vista no es el éxito. - -El intent debe vivir en el evento que porta evaluación. - -Regla: - -> El intent modula la figura evaluable, no el fondo estructural. - -MAPA DE FAMILIAS SEGÚN EVALUABILIDAD ------------------------------------- - -| Familia | Naturaleza dominante | Relación con intent | -|---|---|---| -| contact | agentiva mínima | leve / anticipatoria | -| commit | evaluable | plena | -| signal | evaluable / atencional | plena | -| handle | mixta | sobre todo en drop | -| emerge | estructural | normalmente no | -| shift | estructural | normalmente no | -| sustain | estructural | normalmente no | - -CAPÍTULO 10 -Intent: más allá del semáforo -============================= - -DIAGNÓSTICO ------------ - -Este capítulo era el más afectado. Había quedado demasiado resumido. Debe recuperar la fuerza del documento original “Más allá del semáforo”. - -PROBLEMAS DETECTADOS --------------------- - -1. La genealogía del semáforo estaba amputada. -2. Russell aparecía poco. -3. El paso de color a espacio afectivo no estaba suficientemente dramatizado. -4. Las líneas de cada intent eran demasiado escuetas. -5. Info no quedaba suficientemente separado de intent. -6. Threat/loss y affirm/fulfill necesitaban más peso. - -ESTRUCTURA REVISADA -------------------- - -1. El intent como problema no examinado -2. La herencia del semáforo -3. De la carretera a la pantalla -4. Qué codifica realmente la convención heredada -5. Qué debería ser un intent -6. El espacio afectivo: Russell, valencia y activación -7. Las seis regiones evaluativas -8. Lo que la convención heredada pierde -9. Cómo modula el intent -10. Neutralidad no es ausencia de intent -11. Familia e intent -12. Antipatrones -13. Ejemplo narrativo -14. Principios -15. Síntesis - -SUSTITUCIÓN AMPLIADA PARA SECCIONES 1–4 ---------------------------------------- - -## 1. El intent como problema no examinado - -Existe en el diseño de interfaces una convención tan extendida que ha dejado de parecer convención. - -Cuando algo ha ido mal, usamos rojo. -Cuando algo requiere precaución, usamos amarillo. -Cuando algo ha ido bien, usamos verde. -Cuando algo es informativo, usamos azul. - -A ese conjunto lo llamamos danger, warning, success, info. O lo llamamos error, positive, negative, notice. Pero la estructura permanece. - -Lo notable no es que usemos esa convención. Lo notable es que casi nunca preguntamos de dónde viene, qué cubre, qué omite y qué mezcla. - -¿Por qué success debe cubrir tanto una operación importante completada como un autoguardado menor? - -¿Por qué danger debe cubrir tanto una amenaza activa como una pérdida ya consumada? - -¿Por qué warning mezcla un riesgo corregible, una precaución leve y a veces una amenaza seria? - -¿Por qué info aparece junto a los intents si muchas veces no comunica valencia, sino solo saliencia atencional? - -¿Por qué el intent se ha definido durante años como color contextual y no como evaluación del evento? - -Estas preguntas son sencillas. Pero no aparecen con frecuencia porque la convención está normalizada. Entró en frameworks, tokens, clases CSS, sistemas de diseño, documentación, temas y componentes. Y una vez que algo entra en infraestructura, deja de parecer una decisión. - -Parece la forma natural de hacer las cosas. - -La pregunta del capítulo es: - -> ¿Qué ocurre cuando dejamos de tratar el intent como color y empezamos a tratarlo como evaluación perceptiva del evento? - -## 2. La herencia del semáforo - -La taxonomía heredada de intents no nació en las interfaces digitales. - -Antes de que existieran botones verdes de success, alertas rojas de danger o banners amarillos de warning, esos colores ya cargaban con una larga historia de señalización industrial, ferroviaria y vial. - -El rojo, el amarillo y el verde pertenecían a entornos donde una señal debía reconocerse rápido, a distancia y con consecuencias físicas claras. No eran colores decorativos. Eran instrucciones operativas. Detenerse. Proceder con precaución. Avanzar. Prestar atención. Evitar una colisión. No entrar. Continuar. - -Esa estructura era adecuada para muchos sistemas físicos de señalización porque resolvía un problema urgente: permitir decisiones rápidas con bajo coste interpretativo. - -Pero esa estructura no era una teoría afectiva completa. - -No pretendía distinguir entre una amenaza activa y una pérdida consumada. No pretendía separar una confirmación suave de una culminación significativa. No pretendía diferenciar saliencia informativa de evaluación emocional. Su función era mucho más concreta: codificar órdenes y estados operativos mediante colores muy reconocibles. - -Cuando las interfaces gráficas empezaron a necesitar comunicar estados del sistema —error, advertencia, éxito, información—, heredaron esa estructura porque ya era comprensible. La cultura ya había aprendido que rojo pesa más que verde, que amarillo pide precaución, que verde permite continuar. - -El diseño digital no inventó esa asociación. La recibió. - -El problema no fue heredar. - -El problema fue olvidar que se estaba heredando. - -Con el tiempo, la asociación rojo / amarillo / verde dejó de parecer una convención histórica y empezó a parecer una estructura natural de la interfaz. El rojo ya no era simplemente una señal cromática útil: pasó a llamarse danger, error, destructive. El verde pasó a llamarse success. El amarillo pasó a llamarse warning. Y el azul, que no pertenecía del mismo modo a la lógica del semáforo, terminó ocupando el cajón residual de lo informativo: info. - -Ahí ocurrió la mutación decisiva: - -> una convención cromática empezó a funcionar como si fuera una taxonomía semántica. - -## 3. De la carretera a la pantalla - -La web estabilizó esa mutación de forma especialmente clara. - -Frameworks como Bootstrap necesitaban resolver un problema práctico: ofrecer clases contextuales simples para botones, alertas, tablas, mensajes y estados. La solución era evidente, reconocible y fácil de usar: - -success -warning -danger -info - -El éxito se volvió verde. -La advertencia, amarilla. -El peligro, rojo. -La información, azul. - -Aquella decisión era razonable. Funcionaba. Era barata cognitivamente y barata técnicamente. Cualquier equipo podía aplicarla sin abrir una discusión teórica sobre afecto, percepción o semántica. Bastaba con asignar una clase. - -Pero al entrar en clases CSS, documentación, temas, tokens, componentes y guías, esa convención dejó de parecer un atajo. Se convirtió en vocabulario. Después otros frameworks y sistemas de diseño heredaron la estructura con variaciones menores. Algunos cambiaron nombres. Otros añadieron tonos intermedios. Otros introdujeron primary, secondary, neutral, destructive. - -Pero el núcleo siguió siendo el mismo: - -positivo -precaución -negativo grave -informativo -neutral - -Ese núcleo parecía suficiente mientras el intent solo modificaba color. - -Si todo lo que hace danger es pintar un borde en rojo, quizá no necesitamos mucha más teoría. Si todo lo que hace success es poner verde un mensaje, quizá la convención basta. - -Pero en una interfaz perceptiva multicanal, el intent ya no cambia solo color. Puede modular: - -motion -sonido -duración -ritmo -intensidad -persistencia -profundidad -forma -háptica -silencio - -En ese momento, la convención heredada empieza a mostrar sus límites. - -Porque una cosa es elegir un color. - -Otra muy distinta es decidir cómo debe moverse, sonar, durar, aparecer, persistir o sentirse un evento. - -## 4. Qué codifica realmente la convención heredada - -Si examinamos con atención success, warning, danger e info, vemos que no forman una taxonomía afectiva completa. - -Forman una taxonomía cromática útil. - -Eso no las vuelve inútiles. Las vuelve insuficientes. - -Danger codifica una región negativa intensa, pero mezcla al menos tres cosas distintas: - -amenaza activa -fallo grave -pérdida consumada - -Una amenaza activa todavía permite actuar. Una pérdida consumada ya ocurrió. Un fallo grave puede bloquear una operación, pero no siempre equivale a destrucción. Tratar todo eso como danger produce una interfaz afectivamente torpe. - -Success codifica una región positiva, pero mezcla también cosas distintas: - -confirmación suave -resolución positiva -culminación de una tensión -estado correcto rutinario - -Un autoguardado correcto no merece la misma energía perceptiva que completar una subida larga o enviar un formulario importante. Si todo es success, nada se siente realmente cumplido. - -Warning codifica precaución, pero no distingue bien entre riesgo corregible, advertencia moderada y amenaza próxima. Algunas advertencias piden revisión tranquila; otras exigen atención inmediata. No son el mismo evento afectivo. - -Info es el caso más problemático. Muchas veces no expresa evaluación afectiva. Expresa simplemente que el sistema quiere que algo se note. No responde a: - -¿cómo se evalúa esto? - -Responde a: - -¿debo atender esto? - -Eso lo coloca más cerca de signal que del sistema de intents. En esta gramática, muchos usos de info se expresan mejor como: - -signal.announce + neutral -signal.inform + neutral -signal.notify + neutral - -La familia signal aporta la saliencia. -El intent neutral indica ausencia de juicio fuerte. - -Esta es la diferencia central: - -> La convención heredada codifica colores útiles. -> La teoría del intent debe codificar regiones evaluativas del evento. - -Por eso necesitamos separar: - -threat ≠ loss -affirm ≠ fulfill -info ≠ intent - -La interfaz puede distinguir esas diferencias. El usuario puede sentir esas diferencias. La gramática también debe poder nombrarlas. - -SECCIÓN 6 AMPLIADA: RUSSELL Y ESPACIO AFECTIVO ----------------------------------------------- - -## El espacio afectivo - -El sistema de intents no se deriva de colores heredados, sino de la evaluación del evento dentro de un espacio afectivo. - -Este capítulo se apoya en un modelo sencillo: el espacio valencia/activación descrito por James A. Russell. No se utiliza como teoría completa de la emoción, sino como base operativa para distinguir tipos de evaluación que la interfaz necesita expresar. - -Los seis intents pueden entenderse como regiones discretas de ese espacio aplicadas al evento interactivo. - -La valencia distingue lo positivo de lo negativo. -La activación distingue baja y alta movilización atencional o corporal. - -No es lo mismo threat que loss. Ambos son negativos, pero uno tiene alta activación y convoca acción; el otro registra una consecuencia consumada con activación más baja. - -Tampoco es lo mismo affirm que fulfill. Ambos son positivos, pero uno confirma suavemente; el otro resuelve una tensión u objetivo. - -El espacio afectivo no nos dice qué color usar. Nos dice qué diferencia evaluativa debe preservar la interfaz. - -LÍNEAS POR INTENT ------------------ - -Incluir debajo de cada intent: - -Neutral: -Neutral ocupa la región de baja activación sin valencia fuerte: el evento no requiere evaluación afectiva significativa. - -Affirm: -Affirm ocupa la región de valencia positiva con baja activación: confirma sin alterar significativamente el foco atencional ni la energía del sistema. - -Fulfill: -Fulfill corresponde a una región positiva con mayor activación: marca la resolución de una tensión o la culminación de una acción relevante. - -Risk: -Risk se sitúa en una región negativa de activación media: indica un problema corregible que requiere atención, pero no una reacción inmediata. - -Threat: -Threat pertenece a la región de alta activación negativa: exige atención urgente y posible acción inmediata. - -Loss: -Loss comparte valencia negativa con threat, pero con menor activación y distinta temporalidad: registra una consecuencia ya consumada. - -FRASE OBLIGATORIA AL FINAL DE LA SECCIÓN DE INTENTS ---------------------------------------------------- - -Los intents no son colores ni estilos. Son regiones evaluativas del evento que pueden expresarse mediante distintos canales perceptivos. - -FIGURA OBLIGATORIA ------------------- - -## Figura — Del evento al intent - -Brief: - -evento evaluable -→ appraisal / evaluación -→ valencia + activación -→ intent -→ canales perceptivos - -Ejemplo: -commit.delete -→ negativo + baja activación + consecuencia consumada -→ loss -→ retirada + huella + color desaturado + posible sonido grave - -CIERRE DE PARTE I REVISADO -========================== - -Con este capítulo se cierra la Parte I. - -La interfaz ya no ha sido definida solo como superficie, sino como dimensión perceptible de un contexto operacional. El evento interactivo ha quedado establecido como unidad mínima de significado. La gramática del evento ha sido presentada como sistema de diferencias, relaciones y reglas de composición. La interacción semántica ha sido definida como configuración perceptiva recurrente. Las familias han sido reducidas a siete preguntas fundamentales. Los eventos evaluables se han separado de los estructurales. Y el intent ha dejado de ser color para convertirse en región evaluativa del evento. - -La Parte II podrá entrar ahora en los canales: - -tiempo -motion -presencia -profundidad -forma -color -sonido -háptica -accesibilidad -coordinación - -Pero esos canales ya no serán efectos. Serán formas de hacer sensible una gramática. - -RESUMEN EJECUTIVO DE CORRECCIONES -================================= - -1. La Introducción debe aclarar que la web es campo de ejemplos, no límite de la teoría. -2. Capítulo 1 debe preparar el paso hacia fundamentos sin sobrecargarse. -3. Capítulo 2 debe anunciar fundamentos, no desarrollarlos. -4. Capítulo 3 debe ser el mapa completo de fundamentos psicoperceptivos. -5. Capítulo 4 debe conectar evento con percepción, affordance, atención, appraisal y multisensorialidad. -6. Capítulo 5 debe fundamentar gramática en diferenciación perceptiva y reducción de carga cognitiva. -7. Capítulo 6 debe mostrar que interacción semántica no pertenece a un canal único. -8. Capítulo 7 debe relacionar cada criterio con su base psicoperceptiva. -9. Capítulo 8 debe mostrar que las familias son categorías perceptivas. -10. Capítulo 9 debe aplicar fundamentos a mensaje/marco, no introducirlos desde cero. -11. Capítulo 10 debe recuperar la fuerza de “Más allá del semáforo” y anclar claramente Russell. -12. Todo el bloque debe sostener esta idea: - Este libro no inventa categorías; nombra estructuras perceptivas recurrentes. - -FIN DEL DOCUMENTO diff --git a/src/web/routes/dating/+layout.svelte b/src/web/routes/dating/+layout.svelte index f051260..ecc7fb1 100644 --- a/src/web/routes/dating/+layout.svelte +++ b/src/web/routes/dating/+layout.svelte @@ -1,11 +1,15 @@ Nexo + + + -
    - {#if showChrome} -
    - - Nexo - consola de moderación - +
    + {#if showProductChrome} + + + + {/if} + + {#if isOpsShell} +
    + + + Nexo + moderación + + +
    + {#if session?.authenticated === true} + {userName} + {/if} @@ -235,15 +295,17 @@
    {/if} - {#if loadError !== null} - - {/if} +
    + {#if loadError !== null} + + {/if} -
    {@render children?.()}
    @@ -251,95 +313,299 @@ diff --git a/src/web/routes/dating/+layout.ts b/src/web/routes/dating/+layout.ts index c4f783f..0a362f3 100644 --- a/src/web/routes/dating/+layout.ts +++ b/src/web/routes/dating/+layout.ts @@ -1,5 +1,5 @@ /** - * Nexo runs against a local standalone server (`servers/dating`) and + * Nexo runs against a local standalone server (`demos/dating/server`) and * authenticates via cookies, so static prerender of the demo shell * doesn't make sense — every request needs the live API. Skip * prerender for everything under `/dating`. diff --git a/src/web/routes/dating/+page.svelte b/src/web/routes/dating/+page.svelte index 5b379d1..23b3d0d 100644 --- a/src/web/routes/dating/+page.svelte +++ b/src/web/routes/dating/+page.svelte @@ -1,6 +1,6 @@ - - - - diff --git a/src/web/routes/dating/_components/AuthLayout.svelte b/src/web/routes/dating/_components/AuthLayout.svelte deleted file mode 100644 index bc37459..0000000 --- a/src/web/routes/dating/_components/AuthLayout.svelte +++ /dev/null @@ -1,103 +0,0 @@ - - - -
    -
    -
    -

    Nexo

    -

    {title}

    - {#if subtitle}

    {subtitle}

    {/if} -
    -
    - {@render children()} -
    - {#if footer} -
    - {@render footer()} -
    - {/if} -
    -
    - - diff --git a/src/web/routes/dating/_components/Field.svelte b/src/web/routes/dating/_components/Field.svelte deleted file mode 100644 index 0dcdb50..0000000 --- a/src/web/routes/dating/_components/Field.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - - -
    - - {@render children()} - {#if error} - - {:else if hint} -

    {hint}

    - {/if} -
    - - diff --git a/src/web/routes/dating/_components/MessageToast.svelte b/src/web/routes/dating/_components/MessageToast.svelte deleted file mode 100644 index 4e3f50f..0000000 --- a/src/web/routes/dating/_components/MessageToast.svelte +++ /dev/null @@ -1,159 +0,0 @@ - - - -{#if toasts.length > 0} -
    - {#each toasts as toast (toast.id)} - - {/each} -
    -{/if} - - diff --git a/src/web/routes/dating/_components/ProfileCard.svelte b/src/web/routes/dating/_components/ProfileCard.svelte deleted file mode 100644 index 0008bf8..0000000 --- a/src/web/routes/dating/_components/ProfileCard.svelte +++ /dev/null @@ -1,157 +0,0 @@ - - - -
    -
    - {#if main} - {`Foto - {:else} - - {/if} - {intentLabel} -
    - -
    -
    -

    - {profile.displayName} - {profile.age} -

    - {#if profile.approxLocation !== ''} -

    {profile.approxLocation}

    - {/if} -
    - - {#if profile.bio !== ''} -

    {profile.bio}

    - {/if} - - {#if profile.interests.length > 0} -
      - {#each profile.interests.slice(0, 8) as interest (interest)} -
    • {interest}
    • - {/each} -
    - {/if} -
    -
    - - diff --git a/src/web/routes/dating/chat/[matchId]/+page.svelte b/src/web/routes/dating/chat/[matchId]/+page.svelte index 290d584..a8ca686 100644 --- a/src/web/routes/dating/chat/[matchId]/+page.svelte +++ b/src/web/routes/dating/chat/[matchId]/+page.svelte @@ -2,14 +2,16 @@ import { goto } from '$app/navigation'; import { page } from '$app/state'; import { tick, untrack } from 'svelte'; - import { getNexoContext } from '../../_lib/context'; - import { DatingApiError } from '../../_lib/api'; - import type { DatingMatch, DatingMessage, DatingProfile } from '../../_lib/types'; + import { getNexoContext } from '$dating/_lib/context'; + import { DatingApiError } from '$dating/_lib/api'; + import type { DatingMatch, DatingMessage, DatingProfile } from '$dating/_lib/types'; + import Icon from '$dating/_components/Icon.svelte'; const nexo = getNexoContext(); const matchId = $derived(page.params.matchId ?? ''); let match = $state(null); + let allMatches = $state([]); let messages = $state([]); let pending = $state([]); let loading = $state(true); @@ -17,11 +19,8 @@ let sending = $state(false); let topError = $state(null); let timeline: HTMLElement | null = null; + let composerInput: HTMLTextAreaElement | null = null; - // Re-run only when the URL match id changes (or the session - // transitions to / from authenticated). `load()` writes to several - // reactive cells; without `untrack` the effect would observe its - // own writes and loop forever. let loadedFor = ''; $effect(() => { const session = nexo.session(); @@ -40,15 +39,17 @@ loading = true; topError = null; try { - const [allMatches, response] = await Promise.all([ + const [list, response] = await Promise.all([ nexo.api.matches(), nexo.api.messages(matchId) ]); - match = allMatches.find((entry) => entry.id === matchId) ?? null; + allMatches = [...list]; + match = list.find((entry) => entry.id === matchId) ?? null; messages = [...response.messages]; pending = []; await tick(); scrollToEnd(); + composerInput?.focus(); } catch (caught) { if (caught instanceof DatingApiError && caught.code === 'match_not_found') { await goto('/dating/matches', { replaceState: true }); @@ -62,22 +63,10 @@ function isAtBottom(): boolean { if (timeline === null) return true; - const slack = 60; // tolerate small layout drift before suppressing autoscroll + const slack = 60; return timeline.scrollTop + timeline.clientHeight >= timeline.scrollHeight - slack; } - /** - * Live message feed via the dating server's WebSocket distributor - * (`servers/dating/realtime.mjs`). On every `dating.message.created` - * event we either: - * - drop the matching optimistic bubble (the server confirmed - * our send via `clientNonce`), or - * - append the incoming message to the timeline (a real new - * message from the counterpart). - * - * Scroll position is preserved unless the user is already pinned - * to the bottom, in which case we follow the new tail. - */ $effect(() => { if (matchId === '') return; const id = matchId; @@ -99,10 +88,10 @@ return off; }); - function counterpart(): DatingProfile | null { + function counterpartFor(m: DatingMatch | null): DatingProfile | null { const session = nexo.session(); - if (session?.authenticated !== true || match === null) return null; - return match.profiles.find((profile) => profile.userId !== session.user.id) ?? null; + if (session?.authenticated !== true || m === null) return null; + return m.profiles.find((profile) => profile.userId !== session.user.id) ?? null; } const myUserId = $derived.by(() => { @@ -110,7 +99,24 @@ return session?.authenticated === true ? session.user.id : ''; }); - const counterpartName = $derived(counterpart()?.displayName || 'Match'); + const counterpartProfile = $derived(counterpartFor(match)); + const counterpartName = $derived(counterpartProfile?.displayName || 'Match'); + const counterpartAvatar = $derived.by(() => { + const profile = counterpartProfile; + if (!profile) return null; + const main = + profile.photoUrls.find((entry) => entry.filename === profile.primaryPhoto) ?? + profile.photoUrls[0]; + return main?.thumb ?? null; + }); + + const intentLabel: Record = { + long_term: 'Algo serio', + dating: 'Conocer gente', + casual: 'Casual', + friends: 'Amistad', + unsure: 'Sin etiqueta' + }; function scrollToEnd() { if (timeline === null) return; @@ -143,14 +149,11 @@ sending = true; await tick(); scrollToEnd(); + composerInput?.focus(); try { const sent = await nexo.api.sendMessage(matchId, { body, clientNonce: nonce }); pending = pending.filter((entry) => entry.clientNonce !== nonce); - // The realtime distributor pushes this same message back — - // whichever arrives first wins; the other branch must skip - // to avoid duplicating the keyed timeline (see the WS - // subscription above). if (!messages.some((entry) => entry.id === sent.id)) { messages = [...messages, sent]; } @@ -183,216 +186,799 @@ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } - const allMessages = $derived([...messages, ...pending]); + function dayOf(iso: string): string { + if (iso === '') return ''; + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return ''; + const today = new Date(); + const isSameDay = + date.getFullYear() === today.getFullYear() && + date.getMonth() === today.getMonth() && + date.getDate() === today.getDate(); + if (isSameDay) return 'Hoy'; + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + const isYesterday = + date.getFullYear() === yesterday.getFullYear() && + date.getMonth() === yesterday.getMonth() && + date.getDate() === yesterday.getDate(); + if (isYesterday) return 'Ayer'; + return date.toLocaleDateString(); + } + + function relativeTime(iso: string): string { + if (iso === '') return ''; + const then = new Date(iso).getTime(); + if (Number.isNaN(then)) return ''; + const diffMs = Date.now() - then; + const minutes = Math.floor(diffMs / 60_000); + if (minutes < 1) return 'ahora'; + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h`; + const days = Math.floor(hours / 24); + if (days < 7) return `${days}d`; + return `${Math.floor(days / 7)}sem`; + } + + function nameFor(m: DatingMatch): string { + return counterpartFor(m)?.displayName || 'Perfil'; + } + function avatarFor(m: DatingMatch): string | null { + const profile = counterpartFor(m); + if (!profile) return null; + const main = + profile.photoUrls.find((entry) => entry.filename === profile.primaryPhoto) ?? + profile.photoUrls[0]; + return main?.thumb ?? null; + } + + const allRendered = $derived([...messages, ...pending]); + const initial = $derived(counterpartName.slice(0, 1).toUpperCase()); + + function onComposerKey(event: KeyboardEvent) { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + void send(); + } + } + + type RenderRow = + | { kind: 'day'; key: string; label: string } + | { kind: 'msg'; key: string; message: DatingMessage; mine: boolean; showAvatar: boolean }; + + const rendered = $derived.by(() => { + const out: RenderRow[] = []; + let lastDay = ''; + let lastSender = ''; + for (const message of allRendered) { + const day = dayOf(message.created); + if (day !== lastDay) { + out.push({ kind: 'day', key: `d-${day}-${message.id}`, label: day }); + lastDay = day; + lastSender = ''; + } + const mine = message.senderId === myUserId; + out.push({ + kind: 'msg', + key: message.id, + message, + mine, + showAvatar: !mine && lastSender !== message.senderId + }); + lastSender = message.senderId; + } + return out; + }); + + function autoGrow(event: Event) { + const el = event.currentTarget as HTMLTextAreaElement; + el.style.height = 'auto'; + el.style.height = `${Math.min(el.scrollHeight, 160)}px`; + } {counterpartName} — Nexo -
    -
    - ← -
    - {counterpartName} - {#if match !== null} - match {match.state} - {/if} -
    -
    - - {#if topError !== null} - - {/if} - -
    - {#if loading} -

    Cargando conversación…

    - {:else if allMessages.length === 0} -

    - Aún no hay mensajes. Rompe el hielo: pregunta por algo que viste en su perfil. -

    - {:else} - {#each allMessages as message (message.id)} - {@const mine = message.senderId === myUserId} - +
    +
    +
    + {nameFor(entry)} + {relativeTime(entry.updated || entry.created)} +
    +
    + {counterpartFor(entry)?.bio?.slice(0, 60) || 'Decid hola'} +
    +
    +
    {/each} + {#if allMatches.length === 0 && !loading} +

    Aún no tienes conversaciones.

    + {/if} +
    + + +
    +
    + + + +
    + +
    +
    {counterpartName}
    + {#if counterpartProfile?.approxLocation} +
    {counterpartProfile.approxLocation}
    + {:else if match} +
    match {relativeTime(match.created)}
    + {/if} +
    +
    + +
    + + {#if topError !== null} + + {/if} + +
    + {#if loading} +
    + {#each [0, 1, 2, 3] as i (i)} +
    + {/each} +
    + {:else if allRendered.length === 0} +
    +
    +

    Decid hola.

    +

    + Pregunta por algo concreto del perfil — funciona mejor que un "hola". +

    +
    + {:else} + {#each rendered as row (row.key)} + {#if row.kind === 'day'} +
    {row.label}
    + {:else} +
    + {#if !row.mine} + + {/if} +
    +
    + {row.message.body} +
    +
    + + {#if row.message.state === 'queued'} + enviando… + {:else if row.message.state === 'failed'} + no enviado + + + {/if} +
    +
    +
    + {/if} + {/each} + {/if} +
    + +
    { + event.preventDefault(); + void send(); + }} + > + + +
    +
    + + + diff --git a/src/web/routes/dating/discover/+page.svelte b/src/web/routes/dating/discover/+page.svelte index 76b617b..a5daa55 100644 --- a/src/web/routes/dating/discover/+page.svelte +++ b/src/web/routes/dating/discover/+page.svelte @@ -1,21 +1,19 @@ Descubre — Nexo -
    - - -
    - {#if topError !== null} - - {/if} +
    +
    +
    +

    Descubre

    +

    Perfiles seleccionados a partir de tus preferencias.

    +
    +
    + + +
    +
    - {#if loading && profiles.length === 0} -
    Buscando perfiles…
    - {:else if top === null} -
    -

    No hay más perfiles por ahora.

    -

    - Prueba a ampliar los filtros o vuelve más tarde. Los perfiles se generan a partir - de las semillas locales. -

    - + {#if showFilters} +
    +
    + Intent +
    + + {#each DATING_INTENTS as option (option)} + + {/each} +
    - {:else} -
    - - {#if profiles.length > 1} -

    {profiles.length - 1} perfiles más en cola

    - {/if} +
    + Edad +
    + + + +
    - -
    - -
    +
    + {/if} + + {#if topError !== null} + + {/if} + +
    +
    + {#if loading && profiles.length === 0} +
    +
    +
    +
    +
    +
    +
    + {:else if top === null} +
    +
    +
    +

    No hay más perfiles por ahora.

    +

    + Prueba a ampliar los filtros o vuelve más tarde — los perfiles se generan a + partir de las semillas locales. +

    + +
    +
    + {:else} +
    +
    + {#if currentPhoto} + {`Foto + {:else} + + {/if} + + + {#if orderedPhotos.length > 1} +
    + {#each orderedPhotos as _, i (i)} + + {/each} +
    + + + + {/if} + +
    + {intentDisplay(top.intent)} +

    + {top.displayName} {top.age} +

    + {#if top.approxLocation !== ''} +

    + + {top.approxLocation} +

    + {/if} +
    +
    + + +
    +
    + {#if top.bio !== '' || top.interests.length > 0} +
    + {#if top.bio !== ''} +

    {top.bio}

    + {/if} + {#if top.interests.length > 0} +
      + {#each top.interests.slice(0, 6) as interest (interest)} +
    • {interest}
    • + {/each} +
    + {/if} +
    + {/if} +
    + {/if} +
    + + {#if top !== null} + {/if}
    -
    + {#if lastMatch !== null} - diff --git a/src/web/routes/dating/register/+page.svelte b/src/web/routes/dating/register/+page.svelte index 8530c79..9a43703 100644 --- a/src/web/routes/dating/register/+page.svelte +++ b/src/web/routes/dating/register/+page.svelte @@ -1,9 +1,10 @@