diff --git a/docs/conventions.md b/docs/conventions.md index 58d1fca..8e7b994 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -11,25 +11,54 @@ decision recorded here. --- -## 1. Module identifier — 4-letter alias +## 1. Module identifier — full English word Every artifact and lib that pairs with one is identified by a stable -**4-letter alias** that already lives in `svelte.config.js` as a -bundler alias (`$buss`, `$conn`, `$sess`, …). - -The same alias is the canonical module identifier across the codebase: - -- **Path scoping in string values** (already enforced after audit - section 2): `'buss.event.published'`, `'conn.auth_failed'`, - `'sess.lifecycle.adopted'`, `'perm.client.remote_check_failed'`. -- **Constant name prefix**: `BUSS_*`, `CONN_*`, `SESS_*`, `PERM_*`, - `TIMR_*`, `LOGR_*`, `CACH_*`, `STOR_*`, `FMTS_*`, `FEND_*`, `ADOM_*`, - `AAPP_*`, `AUTH_*`, `LANG_*`, `HTTP_*`, `SIUM_*`, `ERRS_*`. - -Forbidden: full English words like `BUS_*`, `CONNECTION_*`, `SESSION_*`, -`PERMISSION_*`, `TIMER_*`, `LOGGER_*`, `CACHE_*`, `STORAGE_*`, -`FORMATS_*`, `FRONTEND_*`. Drift on this axis is being closed in a -dedicated audit pass. +**full English word** that doubles as filesystem name, alias and +canonical wire identifier: + +| Folder | Alias | Wire / value | +|---|---|---| +| `arts/active-app/`, `libs/active-app/` | `$active-app` | `'app'` | +| `arts/auth/`, `libs/auth/`, `svrs/auth/` | `$auth` | `'auth'` | +| `arts/bus/`, `libs/bus/` | `$bus` | `'bus'` | +| `arts/cache/`, `libs/cache/`, `svrs/cache/` | `$cache` | `'cache'` | +| `arts/connection/` | `$connection` | `'connection'` | +| `libs/dom/` | `$libs/dom` | `'dom'` | +| `libs/errs/` | `$libs/errs` | `'errs'` | +| `arts/frontend/` | `$frontend` | `'frontend'` | +| `arts/formats/` (+ `currency/`, `numbers/`, `units/`, `dates/`) | `$formats` | `'formats'` | +| `arts/http/`, `libs/http/` | `$http` | `'http'` | +| `arts/lang/`, `libs/lang/` | `$lang` | `'lang'` | +| `arts/logger/`, `libs/logger/` | `$logger` | `'logger'` | +| `arts/permissions/`, `libs/permissions/`, `svrs/permissions/` | `$permissions` | `'permissions'` | +| `arts/session/` | `$session` | `'session'` | +| `arts/sium/` | `$sium` | `'sium'` (proper name of the validator) | +| `arts/storage/` | `$storage` | `'storage'` | +| `arts/timer/`, `libs/timer/` | `$timer` | `'timer'` | + +### Special cases + +- **`active-app`** uses a hyphen in the alias because `$app` is a + reserved namespace in SvelteKit (`$app/stores`, `$app/navigation`). + The constants and class names still use `App` / `APP_*`; the + `active-` prefix only appears in filesystem and alias. + +- **`lang`** and **`sium`** are kept as-is. `lang` is the standard + HTML/i18n term (``); `sium` is the proper name of + the validator (like `zod`, `valibot`). They are not abbreviations. + +- **`errs`** is the framework's own error system. Kept short + intentionally because it sits at the foundation; lowering it to + `'errs'` keeps the wire format compact for codes like + `'errs::format_invalid'`. + +### What this replaces + +Earlier the codebase used 4-letter abbreviations (`buss`, `cach`, +`conn`, `sess`, `stor`, `timr`, `logr`, `aapp`, `fmts`, `fend`, +`perm`, `curr`, `unts`, `nums`). That convention has been removed: +abbreviations created cognitive load with no real benefit. --- @@ -41,7 +70,21 @@ Every categorical module-level constant follows the pattern: __ ``` -- **``** — the module's 4-letter alias in caps (rule 1). +- **``** — the module's identifier in caps. For most modules this + matches the filesystem (`STORAGE_*`, `BUS_*`, `CACHE_*`, `LOGGER_*`, + `TIMER_*`, `CONNECTION_*`, `SESSION_*`, `FORMATS_*`, `FRONTEND_*`, + `HTTP_*`, `LANG_*`, `AUTH_*`, `SIUM_*`, `DOM_*`, `ERRS_*`). + + **Exception**: the `permissions` module uses `PERMISSION_*` (singular) + because the constants describe the concept ("a permission effect", + "a permission decision"), not the module collection. The `S` is + reserved for the folder/alias/wire which group the system as a + whole. + + **Exception**: the `active-app` module uses `APP_*` (without the + `active-` prefix) because the `active-` prefix is purely a + disambiguator forced by SvelteKit's reserved `$app` namespace. + - **``** — what kind of constant it is. Picked from the fixed vocabulary in section 3. - **``** — the specific value's identifier in @@ -51,21 +94,23 @@ Every categorical module-level constant follows the pattern: ```ts // good -BUSS_DIAGNOSTIC_EVENTS -BUSS_DEFAULT_MAX_LISTENERS_PER_EVENT -BUSS_ERR_DISPOSED -SESS_EVENT_LIFECYCLE_ADOPTED -PERM_METHOD_CHECK -TIMR_STATE_PENDING -CONN_REASON_AUTH_FAILED -CACH_LIMIT_DEFAULT_MAX_ENTRIES -STOR_MODULE - -// bad (will be flagged in audit) -BUS_DIAGNOSTIC_EVENTS // wrong module prefix (rule 1) -DEFAULT_TIMER_SCOPE_SEPARATOR // category before module (rule 2) -PERMISSION_METHOD_CHECK // wrong module prefix (rule 1) -AUTO_REAUTH_USER_IDENTITY_CHANGE // missing module prefix (rule 1) +BUS_DIAGNOSTIC_EVENTS +BUS_DEFAULT_MAX_LISTENERS_PER_EVENT +BUS_ERR_DISPOSED +SESSION_EVENT_LIFECYCLE_ADOPTED +PERMISSION_METHOD_CHECK +TIMER_STATE_PENDING +CONNECTION_REASON_AUTH_FAILED +CACHE_LIMIT_DEFAULT_MAX_ENTRIES +STORAGE_MODULE // value: 'storage' +APP_MODULE // value: 'app' +PERMISSION_MODULE // value: 'permissions' + +// bad +BUSS_DIAGNOSTIC_EVENTS // 4-letter alias is dead (rule 1) +DEFAULT_TIMER_SCOPE_SEPARATOR // category before module (rule 2) +PERMISSIONS_EFFECT_ALLOW // wrong shape: concept is singular +AUTO_REAUTH_USER_IDENTITY_CHANGE // missing module prefix (rule 1) ``` ### Sub-categories @@ -75,13 +120,13 @@ in the `` part, not in ``: ```ts // good -BUSS_LISTENER_ERROR_MODE_THROW -SESS_EVENT_LIFECYCLE_ADOPTED -SESS_EVENT_LIFECYCLE_REVOKED -PERM_DEFAULT_REMOTE_FAILURE_BACKOFF_MS +BUS_LISTENER_ERROR_MODE_THROW +SESSION_EVENT_LIFECYCLE_ADOPTED +SESSION_EVENT_LIFECYCLE_REVOKED +PERMISSION_DEFAULT_REMOTE_FAILURE_BACKOFF_MS // bad -BUSS_LISTENER_ERR_MODE_THROW // category split across underscores +BUS_LISTENER_ERR_MODE_THROW // category split across underscores ``` --- @@ -94,7 +139,7 @@ inside a module. | Category | Use | |---|---| -| `MODULE` | The module's canonical 4-letter alias as a string constant. **Single source of truth** for the module's identity. Used wherever the module's identifier is needed: logger category, error message prefix, event scope, etc. Every module declares exactly one `_MODULE = ''`. | +| `MODULE` | The module's identifier as a string constant. **Single source of truth** for the module's identity. Used wherever the module's identifier is needed: logger category, error message prefix, event scope, etc. Every module declares exactly one `_MODULE = ''`. | | `ERR` | Error codes (`ErrCode` values from `$libs/errs`) | | `EVENT` | Bus or lifecycle event names (string discriminators) | | `DIAGNOSTIC_EVENTS` | Event values published through `Logger` (catalog object) | @@ -110,18 +155,18 @@ inside a module. | `ID_PREFIX` | Prefix used by ID factories | | `LOG_MSG` | Log message strings | | `ERROR_MSG` | Error message strings (technical, dev-facing). Must use a template referencing `_MODULE` for the prefix, never a hardcoded literal: `` `[${MOD_MODULE}] ...` ``. | -| `ERROR_NAME` | Legacy `Error.name` strings (being replaced by `ERR` codes) | +| `ERROR_MESSAGES` | Catalogue object (`_ERROR_MESSAGES: ErrorMessages`) indexed by `ErrCode`. Static strings or `(...args) => string` factories. | | `CONTEXT_KEY` | Svelte context keys | Categories that are **not** in this list (e.g. `AUTO_REAUTH`, `AUTO_INVALIDATE`, `BACKOFF`, `BUFFER_POLICY`, `CHANNEL_STATE`) must fold into one of the above. Examples: -- `CONNECTION_AUTO_REAUTH_*` → `CONN_REASON_AUTO_REAUTH_*` if the value - drives a "why this happened" branch, or `CONN_MODE_AUTO_REAUTH_*` if - it configures behavior. -- `CACHE_AUTO_INVALIDATE_*` → `CACH_MODE_AUTO_INVALIDATE_*`. -- `CONN_BUFFER_POLICY_*` → `CONN_MODE_BUFFER_*`. +- `CONNECTION_AUTO_REAUTH_*` → `CONNECTION_REASON_AUTO_REAUTH_*` if + the value drives a "why this happened" branch, or + `CONNECTION_MODE_AUTO_REAUTH_*` if it configures behavior. +- `CACHE_AUTO_INVALIDATE_*` → `CACHE_MODE_AUTO_INVALIDATE_*`. +- `CONNECTION_BUFFER_POLICY_*` → `CONNECTION_MODE_BUFFER_*`. When in doubt, propose the addition here before introducing it. @@ -137,21 +182,21 @@ appears as a literal in error declarations: ```ts import { errCode, moduleSeed, type ErrCode, type ModuleSeed } from '$libs/errs'; -export const BUSS_ERR: ModuleSeed = moduleSeed('buss'); // 'buss::' -export const BUSS_ERR_DISPOSED: ErrCode = errCode(BUSS_ERR, 'disposed'); // 'buss::disposed' -export const BUSS_ERR_INVALID_PAYLOAD: ErrCode = errCode(BUSS_ERR, 'invalid_payload'); -export const BUSS_ERR_LISTENER: ErrCode = errCode(BUSS_ERR, 'listener'); // 'buss::listener' -export const BUSS_ERR_LISTENER_FAILED: ErrCode = errCode(BUSS_ERR_LISTENER, 'failed'); // 'buss::listener.failed' +export const BUS_ERR: ModuleSeed = moduleSeed('bus'); // 'bus::' +export const BUS_ERR_DISPOSED: ErrCode = errCode(BUS_ERR, 'disposed'); // 'bus::disposed' +export const BUS_ERR_INVALID_PAYLOAD: ErrCode = errCode(BUS_ERR, 'invalid_payload'); +export const BUS_ERR_LISTENER: ErrCode = errCode(BUS_ERR, 'listener'); // 'bus::listener' +export const BUS_ERR_LISTENER_FAILED: ErrCode = errCode(BUS_ERR_LISTENER, 'failed'); // 'bus::listener.failed' ``` The constant's `` part mirrors the path segments inside the -runtime value: `BUSS_ERR_LISTENER_FAILED` ↔ `'buss::listener.failed'`. -The module name lives in exactly one place: `moduleSeed('buss')`. +runtime value: `BUS_ERR_LISTENER_FAILED` ↔ `'bus::listener.failed'`. +The module name lives in exactly one place: `moduleSeed('bus')`. The `::` separator distinguishes module from hierarchy. `errCode` picks the right separator automatically — `::` after a seed, `.` between segments. A helper `codeToLangPath(c)` converts -`'buss::listener.failed'` → `'buss.listener.failed'` for the i18n +`'bus::listener.failed'` → `'bus.listener.failed'` for the i18n path. ### Family matching @@ -159,41 +204,60 @@ path. A single predicate `matches(err, family)` from `$libs/errs` covers both common cases. `family` can be: -- A **module seed** (`BUSS_ERR`) — matches any error from that +- A **module seed** (`BUS_ERR`) — matches any error from that module regardless of hierarchy. - An **`ErrCode`** — matches the code itself or any hierarchical descendant within the same module. ```ts -matches(err, BUSS_ERR_DISPOSED) // exact code -matches(err, BUSS_ERR_LISTENER) // any descendant of buss::listener -matches(err, BUSS_ERR) // any error declared by buss +matches(err, BUS_ERR_DISPOSED) // exact code +matches(err, BUS_ERR_LISTENER) // any descendant of bus::listener +matches(err, BUS_ERR) // any error declared by bus +``` + +### Error class names — full English word + +Error classes use the full English word **of the concept**, not the +module's `_*` prefix. The class name is consumer-facing API; the +constant prefix is internal organization: + +```ts +// good +StorageInvalidTypeError // class +STORAGE_ERR_INVALID_TYPE // constant + +BusDisposedError // class +BUS_ERR_DISPOSED // constant + +PermissionDeniedError // class — singular concept +PERMISSION_ERR_DENIED // constant — singular prefix + +AppAlreadyCreatedError // class — no Active prefix +APP_ERR_ALREADY_CREATED // constant ``` --- ## 5. String values inside diagnostic / event constants -Already enforced after audit section 2: +Every string value emitted as an event identifier must start with the +module's identifier (rule 1) followed by `.` and the value-specific +path: ```ts // good — value carries the module scope -export const CONN_DIAGNOSTIC_EVENTS = { - AUTH_FAILED: 'conn.auth_failed', - RECONNECT_EXHAUSTED: 'conn.reconnect_exhausted' +export const CONNECTION_DIAGNOSTIC_EVENTS = { + AUTH_FAILED: 'connection.auth_failed', + RECONNECT_EXHAUSTED: 'connection.reconnect_exhausted' } as const; // bad — bare value collides across modules -export const CONN_DIAGNOSTIC_EVENTS = { +export const CONNECTION_DIAGNOSTIC_EVENTS = { AUTH_FAILED: 'auth_failed', // collides with auth.* events - LISTENER_THREW: 'listener_threw' // collides with sess and timr + LISTENER_THREW: 'listener_threw' // collides with session and timer } as const; ``` -Rule: every string value emitted as an event identifier must start -with the module's 4-letter alias (rule 1) followed by `.` and the -value-specific path. - --- ## 6. File and folder structure @@ -203,12 +267,13 @@ value-specific path. | `libs//` | Pure contracts: types, constants, error classes, helpers. No Svelte runes, no engine state. | | `arts//` | Runtime engine + `*.svelte.ts` active wrappers. May import from `$libs/` and from its own files. | | `arts//index.ts` | Public barrel. Re-exports the artifact's surface. | +| `arts//errors.ts` | Single home for the module's error infrastructure: `_ERR` seed, `_ERR_*` codes, `_ERROR_MESSAGES` catalog, error classes and `is*Error` guards. **Do not split** these across `consts.ts` and `errors.ts`; everything error-related lives together. | | `arts//test/` | Tests for engine and active wrappers. | | `svrs//` | Server-side counterparts (cookies, db adapters, route handlers). | -| `arts/aapp/` | Composition root. Allowed to import any `$`. | +| `arts/active-app/` | Composition root. Allowed to import any `$`. | **Cross-artifact imports are forbidden** (`arts/X` cannot import -`$Y`). Sole exception: `arts/aapp/`. +`$Y`). Sole exception: `arts/active-app/`. --- @@ -216,10 +281,10 @@ value-specific path. - A new category in section 3 requires a record-of-decision: who, when, why. -- Module aliases are frozen: a new artifact picks a 4-letter alias - not yet taken; renaming an existing alias is a breaking change. +- Module names are frozen: a new artifact picks a name not yet taken; + renaming an existing module is a breaking change. - Drift found in the codebase that violates these rules is filed as an audit finding and resolved in a dedicated commit. -Last updated: `2026-05-02` — initial version after audit-1-5.md -sections 1–2 closed. +Last updated: `2026-05-02` — full English word convention, dropping +the 4-letter alias rule. diff --git a/parte_I_revision_completa_para_claude.txt b/parte_I_revision_completa_para_claude.txt new file mode 100644 index 0000000..ccf014a --- /dev/null +++ b/parte_I_revision_completa_para_claude.txt @@ -0,0 +1,988 @@ +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/arts/README.md b/src/arts/README.md index 455651c..67eab52 100644 --- a/src/arts/README.md +++ b/src/arts/README.md @@ -98,7 +98,7 @@ errors are typed. - Runtime code must not throw inline string/template errors outside tests or vendored code. - Public programmer errors use artifact-specific classes and guards: - `SessDisposedError`, `ConnInvalidNameError`, `UnitsUnknownUnitError`, etc. + `SessionDisposedError`, `ConnInvalidNameError`, `UnitsUnknownUnitError`, etc. - Expected runtime failures should be returned as tagged data/results when the artifact already has such a contract (`http`, `conn`, `perm`, `cach`). - Validation failures are data (`SiumValidationError.issues`) and diagnostics @@ -121,7 +121,7 @@ errors are typed. | [`conn`](./conn/README.md) | `EngineConnections`, `ActiveConnections` | Realtime connection registry: transports, reconnect, heartbeat, request/reply, channels, session bridge | `$timr`, `$logr` (optional), `$sess` bridge (optional) | | [`auth`](./auth/README.md) | `ActiveAuth` (`EngineAuth` in `$svrs/auth`) | Authentication: password flows, CSRF, current session reflector, devices, logout, server-authoritative auth handlers | `$libs/auth`, `$http`, `$cach`, `$svrs/auth` | | [`perm`](./perm/README.md) | `ActivePermissions` (`EnginePermissions` in `$svrs/perm`) | Authorization: policy runtime adapter, HTTP client/handlers, cache snapshot, `` guard | `$libs/perm`, `$libs/svrs`, `$http`, `$logr` (optional) | -| [`cach`](./cach/README.md) | `ActiveCache` (`EngineCache` in `$svrs/cach`) | Data cache: deterministic keys, policies, scopes, stale/revalidate, tags, memory/storage adapters | `$libs/cach`, `$stor` (adapter), `$logr` (optional) | +| [`cach`](./cach/README.md) | `ActiveCache` (`EngineCache` in `$svrs/cache`) | Data cache: deterministic keys, policies, scopes, stale/revalidate, tags, memory/storage adapters | `$libs/cach`, `$stor` (adapter), `$logr` (optional) | | [`aapp`](./aapp/README.md) | `ActiveApp` | App composition: wires Logger + Lang + Formats + Frontend + Dom + Storage + Http + Timers + Cache; factories for Sess, Conn, Auth, Perm, Sium | every artifact above | ## Composition @@ -193,7 +193,7 @@ adom ─── fend \ | / | conn and `cach` through ports rather than owning their state. - `perm` splits cleanly: `$svrs/perm` owns the authoritative engine/HTTP handlers, while `$perm` owns the active UI reflector and ``. -- `cach` splits cleanly: `$svrs/cach` owns the imperative engine, while +- `cach` splits cleanly: `$svrs/cache` owns the imperative engine, while `$cach` owns the active Svelte wrapper and can consume `stor` through its storage adapter. - `adom` depends only on the pure helpers in `libs/dom` and on `libs/reactive`. @@ -216,9 +216,9 @@ alias: { $aapp: 'src/arts/aapp', $adom: 'src/arts/adom', $auth: 'src/arts/auth', - $cach: 'src/arts/cach', + $cache: 'src/arts/cach', $conn: 'src/arts/conn', - $fend: 'src/arts/fend', + $frontend: 'src/arts/fend', $fmts: 'src/arts/fmts', $http: 'src/arts/http', $lang: 'src/arts/lang', diff --git a/src/arts/aapp/consts.ts b/src/arts/aapp/consts.ts deleted file mode 100644 index fadabb8..0000000 --- a/src/arts/aapp/consts.ts +++ /dev/null @@ -1,29 +0,0 @@ -export { AAPP_MODULE } from '$libs/aapp/consts'; -import { AAPP_ERR, AAPP_MODULE } from '$libs/aapp/consts'; -import { errCode, type ErrCode } from '$libs/errs'; - -export const AAPP_LOG_MSG_SESSION_CACHE_CLEAR_FAILED = - 'session change cache clear failed'; -export const AAPP_CONNECTION_CLOSE_REASON_IDENTITY_CLEARED = 'identity_cleared'; - -export const AAPP_ORCHESTRATION_STANDARD = 'standard'; -export const AAPP_ORCHESTRATION_SILENT = 'silent'; -export const AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY = 'identity'; -export const AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH = 'permissions-refresh'; -export const AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED = 'tenant-switched'; -export const AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY = 'connectivity'; -export const AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE = 'dispose'; - -export const AAPP_ERR_ALREADY_CREATED: ErrCode = errCode(AAPP_ERR, 'already_created'); - -export const AAPP_ERROR_ALREADY_CREATED_SESSION = - `[${AAPP_MODULE}] App.createActiveSession() called twice — only one session per App.`; - -export const AAPP_ERROR_ALREADY_CREATED_PERMISSIONS = - `[${AAPP_MODULE}] App.createActivePermissions() called twice — only one permissions client per App.`; - -export const AAPP_ERROR_ALREADY_CREATED_AUTH = - `[${AAPP_MODULE}] App.createActiveAuth() called twice — only one active auth client per App.`; - -export const AAPP_ERROR_CREATE_PERMISSIONS_ENDPOINT_REQUIRED = - `[${AAPP_MODULE}] App.createActivePermissions() requires endpoint via options.permissions or argument.`; diff --git a/src/arts/aapp/integrations/session-translator.ts b/src/arts/aapp/integrations/session-translator.ts deleted file mode 100644 index a0313f7..0000000 --- a/src/arts/aapp/integrations/session-translator.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { AppEventMap, AppUserIdentityChangeCause } from '$libs/aapp/events'; -import { - AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED, - AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER, - AAPP_USER_IDENTITY_CAUSE_SESSION_EXPIRED, - AAPP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED, - AAPP_USER_IDENTITY_CAUSE_SESSION_REFRESHED, - AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED, - publishAppUserIdentityChanged -} from '$libs/aapp/events'; -import { - SESS_EVENT_LIFECYCLE_ADOPTED, - SESS_EVENT_LIFECYCLE_ADOPTED_SERVER, - SESS_EVENT_LIFECYCLE_EXPIRED, - SESS_EVENT_LIFECYCLE_EXTERNAL_CHANGED, - SESS_EVENT_LIFECYCLE_REFRESHED, - SESS_EVENT_LIFECYCLE_REVOKED, - SESS_EVENT_CHANGED -} from '$sess/consts'; -import type { SessEventMap, SessLifecyclePayload, SessionEvent } from '$sess/types'; -import type { EngineBus } from '$libs/buss'; -import { AAPP_MODULE } from '../consts.ts'; - -type SessionTranslatorBusEvents = AppEventMap & SessEventMap; - -export function wireSessionTranslator(bus: EngineBus): () => void { - const subscription = bus.on(SESS_EVENT_CHANGED, (event) => { - publishAppIdentityFromSessionLifecycleEvent(bus, event.payload); - }); - return () => subscription.unsubscribe(); -} - -export function publishAppIdentityFromSessionLifecycleEvent( - bus: EngineBus, - payload: SessLifecyclePayload -): void { - const cause = resolveAppIdentityCause(payload.event); - if (cause === undefined) return; - publishAppUserIdentityChanged( - bus, - { - event: payload.event, - generation: payload.generation, - identity: { - from: payload.identity.from, - to: payload.identity.to - }, - cause - }, - { - source: AAPP_MODULE - } - ); -} - -function resolveAppIdentityCause(event: SessionEvent): AppUserIdentityChangeCause | undefined { - if (event === SESS_EVENT_LIFECYCLE_ADOPTED) return AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED; - if (event === SESS_EVENT_LIFECYCLE_ADOPTED_SERVER) return AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER; - if (event === SESS_EVENT_LIFECYCLE_REFRESHED) return AAPP_USER_IDENTITY_CAUSE_SESSION_REFRESHED; - if (event === SESS_EVENT_LIFECYCLE_REVOKED) return AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED; - if (event === SESS_EVENT_LIFECYCLE_EXPIRED) return AAPP_USER_IDENTITY_CAUSE_SESSION_EXPIRED; - if (event === SESS_EVENT_LIFECYCLE_EXTERNAL_CHANGED) return AAPP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED; - return undefined; -} diff --git a/src/arts/aapp/README.md b/src/arts/active-app/README.md similarity index 98% rename from src/arts/aapp/README.md rename to src/arts/active-app/README.md index d8a906d..0c13684 100644 --- a/src/arts/aapp/README.md +++ b/src/arts/active-app/README.md @@ -49,7 +49,7 @@ App.dispose(); Server-authoritative engines that have a browser reflector live under `$svrs/*`: use `$svrs/auth` for `createEngineAuth()` and auth HTTP handlers, `$svrs/perm` for `createEnginePermissions()` and authorization handlers, and -`$svrs/cach` for `createEngineCache()` in services, repositories or server +`$svrs/cache` for `createEngineCache()` in services, repositories or server load code. `aapp` composes only the active/client side. `Sium` is **not** a member of App. Validation is page-scoped — pages with @@ -119,7 +119,7 @@ module event -> aapp translator -> app event -> consumer opt-in reaction Current identity flow: ```txt -sess emits SESS_EVENT_CHANGED +sess emits SESSION_EVENT_CHANGED aapp's session translator publishes APP_EVENT_USER_IDENTITY_CHANGED cach/perm/conn may react only when their own auto*On option opts in ``` @@ -136,7 +136,7 @@ Current translator contract: | Translator | Current source | Publishes | | --- | --- | --- | -| `identity` | built-in: `SESS_EVENT_CHANGED` from `App.createActiveSession(...)` | `APP_EVENT_USER_IDENTITY_CHANGED` | +| `identity` | built-in: `SESSION_EVENT_CHANGED` from `App.createActiveSession(...)` | `APP_EVENT_USER_IDENTITY_CHANGED` | | `dispose` | built-in: `App.dispose()` | `APP_EVENT_DISPOSE_STARTING` | | `permissions-refresh` | typed public contract / explicit publish point | `APP_EVENT_PERMISSIONS_REFRESH_REQUESTED` | | `tenant-switched` | typed public contract / explicit publish point | `APP_EVENT_TENANT_SWITCHED` | @@ -563,7 +563,7 @@ interface ActiveAppOptions { cache?: Omit; connections?: Omit; permissions?: Omit; - auth?: Omit; + auth?: Omit; orchestration?: 'standard' | 'silent' | false | readonly ActiveAppOrchestrationTranslator[]; } @@ -593,7 +593,7 @@ interface ActiveApp { createActivePermissions( options?: Partial> ): ActivePermissions; - createActiveAuth(options?: Omit): ActiveAuth; + createActiveAuth(options?: Omit): ActiveAuth; readonly Sess: ActiveSession | undefined; readonly Permissions: ActivePermissions | undefined; diff --git a/src/arts/aapp/active-app.svelte.ts b/src/arts/active-app/active-app.svelte.ts similarity index 73% rename from src/arts/aapp/active-app.svelte.ts rename to src/arts/active-app/active-app.svelte.ts index e2f8d54..4a6523a 100644 --- a/src/arts/aapp/active-app.svelte.ts +++ b/src/arts/active-app/active-app.svelte.ts @@ -2,27 +2,27 @@ import { createActiveDom } from '$adom/active-dom.svelte'; import { createActiveAuth } from '$auth/active-auth.svelte'; import { createEngineHttpAuthClient } from '$auth/client'; import type { ActiveAuth, ActiveAuthOptions } from '$auth/types'; -import { createSvelteEngineBus } from '$buss'; -import { createActiveCache } from '$cach/active-cache.svelte'; -import { createActiveConnections as createActiveConnectionsRegistry } from '$conn/active-connections.svelte'; -import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$conn/types'; -import { createActiveFrontend } from '$fend/active-frontend.svelte'; -import { createActiveFormats } from '$fmts/active-formats.svelte'; +import { createSvelteEngineBus } from '$bus'; +import { createActiveCache } from '$cache/active-cache.svelte'; +import { createActiveConnections as createActiveConnectionsRegistry } from '$connection/active-connections.svelte'; +import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$connection/types'; +import { createActiveFrontend } from '$frontend/active-frontend.svelte'; +import { createActiveFormats } from '$formats/active-formats.svelte'; import { createEngineHttp } from '$http/engine-http'; import type { ActiveLang, LangNode } from '$libs/lang'; import { createActiveLang } from '$lang/active-lang.svelte'; import { createActiveMonoLang } from '$lang/mono-lang.svelte'; -import { createEngineLogger } from '$logr/engine-logger'; -import { createActivePermissions as createActivePermissionsClient } from '$perm/active-permissions.svelte'; -import { PermInvalidEndpointError } from '$perm/errors'; -import type { ActivePermissions, ActivePermissionsOptions } from '$perm/types'; -import { createActiveSession } from '$sess/active-session.svelte'; -import { SessAlreadyCreatedError } from '$sess/errors'; -import type { ActiveSession, EngineSessionOptions } from '$sess/types'; +import { createEngineLogger } from '$logger/engine-logger'; +import { createActivePermissions as createActivePermissionsClient } from '$permissions/active-permissions.svelte'; +import { PermInvalidEndpointError } from '$permissions/errors'; +import type { ActivePermissions, ActivePermissionsOptions } from '$permissions/types'; +import { createActiveSession } from '$session/active-session.svelte'; +import { SessionAlreadyCreatedError } from '$session/errors'; +import type { ActiveSession, EngineSessionOptions } from '$session/types'; import { createEngineSium } from '$sium/engine-sium'; -import { createActiveStorage } from '$stor/active-storage.svelte'; -import { createActiveTimers } from '$timr/active-timers.svelte'; -import { publishAppDisposeStarting } from '$libs/aapp/events'; +import { createActiveStorage } from '$storage/active-storage.svelte'; +import { createActiveTimers } from '$timer/active-timers.svelte'; +import { publishAppDisposeStarting } from '$libs/active-app/events'; import { applyFrontendPreferenceSnapshot, @@ -32,18 +32,18 @@ import { import { createAuthCacheInvalidator } from './integrations/auth-cache'; import { wireSessionTranslator } from './integrations/session-translator'; import { - AAPP_ERROR_ALREADY_CREATED_PERMISSIONS, - AAPP_ERROR_ALREADY_CREATED_AUTH, - AAPP_ERROR_ALREADY_CREATED_SESSION, - AAPP_ERROR_CREATE_PERMISSIONS_ENDPOINT_REQUIRED, - AAPP_ORCHESTRATION_SILENT, - AAPP_ORCHESTRATION_STANDARD, - AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, - AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE, - AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY, - AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH, - AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED, - AAPP_MODULE + APP_ERROR_ALREADY_CREATED_PERMISSIONS, + APP_ERROR_ALREADY_CREATED_AUTH, + APP_ERROR_ALREADY_CREATED_SESSION, + APP_ERROR_CREATE_PERMISSION_ENDPOINT_REQUIRED, + APP_ORCHESTRATION_SILENT, + APP_ORCHESTRATION_STANDARD, + APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, + APP_ORCHESTRATION_TRANSLATOR_DISPOSE, + APP_ORCHESTRATION_TRANSLATOR_IDENTITY, + APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH, + APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED, + APP_MODULE } from './consts.ts'; import { AappAlreadyCreatedError } from './errors.ts'; import type { @@ -121,7 +121,7 @@ export function createActiveApp( clock: Timers.clock }); const orchestration = resolveActiveAppOrchestration(options.orchestration); - const detachSessionTranslator = orchestration.has(AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY) + const detachSessionTranslator = orchestration.has(APP_ORCHESTRATION_TRANSLATOR_IDENTITY) ? wireSessionTranslator(Bus) : undefined; @@ -178,7 +178,7 @@ export function createActiveApp( sessOptions: Omit, 'logger' | 'bus'> = {} ): ActiveSession { if (Sess !== undefined) { - throw new SessAlreadyCreatedError(AAPP_ERROR_ALREADY_CREATED_SESSION); + throw new SessionAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_SESSION); } const built = createActiveSession({ ...sessOptions, @@ -210,14 +210,14 @@ export function createActiveApp( permissionOptions: Partial> = {} ): ActivePermissions { if (Permissions !== undefined) { - throw new AappAlreadyCreatedError(AAPP_ERROR_ALREADY_CREATED_PERMISSIONS); + throw new AappAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_PERMISSIONS); } const merged = { ...options.permissions, ...permissionOptions }; if (!merged.endpoint) { - throw new PermInvalidEndpointError(AAPP_ERROR_CREATE_PERMISSIONS_ENDPOINT_REQUIRED); + throw new PermInvalidEndpointError(APP_ERROR_CREATE_PERMISSION_ENDPOINT_REQUIRED); } const built = createActivePermissionsClient({ ...merged, @@ -231,17 +231,17 @@ export function createActiveApp( }, createActiveAuth( - authOptions: Omit = {} + authOptions: Omit = {} ): ActiveAuth { if (Auth !== undefined) { - throw new AappAlreadyCreatedError(AAPP_ERROR_ALREADY_CREATED_AUTH); + throw new AappAlreadyCreatedError(APP_ERROR_ALREADY_CREATED_AUTH); } const built = createActiveAuth({ ...options.auth, ...authOptions, logger: Logger, http: createEngineHttpAuthClient(Http), - cach: createAuthCacheInvalidator({ + cache: createAuthCacheInvalidator({ cache: Cache, permissions: () => Permissions }) @@ -253,8 +253,8 @@ export function createActiveApp( dispose() { if (disposed) return; disposed = true; - if (orchestration.has(AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE)) { - publishAppDisposeStarting(Bus, { cause: AAPP_MODULE }); + if (orchestration.has(APP_ORCHESTRATION_TRANSLATOR_DISPOSE)) { + publishAppDisposeStarting(Bus, { cause: APP_MODULE }); } Auth?.dispose(); Auth = undefined; @@ -284,19 +284,19 @@ export function createActiveApp( function resolveActiveAppOrchestration( orchestration: ActiveAppOrchestrationOptions | undefined ): ReadonlySet { - if (orchestration === false || orchestration === AAPP_ORCHESTRATION_SILENT) { + if (orchestration === false || orchestration === APP_ORCHESTRATION_SILENT) { return new Set(); } - if (orchestration === undefined || orchestration === AAPP_ORCHESTRATION_STANDARD) { + if (orchestration === undefined || orchestration === APP_ORCHESTRATION_STANDARD) { return new Set(STANDARD_ORCHESTRATION_TRANSLATORS); } return new Set(orchestration); } const STANDARD_ORCHESTRATION_TRANSLATORS = [ - AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY, - AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH, - AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED, - AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, - AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE + APP_ORCHESTRATION_TRANSLATOR_IDENTITY, + APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH, + APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED, + APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, + APP_ORCHESTRATION_TRANSLATOR_DISPOSE ] as const; diff --git a/src/arts/active-app/consts.ts b/src/arts/active-app/consts.ts new file mode 100644 index 0000000..24eb076 --- /dev/null +++ b/src/arts/active-app/consts.ts @@ -0,0 +1,29 @@ +export { APP_MODULE } from '$libs/active-app/consts'; +import { APP_ERR, APP_MODULE } from '$libs/active-app/consts'; +import { errCode, type ErrCode } from '$libs/errs'; + +export const APP_LOG_MSG_SESSION_CACHE_CLEAR_FAILED = + 'session change cache clear failed'; +export const APP_CONNECTION_CLOSE_REASON_IDENTITY_CLEARED = 'identity_cleared'; + +export const APP_ORCHESTRATION_STANDARD = 'standard'; +export const APP_ORCHESTRATION_SILENT = 'silent'; +export const APP_ORCHESTRATION_TRANSLATOR_IDENTITY = 'identity'; +export const APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH = 'permissions-refresh'; +export const APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED = 'tenant-switched'; +export const APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY = 'connectivity'; +export const APP_ORCHESTRATION_TRANSLATOR_DISPOSE = 'dispose'; + +export const APP_ERR_ALREADY_CREATED: ErrCode = errCode(APP_ERR, 'already_created'); + +export const APP_ERROR_ALREADY_CREATED_SESSION = + `[${APP_MODULE}] App.createActiveSession() called twice — only one session per App.`; + +export const APP_ERROR_ALREADY_CREATED_PERMISSIONS = + `[${APP_MODULE}] App.createActivePermissions() called twice — only one permissions client per App.`; + +export const APP_ERROR_ALREADY_CREATED_AUTH = + `[${APP_MODULE}] App.createActiveAuth() called twice — only one active auth client per App.`; + +export const APP_ERROR_CREATE_PERMISSION_ENDPOINT_REQUIRED = + `[${APP_MODULE}] App.createActivePermissions() requires endpoint via options.permissions or argument.`; diff --git a/src/arts/aapp/errors.ts b/src/arts/active-app/errors.ts similarity index 73% rename from src/arts/aapp/errors.ts rename to src/arts/active-app/errors.ts index c8967be..4528b42 100644 --- a/src/arts/aapp/errors.ts +++ b/src/arts/active-app/errors.ts @@ -1,9 +1,9 @@ import { CodeError } from '$libs/errs'; -import { AAPP_ERR_ALREADY_CREATED } from './consts.ts'; +import { APP_ERR_ALREADY_CREATED } from './consts.ts'; export class AappAlreadyCreatedError extends CodeError { constructor(message: string) { - super(AAPP_ERR_ALREADY_CREATED, { message }); + super(APP_ERR_ALREADY_CREATED, { message }); } } diff --git a/src/arts/aapp/index.ts b/src/arts/active-app/index.ts similarity index 70% rename from src/arts/aapp/index.ts rename to src/arts/active-app/index.ts index ade138d..d192cf0 100644 --- a/src/arts/aapp/index.ts +++ b/src/arts/active-app/index.ts @@ -1,13 +1,13 @@ export { createActiveApp } from './active-app.svelte'; export { - AAPP_ORCHESTRATION_SILENT, - AAPP_ORCHESTRATION_STANDARD, - AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, - AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE, - AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY, - AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH, - AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED, - AAPP_MODULE + APP_ORCHESTRATION_SILENT, + APP_ORCHESTRATION_STANDARD, + APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, + APP_ORCHESTRATION_TRANSLATOR_DISPOSE, + APP_ORCHESTRATION_TRANSLATOR_IDENTITY, + APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH, + APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED, + APP_MODULE } from './consts'; export { AappAlreadyCreatedError, isAappAlreadyCreatedError } from './errors'; export type { @@ -23,18 +23,18 @@ export type { FrontendPersistKeyOverride } from './types'; -// `createTestApp` lives at `$aapp/testing` so it does not ship with bundles +// `createTestApp` lives at `$active-app/testing` so it does not ship with bundles // that import the production barrel. See `aapp/testing/index.ts`. // Common storage adapters re-exported for ergonomic single import. -// `createMemoryAdapter` lives in `$stor` only — testing-specific. -export { localAdapter, sessionAdapter, cookieAdapter, withBroadcast } from '$stor'; +// `createMemoryAdapter` lives in `$storage` only — testing-specific. +export { localAdapter, sessionAdapter, cookieAdapter, withBroadcast } from '$storage'; export type { CookieAdapterOptions, ServerCookiesLike, BroadcastAdapter, BroadcastOptions -} from '$stor'; +} from '$storage'; // `createActiveMonoLang` is intentionally NOT re-exported — callers that // compose without `createActiveApp` should import it directly from diff --git a/src/arts/aapp/integrations/auth-cache.ts b/src/arts/active-app/integrations/auth-cache.ts similarity index 55% rename from src/arts/aapp/integrations/auth-cache.ts rename to src/arts/active-app/integrations/auth-cache.ts index 9bead87..93f0a31 100644 --- a/src/arts/aapp/integrations/auth-cache.ts +++ b/src/arts/active-app/integrations/auth-cache.ts @@ -1,12 +1,12 @@ -import { CACH_SCOPE_PUBLIC } from '$libs/cach'; -import type { AuthClientCachPort } from '$libs/auth/contracts'; -import type { ActiveCache } from '$cach'; -import type { ActivePermissions } from '$perm'; +import { CACHE_SCOPE_PUBLIC } from '$libs/cache'; +import type { AuthClientCachePort } from '$libs/auth/contracts'; +import type { ActiveCache } from '$cache'; +import type { ActivePermissions } from '$permissions'; export function createAuthCacheInvalidator(input: { readonly cache: ActiveCache; readonly permissions: () => ActivePermissions | undefined; -}): AuthClientCachPort { +}): AuthClientCachePort { return { async invalidate(event) { input.permissions()?.invalidate(); @@ -14,7 +14,7 @@ export function createAuthCacheInvalidator(input: { event.tags.map((tag) => input.cache.invalidate({ tag, - scope: CACH_SCOPE_PUBLIC + scope: CACHE_SCOPE_PUBLIC }) ) ); diff --git a/src/arts/aapp/integrations/frontend-storage.ts b/src/arts/active-app/integrations/frontend-storage.ts similarity index 98% rename from src/arts/aapp/integrations/frontend-storage.ts rename to src/arts/active-app/integrations/frontend-storage.ts index 17703c3..f3df4f8 100644 --- a/src/arts/aapp/integrations/frontend-storage.ts +++ b/src/arts/active-app/integrations/frontend-storage.ts @@ -8,8 +8,8 @@ import { type FrontendPreferenceKey, type FrontendPreferenceSnapshot, type FrontendPreferenceValue -} from '$fend'; -import type { ActiveStorage, ActiveStorageEntry, SyncStorageAdapter } from '$stor'; +} from '$frontend'; +import type { ActiveStorage, ActiveStorageEntry, SyncStorageAdapter } from '$storage'; import type { FrontendPersist, FrontendPersistKeyOverride } from '../types'; diff --git a/src/arts/active-app/integrations/session-translator.ts b/src/arts/active-app/integrations/session-translator.ts new file mode 100644 index 0000000..e0b7f3f --- /dev/null +++ b/src/arts/active-app/integrations/session-translator.ts @@ -0,0 +1,64 @@ +import type { AppEventMap, AppUserIdentityChangeCause } from '$libs/active-app/events'; +import { + APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED, + APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER, + APP_USER_IDENTITY_CAUSE_SESSION_EXPIRED, + APP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED, + APP_USER_IDENTITY_CAUSE_SESSION_REFRESHED, + APP_USER_IDENTITY_CAUSE_SESSION_REVOKED, + publishAppUserIdentityChanged +} from '$libs/active-app/events'; +import { + SESSION_EVENT_LIFECYCLE_ADOPTED, + SESSION_EVENT_LIFECYCLE_ADOPTED_SERVER, + SESSION_EVENT_LIFECYCLE_EXPIRED, + SESSION_EVENT_LIFECYCLE_EXTERNAL_CHANGED, + SESSION_EVENT_LIFECYCLE_REFRESHED, + SESSION_EVENT_LIFECYCLE_REVOKED, + SESSION_EVENT_CHANGED +} from '$session/consts'; +import type { SessEventMap, SessLifecyclePayload, SessionEvent } from '$session/types'; +import type { EngineBus } from '$libs/bus'; +import { APP_MODULE } from '../consts.ts'; + +type SessionTranslatorBusEvents = AppEventMap & SessEventMap; + +export function wireSessionTranslator(bus: EngineBus): () => void { + const subscription = bus.on(SESSION_EVENT_CHANGED, (event) => { + publishAppIdentityFromSessionLifecycleEvent(bus, event.payload); + }); + return () => subscription.unsubscribe(); +} + +export function publishAppIdentityFromSessionLifecycleEvent( + bus: EngineBus, + payload: SessLifecyclePayload +): void { + const cause = resolveAppIdentityCause(payload.event); + if (cause === undefined) return; + publishAppUserIdentityChanged( + bus, + { + event: payload.event, + generation: payload.generation, + identity: { + from: payload.identity.from, + to: payload.identity.to + }, + cause + }, + { + source: APP_MODULE + } + ); +} + +function resolveAppIdentityCause(event: SessionEvent): AppUserIdentityChangeCause | undefined { + if (event === SESSION_EVENT_LIFECYCLE_ADOPTED) return APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED; + if (event === SESSION_EVENT_LIFECYCLE_ADOPTED_SERVER) return APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED_SERVER; + if (event === SESSION_EVENT_LIFECYCLE_REFRESHED) return APP_USER_IDENTITY_CAUSE_SESSION_REFRESHED; + if (event === SESSION_EVENT_LIFECYCLE_REVOKED) return APP_USER_IDENTITY_CAUSE_SESSION_REVOKED; + if (event === SESSION_EVENT_LIFECYCLE_EXPIRED) return APP_USER_IDENTITY_CAUSE_SESSION_EXPIRED; + if (event === SESSION_EVENT_LIFECYCLE_EXTERNAL_CHANGED) return APP_USER_IDENTITY_CAUSE_SESSION_EXTERNAL_CHANGED; + return undefined; +} diff --git a/src/arts/aapp/test/active-app.test.ts b/src/arts/active-app/test/active-app.test.ts similarity index 92% rename from src/arts/aapp/test/active-app.test.ts rename to src/arts/active-app/test/active-app.test.ts index f73e01f..8e2db0d 100644 --- a/src/arts/aapp/test/active-app.test.ts +++ b/src/arts/active-app/test/active-app.test.ts @@ -16,22 +16,22 @@ import { describe, it, expect, vi } from 'vitest'; import { createActiveApp } from '../active-app.svelte'; -import { LogLevel, type LogEntry } from '$logr'; +import { LogLevel, type LogEntry } from '$logger'; import type { LangNode } from '$lang'; import { LANG_MONO_LANG_CATEGORY } from '$lang/mono-lang.svelte'; import { - AAPP_EVENT_DISPOSE_STARTING, - AAPP_EVENT_USER_IDENTITY_CHANGED, - AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED, + APP_EVENT_DISPOSE_STARTING, + APP_EVENT_USER_IDENTITY_CHANGED, + APP_USER_IDENTITY_CAUSE_SESSION_REVOKED, publishAppUserIdentityChanged -} from '$libs/aapp/events'; -import { CONN_STATE_CLOSED, CONN_STATE_OPEN, createMockTransport } from '$conn'; +} from '$libs/active-app/events'; +import { CONNECTION_STATE_CLOSED, CONNECTION_STATE_OPEN, createMockTransport } from '$connection'; import { - CACH_EVENT_INVALIDATE, - CACH_POLICY_INTERACTIVE, - CACH_SCOPE_PUBLIC, + CACHE_EVENT_INVALIDATE, + CACHE_POLICY_INTERACTIVE, + CACHE_SCOPE_PUBLIC, memoryCacheAdapter -} from '$libs/cach'; +} from '$libs/cache'; import { AUTH_AAL, AUTH_CACHE_TAGS, @@ -39,9 +39,9 @@ import { AUTH_ROUTE_PATHS, AUTH_SESSION_STATUSES } from '$libs/auth'; -import { PERM_EFFECT_ALLOW } from '$libs/perm'; -import { PERM_AUTO_INVALIDATE_STANDARD } from '$perm'; -import { SESS_EVENT_LIFECYCLE_REVOKED } from '$sess/consts'; +import { PERMISSION_EFFECT_ALLOW } from '$libs/permissions'; +import { PERMISSION_AUTO_INVALIDATE_STANDARD } from '$permissions'; +import { SESSION_EVENT_LIFECYCLE_REVOKED } from '$session/consts'; const schema = { greeting: { es: 'Hola', en: 'Hello', 'es-MX': 'Qué onda' }, @@ -626,20 +626,20 @@ describe('createActiveApp — composition', () => { }); const Permissions = App.createActivePermissions({ endpoint: 'https://active.test/permissions', - autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD + autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD }); Permissions.hydrate({ decisions: { - 'post.read:p1': { effect: PERM_EFFECT_ALLOW, policy: 'test.allow' } + 'post.read:p1': { effect: PERMISSION_EFFECT_ALLOW, policy: 'test.allow' } } }); let sizeSeenDuringDispose = -1; - App.Bus.on(AAPP_EVENT_DISPOSE_STARTING, () => { + App.Bus.on(APP_EVENT_DISPOSE_STARTING, () => { publishAppUserIdentityChanged(App.Bus, { - event: SESS_EVENT_LIFECYCLE_REVOKED, + event: SESSION_EVENT_LIFECYCLE_REVOKED, generation: 1, identity: { from: 'identified', to: 'none' }, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED + cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED }); sizeSeenDuringDispose = Permissions.size; }); @@ -657,7 +657,7 @@ describe('createActiveApp — composition', () => { logger: { level: LogLevel.NONE, transports: [] } }); let disposeStartingCount = 0; - App.Bus.on(AAPP_EVENT_DISPOSE_STARTING, () => { + App.Bus.on(APP_EVENT_DISPOSE_STARTING, () => { disposeStartingCount += 1; }); const Connections = App.createActiveConnections(); @@ -666,18 +666,18 @@ describe('createActiveApp — composition', () => { heartbeat: false, reconnect: false }); - const handle = App.Timers.schedule('aapp:dispose:late-task', 1_000, timerTask); + const handle = App.Timers.schedule('app:dispose:late-task', 1_000, timerTask); await Main.connect(); - expect(Main.state).toBe(CONN_STATE_OPEN); - expect(App.Timers.has('aapp:dispose:late-task')).toBe(true); + expect(Main.state).toBe(CONNECTION_STATE_OPEN); + expect(App.Timers.has('app:dispose:late-task')).toBe(true); App.dispose(); App.dispose(); vi.advanceTimersByTime(1_000); expect(disposeStartingCount).toBe(1); - expect(Main.state).toBe(CONN_STATE_CLOSED); + expect(Main.state).toBe(CONNECTION_STATE_CLOSED); expect(Connections.size).toBe(0); expect(App.Timers.disposed).toBe(true); expect(handle.active).toBe(false); @@ -685,10 +685,10 @@ describe('createActiveApp — composition', () => { expect(App.Cache.disposed).toBe(true); expect(() => publishAppUserIdentityChanged(App.Bus, { - event: SESS_EVENT_LIFECYCLE_REVOKED, + event: SESSION_EVENT_LIFECYCLE_REVOKED, generation: 1, identity: { from: 'identified', to: 'none' }, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED + cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED }) ).toThrow(); } finally { @@ -712,16 +712,16 @@ describe('createActiveApp — composition', () => { }); const Permissions = App.createActivePermissions({ endpoint: 'https://active.test/permissions', - autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD + autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD }); - App.Bus.on(AAPP_EVENT_DISPOSE_STARTING, () => { + App.Bus.on(APP_EVENT_DISPOSE_STARTING, () => { void Promise.resolve().then(() => { try { publishAppUserIdentityChanged(App.Bus, { - event: SESS_EVENT_LIFECYCLE_REVOKED, + event: SESSION_EVENT_LIFECYCLE_REVOKED, generation: 2, identity: { from: 'identified', to: 'none' }, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED + cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED }); } catch (error) { deferredPublishError = error; @@ -736,18 +736,18 @@ describe('createActiveApp — composition', () => { }); const pendingCache = App.Cache.query({ key: ['dispose', 'pending'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, fetcher: async () => cacheReply.promise }); await Promise.resolve(); App.dispose(); - permissionReply.resolve({ effect: PERM_EFFECT_ALLOW, policy: 'test.allow' }); + permissionReply.resolve({ effect: PERMISSION_EFFECT_ALLOW, policy: 'test.allow' }); cacheReply.resolve({ actorId: 'actor-ada' }); await expect(pendingPermission).resolves.toMatchObject({ - effect: PERM_EFFECT_ALLOW + effect: PERMISSION_EFFECT_ALLOW }); await expect(pendingCache).resolves.toEqual({ actorId: 'actor-ada' }); await Promise.resolve(); @@ -782,7 +782,7 @@ describe('createActiveApp — composition', () => { logger: { level: LogLevel.NONE, transports: [] } }); const appIdentityEvents: unknown[] = []; - const off = App.Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => { + const off = App.Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => { appIdentityEvents.push(event.payload); }); const Connections = App.createActiveConnections(); @@ -803,7 +803,7 @@ describe('createActiveApp — composition', () => { await Session.revoke(); expect(appIdentityEvents).toHaveLength(2); - expect(Main.state).toBe(CONN_STATE_OPEN); + expect(Main.state).toBe(CONNECTION_STATE_OPEN); off.unsubscribe(); App.dispose(); @@ -850,7 +850,7 @@ describe('createActiveApp — composition', () => { }); } if (path === '/permissions/check') { - return json({ effect: PERM_EFFECT_ALLOW, policy: 'remote.allow' }); + return json({ effect: PERMISSION_EFFECT_ALLOW, policy: 'remote.allow' }); } return new Response(null, { status: 404 }); }) as typeof fetch; @@ -859,7 +859,7 @@ describe('createActiveApp — composition', () => { http: { baseUrl: 'https://active.test', fetch: fetcher }, cache: { onEvent(event) { - if (event.type === CACH_EVENT_INVALIDATE) invalidatedTags.push(...(event.tags ?? [])); + if (event.type === CACHE_EVENT_INVALIDATE) invalidatedTags.push(...(event.tags ?? [])); } } }); diff --git a/src/arts/aapp/test/create-sium-engine.test.ts b/src/arts/active-app/test/create-sium-engine.test.ts similarity index 98% rename from src/arts/aapp/test/create-sium-engine.test.ts rename to src/arts/active-app/test/create-sium-engine.test.ts index 933d050..805245a 100644 --- a/src/arts/aapp/test/create-sium-engine.test.ts +++ b/src/arts/active-app/test/create-sium-engine.test.ts @@ -11,7 +11,7 @@ import { describe, it, expect } from 'vitest'; import { createTestApp } from '../testing'; import { object, pipe, string, email, min, meta } from '$sium/core'; import { siumLangs } from '$sium'; -import { LogLevel } from '$logr'; +import { LogLevel } from '$logger'; import type { LangNode } from '$lang'; const langSchema = { diff --git a/src/arts/aapp/test/ecosystem.integration.test.ts b/src/arts/active-app/test/ecosystem.integration.test.ts similarity index 88% rename from src/arts/aapp/test/ecosystem.integration.test.ts rename to src/arts/active-app/test/ecosystem.integration.test.ts index 5ee3c15..f1028e4 100644 --- a/src/arts/aapp/test/ecosystem.integration.test.ts +++ b/src/arts/active-app/test/ecosystem.integration.test.ts @@ -1,28 +1,28 @@ import { describe, expect, it } from 'vitest'; import { createActiveApp } from '../active-app.svelte'; import { - CACH_EVENT_ALL, - CACH_EVENT_INVALIDATE, - CACH_AUTO_INVALIDATE_STANDARD, - CACH_POLICY_INTERACTIVE, - CACH_READ_MODE_STALE_WHILE_REVALIDATE, - CACH_SCOPE_PUBLIC, - CACH_SCOPE_TENANT, + CACHE_EVENT_ALL, + CACHE_EVENT_INVALIDATE, + CACHE_AUTO_INVALIDATE_STANDARD, + CACHE_POLICY_INTERACTIVE, + CACHE_READ_MODE_STALE_WHILE_REVALIDATE, + CACHE_SCOPE_PUBLIC, + CACHE_SCOPE_TENANT, memoryCacheAdapter, type CacheEvent, type ResolvedScopeValues -} from '$cach'; -import { SESS_EVENT_CHANGED, type SessLifecyclePayload } from '$sess'; +} from '$cache'; +import { SESSION_EVENT_CHANGED, type SessLifecyclePayload } from '$session'; import { - CONN_FRAME_TYPE_AUTH, - CONN_STATE_CLOSED, - CONN_STATE_OPEN, - CONN_AUTO_REAUTH_STANDARD, + CONNECTION_FRAME_TYPE_AUTH, + CONNECTION_STATE_CLOSED, + CONNECTION_STATE_OPEN, + CONNECTION_AUTO_REAUTH_STANDARD, createFrame, createMockTransport, type ConnectionFrame, type MockConnectionTransport -} from '$conn'; +} from '$connection'; import { HTTP_CONTENT_TYPE_JSON, HTTP_HEADER_CONTENT_TYPE } from '$libs/http'; import type { StandardSchemaV1 } from '$libs/standard-schema'; import { @@ -36,11 +36,11 @@ import { type AuthSessionId, type AuthTenantId } from '$libs/auth'; -import { LogLevel, type LogEntry } from '$logr'; +import { LogLevel, type LogEntry } from '$logger'; import { - PERM_EFFECT_ALLOW, - PERM_EFFECT_NOT_APPLICABLE, - PERM_AUTO_INVALIDATE_STANDARD, + PERMISSION_EFFECT_ALLOW, + PERMISSION_EFFECT_NOT_APPLICABLE, + PERMISSION_AUTO_INVALIDATE_STANDARD, actor, allow, and, @@ -50,22 +50,22 @@ import { rel, type ResourceRef, type SubjectRef -} from '$perm'; -import { createEnginePermissions, createPermissionHttpHandlers } from '$svrs/perm'; -import { createMemoryAdapter } from '$stor'; -import { SESS_EVENT_LIFECYCLE_ADOPTED } from '$sess/consts'; +} from '$permissions'; +import { createEnginePermissions, createPermissionHttpHandlers } from '$svrs/permissions'; +import { createMemoryAdapter } from '$storage'; +import { SESSION_EVENT_LIFECYCLE_ADOPTED } from '$session/consts'; import { - AAPP_EVENT_USER_IDENTITY_CHANGED, - AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED, - AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED, + APP_EVENT_USER_IDENTITY_CHANGED, + APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED, + APP_USER_IDENTITY_CAUSE_SESSION_REVOKED, onAppPermissionsRefreshRequested, publishAppPermissionsRefreshRequested, publishAppTenantSwitched, publishAppUserIdentityChanged -} from '$libs/aapp/events'; -import { SESS_EVENT_LIFECYCLE_REVOKED } from '$sess/consts'; +} from '$libs/active-app/events'; +import { SESSION_EVENT_LIFECYCLE_REVOKED } from '$session/consts'; -const PERM_ENDPOINT = 'https://ecosystem.test/api/permissions'; +const PERMISSION_ENDPOINT = 'https://ecosystem.test/api/permissions'; const HTTP_PROJECT_PATH = '/api/demo/project'; const TENANT_ID = 'tenant-acme'; const SECOND_TENANT_ID = 'tenant-umbrella'; @@ -78,8 +78,8 @@ const PROJECT_RELATION_MEMBER = 'project.member'; const PROJECT_SCHEMA_VERSION = 'ProjectPayload:v1'; const ROLE_ADMIN = 'admin'; const ROLE_VIEWER = 'viewer'; -const TIMR_KEY = 'ecosystem:test'; -const CONN_NAME = 'updates'; +const TIMER_KEY = 'ecosystem:test'; +const CONNECTION_NAME = 'updates'; const CHAT_CONNECTION_NAME = 'chat'; const LOG_CATEGORY = 'test.ecosystem'; const LOG_MESSAGE_BOOT = 'ecosystem.boot'; @@ -127,7 +127,7 @@ interface DemoSessionData { } describe('ActiveApp — total ecosystem integration', () => { - it('wires auth, sess, perm, cach, http, stor, sium, fmts, fend, adom, timr, conn and logr', async () => { + it('wires auth, sess, perm, cache, http, stor, sium, fmts, fend, adom, timer, conn and logr', async () => { const entries: LogEntry[] = []; const authCurrent = createAuthCurrent(); let actorRole = ROLE_ADMIN; @@ -217,18 +217,18 @@ describe('ActiveApp — total ecosystem integration', () => { locale: App.getLocale() }), policies: { - [CACH_POLICY_INTERACTIVE]: { + [CACHE_POLICY_INTERACTIVE]: { freshFor: 10_000, staleFor: 20_000, staleIfErrorFor: 30_000, gcAfter: 60_000, - mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE, + mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE, persist: true } } } }); - const offCacheEvents = App.Cache.on(CACH_EVENT_ALL, (event) => { + const offCacheEvents = App.Cache.on(CACHE_EVENT_ALL, (event) => { cacheEvents.push(event); }); @@ -269,7 +269,7 @@ describe('ActiveApp — total ecosystem integration', () => { expect(Sess.current?.user?.id).toBe(ACTOR_ID); const Permissions = App.createActivePermissions({ - endpoint: PERM_ENDPOINT, + endpoint: PERMISSION_ENDPOINT, scopeKey: () => `${ACTOR_ID}:${actorRole}` }); const decision = await Permissions.check({ @@ -277,7 +277,7 @@ describe('ActiveApp — total ecosystem integration', () => { resource, context: { risk: { mfa: true } } }); - expect(decision.effect).toBe(PERM_EFFECT_ALLOW); + expect(decision.effect).toBe(PERMISSION_EFFECT_ALLOW); actorRole = ROLE_VIEWER; Permissions.invalidate(); @@ -286,7 +286,7 @@ describe('ActiveApp — total ecosystem integration', () => { resource, context: { risk: { mfa: true } } }); - expect(viewerDecision.effect).toBe(PERM_EFFECT_NOT_APPLICABLE); + expect(viewerDecision.effect).toBe(PERMISSION_EFFECT_NOT_APPLICABLE); expect(Permissions.size).toBeGreaterThan(0); actorRole = ROLE_ADMIN; @@ -294,8 +294,8 @@ describe('ActiveApp — total ecosystem integration', () => { const project = await App.Cache.query({ key: ['project', PROJECT_ID], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'project', id: PROJECT_ID }], fetcher: async () => { @@ -308,8 +308,8 @@ describe('ActiveApp — total ecosystem integration', () => { }); const cached = await App.Cache.query({ key: ['project', PROJECT_ID], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'project', id: PROJECT_ID }], fetcher: async () => { @@ -323,8 +323,8 @@ describe('ActiveApp — total ecosystem integration', () => { App.setLocale('es'); const localizedProject = await App.Cache.query({ key: ['project', PROJECT_ID], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'project', id: PROJECT_ID }], fetcher: async () => { @@ -339,7 +339,7 @@ describe('ActiveApp — total ecosystem integration', () => { expect(projectFetches).toBe(2); let timerRan = false; - App.Timers.schedule(TIMR_KEY, 0, () => { + App.Timers.schedule(TIMER_KEY, 0, () => { timerRan = true; }); await new Promise((resolve) => setTimeout(resolve, 0)); @@ -347,7 +347,7 @@ describe('ActiveApp — total ecosystem integration', () => { const Connections = App.createActiveConnections(); const transport = createMockTransport(); - const Updates = Connections.createConnection(CONN_NAME, { + const Updates = Connections.createConnection(CONNECTION_NAME, { transport, heartbeat: false, reconnect: false @@ -360,7 +360,7 @@ describe('ActiveApp — total ecosystem integration', () => { await Auth.signOut(); expect(Auth.authenticated).toBe(false); expect(Permissions.size).toBe(0); - expect(cacheEvents.some((event) => event.type === CACH_EVENT_INVALIDATE)).toBe(true); + expect(cacheEvents.some((event) => event.type === CACHE_EVENT_INVALIDATE)).toBe(true); } finally { offCacheEvents(); App.dispose(); @@ -376,10 +376,10 @@ describe('ActiveApp — total ecosystem integration', () => { try { const Connections = App.createActiveConnections({ - autoReauthOn: CONN_AUTO_REAUTH_STANDARD + autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD }); const transport = createMockTransport(); - const Updates = Connections.createConnection(CONN_NAME, { + const Updates = Connections.createConnection(CONNECTION_NAME, { transport, heartbeat: false, reconnect: false, @@ -387,7 +387,7 @@ describe('ActiveApp — total ecosystem integration', () => { }); await Updates.connect(); - expect(Updates.state).toBe(CONN_STATE_OPEN); + expect(Updates.state).toBe(CONNECTION_STATE_OPEN); const Sess = App.createActiveSession(); Sess.adoptServer({ @@ -397,7 +397,7 @@ describe('ActiveApp — total ecosystem integration', () => { }); await Sess.revoke(); - expect(Updates.state).toBe(CONN_STATE_CLOSED); + expect(Updates.state).toBe(CONNECTION_STATE_CLOSED); } finally { App.dispose(); } @@ -440,7 +440,7 @@ describe('ActiveApp — total ecosystem integration', () => { retry: { limit: 0 } }, cache: { - autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD, + autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD, scopeResolver: (): ResolvedScopeValues => ({ tenantId: TENANT_ID, actorId: activeActorId, @@ -450,7 +450,7 @@ describe('ActiveApp — total ecosystem integration', () => { } }); const appIdentityEvents: unknown[] = []; - const offAppIdentity = App.Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => { + const offAppIdentity = App.Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => { appIdentityEvents.push(event.payload); }); @@ -459,12 +459,12 @@ describe('ActiveApp — total ecosystem integration', () => { storage: { adapter: createMemoryAdapter(), key: 'session' } }); const Permissions = App.createActivePermissions({ - endpoint: PERM_ENDPOINT, + endpoint: PERMISSION_ENDPOINT, scopeKey: () => activeActorId, - autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD + autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD }); const Connections = App.createActiveConnections({ - autoReauthOn: CONN_AUTO_REAUTH_STANDARD + autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD }); const transport = createMockTransport(); const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, { @@ -483,8 +483,8 @@ describe('ActiveApp — total ecosystem integration', () => { }); const firstCached = await App.Cache.query({ key: ['silent', 'identity'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, fetcher: async () => { fetches += 1; @@ -513,12 +513,12 @@ describe('ActiveApp — total ecosystem integration', () => { expect(appIdentityEvents).toHaveLength(0); expect(Permissions.size).toBe(1); - expect(Chat.state).toBe(CONN_STATE_OPEN); + expect(Chat.state).toBe(CONNECTION_STATE_OPEN); await expect( App.Cache.query({ key: ['silent', 'identity'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, fetcher: async () => { throw new Error('silent orchestration must not clear cache'); @@ -527,21 +527,21 @@ describe('ActiveApp — total ecosystem integration', () => { ).resolves.toEqual(firstCached); publishAppUserIdentityChanged(App.Bus, { - event: SESS_EVENT_LIFECYCLE_REVOKED, + event: SESSION_EVENT_LIFECYCLE_REVOKED, generation: 3, identity: { from: 'identified', to: 'none' }, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED + cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED }); await drainMicrotasks(); expect(appIdentityEvents).toHaveLength(1); expect(Permissions.size).toBe(0); - expect(Chat.state).toBe(CONN_STATE_CLOSED); + expect(Chat.state).toBe(CONNECTION_STATE_CLOSED); await expect( App.Cache.query({ key: ['silent', 'identity'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, fetcher: async () => { fetches += 1; @@ -611,7 +611,7 @@ describe('ActiveApp — total ecosystem integration', () => { retry: { limit: 0 } }, cache: { - autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD, + autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD, scopeResolver: (): ResolvedScopeValues => ({ tenantId: TENANT_ID, actorId: activeActorId, @@ -619,23 +619,23 @@ describe('ActiveApp — total ecosystem integration', () => { locale: App.getLocale() }), policies: { - [CACH_POLICY_INTERACTIVE]: { + [CACHE_POLICY_INTERACTIVE]: { freshFor: 10_000, staleFor: 20_000, staleIfErrorFor: 30_000, gcAfter: 60_000, - mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE, + mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE, persist: true } } } }); const sessionEvents: SessLifecyclePayload[] = []; - const offSessionEvents = App.Bus.on(SESS_EVENT_CHANGED, (event) => { + const offSessionEvents = App.Bus.on(SESSION_EVENT_CHANGED, (event) => { sessionEvents.push(event.payload); }); const appIdentityEvents: unknown[] = []; - const offAppIdentityEvents = App.Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => { + const offAppIdentityEvents = App.Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => { appIdentityEvents.push(event.payload); }); @@ -652,18 +652,18 @@ describe('ActiveApp — total ecosystem integration', () => { }); expect(appIdentityEvents).toHaveLength(1); expect(appIdentityEvents[0]).toMatchObject({ - event: SESS_EVENT_LIFECYCLE_ADOPTED, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED + event: SESSION_EVENT_LIFECYCLE_ADOPTED, + cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED }); expect(JSON.stringify(appIdentityEvents[0])).not.toContain(TOKEN_ADA); const Permissions = App.createActivePermissions({ - endpoint: PERM_ENDPOINT, + endpoint: PERMISSION_ENDPOINT, scopeKey: () => `${activeActorId}:${activeRole}`, - autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD + autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD }); const Connections = App.createActiveConnections({ - autoReauthOn: CONN_AUTO_REAUTH_STANDARD + autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD }); const transport = createMockTransport(); const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, { @@ -689,25 +689,25 @@ describe('ActiveApp — total ecosystem integration', () => { await waitForSentCount(transport, 1); const firstAuth = latestFrame(transport); expect(firstAuth).toMatchObject({ - type: CONN_FRAME_TYPE_AUTH, + type: CONNECTION_FRAME_TYPE_AUTH, payload: { actorId: ACTOR_ID, token: TOKEN_ADA } }); ackFrame(transport, firstAuth); await expect(pendingConnect).resolves.toMatchObject({ ok: true }); - expect(Chat.state).toBe(CONN_STATE_OPEN); + expect(Chat.state).toBe(CONNECTION_STATE_OPEN); const adminDecision = await Permissions.check({ action: PROJECT_ACTION_UPDATE, resource, context: { risk: { mfa: true } } }); - expect(adminDecision.effect).toBe(PERM_EFFECT_ALLOW); + expect(adminDecision.effect).toBe(PERMISSION_EFFECT_ALLOW); expect(Permissions.size).toBe(1); const cachedPresence = await App.Cache.query({ key: ['chat', 'presence'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }], fetcher: async () => { @@ -719,8 +719,8 @@ describe('ActiveApp — total ecosystem integration', () => { const stalePresence = await App.Cache.query({ key: ['chat', 'presence'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }], fetcher: async () => { @@ -741,26 +741,26 @@ describe('ActiveApp — total ecosystem integration', () => { await drainMicrotasks(); expect(Permissions.size).toBe(0); - expect(sessionEvents.some((event) => event.event === SESS_EVENT_LIFECYCLE_ADOPTED)).toBe(true); + expect(sessionEvents.some((event) => event.event === SESSION_EVENT_LIFECYCLE_ADOPTED)).toBe(true); expect(appIdentityEvents).toHaveLength(2); expect(appIdentityEvents[1]).toMatchObject({ - event: SESS_EVENT_LIFECYCLE_ADOPTED, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED + event: SESSION_EVENT_LIFECYCLE_ADOPTED, + cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED }); expect(JSON.stringify(appIdentityEvents[1])).not.toContain(TOKEN_ADA); expect(JSON.stringify(appIdentityEvents[1])).not.toContain(TOKEN_LINUS); await waitForSentCount(transport, 2); const nextAuth = latestFrame(transport); expect(nextAuth).toMatchObject({ - type: CONN_FRAME_TYPE_AUTH, + type: CONNECTION_FRAME_TYPE_AUTH, payload: { actorId: NEXT_ACTOR_ID, token: TOKEN_LINUS } }); ackFrame(transport, nextAuth); const refreshedPresence = await App.Cache.query({ key: ['chat', 'presence'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }], fetcher: async () => { @@ -781,7 +781,7 @@ describe('ActiveApp — total ecosystem integration', () => { resource, context: { risk: { mfa: true } } }); - expect(viewerDecision.effect).toBe(PERM_EFFECT_NOT_APPLICABLE); + expect(viewerDecision.effect).toBe(PERMISSION_EFFECT_NOT_APPLICABLE); expect(Permissions.size).toBe(1); await Sess.revoke(); @@ -789,19 +789,19 @@ describe('ActiveApp — total ecosystem integration', () => { expect(Sess.current).toBeNull(); expect(Permissions.size).toBe(0); - expect(Chat.state).toBe(CONN_STATE_CLOSED); + expect(Chat.state).toBe(CONNECTION_STATE_CLOSED); expect(appIdentityEvents).toHaveLength(3); expect(appIdentityEvents[2]).toMatchObject({ - event: SESS_EVENT_LIFECYCLE_REVOKED, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED + event: SESSION_EVENT_LIFECYCLE_REVOKED, + cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED }); expect(JSON.stringify(appIdentityEvents[2])).not.toContain(TOKEN_ADA); expect(JSON.stringify(appIdentityEvents[2])).not.toContain(TOKEN_LINUS); await expect( App.Cache.query({ key: ['chat', 'presence'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }], fetcher: async () => { @@ -895,7 +895,7 @@ describe('ActiveApp — total ecosystem integration', () => { retry: { limit: 0 } }, cache: { - autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD, + autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD, scopeResolver: (): ResolvedScopeValues => ({ tenantId: TENANT_ID, actorId: activeActorId, @@ -903,19 +903,19 @@ describe('ActiveApp — total ecosystem integration', () => { locale: App.getLocale() }), policies: { - [CACH_POLICY_INTERACTIVE]: { + [CACHE_POLICY_INTERACTIVE]: { freshFor: 10_000, staleFor: 20_000, staleIfErrorFor: 30_000, gcAfter: 60_000, - mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE, + mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE, persist: true } } } }); const appIdentityEvents: unknown[] = []; - const offAppIdentityEvents = App.Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => { + const offAppIdentityEvents = App.Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => { appIdentityEvents.push(event.payload); }); @@ -960,12 +960,12 @@ describe('ActiveApp — total ecosystem integration', () => { }); }); const Permissions = App.createActivePermissions({ - endpoint: PERM_ENDPOINT, + endpoint: PERMISSION_ENDPOINT, scopeKey: () => `${activeActorId}:${activeRole}`, - autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD + autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD }); const Connections = App.createActiveConnections({ - autoReauthOn: CONN_AUTO_REAUTH_STANDARD + autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD }); const transport = createMockTransport(); const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, { @@ -993,8 +993,8 @@ describe('ActiveApp — total ecosystem integration', () => { expect(Sess.current?.user?.id).toBe(ACTOR_ID); expect(appIdentityEvents).toHaveLength(1); expect(appIdentityEvents[0]).toMatchObject({ - event: SESS_EVENT_LIFECYCLE_ADOPTED, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED + event: SESSION_EVENT_LIFECYCLE_ADOPTED, + cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED }); expect(JSON.stringify(appIdentityEvents[0])).not.toContain(TOKEN_ADA); @@ -1002,7 +1002,7 @@ describe('ActiveApp — total ecosystem integration', () => { await waitForSentCount(transport, 1); const firstAuth = latestFrame(transport); expect(firstAuth).toMatchObject({ - type: CONN_FRAME_TYPE_AUTH, + type: CONNECTION_FRAME_TYPE_AUTH, payload: { actorId: ACTOR_ID, token: TOKEN_ADA } }); ackFrame(transport, firstAuth); @@ -1014,13 +1014,13 @@ describe('ActiveApp — total ecosystem integration', () => { resource, context: { risk: { mfa: true } } }) - ).resolves.toMatchObject({ effect: PERM_EFFECT_ALLOW }); + ).resolves.toMatchObject({ effect: PERMISSION_EFFECT_ALLOW }); expect(Permissions.size).toBe(1); const adaPresence = await App.Cache.query({ key: ['auth', 'chat', 'presence'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }], fetcher: async () => { @@ -1039,15 +1039,15 @@ describe('ActiveApp — total ecosystem integration', () => { expect(Permissions.size).toBe(0); expect(appIdentityEvents).toHaveLength(2); expect(appIdentityEvents[1]).toMatchObject({ - event: SESS_EVENT_LIFECYCLE_ADOPTED, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED + event: SESSION_EVENT_LIFECYCLE_ADOPTED, + cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED }); expect(JSON.stringify(appIdentityEvents[1])).not.toContain(TOKEN_ADA); expect(JSON.stringify(appIdentityEvents[1])).not.toContain(TOKEN_LINUS); await waitForSentCount(transport, 2); const nextAuth = latestFrame(transport); expect(nextAuth).toMatchObject({ - type: CONN_FRAME_TYPE_AUTH, + type: CONNECTION_FRAME_TYPE_AUTH, payload: { actorId: NEXT_ACTOR_ID, token: TOKEN_LINUS } }); ackFrame(transport, nextAuth); @@ -1055,8 +1055,8 @@ describe('ActiveApp — total ecosystem integration', () => { await expect( App.Cache.query({ key: ['auth', 'chat', 'presence'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }], fetcher: async () => { @@ -1071,7 +1071,7 @@ describe('ActiveApp — total ecosystem integration', () => { resource, context: { risk: { mfa: true } } }) - ).resolves.toMatchObject({ effect: PERM_EFFECT_NOT_APPLICABLE }); + ).resolves.toMatchObject({ effect: PERMISSION_EFFECT_NOT_APPLICABLE }); expect(Permissions.size).toBe(1); await Auth.signOut(); @@ -1081,19 +1081,19 @@ describe('ActiveApp — total ecosystem integration', () => { expect(Auth.authenticated).toBe(false); expect(Sess.current).toBeNull(); expect(Permissions.size).toBe(0); - expect(Chat.state).toBe(CONN_STATE_CLOSED); + expect(Chat.state).toBe(CONNECTION_STATE_CLOSED); expect(appIdentityEvents).toHaveLength(3); expect(appIdentityEvents[2]).toMatchObject({ - event: SESS_EVENT_LIFECYCLE_REVOKED, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED + event: SESSION_EVENT_LIFECYCLE_REVOKED, + cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED }); expect(JSON.stringify(appIdentityEvents[2])).not.toContain(TOKEN_ADA); expect(JSON.stringify(appIdentityEvents[2])).not.toContain(TOKEN_LINUS); await expect( App.Cache.query({ key: ['auth', 'chat', 'presence'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }], fetcher: async () => { @@ -1191,7 +1191,7 @@ describe('ActiveApp — total ecosystem integration', () => { retry: { limit: 0 } }, cache: { - autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD, + autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD, scopeResolver: (): ResolvedScopeValues => ({ tenantId: TENANT_ID, actorId: activeActorId, @@ -1199,12 +1199,12 @@ describe('ActiveApp — total ecosystem integration', () => { locale: App.getLocale() }), policies: { - [CACH_POLICY_INTERACTIVE]: { + [CACHE_POLICY_INTERACTIVE]: { freshFor: 10_000, staleFor: 20_000, staleIfErrorFor: 30_000, gcAfter: 60_000, - mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE, + mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE, persist: true } } @@ -1244,12 +1244,12 @@ describe('ActiveApp — total ecosystem integration', () => { }); }); const Permissions = App.createActivePermissions({ - endpoint: PERM_ENDPOINT, + endpoint: PERMISSION_ENDPOINT, scopeKey: () => `${activeActorId}:${activeRole}`, - autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD + autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD }); const Connections = App.createActiveConnections({ - autoReauthOn: CONN_AUTO_REAUTH_STANDARD + autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD }); const transport = createMockTransport(); const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, { @@ -1286,8 +1286,8 @@ describe('ActiveApp — total ecosystem integration', () => { }); const oldPresence = App.Cache.query({ key: ['race', 'chat', 'presence'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }], fetcher: async () => stalePresence.promise @@ -1303,20 +1303,20 @@ describe('ActiveApp — total ecosystem integration', () => { await waitForSentCount(transport, 2); const nextAuth = latestFrame(transport); expect(nextAuth).toMatchObject({ - type: CONN_FRAME_TYPE_AUTH, + type: CONNECTION_FRAME_TYPE_AUTH, payload: { actorId: NEXT_ACTOR_ID, token: TOKEN_LINUS } }); ackFrame(transport, nextAuth); stalePermission.resolve({ - effect: PERM_EFFECT_ALLOW, + effect: PERMISSION_EFFECT_ALLOW, reason: 'stale-ada-response', ttl: 60_000 }); stalePresence.resolve({ actorId: ACTOR_ID, fetch: 1 }); await expect(oldPermission).resolves.toMatchObject({ - effect: PERM_EFFECT_ALLOW, + effect: PERMISSION_EFFECT_ALLOW, reason: 'stale-ada-response' }); await expect(oldPresence).resolves.toEqual({ actorId: ACTOR_ID, fetch: 1 }); @@ -1328,14 +1328,14 @@ describe('ActiveApp — total ecosystem integration', () => { resource, context: { risk: { mfa: true } } }) - ).resolves.toMatchObject({ effect: PERM_EFFECT_NOT_APPLICABLE }); + ).resolves.toMatchObject({ effect: PERMISSION_EFFECT_NOT_APPLICABLE }); expect(Permissions.size).toBe(1); await expect( App.Cache.query({ key: ['race', 'chat', 'presence'], - scope: CACH_SCOPE_PUBLIC, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_PUBLIC, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'chat', id: CHAT_CONNECTION_NAME }], fetcher: async () => { @@ -1398,7 +1398,7 @@ describe('ActiveApp — total ecosystem integration', () => { }, cache: { adapter: cacheAdapter, - autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD, + autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD, scopeResolver: (): ResolvedScopeValues => ({ tenantId: activeTenantId, actorId: activeActorId, @@ -1406,12 +1406,12 @@ describe('ActiveApp — total ecosystem integration', () => { locale: App.getLocale() }), policies: { - [CACH_POLICY_INTERACTIVE]: { + [CACHE_POLICY_INTERACTIVE]: { freshFor: 10_000, staleFor: 20_000, staleIfErrorFor: 30_000, gcAfter: 60_000, - mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE, + mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE, persist: true } } @@ -1430,12 +1430,12 @@ describe('ActiveApp — total ecosystem integration', () => { expiresAt: Date.now() + 60_000 }); const Permissions = App.createActivePermissions({ - endpoint: PERM_ENDPOINT, + endpoint: PERMISSION_ENDPOINT, scopeKey: () => `${activeTenantId}:${activeActorId}:${activeRole}`, - autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD + autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD }); const Connections = App.createActiveConnections({ - autoReauthOn: CONN_AUTO_REAUTH_STANDARD + autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD }); const transport = createMockTransport(); const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, { @@ -1464,13 +1464,13 @@ describe('ActiveApp — total ecosystem integration', () => { resource: resourceForTenant(), context: { risk: { mfa: true } } }) - ).resolves.toMatchObject({ effect: PERM_EFFECT_ALLOW }); + ).resolves.toMatchObject({ effect: PERMISSION_EFFECT_ALLOW }); expect(Permissions.size).toBe(1); const firstDashboard = await App.Cache.query({ key: ['tenant', 'dashboard'], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'tenant-dashboard', id: activeTenantId }], fetcher: async () => { @@ -1501,8 +1501,8 @@ describe('ActiveApp — total ecosystem integration', () => { await expect( App.Cache.query({ key: ['tenant', 'dashboard'], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'tenant-dashboard', id: activeTenantId }], fetcher: async () => { @@ -1517,7 +1517,7 @@ describe('ActiveApp — total ecosystem integration', () => { resource: resourceForTenant(), context: { risk: { mfa: true } } }) - ).resolves.toMatchObject({ effect: PERM_EFFECT_ALLOW }); + ).resolves.toMatchObject({ effect: PERMISSION_EFFECT_ALLOW }); expect(Permissions.size).toBe(1); activeTenantId = SECOND_TENANT_ID; @@ -1533,8 +1533,8 @@ describe('ActiveApp — total ecosystem integration', () => { expect(cacheAdapter.inspect().entries).toHaveLength(0); const secondTenantDashboard = await App.Cache.query({ key: ['tenant', 'dashboard'], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'tenant-dashboard', id: activeTenantId }], fetcher: async () => { @@ -1557,8 +1557,8 @@ describe('ActiveApp — total ecosystem integration', () => { const frameCountAfterTenantSwitch = transport.sentMessages().length; const arabicDashboard = await App.Cache.query({ key: ['tenant', 'dashboard'], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'tenant-dashboard', id: activeTenantId }], fetcher: async () => { @@ -1639,7 +1639,7 @@ describe('ActiveApp — total ecosystem integration', () => { }, cache: { adapter: cacheAdapter, - autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD, + autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD, scopeResolver: (): ResolvedScopeValues => ({ tenantId: activeTenantId, actorId: activeActorId, @@ -1647,12 +1647,12 @@ describe('ActiveApp — total ecosystem integration', () => { locale: App.getLocale() }), policies: { - [CACH_POLICY_INTERACTIVE]: { + [CACHE_POLICY_INTERACTIVE]: { freshFor: 10_000, staleFor: 20_000, staleIfErrorFor: 30_000, gcAfter: 60_000, - mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE, + mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE, persist: true } } @@ -1676,12 +1676,12 @@ describe('ActiveApp — total ecosystem integration', () => { expiresAt: Date.now() + 60_000 }); const Permissions = App.createActivePermissions({ - endpoint: PERM_ENDPOINT, + endpoint: PERMISSION_ENDPOINT, scopeKey: () => `${activeTenantId}:${activeActorId}:${activeRole}`, - autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD + autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD }); const Connections = App.createActiveConnections({ - autoReauthOn: CONN_AUTO_REAUTH_STANDARD + autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD }); const transport = createMockTransport(); const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, { @@ -1720,13 +1720,13 @@ describe('ActiveApp — total ecosystem integration', () => { resource, context: { risk: { mfa: true } } }) - ).resolves.toMatchObject({ effect: PERM_EFFECT_ALLOW }); + ).resolves.toMatchObject({ effect: PERMISSION_EFFECT_ALLOW }); expect(Permissions.size).toBe(1); const cachedDashboard = await App.Cache.query({ key: ['backend-webhook', 'dashboard'], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'tenant-dashboard', id: activeTenantId }], fetcher: async () => { @@ -1765,7 +1765,7 @@ describe('ActiveApp — total ecosystem integration', () => { ); await drainMicrotasks(); delayedPermission.resolve({ - effect: PERM_EFFECT_ALLOW, + effect: PERMISSION_EFFECT_ALLOW, reason: 'stale-pre-webhook-permission', ttl: 60_000 }); @@ -1779,8 +1779,8 @@ describe('ActiveApp — total ecosystem integration', () => { await expect( App.Cache.query({ key: ['backend-webhook', 'dashboard'], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'tenant-dashboard', id: activeTenantId }], fetcher: async () => { @@ -1789,7 +1789,7 @@ describe('ActiveApp — total ecosystem integration', () => { }) ).resolves.toEqual(cachedDashboard); await expect(stalePermission).resolves.toMatchObject({ - effect: PERM_EFFECT_ALLOW, + effect: PERMISSION_EFFECT_ALLOW, reason: 'stale-pre-webhook-permission' }); expect(Permissions.size).toBe(0); @@ -1800,7 +1800,7 @@ describe('ActiveApp — total ecosystem integration', () => { resource: delayedResource, context: { risk: { mfa: true } } }) - ).resolves.toMatchObject({ effect: PERM_EFFECT_NOT_APPLICABLE }); + ).resolves.toMatchObject({ effect: PERMISSION_EFFECT_NOT_APPLICABLE }); expect(Permissions.size).toBe(1); } finally { offChatWebhook?.(); @@ -1859,7 +1859,7 @@ describe('ActiveApp — total ecosystem integration', () => { }, cache: { adapter: cacheAdapter, - autoInvalidateOn: CACH_AUTO_INVALIDATE_STANDARD, + autoInvalidateOn: CACHE_AUTO_INVALIDATE_STANDARD, scopeResolver: (): ResolvedScopeValues => ({ tenantId: activeTenantId, actorId: activeActorId, @@ -1867,18 +1867,18 @@ describe('ActiveApp — total ecosystem integration', () => { locale: App.getLocale() }), policies: { - [CACH_POLICY_INTERACTIVE]: { + [CACHE_POLICY_INTERACTIVE]: { freshFor: 10_000, staleFor: 20_000, staleIfErrorFor: 30_000, gcAfter: 60_000, - mode: CACH_READ_MODE_STALE_WHILE_REVALIDATE, + mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE, persist: true } } } }); - const offAppIdentityEvents = App.Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => { + const offAppIdentityEvents = App.Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => { appIdentityEvents.push(event.payload); }); let offChatWebhook: (() => void) | undefined; @@ -1895,12 +1895,12 @@ describe('ActiveApp — total ecosystem integration', () => { expiresAt: Date.now() + 60_000 }); const Permissions = App.createActivePermissions({ - endpoint: PERM_ENDPOINT, + endpoint: PERMISSION_ENDPOINT, scopeKey: () => `${activeTenantId}:${activeActorId}:${activeRole}`, - autoInvalidateOn: PERM_AUTO_INVALIDATE_STANDARD + autoInvalidateOn: PERMISSION_AUTO_INVALIDATE_STANDARD }); const Connections = App.createActiveConnections({ - autoReauthOn: CONN_AUTO_REAUTH_STANDARD + autoReauthOn: CONNECTION_AUTO_REAUTH_STANDARD }); const transport = createMockTransport(); const Chat = Connections.createConnection(CHAT_CONNECTION_NAME, { @@ -1930,12 +1930,12 @@ describe('ActiveApp — total ecosystem integration', () => { await waitForSentCount(transport, 1); const firstAuth = latestFrame(transport); expect(firstAuth).toMatchObject({ - type: CONN_FRAME_TYPE_AUTH, + type: CONNECTION_FRAME_TYPE_AUTH, payload: { actorId: ACTOR_ID, token: TOKEN_ADA } }); ackFrame(transport, firstAuth); await expect(pendingConnect).resolves.toMatchObject({ ok: true }); - expect(Chat.state).toBe(CONN_STATE_OPEN); + expect(Chat.state).toBe(CONNECTION_STATE_OPEN); await expect( Permissions.check({ @@ -1943,14 +1943,14 @@ describe('ActiveApp — total ecosystem integration', () => { resource, context: { risk: { mfa: true } } }) - ).resolves.toMatchObject({ effect: PERM_EFFECT_ALLOW }); + ).resolves.toMatchObject({ effect: PERMISSION_EFFECT_ALLOW }); expect(Permissions.size).toBe(1); await expect( App.Cache.query({ key: ['remote-revoke', 'dashboard'], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'tenant-dashboard', id: activeTenantId }], fetcher: async () => { @@ -1974,20 +1974,20 @@ describe('ActiveApp — total ecosystem integration', () => { expect(Sess.current).toBeNull(); expect(Permissions.size).toBe(0); - expect(Chat.state).toBe(CONN_STATE_CLOSED); + expect(Chat.state).toBe(CONNECTION_STATE_CLOSED); expect(cacheAdapter.inspect().entries).toHaveLength(0); expect(appIdentityEvents).toHaveLength(2); expect(appIdentityEvents[1]).toMatchObject({ - event: SESS_EVENT_LIFECYCLE_REVOKED, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_REVOKED + event: SESSION_EVENT_LIFECYCLE_REVOKED, + cause: APP_USER_IDENTITY_CAUSE_SESSION_REVOKED }); expect(JSON.stringify(appIdentityEvents[1])).not.toContain(TOKEN_ADA); expect(transport.sentMessages()).toHaveLength(1); await expect( App.Cache.query({ key: ['remote-revoke', 'dashboard'], - scope: CACH_SCOPE_TENANT, - policy: CACH_POLICY_INTERACTIVE, + scope: CACHE_SCOPE_TENANT, + policy: CACHE_POLICY_INTERACTIVE, schemaVersion: PROJECT_SCHEMA_VERSION, tags: [{ type: 'tenant-dashboard', id: activeTenantId }], fetcher: async () => { diff --git a/src/arts/aapp/test/session-translator.test.ts b/src/arts/active-app/test/session-translator.test.ts similarity index 63% rename from src/arts/aapp/test/session-translator.test.ts rename to src/arts/active-app/test/session-translator.test.ts index 294e27d..a001fab 100644 --- a/src/arts/aapp/test/session-translator.test.ts +++ b/src/arts/active-app/test/session-translator.test.ts @@ -1,26 +1,26 @@ import { describe, expect, it } from 'vitest'; -import { createEngineBus } from '$buss'; +import { createEngineBus } from '$bus'; import { - AAPP_EVENT_USER_IDENTITY_CHANGED, - AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED, + APP_EVENT_USER_IDENTITY_CHANGED, + APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED, type AppEventMap -} from '$libs/aapp/events'; -import { SESS_EVENT_LIFECYCLE_ADOPTED } from '$sess/consts'; -import type { SessEventMap, SessLifecyclePayload } from '$sess/types'; +} from '$libs/active-app/events'; +import { SESSION_EVENT_LIFECYCLE_ADOPTED } from '$session/consts'; +import type { SessEventMap, SessLifecyclePayload } from '$session/types'; import { publishAppIdentityFromSessionLifecycleEvent } from '../integrations/session-translator.ts'; -describe('aapp session translator', () => { +describe('app session translator', () => { it('maps session lifecycle payloads explicitly without leaking extra sensitive fields', () => { const Bus = createEngineBus(); const appPayloads: unknown[] = []; - const off = Bus.on(AAPP_EVENT_USER_IDENTITY_CHANGED, (event) => { + const off = Bus.on(APP_EVENT_USER_IDENTITY_CHANGED, (event) => { appPayloads.push(event.payload); }); publishAppIdentityFromSessionLifecycleEvent( Bus, { - event: SESS_EVENT_LIFECYCLE_ADOPTED, + event: SESSION_EVENT_LIFECYCLE_ADOPTED, generation: 1, identity: { from: 'none', to: 'identified' }, credential: { token: 'secret-token' }, @@ -31,10 +31,10 @@ describe('aapp session translator', () => { expect(appPayloads).toHaveLength(1); expect(JSON.stringify(appPayloads[0])).not.toContain('secret-token'); expect(appPayloads[0]).toEqual({ - event: SESS_EVENT_LIFECYCLE_ADOPTED, + event: SESSION_EVENT_LIFECYCLE_ADOPTED, generation: 1, identity: { from: 'none', to: 'identified' }, - cause: AAPP_USER_IDENTITY_CAUSE_SESSION_ADOPTED + cause: APP_USER_IDENTITY_CAUSE_SESSION_ADOPTED }); off.unsubscribe(); diff --git a/src/arts/aapp/test/storage-integration.test.ts b/src/arts/active-app/test/storage-integration.test.ts similarity index 95% rename from src/arts/aapp/test/storage-integration.test.ts rename to src/arts/active-app/test/storage-integration.test.ts index d6104b4..acad2b0 100644 --- a/src/arts/aapp/test/storage-integration.test.ts +++ b/src/arts/active-app/test/storage-integration.test.ts @@ -12,8 +12,8 @@ import { describe, it, expect } from 'vitest'; import { createActiveApp } from '../active-app.svelte'; -import { createMemoryAdapter, encodeEnvelope } from '$stor'; -import { LogLevel } from '$logr'; +import { createMemoryAdapter, encodeEnvelope } from '$storage'; +import { LogLevel } from '$logger'; describe('App.Storage', () => { it('is always present even without storage options', () => { @@ -61,7 +61,7 @@ describe('App.Storage', () => { storage: { adapter: broken } }); App.Storage.entry('x', 'd').set('v'); - expect(captured.some((e) => e.category === 'stor' && /write/.test(e.message))).toBe(true); + expect(captured.some((e) => e.category === 'storage' && /write/.test(e.message))).toBe(true); App.dispose(); }); }); diff --git a/src/arts/aapp/test/test-app.test.ts b/src/arts/active-app/test/test-app.test.ts similarity index 99% rename from src/arts/aapp/test/test-app.test.ts rename to src/arts/active-app/test/test-app.test.ts index f56cb73..3a27f8a 100644 --- a/src/arts/aapp/test/test-app.test.ts +++ b/src/arts/active-app/test/test-app.test.ts @@ -10,7 +10,7 @@ import { describe, it, expect } from 'vitest'; import { createTestApp } from '../testing'; -import { LogLevel } from '$logr'; +import { LogLevel } from '$logger'; import type { LangNode } from '$lang'; const schema = { diff --git a/src/arts/aapp/testing/index.ts b/src/arts/active-app/testing/index.ts similarity index 94% rename from src/arts/aapp/testing/index.ts rename to src/arts/active-app/testing/index.ts index eb6d060..9ee95e7 100644 --- a/src/arts/aapp/testing/index.ts +++ b/src/arts/active-app/testing/index.ts @@ -1,4 +1,4 @@ -import { LogLevel, type LogEntry, type LoggerOptions, type Transport } from '$logr'; +import { LogLevel, type LogEntry, type LoggerOptions, type Transport } from '$logger'; import type { LangNode } from '$libs/lang'; import { createActiveApp } from '../active-app.svelte'; @@ -40,8 +40,8 @@ export interface TestAppHandle extends ActiveApp< * `createActiveApp()` with test-friendly defaults — the canonical way to * build an `App` inside a unit/integration test. * - * Lives under `$aapp/testing` so the helper does not ship with production - * bundles that import the main `$aapp` barrel. + * Lives under `$active-app/testing` so the helper does not ship with production + * bundles that import the main `$active-app` barrel. * * Defaults that differ from production: * @@ -56,7 +56,7 @@ export interface TestAppHandle extends ActiveApp< * * @example * // Smoke test — no logs needed - * import { createTestApp } from '$aapp/testing'; + * import { createTestApp } from '$active-app/testing'; * const App = createTestApp({ lang: { schema } }); * expect(App.Lang.t('common.ok')).toBe('Aceptar'); * App.dispose(); diff --git a/src/arts/aapp/types.ts b/src/arts/active-app/types.ts similarity index 86% rename from src/arts/aapp/types.ts rename to src/arts/active-app/types.ts index 8593af6..84758d3 100644 --- a/src/arts/aapp/types.ts +++ b/src/arts/active-app/types.ts @@ -1,29 +1,29 @@ import type { ActiveDom, ActiveDomProps } from '$adom'; import type { ActiveAuth, ActiveAuthOptions } from '$auth'; -import type { EngineBus, EngineBusOptions } from '$libs/buss'; -import type { ActiveCache, ActiveCacheOptions } from '$cach'; -import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$conn'; -import type { ActiveFrontend, ActiveFrontendOptions } from '$fend'; -import type { FrontendPreferenceKey } from '$fend'; -import type { ActiveFormats, ActiveFormatsOptions } from '$fmts'; +import type { EngineBus, EngineBusOptions } from '$libs/bus'; +import type { ActiveCache, ActiveCacheOptions } from '$cache'; +import type { ActiveConnections, ActiveConnectionsOptions, ConnectionMap } from '$connection'; +import type { ActiveFrontend, ActiveFrontendOptions } from '$frontend'; +import type { FrontendPreferenceKey } from '$frontend'; +import type { ActiveFormats, ActiveFormatsOptions } from '$formats'; import type { EngineHttp, EngineHttpOptions } from '$http'; -import type { AppEventMap } from '$libs/aapp/events'; +import type { AppEventMap } from '$libs/active-app/events'; import type { ActiveLang, LangNode, SupportedLocale } from '$libs/lang'; -import type { EngineLogger, LoggerOptions } from '$logr'; -import type { ActivePermissions, ActivePermissionsOptions } from '$perm'; -import type { ActiveSession, EngineSessionOptions, SessEventMap } from '$sess'; +import type { EngineLogger, LoggerOptions } from '$logger'; +import type { ActivePermissions, ActivePermissionsOptions } from '$permissions'; +import type { ActiveSession, EngineSessionOptions, SessEventMap } from '$session'; import type { EngineSium } from '$sium'; -import type { ActiveStorage, SyncStorageAdapter } from '$stor'; -import type { ActiveTimers } from '$timr'; -import type { EngineTimersOptions } from '$libs/timers'; +import type { ActiveStorage, SyncStorageAdapter } from '$storage'; +import type { ActiveTimers } from '$timer'; +import type { EngineTimersOptions } from '$libs/timer'; import type { - AAPP_ORCHESTRATION_SILENT, - AAPP_ORCHESTRATION_STANDARD, - AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, - AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE, - AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY, - AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH, - AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED + APP_ORCHESTRATION_SILENT, + APP_ORCHESTRATION_STANDARD, + APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY, + APP_ORCHESTRATION_TRANSLATOR_DISPOSE, + APP_ORCHESTRATION_TRANSLATOR_IDENTITY, + APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH, + APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED } from './consts.ts'; /** Frontend preferences eligible for App-managed persistence. */ @@ -73,15 +73,15 @@ export interface ActiveAppStorageOptions { export type ActiveAppOrchestrationPreset = | false - | typeof AAPP_ORCHESTRATION_SILENT - | typeof AAPP_ORCHESTRATION_STANDARD; + | typeof APP_ORCHESTRATION_SILENT + | typeof APP_ORCHESTRATION_STANDARD; export type ActiveAppOrchestrationTranslator = - | typeof AAPP_ORCHESTRATION_TRANSLATOR_IDENTITY - | typeof AAPP_ORCHESTRATION_TRANSLATOR_PERMISSIONS_REFRESH - | typeof AAPP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED - | typeof AAPP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY - | typeof AAPP_ORCHESTRATION_TRANSLATOR_DISPOSE; + | typeof APP_ORCHESTRATION_TRANSLATOR_IDENTITY + | typeof APP_ORCHESTRATION_TRANSLATOR_PERMISSION_REFRESH + | typeof APP_ORCHESTRATION_TRANSLATOR_TENANT_SWITCHED + | typeof APP_ORCHESTRATION_TRANSLATOR_CONNECTIVITY + | typeof APP_ORCHESTRATION_TRANSLATOR_DISPOSE; export type ActiveAppOrchestrationOptions = | ActiveAppOrchestrationPreset @@ -96,7 +96,7 @@ export type ActiveAppOrchestrationOptions = * - `lang` defaults to a mono lang when absent (single-language passthrough, * reactive locale still owned so Formats/Frontend stay in sync). * - `formats` is always built; sub-engines accept their own knobs (currency, - * date order, etc.). When absent the locale falls back to `FMTS_DEFAULT_LOCALE`. + * date order, etc.). When absent the locale falls back to `FORMATS_DEFAULT_LOCALE`. * * Sium is intentionally **not** part of App — validation is page-scoped. * Pages that need it construct an `EngineSium` directly: @@ -174,7 +174,7 @@ export interface ActiveAppOptions { * automatically; the authoritative auth engine still lives server-side * under `$svrs/auth`. */ - auth?: Omit; + auth?: Omit; /** * Cross-artifact orchestration policy. App always exposes `App.Bus`; * orchestration only controls translators from module events to stable @@ -197,7 +197,7 @@ export interface ActiveAppOptions { * | -------- | ---------- | ------------------------------------------------ | * | Logger | real | `level: WARN` + `consoleTransport()` (engine default; pass `{ level: NONE, transports: [] }` for silence) | * | Lang | real | mono — returns paths and `\|fallback` literals; warns once per path in DEV via Logger under `lang.mono` | - * | Formats | real | real with locale = `FMTS_DEFAULT_LOCALE` (`'en-US'`) | + * | Formats | real | real with locale = `FORMATS_DEFAULT_LOCALE` (`'en-US'`) | * | Frontend | real | real with default theme/mode/density | * | Dom | real | real with default breakpoints | */ @@ -240,7 +240,7 @@ export interface ActiveApp { * `onRefresh`, `onRevoke`, `broadcastChannel`). * * Single session per App — the second call throws - * `SessAlreadyCreatedError`. Multi-account scenarios compose multiple + * `SessionAlreadyCreatedError`. Multi-account scenarios compose multiple * App instances. The session is auto-disposed by `App.dispose()`. * * Generics let the caller declare the exact shape: @@ -286,7 +286,7 @@ export interface ActiveApp { /** * Build the App-scoped reactive permissions client. The authoritative - * runtime is `createEnginePermissions()` from `$svrs/perm`; this client + * runtime is `createEnginePermissions()` from `$svrs/permissions`; this client * is only for UI/UX reflection, snapshots and cache. */ createActivePermissions( @@ -299,7 +299,7 @@ export interface ActiveApp { * reflects `/api/auth/*` state, sends CSRF headers and exposes pending / * error state for UI. */ - createActiveAuth(options?: Omit): ActiveAuth; + createActiveAuth(options?: Omit): ActiveAuth; /** * The active session, when one has been built via diff --git a/src/arts/auth/README.md b/src/arts/auth/README.md index cfac0dd..bfc53ce 100644 --- a/src/arts/auth/README.md +++ b/src/arts/auth/README.md @@ -408,7 +408,7 @@ Antes de usarlo fuera de tests: - Usar un `AuthStoreAdapter` transaccional. - Conectar `AuthSessPort` al módulo real `sess`. - Conectar `AuthLogrPort` a `logr`. -- Conectar `AuthCachPort` a `cach`. +- Conectar `AuthCachePort` a `cach`. - Usar `createNodeScryptPasswordHasher()` o un adapter Argon2id propio. - Resolver `tenantId` desde request, subdominio, organización o app config. - Mantener `security.csrf.signingKey` fuera del repo. diff --git a/src/arts/auth/active-auth-runtime.ts b/src/arts/auth/active-auth-runtime.ts index fa27eec..3dda1cc 100644 --- a/src/arts/auth/active-auth-runtime.ts +++ b/src/arts/auth/active-auth-runtime.ts @@ -55,13 +55,13 @@ export async function invalidateAuthClientCache( diagnostics: AuthClientDiagnostics, reason: AuthEventName ): Promise { - if (!options.cach) return; + if (!options.cache) return; try { - await options.cach.invalidate({ tags: authCacheTagsForIdentity(), reason }); + await options.cache.invalidate({ tags: authCacheTagsForIdentity(), reason }); } catch (error) { emitAuthClientDiagnostic( diagnostics, - AUTH_CLIENT_DIAGNOSTIC_EVENTS.CACH_INVALIDATION_FAILED, + AUTH_CLIENT_DIAGNOSTIC_EVENTS.CACHE_INVALIDATION_FAILED, { reason, error } ); options.onCacheError?.(error); diff --git a/src/arts/auth/consts.ts b/src/arts/auth/consts.ts index f5aac87..6538f66 100644 --- a/src/arts/auth/consts.ts +++ b/src/arts/auth/consts.ts @@ -47,7 +47,7 @@ export const AUTH_ERR_INVALID_RESPONSE: ErrCode = errCode(AUTH_ERR, 'invalid_res export const AUTH_CLIENT_DIAGNOSTIC_EVENTS = { OPERATION_FAILED: 'auth.client.operation_failed', - CACH_INVALIDATION_FAILED: 'auth.client.cache_invalidation_failed' + CACHE_INVALIDATION_FAILED: 'auth.client.cache_invalidation_failed' } as const; export const AUTH_CLIENT_LOG_MESSAGE_OPERATION_FAILED = 'auth client operation failed'; diff --git a/src/arts/auth/diagnostics.ts b/src/arts/auth/diagnostics.ts index 220bc5c..c463238 100644 --- a/src/arts/auth/diagnostics.ts +++ b/src/arts/auth/diagnostics.ts @@ -5,7 +5,7 @@ import { type DiagnosticEvent, type Diagnostics, type Logger -} from '$libs/logr'; +} from '$libs/logger'; import { AUTH_CLIENT_DIAGNOSTIC_EVENTS, AUTH_CLIENT_LOG_MESSAGE_CACHE_INVALIDATION_FAILED, @@ -34,7 +34,7 @@ const AUTH_CLIENT_DIAGNOSTIC_LOGS: DiagnosticCatalog level: LogLevel.WARN, message: AUTH_CLIENT_LOG_MESSAGE_OPERATION_FAILED }, - [AUTH_CLIENT_DIAGNOSTIC_EVENTS.CACH_INVALIDATION_FAILED]: { + [AUTH_CLIENT_DIAGNOSTIC_EVENTS.CACHE_INVALIDATION_FAILED]: { level: LogLevel.WARN, message: AUTH_CLIENT_LOG_MESSAGE_CACHE_INVALIDATION_FAILED } diff --git a/src/arts/auth/index.ts b/src/arts/auth/index.ts index bf83527..c3ae8bd 100644 --- a/src/arts/auth/index.ts +++ b/src/arts/auth/index.ts @@ -86,8 +86,8 @@ export type { AuthActiveRevokeDeviceInput } from './types.ts'; export type { - AuthClientCachInvalidationInput, - AuthClientCachPort, + AuthClientCacheInvalidationInput, + AuthClientCachePort, AuthClientHttpPort, AuthClientRequestOptions, AuthClientStoragePort diff --git a/src/arts/auth/test/active-auth.test.ts b/src/arts/auth/test/active-auth.test.ts index a96c3a4..b67391d 100644 --- a/src/arts/auth/test/active-auth.test.ts +++ b/src/arts/auth/test/active-auth.test.ts @@ -103,7 +103,7 @@ describe('createActiveAuth', () => { const Auth = createActiveAuth({ http: createFetchAuthClient(fetcher), - cach: { + cache: { invalidate(input) { invalidations.push(input); } diff --git a/src/arts/auth/types.ts b/src/arts/auth/types.ts index 5bf2885..65f2393 100644 --- a/src/arts/auth/types.ts +++ b/src/arts/auth/types.ts @@ -12,15 +12,15 @@ import type { } from '$libs/auth/types'; import type { ActiveChangeListener, ActiveEngine } from '$libs/active'; import type { - AuthClientCachPort, + AuthClientCachePort, AuthClientHttpPort, AuthClientStoragePort } from '$libs/auth/contracts'; -import type { Logger } from '$libs/logr'; +import type { Logger } from '$libs/logger'; export interface ActiveAuthOptions { readonly http: AuthClientHttpPort; - readonly cach?: AuthClientCachPort; + readonly cache?: AuthClientCachePort; readonly stor?: AuthClientStoragePort; readonly initial?: AuthCurrentView; readonly logger?: Logger; diff --git a/src/arts/buss/README.md b/src/arts/bus/README.md similarity index 82% rename from src/arts/buss/README.md rename to src/arts/bus/README.md index 5ec09aa..0d753f5 100644 --- a/src/arts/buss/README.md +++ b/src/arts/bus/README.md @@ -37,10 +37,10 @@ comes from each owner declaring constants, payload shapes, and typed The framework distinguishes two layers of events that share a single `App.Bus` instance: -- **Module events** (`SESS_EVENT_*`, `AUTH_EVENT_*`, `CACH_EVENT_*`, …) +- **Module events** (`SESSION_EVENT_*`, `AUTH_EVENT_*`, `CACHE_EVENT_*`, …) — internal facts emitted by the artifact that owns them. They can iterate; they are not part of the public contract. -- **App events** (`AAPP_EVENT_*`) — the **stable public contract**. +- **App events** (`APP_EVENT_*`) — the **stable public contract**. Universal facts that consumers and external plugins listen to. Renaming or removing one is a breaking change. @@ -54,8 +54,8 @@ The model is **Domain Events + Integration Events** with an | Active term | DDD term | | ------------------------------ | ----------------------------------- | -| Module events (`SESS_EVENT_*`, …) | Domain events (private, iterable) | -| App events (`AAPP_EVENT_*`) | Integration events (public, stable) | +| Module events (`SESSION_EVENT_*`, …) | Domain events (private, iterable) | +| App events (`APP_EVENT_*`) | Integration events (public, stable) | | `aapp/integrations/*-translator.ts` | Anti-corruption layer + event mapper | | Per-consumer auto-reactions | Stateless process managers | @@ -68,7 +68,7 @@ arts/buss/ ← engine, framework-agnostic types.ts EngineBus, BusEnvelope, BusListener, BusPublishOptions, BusPublishResult, EventPublisher, BusAnyListener - consts.ts BUSS_*, listener error modes, diagnostics + consts.ts BUS_*, listener error modes, diagnostics engine-bus.ts createEngineBus() no Svelte imports errors.ts / matching.ts / diagnostics.ts @@ -81,8 +81,8 @@ arts/buss/svelte/ ← Svelte adapter arts/buss/active-bus.svelte.ts ← reactive wrappers (minimal surface) createBusRecent() { lastEvent, count, clear, dispose } -libs/aapp/events.ts ← AAPP_EVENT_* constants + payloads - + AAPP_EVENT_RUNTIMES metadata +libs/aapp/events.ts ← APP_EVENT_* constants + payloads + + APP_EVENT_RUNTIMES metadata libs/aapp/bus-context.svelte.ts ← getBus / setBus via createContext @@ -244,7 +244,7 @@ export type BusAnyListener = ( The convention remains conservative: `onAny` is for diagnostics, devtools, event capture and tests. Business side effects should subscribe -to the exact `AAPP_EVENT_*` they need. +to the exact `APP_EVENT_*` they need. ### Re-entrancy @@ -275,11 +275,11 @@ Each module owns and emits its own facts. Constants live in the module's ```ts // arts/sess/consts.ts -export const SESS_EVENT_CHANGED = 'sess.changed'; -export const SESS_EVENT_IDENTITY_CHANGED = 'sess.identity.changed'; -export const SESS_EVENT_REVOKED = 'sess.revoked'; -export const SESS_EVENT_EXPIRED = 'sess.expired'; -export const SESS_EVENT_REFRESHED = 'sess.refreshed'; +export const SESSION_EVENT_CHANGED = 'session.changed'; +export const SESSION_EVENT_IDENTITY_CHANGED = 'session.identity.changed'; +export const SESSION_EVENT_REVOKED = 'session.revoked'; +export const SESSION_EVENT_EXPIRED = 'session.expired'; +export const SESSION_EVENT_REFRESHED = 'session.refreshed'; // arts/sess/types.ts export interface SessLifecyclePayload { @@ -303,32 +303,32 @@ export function publishSessLifecycleEvent( payload: SessLifecyclePayload, options: BusPublishOptions = {} ): void { - if (payload.event === SESS_EVENT_LIFECYCLE_INITIAL) return; - const opts = { source: SESS_MODULE, ...options }; + if (payload.event === SESSION_EVENT_LIFECYCLE_INITIAL) return; + const opts = { source: SESSION_MODULE, ...options }; - bus.publish(SESS_EVENT_CHANGED, payload, opts); + bus.publish(SESSION_EVENT_CHANGED, payload, opts); if (payload.identity.from !== payload.identity.to) { - bus.publish(SESS_EVENT_IDENTITY_CHANGED, payload, opts); + bus.publish(SESSION_EVENT_IDENTITY_CHANGED, payload, opts); } - if (payload.event === SESS_EVENT_LIFECYCLE_REVOKED) { - bus.publish(SESS_EVENT_REVOKED, payload, opts); + if (payload.event === SESSION_EVENT_LIFECYCLE_REVOKED) { + bus.publish(SESSION_EVENT_REVOKED, payload, opts); } - if (payload.event === SESS_EVENT_LIFECYCLE_EXPIRED) { - bus.publish(SESS_EVENT_EXPIRED, payload, opts); + if (payload.event === SESSION_EVENT_LIFECYCLE_EXPIRED) { + bus.publish(SESSION_EVENT_EXPIRED, payload, opts); } - if (payload.event === SESS_EVENT_LIFECYCLE_REFRESHED) { - bus.publish(SESS_EVENT_REFRESHED, payload, opts); + if (payload.event === SESSION_EVENT_LIFECYCLE_REFRESHED) { + bus.publish(SESSION_EVENT_REFRESHED, payload, opts); } } // Subscriber: one event, one listener. `onSessChanged` covers every -// lifecycle transition; subscribe to `SESS_EVENT_IDENTITY_CHANGED` / -// `SESS_EVENT_REVOKED` / etc directly when you only care about a slice. +// lifecycle transition; subscribe to `SESSION_EVENT_IDENTITY_CHANGED` / +// `SESSION_EVENT_REVOKED` / etc directly when you only care about a slice. export function onSessChanged( bus: EngineBus, listener: (event: SessChangedEnvelope) => void | Promise ): BusSubscription { - return bus.on(SESS_EVENT_CHANGED, (event) => listener(event as SessChangedEnvelope)); + return bus.on(SESSION_EVENT_CHANGED, (event) => listener(event as SessChangedEnvelope)); } ``` @@ -342,15 +342,15 @@ payloads — never `any`. No credentials.** ```ts // libs/aapp/events.ts -export const AAPP_EVENT_USER_IDENTITY_CHANGED = 'aapp.user.identity.changed'; -export const AAPP_EVENT_TENANT_SWITCHED = 'aapp.tenant.switched'; -export const AAPP_EVENT_PERMISSIONS_REFRESH_REQUESTED = 'aapp.permissions.refresh.requested'; -export const AAPP_EVENT_CONNECTIVITY_CHANGED = 'aapp.connectivity.changed'; -export const AAPP_EVENT_CACHE_INVALIDATE_REQUESTED = 'aapp.cache.invalidate.requested'; -export const AAPP_EVENT_DISPOSE_STARTING = 'aapp.dispose.starting'; +export const APP_EVENT_USER_IDENTITY_CHANGED = 'app.user.identity.changed'; +export const APP_EVENT_TENANT_SWITCHED = 'app.tenant.switched'; +export const APP_EVENT_PERMISSIONS_REFRESH_REQUESTED = 'app.permissions.refresh.requested'; +export const APP_EVENT_CONNECTIVITY_CHANGED = 'app.connectivity.changed'; +export const APP_EVENT_CACHE_INVALIDATE_REQUESTED = 'app.cache.invalidate.requested'; +export const APP_EVENT_DISPOSE_STARTING = 'app.dispose.starting'; ``` -Renaming any `AAPP_EVENT_*`, removing it, or changing its payload shape +Renaming any `APP_EVENT_*`, removing it, or changing its payload shape non-additively is a **major bump**. Adding new app events is a minor. ## App.Bus is always-present per render scope @@ -441,11 +441,11 @@ function directly) inside `$effect`: ```svelte diff --git a/src/web/routes/active/+page.svelte b/src/web/routes/active/+page.svelte index 261707c..e98141c 100644 --- a/src/web/routes/active/+page.svelte +++ b/src/web/routes/active/+page.svelte @@ -3,7 +3,7 @@ import CodeBlock from './_components/CodeBlock.svelte'; import PageNav from './_components/PageNav.svelte'; - const composition = `import { createActiveApp } from '$aapp'; + const composition = `import { createActiveApp } from '$active-app'; const App = createActiveApp({ lang: { schema, defaultLocale: 'es', fallbackChain: ['en'] }, @@ -24,7 +24,7 @@ App.setLocale('es-MX');`; alias: 'overview', href: '/active/get-started/ecosystem', description: 'Layer map, module domains, request flows and integration rules.', - factories: ['libs', 'svrs', 'arts', 'aapp'] + factories: ['libs', 'svrs', 'arts', 'app'] }, { title: 'AI Agents', @@ -35,7 +35,7 @@ App.setLocale('es-MX');`; }, { title: 'App', - alias: '$aapp', + alias: '$active-app', href: '/active/docs/aapp', description: 'Composes Lang, Logger, Formats, Frontend, Dom, Storage, Http, Timers and Cache.', factories: ['createActiveApp'] @@ -66,14 +66,14 @@ App.setLocale('es-MX');`; }, { title: 'Session', - alias: '$sess', + alias: '$session', href: '/active/docs/sess', description: 'Session lifecycle: adopt, revoke, refresh, auto-refresh, SSR adoption.', factories: ['createActiveSession', 'createEngineSession'] }, { title: 'Permissions', - alias: '$perm', + alias: '$permissions', href: '/active/docs/perm', description: 'Authorization runtime: policies, decisions, snapshot cache, guard.', factories: ['createActivePermissions', 'createEnginePermissions'] @@ -85,14 +85,14 @@ App.setLocale('es-MX');`; items: [ { title: 'Cache', - alias: '$cach', + alias: '$cache', href: '/active/docs/cach', description: 'Data cache: deterministic keys, scopes, stale/revalidate, tags.', factories: ['createActiveCache', 'createEngineCache'] }, { title: 'Storage', - alias: '$stor', + alias: '$storage', href: '/active/docs/stor', description: 'Reactive sync key/value: adapters, version+migrate, TTL, validation.', factories: ['createActiveStorage', 'createEngineStorage'] @@ -118,7 +118,7 @@ App.setLocale('es-MX');`; }, { title: 'Formats', - alias: '$fmts', + alias: '$formats', href: '/active/docs/fmts', description: 'Localized formatting: numbers, currency, units, dates.', factories: ['createActiveFormats', 'createEngineFormats'] @@ -130,7 +130,7 @@ App.setLocale('es-MX');`; items: [ { title: 'Frontend', - alias: '$fend', + alias: '$frontend', href: '/active/docs/fend', description: 'Frontend preferences: theme, mode, dir, density.', factories: ['createActiveFrontend'] @@ -161,21 +161,21 @@ App.setLocale('es-MX');`; items: [ { title: 'Logger', - alias: '$logr', + alias: '$logger', href: '/active/docs/logr', description: 'Structured logger: levels, transports, filters, vitals, dispose.', factories: ['createEngineLogger'] }, { title: 'Timers', - alias: '$timr', - href: '/active/docs/timr', + alias: '$timer', + href: '/active/docs/timer', description: 'Deterministic timer scheduler: clock injection, intervals, snapshots.', factories: ['createActiveTimers', 'createEngineTimers'] }, { title: 'Connections', - alias: '$conn', + alias: '$connection', href: '/active/docs/conn', description: 'Realtime connection registry: transports, reconnect, heartbeat, channels.', factories: ['createActiveConnections', 'createEngineConnections'] @@ -243,7 +243,7 @@ App.setLocale('es-MX');`;

Quick look

- Every app starts from $aapp, which wires every artifact and exposes them on + Every app starts from $active-app, which wires every artifact and exposes them on a single App object. Identity, permissions, cache and connections are factories built from the same App.

diff --git a/src/web/routes/active/_components/ArtifactDoc.svelte b/src/web/routes/active/_components/ArtifactDoc.svelte index 0b165aa..71a4ca0 100644 --- a/src/web/routes/active/_components/ArtifactDoc.svelte +++ b/src/web/routes/active/_components/ArtifactDoc.svelte @@ -81,7 +81,7 @@ rows.push( { step: 'Logging', - where: '$libs/logr.Logger plus module consts.ts', + where: '$libs/logger.Logger plus module consts.ts', rule: 'Use the shared Logger contract. Do not create local logger interfaces or hard-code categories/messages.' }, { diff --git a/src/web/routes/active/_components/PageNav.svelte b/src/web/routes/active/_components/PageNav.svelte index 1384a4a..0947095 100644 --- a/src/web/routes/active/_components/PageNav.svelte +++ b/src/web/routes/active/_components/PageNav.svelte @@ -1,5 +1,5 @@ - App ($aapp) — Active + App ($active-app) — Active
App.dispose());`;

Overview

- $aapp is the single composition root. It owns nine always-present + $active-app is the single composition root. It owns nine always-present artifacts, exposes them through stable getters, and provides factories for the five feature-scoped artifacts. Every member of App is present whether or not the corresponding option was passed: missing configurations get a @@ -225,7 +225,7 @@ onDestroy(() => App.dispose());`;

createActiveSession, createActiveAuth and createActivePermissions can only be called once per App. - A second call throws SessAlreadyCreatedError / + A second call throws SessionAlreadyCreatedError / AappAlreadyCreatedError. createActiveConnections is currently multi-instance.

@@ -248,8 +248,8 @@ onDestroy(() => App.dispose());`;

orchestration controls translators only. Today the built-in sources are - identity (SESS_EVENT_CHANGED to AAPP_EVENT_USER_IDENTITY_CHANGED) - and dispose (App.dispose() to AAPP_EVENT_DISPOSE_STARTING). + identity (SESSION_EVENT_CHANGED to APP_EVENT_USER_IDENTITY_CHANGED) + and dispose (App.dispose() to APP_EVENT_DISPOSE_STARTING). Other public events such as tenant switch, permission refresh, connectivity and cache invalidation are typed contracts for explicit publication or future translators.

@@ -269,7 +269,7 @@ onDestroy(() => App.dispose());`;

- AAPP_EVENT_* payloads are observable framework contracts. Do not put + APP_EVENT_* payloads are observable framework contracts. Do not put tokens, passwords, authorization headers, refresh secrets or sensitive hashes in them. Use actor ids, tenant ids, causes and correlation ids.

@@ -299,7 +299,7 @@ onDestroy(() => App.dispose());`;
  • aapp is browser-first. Server entry points should consume the - Engine* factories directly from each artifact, not $aapp. + Engine* factories directly from each artifact, not $active-app.
  • @@ -325,13 +325,13 @@ onDestroy(() => App.dispose());`;

    Testing

    - $aapp/testing exposes a deterministic clock and synchronous transports for + $active-app/testing exposes a deterministic clock and synchronous transports for integration tests. Suites under src/arts/aapp/test/ verify composition, factory enforcement, and disposal order.

    diff --git a/src/web/routes/active/docs/lang/+page.svelte b/src/web/routes/active/docs/lang/+page.svelte index b28c147..482850c 100644 --- a/src/web/routes/active/docs/lang/+page.svelte +++ b/src/web/routes/active/docs/lang/+page.svelte @@ -72,7 +72,7 @@ export const schema = { export type AppLangSchema = typeof schema; // src/lib/app.ts -import { createActiveApp } from '$aapp'; +import { createActiveApp } from '$active-app'; import { schema } from '$lib/i18n/schema'; export const App = createActiveApp({ @@ -314,7 +314,7 @@ App.Lang.t('home.title'); // 'home.title'`; }, { step: 'Diagnostics', - where: 'src/arts/lang/consts.ts and $libs/logr.Logger', + where: 'src/arts/lang/consts.ts and $libs/logger.Logger', rule: 'Use the shared Logger contract and constants for diagnostics; no local logger shapes or hard-coded messages.' }, { diff --git a/src/web/routes/active/docs/perm/+page.svelte b/src/web/routes/active/docs/perm/+page.svelte index 833bb88..0e1f9e9 100644 --- a/src/web/routes/active/docs/perm/+page.svelte +++ b/src/web/routes/active/docs/perm/+page.svelte @@ -9,10 +9,10 @@ import { createEnginePermissions, createPermissionHttpHandlers, - PERM_SQL_SCHEMA_MODEL, + PERMISSION_SQL_SCHEMA_MODEL, type PermissionPolicyDbRow, type PermissionRelationDbRow -} from '$svrs/perm'; +} from '$svrs/permissions'; // Shared policy language and active client import { @@ -34,10 +34,10 @@ import { audit, requireMfa, createSqlCompiler -} from '$perm'; +} from '$permissions'; // UI helper is a component file, not a barrel export. -import Can from '$perm/Can.svelte';`; +import Can from '$permissions/Can.svelte';`; const schemaExample = `const schema = definePermSchema({ actors: { @@ -163,20 +163,20 @@ type DbPostTeamMember = { role: 'member' | 'admin'; };`; - const serverSqlModel = `// Server-side model exported by $svrs/perm. + const serverSqlModel = `// Server-side model exported by $svrs/permissions. import { - PERM_SQL_SCHEMA_MODEL, + PERMISSION_SQL_SCHEMA_MODEL, type PermissionPolicyDbRow, type PermissionRelationDbRow, type PermissionDecisionAuditDbRow -} from '$svrs/perm'; +} from '$svrs/permissions'; -PERM_SQL_SCHEMA_MODEL.tables.POLICIES; // permission_policies -PERM_SQL_SCHEMA_MODEL.tables.RELATIONS; // permission_relations -PERM_SQL_SCHEMA_MODEL.tables.DECISION_AUDIT; // permission_decision_audit +PERMISSION_SQL_SCHEMA_MODEL.tables.POLICIES; // permission_policies +PERMISSION_SQL_SCHEMA_MODEL.tables.RELATIONS; // permission_relations +PERMISSION_SQL_SCHEMA_MODEL.tables.DECISION_AUDIT; // permission_decision_audit // Reference migration: -// src/svrs/perm/sql/postgres.sql`; +// src/svrs/permissions/sql/postgres.sql`; const policyPersistenceExample = `// Static apps can keep policies in TypeScript. // Dynamic/tenant apps can load validated PolicyIR rows from the database. @@ -384,8 +384,8 @@ if (decision.effect === 'allow') { }`; const canExample = ` - Permissions ($perm) — Active + Permissions ($permissions) — Active
    @@ -574,7 +574,7 @@ for (const obligation of decision.obligations ?? []) {

    Mental model

    Authorization is a server decision with a client mirror. The policy language in - $libs/perm defines what can be said, $svrs/perm evaluates it against + $libs/permissions defines what can be said, $svrs/permissions evaluates it against trusted actor/resource/context data, and $arts/perm only mirrors decisions for UI responsiveness. If a route, action, WebSocket channel or job touches protected data, the server engine must decide again even if the button was hidden by @@ -796,7 +796,7 @@ for (const obligation of decision.obligations ?? []) {

    The repository includes a PostgreSQL reference migration at - src/svrs/perm/sql/postgres.sql. It defines + src/svrs/permissions/sql/postgres.sql. It defines permission_policies, permission_relations and permission_decision_audit. Treat it as the supported starting point for a real database model, then adapt naming, migrations and ORM mappings to the application. @@ -1026,7 +1026,7 @@ for (const obligation of decision.obligations ?? []) {

    diff --git a/src/web/routes/active/docs/svrs/+page.svelte b/src/web/routes/active/docs/svrs/+page.svelte index 2082f9c..4438904 100644 --- a/src/web/routes/active/docs/svrs/+page.svelte +++ b/src/web/routes/active/docs/svrs/+page.svelte @@ -7,13 +7,13 @@ const serverImports = `import { AuthServer, - CachServer, + CacheServer, PermServer } from '$svrs'; const Auth = AuthServer.createEngineAuth(options); const Permissions = PermServer.createEnginePermissions(options); -const Cache = CachServer.createEngineCache(options);`; +const Cache = CacheServer.createEngineCache(options);`; const authExample = `import { createEngineAuth, @@ -40,8 +40,8 @@ const Auth = createEngineAuth({ store: createMemoryAuthAdapter(), actors: createMemoryAuthActors(), sess: createMemoryAuthSessPort(), - logr: App.Logger, - timr: { nowMs: () => Date.now() }, + logger: App.Logger, + timer: { nowMs: () => Date.now() }, crypto: createWebCryptoAuthCrypto(), passwordHasher: createTestPasswordHasher() } @@ -54,7 +54,7 @@ const Auth = createEngineAuth({ definePolicies, allow, attr -} from '$svrs/perm'; +} from '$svrs/permissions'; const schema = definePermSchema({ actors: { user: { attributes: { role: 'string' } } }, @@ -75,7 +75,7 @@ const handlers = createPermissionHttpHandlers(Permissions, resolveActorFromReque createEngineCache, memoryCacheAdapter, defaultCachePolicies -} from '$svrs/cach'; +} from '$svrs/cache'; const Cache = createEngineCache({ namespace: 'api', @@ -144,7 +144,7 @@ export const POST = async ({ request }) => { }, { step: 'Shared contracts', - where: 'src/libs/auth, src/libs/perm, src/libs/cach', + where: 'src/libs/auth, src/libs/permissions, src/libs/cache', rule: 'Put reusable types, constants and pure helpers in libs before duplicating them inside a server module.' }, { @@ -154,17 +154,17 @@ export const POST = async ({ request }) => { }, { step: 'Security boundaries', - where: '$svrs/auth, $svrs/perm, $svrs/cach', + where: '$svrs/auth, $svrs/permissions, $svrs/cache', rule: 'Auth proves identity, perm authorizes actions and cach preserves private scopes. Do not merge those responsibilities.' }, { step: 'Diagnostics', - where: '$libs/logr.Logger plus module consts.ts', + where: '$libs/logger.Logger plus module consts.ts', rule: 'Use the shared Logger contract and constants. Server logs must avoid secrets, tokens and raw credentials.' }, { step: 'Tests', - where: 'src/svrs/auth/test, src/svrs/perm/test, src/svrs/cach/test and /test/ecosystem', + where: 'src/svrs/auth/test, src/svrs/permissions/test, src/svrs/cache/test and /test/ecosystem', rule: 'Any server behavior change needs contract tests and at least one integration scenario.' } ] as const; @@ -181,7 +181,7 @@ export const POST = async ({ request }) => { alias="$svrs" summary="Server-authoritative engines for auth, permissions and cache. This is where security decisions, request-scoped composition and backend adapters live." factories={['createEngineAuth', 'createEnginePermissions', 'createEngineCache']} - dependsOn={['$libs/auth', '$libs/perm', '$libs/cach', '$logr', '$timr', '$http']} + dependsOn={['$libs/auth', '$libs/permissions', '$libs/cache', '$logger', '$timer', '$http']} layer="Server engines" /> @@ -196,13 +196,13 @@ export const POST = async ({ request }) => {

    Overview

    The framework has three server modules today: $svrs/auth, - $svrs/perm and $svrs/cach. They exist because these artifacts + $svrs/permissions and $svrs/cache. They exist because these artifacts have a backend half and a frontend half. The shared language lives in $libs/*, the server authority lives in $svrs/*, and the reactive browser/client wrappers live in $arts/*.

    - This is not a duplicate of App. $aapp is a browser/client composition root. + This is not a duplicate of App. $active-app is a browser/client composition root. Server code should import explicit engines from $svrs or from each submodule.

    @@ -239,12 +239,12 @@ export const POST = async ({ request }) => { Identity proof, CSRF, password/recovery flows, session binding, device primitives and security events. - $svrs/perm + $svrs/permissions createEnginePermissions(options) Authorization decisions, policy evaluation, explanations, query plans and HTTP handlers for the active client. - $svrs/cach + $svrs/cache createEngineCache(options) Backend data coherence: query cache, scopes, policies, tags, epochs, explainability and events. @@ -269,13 +269,13 @@ export const POST = async ({ request }) => { EnginecreateEngineAuth, EngineAuthCurrent view, password flows, CSRF, recovery, devices, OAuth/MFA primitives and event subscriptions. HandlerscreateAuthRouteHandlers, createSvelteKitAuthHandleHTTP/SvelteKit integration. Default route handlers cover current, CSRF, password, recovery and sign-out. AdapterscreateMemoryAuthAdapter, createDbAuthAdapter, password/crypto/mailer/test adaptersPersistence and mechanism ports without hard dependency on an ORM/provider. - IntegrationsAuthSessPort, AuthCachPort, AuthPermPort, AuthHttpPortPorts for sess, cach, perm, http, timr and logr. + IntegrationsAuthSessPort, AuthCachePort, AuthPermissionsPort, AuthHttpPortPorts for sess, cach, perm, http, timer and logr.

    Permissions server

    - $svrs/perm is the authorization authority. The browser can ask for a + $svrs/permissions is the authorization authority. The browser can ask for a decision, but protected routes must still call the server engine.

    @@ -297,7 +297,7 @@ export const POST = async ({ request }) => {

    Cache server

    - $svrs/cach wraps the pure $libs/cach runtime with disposal, + $svrs/cache wraps the pure $libs/cache runtime with disposal, diagnostics and the server barrel. Use it for server reads, SSR, API handlers and jobs.

    @@ -330,7 +330,7 @@ export const POST = async ({ request }) => {
  • $libs/* defines shared contracts, constants and pure helpers.
  • $svrs/* owns server authority, secrets, ports and backend adapters.
  • $arts/* owns active/client state and browser ergonomics.
  • -
  • $aapp composes client roots; it should not be imported as the server authority.
  • +
  • $active-app composes client roots; it should not be imported as the server authority.
  • Do not expose server stores, password hashes, refresh tokens, CSRF secrets or provider tokens to active/client modules.
  • @@ -365,8 +365,8 @@ export const POST = async ({ request }) => { src/svrs/auth/testServer auth flows.CSRF, password and error guards. - src/svrs/perm/testPermission engine.Policy runtime and handler behavior. - src/svrs/cach/testCache engine.Query, invalidation, explain and disposal. + src/svrs/permissions/testPermission engine.Policy runtime and handler behavior. + src/svrs/cache/testCache engine.Query, invalidation, explain and disposal. /test/ecosystemCross-module scenario.Client route that exercises active/server interactions where available. diff --git a/src/web/routes/active/docs/timr/+page.svelte b/src/web/routes/active/docs/timr/+page.svelte index aaae0b8..0a15ec0 100644 --- a/src/web/routes/active/docs/timr/+page.svelte +++ b/src/web/routes/active/docs/timr/+page.svelte @@ -3,4 +3,4 @@ import { artifactDocs } from '../../_data/artifact-docs'; - + diff --git a/src/web/routes/active/get-started/ai-agents/+page.svelte b/src/web/routes/active/get-started/ai-agents/+page.svelte index 1afc80e..608e370 100644 --- a/src/web/routes/active/get-started/ai-agents/+page.svelte +++ b/src/web/routes/active/get-started/ai-agents/+page.svelte @@ -20,16 +20,16 @@ arts/* → public runtime artifacts and active/client wrappers aapp → client composition root, not server authority`; const importRules = `// Good: modules depend on shared contracts -import type { Logger } from '$libs/logr'; +import type { Logger } from '$libs/logger'; // Good: server authority comes from $svrs import { createEngineAuth } from '$svrs/auth'; -import { createEnginePermissions } from '$svrs/perm'; -import { createEngineCache } from '$svrs/cach'; +import { createEnginePermissions } from '$svrs/permissions'; +import { createEngineCache } from '$svrs/cache'; // Good: active/client wrappers come from $arts aliases -import { createActiveStorage } from '$stor'; -import { createActiveConnections } from '$conn'; +import { createActiveStorage } from '$storage'; +import { createActiveConnections } from '$connection'; // Bad: inventing a per-module logger shape instead of using Logger type LocalModuleLogger = Pick;`; @@ -46,11 +46,11 @@ type LocalModuleLogger = Pick;`; }`; const constantsRule = `// Good -export const CONN_LOG_MESSAGES = { +export const CONNECTION_LOG_MESSAGES = { RECONNECT_SCHEDULED: 'connection.reconnect.scheduled' } as const; -logger.debug(LOGGER_CATEGORY, CONN_LOG_MESSAGES.RECONNECT_SCHEDULED, { +logger.debug(LOGGER_CATEGORY, CONNECTION_LOG_MESSAGES.RECONNECT_SCHEDULED, { context: { name, attempt } }); @@ -64,7 +64,7 @@ logger.debug('conn', 'reconnect scheduled', { name, attempt });`; - Tests added or updated when behavior changed - No invented public API - No magic strings for logs/events/errors/protocol methods -- No new local logger shape when Logger from $libs/logr is enough +- No new local logger shape when Logger from $libs/logger is enough - Server authority remains under $svrs - Active/client code does not store secrets or make security decisions - Dirty unrelated files were not reverted or formatted`; @@ -165,7 +165,7 @@ logger.debug('conn', 'reconnect scheduled', { name, attempt });`;

    Logging and diagnostics

    - Every module receives the minimal Logger contract from $libs/logr. + Every module receives the minimal Logger contract from $libs/logger. Diagnostics are allowed as a catalog layer, but diagnostics emit normal logger calls. Do not create module-local logger contracts or aliases unless there is a documented integration reason. @@ -175,7 +175,7 @@ logger.debug('conn', 'reconnect scheduled', { name, attempt });`;

    • Logger categories, messages and diagnostic event names must be constants.
    • Do not hard-code logger strings inside implementation bodies.
    • -
    • Do not couple every module to $logr; depend on $libs/logr for the contract.
    • +
    • Do not couple every module to $logger; depend on $libs/logger for the contract.
    • EngineLogger is the implementation; Logger is the dependency accepted by modules.
    @@ -234,7 +234,7 @@ logger.debug('conn', 'reconnect scheduled', { name, attempt });`; - Verify public docs match real exports and types. - Check layer boundaries: libs vs svrs vs arts vs aapp. - Find magic strings in logs, events, errors, methods, protocol messages and routes. -- Check logger usage: modules should accept Logger from $libs/logr. +- Check logger usage: modules should accept Logger from $libs/logger. - Verify ActiveEngine consistency: loading, lastError, disposed, snapshot, clearError, onChange, dispose. - Verify server authority for auth, perm and cach. - Identify duplicated boilerplate, oversized files, missing constants and missing tests. diff --git a/src/web/routes/active/get-started/composition/+page.svelte b/src/web/routes/active/get-started/composition/+page.svelte index 97c820c..ec319f3 100644 --- a/src/web/routes/active/get-started/composition/+page.svelte +++ b/src/web/routes/active/get-started/composition/+page.svelte @@ -6,7 +6,7 @@ const dependencyDiagram = ` lang logr \\ / | \\ \\ / | \\ - fmts http timr + fmts http timer \\ | /|\\ adom ─── fend \\ | / | conn \\ \\ \\ | / | \\ @@ -29,7 +29,7 @@ adom ─── fend \\ | / | conn

    Composition

    - Active is wired through one entry point — $aapp — and follows two factory + Active is wired through one entry point — $active-app — and follows two factory conventions and one shared contract. Once you internalise the three pieces, every artifact looks the same.

    @@ -92,16 +92,16 @@ adom ─── fend \\ | / | conn
    • fmts consumes logr only inside its currency rate fetcher diagnostics.
    • sium takes lang and logr via injection and falls back to local message interpolation when omitted.
    • -
    • timr is the deterministic scheduler consumed by sess and conn.
    • +
    • timer is the deterministic scheduler consumed by sess and conn.
    • http takes logr via injection (auto-wired through aapp).
    • -
    • sess uses stor for persistence, timr for auto-refresh, and http for 401-rescue integration.
    • -
    • conn uses timr for reconnect / heartbeat / ack timeouts and accepts the App session bridge when composed through aapp.
    • +
    • sess uses stor for persistence, timer for auto-refresh, and http for 401-rescue integration.
    • +
    • conn uses timer for reconnect / heartbeat / ack timeouts and accepts the App session bridge when composed through aapp.
    • auth / perm / cach split cleanly: server side under $svrs/, client reflector under $arts/.

    Always-present roots vs scoped factories

    - The composer in $aapp distinguishes two kinds of artifacts: + The composer in $active-app distinguishes two kinds of artifacts:

    @@ -132,7 +132,7 @@ adom ─── fend \\ | / | conn

    Sess, Auth and Permissions are singletons inside an - App. Calling their factory twice throws SessAlreadyCreatedError, + App. Calling their factory twice throws SessionAlreadyCreatedError, AappAlreadyCreatedError respectively. Connections is currently multi-instance — each call returns a fresh registry.

    diff --git a/src/web/routes/active/get-started/ecosystem/+page.svelte b/src/web/routes/active/get-started/ecosystem/+page.svelte index c3dbfcb..89cd975 100644 --- a/src/web/routes/active/get-started/ecosystem/+page.svelte +++ b/src/web/routes/active/get-started/ecosystem/+page.svelte @@ -8,7 +8,7 @@ src/svrs/* server-authoritative engines and backend adapters src/arts/* client/runtime artifacts, active wrappers and browser ergonomics src/web/routes documentation, test pages and app routes -$aapp client composition root that wires the active ecosystem`; +$active-app client composition root that wires the active ecosystem`; const appFlow = `const App = createActiveApp({ logger: { level: LogLevel.INFO, transports: [consoleTransport()] }, @@ -27,7 +27,7 @@ const Connections = App.createActiveConnections();`; const serverFlow = `const Auth = createEngineAuth({ security, - ports: { store, actors, sess, cach, logr, timr, crypto, passwordHasher, mailer } + ports: { store, actors, sess, cache, logger, timer, crypto, passwordHasher, mailer } }); const Permissions = createEnginePermissions({ @@ -50,8 +50,8 @@ const Cache = createEngineCache({ → ActiveAuth / ActivePermissions / App.Http / ActiveCache → SvelteKit endpoint or action → $svrs/auth reads Auth current and session binding - → $svrs/perm asserts actor can perform action - → $svrs/cach serves or fetches scoped data + → $svrs/permissions asserts actor can perform action + → $svrs/cache serves or fetches scoped data → App.Http receives typed result → Active roots update snapshots and UI`; @@ -131,7 +131,7 @@ const Cache = createEngineCache({

    - auth, perm and cach have server-level modules + auth, perm and cache have server-level modules because they can affect identity, access or private data coherence. Their active counterparts are reflections for UX, not the source of truth.

    @@ -154,11 +154,11 @@ const Cache = createEngineCache({
    - + - +
    CompositionaappSingle client root, always-present services, feature factories, disposal order.
    Identityauth, sess, permProve identity, keep session continuity, decide access.
    Datahttp, cach, storRemote calls, coherent cached data, safe local persistence.
    Datahttp, cache, storRemote calls, coherent cached data, safe local persistence.
    I18n and formatslang, fmtsText translation plus locale-driven numbers, currency, units and dates.
    Frontend runtimefend, adomGlobal visual preferences, direction, theme, DOM writes and responsive helpers.
    ValidationsiumPage-scoped schemas, issues, metadata and translated validation messages.
    Infrastructurelogr, timr, connStructured logs, deterministic timers and realtime connections.
    Infrastructurelogr, timer, connStructured logs, deterministic timers and realtime connections.
    @@ -211,7 +211,7 @@ const Cache = createEngineCache({

    Identity-sensitive work follows a stricter chain. auth proves identity, sess keeps continuity, perm decides access, and - cach must scope or invalidate private data. + cache must scope or invalidate private data.

    @@ -238,9 +238,9 @@ const Cache = createEngineCache({ Persist non-secret preferences or drafts.storsess, localStorage calls spread through pages. Keep logged-in continuity.sessauth alone. Prove identity or run login/recovery flows.$svrs/auth plus $authperm or client-only checks. - Decide if an actor can do something.$svrs/perm plus $permauth, roles hard-coded in UI. - Cache data with scopes and invalidation.$svrs/cach or $cachstor as a query cache. - Schedule retries, refreshes or timeouts.timrraw setTimeout scattered across modules. + Decide if an actor can do something.$svrs/permissions plus $permissionsauth, roles hard-coded in UI. + Cache data with scopes and invalidation.$svrs/cache or $cachestor as a query cache. + Schedule retries, refreshes or timeouts.timerraw setTimeout scattered across modules. Open realtime sockets and channels.conncustom WebSocket state in components. Validate forms and generate issues.siumperm or manual string errors. @@ -283,7 +283,7 @@ const Cache = createEngineCache({ Unitsrc/arts/*/test, src/libs/*/test, src/svrs/*/testEach artifact obeys its own contract. Integrationsrc/arts/aapp/testApp wiring, locale propagation, session bridge, disposal order. - Scenario/test/ecosystemA realistic app story with auth, sess, perm, cach, http, sium, conn and UI state together. + Scenario/test/ecosystemA realistic app story with auth, sess, perm, cache, http, sium, conn and UI state together. diff --git a/src/web/routes/active/get-started/installation/+page.svelte b/src/web/routes/active/get-started/installation/+page.svelte index de17a26..385e01e 100644 --- a/src/web/routes/active/get-started/installation/+page.svelte +++ b/src/web/routes/active/get-started/installation/+page.svelte @@ -26,7 +26,7 @@
    • SvelteKit 2.x with Svelte 5.x (runes mode).
    • TypeScript 5.4+ recommended for full inference.
    • -
    • A Node 22+ runtime if you plan to use the server-authoritative engines ($svrs/auth, $svrs/perm, $svrs/cach).
    • +
    • A Node 22+ runtime if you plan to use the server-authoritative engines ($svrs/auth, $svrs/permissions, $svrs/cache).

    Path aliases

    @@ -40,22 +40,22 @@ title="svelte.config.js" lang="js" code={`alias: { - $aapp: 'src/arts/aapp', + $active-app: 'src/arts/aapp', $adom: 'src/arts/adom', $auth: 'src/arts/auth', - $cach: 'src/arts/cach', - $conn: 'src/arts/conn', - $fend: 'src/arts/fend', - $fmts: 'src/arts/fmts', + $cache: 'src/arts/cach', + $connection: 'src/arts/conn', + $frontend: 'src/arts/fend', + $formats: 'src/arts/fmts', $http: 'src/arts/http', $lang: 'src/arts/lang', - $logr: 'src/arts/logr', - $perm: 'src/arts/perm', - $sess: 'src/arts/sess', + $logger: 'src/arts/logr', + $permissions: 'src/arts/perm', + $session: 'src/arts/sess', $sium: 'src/arts/sium', - $stor: 'src/arts/stor', + $storage: 'src/arts/stor', $svrs: 'src/svrs', - $timr: 'src/arts/timr', + $timer: 'src/arts/timer', $libs: 'src/libs', $locale: 'src/libs/locale', $reactive: 'src/libs/reactive' @@ -65,7 +65,7 @@

    Aliases make every artifact import its peers as $lang / - $logr, regardless of where it lives. This keeps the boundary between layers + $logger, regardless of where it lives. This keeps the boundary between layers explicit and prevents accidental cross-imports.

    @@ -115,7 +115,7 @@ ACTIVE_BUNDLE_GZIP_LIMIT_KB=70 npm run test:bundle`} Existing method namesNo silent removal or semantic inversion.Optional parameters and overloads. Error classes and guardsKeep type guards valid.New error subclasses/codes. Constants for public stringsNames stay searchable and centralized.New constants for new events/routes/methods. - Logger contractModules depend on $libs/logr.Logger.EngineLogger may add runtime helpers. + Logger contractModules depend on $libs/logger.Logger.EngineLogger may add runtime helpers. diff --git a/src/web/routes/test/+page.svelte b/src/web/routes/test/+page.svelte index a4fae67..6931a99 100644 --- a/src/web/routes/test/+page.svelte +++ b/src/web/routes/test/+page.svelte @@ -1,5 +1,5 @@ @@ -17,7 +17,7 @@
  • /test/ecosystem — app demo de integración total: auth, - sess, perm, cach, http, stor, sium, fmts, fend, adom, timr, conn, lang y logr trabajando en el mismo + sess, perm, cach, http, stor, sium, fmts, fend, adom, timer, conn, lang y logr trabajando en el mismo flujo.
  • @@ -64,7 +64,7 @@ eventos.
  • - /test/timr — runtime timer scheduler con + /test/timer — runtime timer scheduler con ActiveTimers: schedule, debounce vía replace:true, intervals con awaitTask y maxRuns, cancelAll por scope, backoff exponencial determinista y tabla reactiva de entries. diff --git a/src/web/routes/test/aapp/+page.svelte b/src/web/routes/test/aapp/+page.svelte index 99c7a89..5846ede 100644 --- a/src/web/routes/test/aapp/+page.svelte +++ b/src/web/routes/test/aapp/+page.svelte @@ -1,9 +1,9 @@ - Test — timr + Test — timer
    -

    arts/timr — runtime timer scheduler

    +

    arts/timer — runtime timer scheduler

    Demo de createActiveTimers (wrapper reactivo de EngineTimers): schedule, debounce vía diff --git a/svelte.config.js b/svelte.config.js index 131772f..3afd9e3 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -15,26 +15,26 @@ const config = { routes: 'src/web/routes' }, alias: { - $aapp: 'src/arts/aapp', + '$active-app': 'src/arts/active-app', $adom: 'src/arts/adom', $auth: 'src/arts/auth', - $buss: 'src/arts/buss', - $cach: 'src/arts/cach', - $conn: 'src/arts/conn', - $fend: 'src/arts/fend', + $bus: 'src/arts/bus', + $cache: 'src/arts/cache', + $connection: 'src/arts/connection', + $frontend: 'src/arts/frontend', $libs: 'src/libs', $locale: 'src/libs/locale', $reactive: 'src/libs/reactive', - $fmts: 'src/arts/fmts', + $formats: 'src/arts/formats', $http: 'src/arts/http', $lang: 'src/arts/lang', - $logr: 'src/arts/logr', - $perm: 'src/arts/perm', - $sess: 'src/arts/sess', + $logger: 'src/arts/logger', + $permissions: 'src/arts/permissions', + $session: 'src/arts/session', $sium: 'src/arts/sium', - $stor: 'src/arts/stor', + $storage: 'src/arts/storage', $svrs: 'src/svrs', - $timr: 'src/arts/timr' + $timer: 'src/arts/timer' } } };