Vault y archivos¶
El autoguardado serializa las escrituras de página dentro del cliente web. Una versión obsoleta devuelve un conflicto (409), no un error de página inexistente (404), aunque la lectura condicional omita el cuerpo. Las páginas inexistentes siguen devolviendo 404; los conflictos no sobrescriben silenciosamente contenido más reciente.
Se puede retirar del registro un alias inactivo si otra entrada del mismo espacio de trabajo conserva la carpeta. La eliminación utiliza la misma identidad seleccionada que el catálogo y sigue protegiendo la entrada activa y la última entrada del vault principal. Retirar una entrada compartida conserva la carpeta, las bases de datos locales, las cachés y el índice de archivos; no se permite borrar los archivos mientras otro vault registrado utilice la misma ruta, aunque pertenezca a otro espacio de trabajo.
Contratos de comentarios y traducciones¶
La persistencia de comentarios valida solo el diccionario o lista raíz
almacenado; conserva las claves desconocidas, los valores anidados y la identidad
de los registros. Los modelos HTTP siguen validando las respuestas.
comments/composition.py construye dependencias resueltas en el momento de uso
en el punto de registro original y permite importar primero la fachada o los
comentarios. La importación Markdown conserva el corte nativo del ID en
colisiones de nombres. Las suscripciones SSE siguen limitadas al vault y
retiran su propia cola al cancelarse.
Los helpers de traducción, la búsqueda, los efectos de metadatos y los servicios de filas y páginas usan metadatos abiertos sin suponer claves YAML solo textuales. El ciclo de vida comprueba los responsables reales de los callbacks y un protocolo de conector cargado cuando se necesita; conserva las dependencias capturadas y resuelve tarde los miembros sustituibles. Los resultados de creación de páginas usan un contrato de lectura de campos, distinto de los metadatos mutables. Los handlers de escritura registrados mantienen el retorno asíncrono y el contexto inyectado opcional. Los resultados de Web Clipper pasan por el modelo de respuesta existente sin coerción de campos.
Estos cambios conservan los límites de captura de errores de los proveedores, la recuperación ordenada del disco, los idiomas de destino duplicados y el orden de escritura de estados. Las pruebas sintéticas usan dobles de proveedor, no cuentas reales de la nube. La composición Drupal y el HTTP de traducción comprueban ahora los responsables reales de las dependencias; esto no certifica todo el backend, un servidor Drupal real ni una migración de proveedor de nube.
Contratos del transporte Drupal y del HTTP¶
El mapeo de campos Drupal, la preparación multimedia, el descubrimiento de idiomas, la coincidencia de títulos y la sincronización de filas conservan metadatos abiertos e identificadores opacos del conector. Las respuestas construidas tienen tipos explícitos; los valores decodificados del transporte no presuponen un esquema de la aplicación. Se conservan el orden de campos, los errores parciales, la identidad de identificadores en caché y los errores nativos de entrada. Las factorías capturan el conector y las clases de error, pero resuelven los miembros sustituibles al llamarlos. Pillow sigue siendo opcional y se carga cuando hace falta.
Las rutas de traducción conservan los esquemas existentes de petición y respuesta. La publicación masiva Drupal convierte cada identificador a texto y conserva duplicados; la traducción masiva filtra identificadores no textuales y elimina duplicados tras recortar espacios. Publicar una fila sigue enviando multimedia por defecto. Las entradas textuales mantienen los valores alternativos para entradas vacías y los errores nativos ante JSON malformado. Los llamadores Python no JSON cuya operación strip personalizada devuelve un valor no textual reciben un error de tipo explícito; esta comprobación solo HTTP no normaliza YAML ni metadatos de plugins. El JSON generado válido con una forma de respuesta incorrecta llega a la validación HTTP, no al resultado alternativo por error de proveedor. Transportes sintéticos y archivos temporales verifican estos contratos sin leer credenciales, publicar contenido ni llamar a proveedores externos.
Responsabilidad¶
El dominio Vault relaciona el Markdown portátil y los recursos con páginas, carpetas, adjuntos, búsquedas, esquemas, historiales, papelera, exportaciones, citas y selección entre varios Vaults. Es el dominio más grande y el principal responsable de la soberanía de los datos.
El reconocimiento local de escritura manuscrita es un adaptador opcional de ingestión en la capa del Vault. Los objetos de modelo y procesador permanecen aislados como valores de terceros en tiempo de ejecución; el servicio expone un resultado tipado que contiene el texto, el reconocimiento sin procesar, los valores por línea, la identidad del modelo y el estado de corrección, sin cambiar el contrato público de subida. Los diccionarios de estado, preparación y reconocimiento se validan mediante modelos de respuesta Pydantic específicos, conservando después su estructura histórica de diccionario para las llamadas directas y la interfaz OpenAPI estable byte a byte.
El catálogo de Vaults marca una sola identidad del registro como activa, aunque varios alias antiguos compartan carpeta. La selección sigue la cabecera de la petición, el parámetro de consulta y después la cookie, limitada a las entradas que coinciden con la carpeta activa resuelta. La selección actual del navegador tiene prioridad sobre los indicadores guardados en caché, manteniendo sincronizados el menú, el selector de configuración y el nombre del Vault mostrado. Cambiar de Vault no reescribe los alias del registro ni los metadatos de favoritos.
Ciclo de vida de las páginas¶
sequenceDiagram
participant UI as Interfaz o editor del vault
participant R as API del dominio Vault
participant C as Contexto del vault
participant F as Proveedor de archivos
participant I as Índices de páginas y enlaces
UI->>R: Leer página por identificador estable
R->>C: Resolver vault activo autorizado
C->>I: Resolver identificador a ruta actual
I->>F: Leer Markdown si la caché no basta
F-->>R: Contenido, metadatos y ETag
R-->>UI: Representación editable
UI->>R: Guardar con el ETag esperado
R->>F: Escritura atómica si el ETag sigue coincidiendo
R->>I: Actualizar entradas de páginas y relaciones
La identidad de una página es independiente de su título y su ruta. El frontmatter
se normaliza en los puntos de escritura, conservando las claves creadas por el
usuario. El estado exclusivamente interno debe almacenarse en archivos auxiliares
de .gnosi cuando exponerlo en el frontmatter contaminaría o desestabilizaría el
contenido portátil.
Las lecturas y escrituras de archivos auxiliares utilizan un único contrato explícito de correspondencia de metadatos, incluidos los resultados de separación, fusión y persistencia portátil. El mecanismo compartido y tolerante de recuperación del frontmatter devuelve los valores escalares de primer nivel como objetos tipados cuando es necesario recuperar YAML; el contenido anidado mal formado se sigue ignorando deliberadamente. Estos contratos no fuerzan la conversión de los valores del usuario ni cambian las protecciones existentes para archivos en la nube.
La lectura de archivos auxiliares admite plataformas como Windows, donde las
estadísticas de archivos no incluyen st_blocks. Si el número de bloques no
está disponible, se lee el JSON local normalmente; un cero explícito sigue
evitando la lectura de un archivo solo en la nube para no bloquearse durante
su descarga. Las páginas guardadas y sus indicadores internos deben seguir
siendo legibles después de reiniciar la aplicación.
pages/markdown_writer.py es la capa canónica de serialización: recupera o crea
un ID estable cuando falta, asigna las claves del esquema a los nombres de
almacenamiento, elimina los campos virtuales, escribe el estado interno en el
archivo auxiliar, decora las relaciones portátiles y materializa las instantáneas
de vistas antes de escribir el archivo de forma atómica.
services/field_resolver.py es responsable de ese contrato de correspondencia de
claves del esquema. Acepta ID de campo inmutables, nombres actuales y alias
históricos, resuelve los conflictos de forma determinista y emite únicamente los
nombres actuales legibles para las personas en las capas de almacenamiento y
respuesta, conservando los metadatos locales ajenos a esa correspondencia.
pages/save_helpers.py se encarga de preparar los metadatos de guardado completo,
seleccionar el destino, reutilizar los ID existentes y crear una versión antes
de escribir. pages/patch_helpers.py se encarga de las lecturas que tienen en
cuenta el ETag, la preparación de metadatos PATCH, la reubicación de archivos y
las actualizaciones coordinadas de las cachés de páginas, cuerpos, citas y
documentos analizados. Los ocho nombres históricos de funciones auxiliares
privadas siguen siendo fachadas ligeras de compatibilidad, y cada colaborador
sustituible o caché mutable se resuelve mediante un puerto tipado en el momento de uso.
Metadatos abiertos y HTTP validado¶
Los documentos internos del registro y de las páginas usan dict[object, object]:
el YAML histórico puede contener claves no textuales y valores de extensiones
sin un esquema declarado. Las comprobaciones de diccionario preservan la
identidad; las fusiones de archivos auxiliares y el mapeo de nombres conservan
los campos desconocidos. Esto es distinto de la validación HTTP pública.
PageInfo mantiene su validación original de claves textuales y el esquema
OpenAPI, mientras la construcción y asignación del índice preservan el documento
abierto. Las cachés de respuesta guardan los objetos de página existentes sin
clonar sus metadatos ni bloqueos.
backend/utils/open_values.py aísla las operaciones nativas sobre entradas opacas.
Sus excepciones de tipado, solo en la entrada, preservan los protocolos de
iteración, conversión numérica, longitud y mapeo de Python y sus errores; nunca
afirman la forma de un registro devuelto. Las pruebas comparan el momento de
las llamadas, las secuencias antiguas, los valores mal formados y la identidad
de objetos compartidos. Esto elimina espacios de nombres dinámicos amplios de
la composición del registro y de la base de páginas, no todos los tipos heredados
del backend.
Contratos de vista previa y escritura¶
pages/preview_routes.py comprueba las dependencias de la fachada con sus
responsables reales. Las respuestas corta y completa comparten una estructura
de caché tipada y preservan los valores opacos de título, icono y portada.
La materialización precede a la lectura; solo errno 35 usa la secuencia de
reintentos existente. Las peticiones simultáneas comparten el mismo resultado,
y la caché se actualiza antes de avisar a las peticiones que esperan.
Los guardados completos y parciales mantienen metadatos abiertos, ETags anulables, el momento de las copias superficiales y la resolución del callback antes de evaluar sus argumentos. Los auxiliares de citas preservan la identidad del diccionario con claves textuales o no textuales. El inventario de documentos y el resolutor de rutas conservan las mutaciones y el comportamiento con entradas malformadas.
Dos casos históricos quedan caracterizados, sin cambiarlos en esta refactorización: un conflicto de ETag en el auxiliar PATCH llega al 404 existente antes de la rama 409 del servicio; cancelar al propietario de una vista previa retira su entrada en curso sin completar su futuro compartido. Son limitaciones de compatibilidad, no una validación satisfactoria de concurrencia. Los esquemas públicos y el comportamiento ordinario de escritura no cambian.
Límites del backend¶
Las lecturas y escrituras de páginas, las vistas previas, la duplicación, el
historial y la papelera se implementan en backend/domains/vault/pages, mientras
que las subidas de recursos, los iconos y el servicio de imágenes están en
backend/domains/vault/assets. El servicio de archivos dentro de las rutas
permitidas, las rutas Library/raw/thumbnail, los tokens de archivos locales, las
subidas de propiedades, los enlaces portátiles y la eliminación física están en
backend/domains/vault/files. Estos paquetes separan los esquemas de solicitud
estrictos, los adaptadores de rutas, los servicios de aplicación, los repositorios
y los responsables únicos de los bloqueos mutables, las cachés y los almacenes
de tokens. El comportamiento nuevo del Vault debe incorporarse a la capa del
dominio que corresponda.
La capa transitoria pages/runtime.py conserva el estado dinámico del módulo
histórico de rutas, pero exige un Vault activo antes de construir rutas del
sistema de archivos o motores de reglas. Sus modelos de solicitud ahora se
vinculan directamente a Pydantic, evitando clases base dependientes del tiempo
de ejecución sin cambiar la identidad pública de su módulo ni el contrato HTTP generado.
backend/domains/media se encarga de resolver las raíces multimedia, del recorrido
recursivo adaptado al proveedor y su caché derivada persistente, de los archivos
auxiliares sincronizados de metadatos y vistas guardadas, los filtros, la
paginación, el árbol de carpetas de carga diferida, las subidas dentro de las
rutas permitidas, la extracción EXIF y la serialización estable de archivos.
backend/services/media_service.py sigue siendo la fachada Python compatible:
conserva la clase histórica, el singleton, la forma de invocación, los
descriptores, el estado y los errores, resolviendo el estado mutable y los
colaboradores sustituibles en el momento de uso. Su constructor interno ahora
tiene una anotación explícita de retorno None, lo que elimina la antigua
excepción de tipado de ese constructor sin cambiar el comportamiento
de construcción. La fachada valida que exista un Vault activo antes de acceder
al sistema de archivos y utiliza los contratos multimedia tipados para raíces,
recorridos, consultas, subidas, datos EXIF e información serializada de archivos.
Los módulos de dominio nunca importan el router HTTP ni la fachada de compatibilidad.
Las rutas HTTP multimedia importan directamente el router compartido y los
servicios estables. media/composition.py conserva la resolución tardía del
servicio y los callbacks de duplicación mediante interfaces con nombre; los
tokens de archivo y bloqueos conservan sus propietarios canónicos. El servicio
concreto se comprueba contra el contrato de las rutas sin conversiones de
resultados. Los valores JSON del proveedor no cambian para los clientes Python;
los modelos HTTP existentes validan la respuesta pública. La conversión única
de fachada y las anotaciones heredadas de metadatos siguen siendo deuda explícita.
Las rutas de dibujos importan directamente el router compartido y los servicios
tipados de dibujos e historial. drawings/composition.py limita los colaboradores
con resolución tardía a DrawingVaultPort: rutas, papelera, serialización y
callbacks del historial. El puerto no tiene miembros Any; su única conversión
de compatibilidad es transitoria hasta separar la composición de los proveedores
heredados. No acredita el tipado completo de la fachada ni del modelo de petición
histórico compartido. No se normalizan resultados solo para tiparlos: las llamadas
directas conservan los datos originales y los modelos HTTP imponen el contrato
existente. Se preservan copias, recuperación, permisos, momento de resolución de
callbacks, valores de metadatos y orden de rutas.
La composición de la vista previa y el guardado de páginas también comparte un único router de tipo acotado para la resolución de títulos y el registro delegado de vistas previas y escrituras. La identidad de las cachés, la correspondencia de alias, las comprobaciones del Vault activo y los esquemas de rutas generados permanecen sin cambios.
Las rutas de traducción y sincronización con Drupal también acotan en la capa del módulo el tipo de su router resuelto en el momento de uso. Las operaciones de traducción de una fila, masivas, por correspondencia, de botones generados y de páginas conservan las comprobaciones de roles, el trabajo en segundo plano y la correspondencia de errores externos, y siguen siendo visibles para el tipado estricto.
El almacenamiento asociado a tablas tiene responsables explícitos.
assets/table_paths.py se encarga de las rutas de recursos confinadas, los
directorios por propiedad, las revisiones y las funciones auxiliares de cambio
de nombre que evitan colisiones; assets/persistence.py, de la ingestión
recursiva de metadatos y la eliminación de recursos de registros dentro de las
rutas permitidas; assets/quarantine.py, de la eliminación de tablas resistente
a fallos y la recuperación al iniciar. tables/folders.py se encarga de crear
y migrar el directorio físico BD/<database>/<table> de la tabla. Estos módulos
reciben puertos acotados del sistema de archivos y del registro de configuración
desde la fachada de compatibilidad y nunca importan el router HTTP.
tables/routes.py ahora es responsable de las 23 operaciones históricas de
bases de datos, tablas, catálogos de opciones, vistas guardadas y esquemas de
carpetas, en su orden original. Sus manejadores estrictos delegan en los servicios
existentes de filas, ciclo de vida, propiedades, opciones y vistas;
tables/composition.py es el conjunto inmutable de dependencias para esas rutas
y para las consultas de filas y el enriquecimiento de metadatos.
tables/security.py expone únicamente las dos fábricas tipadas de autorización
del espacio de trabajo, evitando una dependencia estática del dominio de tablas
respecto a la amplia composición heredada de autenticación. El router heredado
registra las rutas del dominio en una estructura plana por compatibilidad con
los consumidores del inventario de rutas y reexporta los objetos invocables de
Python admitidos.
backend/api/vault_routes.py es ahora un módulo de inicialización de
compatibilidad, sin asumir la implementación del dominio.
Los módulos tipados de backend/domains/vault se encargan del comportamiento
restante de API, anotaciones, citas, dibujos, Drupal, archivos, conocimiento,
enlaces, multimedia, páginas, registro de configuración, tablas y traducción.
El módulo de inicialización carga y registra esos responsables en el orden
histórico del código fuente, mientras que facade_bridge.py conserva las
importaciones admitidas, las variables globales mutables y los puntos de
sustitución dinámica mediante monkeypatch resueltos en el momento de uso. El
router padre sigue exponiendo el mismo inventario plano de APIRoute y un
OpenAPI determinista idéntico byte a byte. Por tanto, la fachada no necesita
ninguna excepción en los controles del código fuente.
pages/foundation.py declara sus funciones antes de cargar la fachada. El punto
de entrada initialize_foundation vincula los proveedores existentes una sola
vez en la posición original del arranque, incluso cuando se importa primero el
módulo de páginas. Las llamadas repetidas conservan los mismos objetos de
dependencias, callbacks capturados y orden de rutas; se rechaza vincular otra
fachada. Las pruebas aisladas comparan ambos órdenes de importación, las
anotaciones resueltas y el OpenAPI completo del Vault, y comprueban claves YAML
antiguas, sidecars y traslados de archivos. Esto elimina un ciclo de
inicialización; no acredita el tipado completo de los contratos de metadatos
heredados que siguen pendientes.
El comportamiento del ciclo de vida de las traducciones es responsabilidad de
backend/domains/vault/translation: la carga opcional de proveedores, la
recuperación de archivos en la nube, la traducción de filas y páginas completas,
los efectos mínimos sobre los metadatos y la propagación de obsolescencia a los
hijos son servicios tipados separados. La capa compartida de funciones auxiliares
puras normaliza las identidades de origen a su forma canónica, detecta cambios
traducibles y campos de idioma, reutiliza etiquetas de opciones existentes y
traduce únicamente los subcampos textuales de las imágenes, conservando el
recurso de origen.
La publicación de filas en Drupal es responsabilidad de
backend/domains/vault/drupal, que separa la correspondencia de campos e
identidades, la preparación de recursos multimedia locales, la conversión de
Markdown y wikilinks, las cachés de idiomas, la correspondencia de títulos y la
sincronización idempotente de nodos. El router de compatibilidad conserva los
decoradores FastAPI originales, los docstrings de las rutas y los puntos de
sustitución Python resueltos en el momento de uso, mientras que el conector de
Drupal sigue siendo la capa de transporte externo. Estos cambios de ubicación
no alteran rutas, datos intercambiados, códigos de estado, tareas en segundo
plano ni el orden de las rutas.
Los proveedores de traducción se cargan dentro de los bloques existentes de gestión de errores durante la petición, con imports de sus propietarios tipados reales y sin afirmaciones de tipos sobre módulos. Las funciones devueltas conservan su identidad, argumentos por nombre originales y sustitución tardía. Los protocolos de página describen el contrato posicional compartido de contenido e idiomas y el argumento de credencial solo por nombre. Los módulos o miembros ausentes conservan los mismos errores HTTP; las credenciales no disponibles conservan el retorno de clave vacía. Cargar estos adaptadores no carga modelos de traducción ni lee credenciales durante el arranque de la aplicación.
La autorización de tablas utiliza los tipos reales de workspace_service.
La dependencia get_workspace_context capturada inicialmente y las consultas
posteriores de require_role conservan su identidad y momento de resolución;
las comprobaciones devuelven la misma instancia de WorkspaceContext o el mismo
error de permisos. No cambian los umbrales de los roles, las reglas de
autenticación ni la selección del workspace.
Índices y cachés¶
El índice de páginas acelera los listados, la resolución de identificadores, el acceso al frontmatter y las búsquedas. El índice de wikilinks resuelve los enlaces entrantes para poder actualizar las referencias al renombrar páginas. Las cachés de cuerpos y documentos analizados evitan lecturas repetidas. Todas las cachés son derivadas y deben admitir una reconstrucción desde cero.
links/document_inventory.py se encarga del inventario con TTL por Vault que
utilizan los enlaces globales. Excluye el historial y la papelera, aísla los
archivos que no se pueden leer, incluye los paneles JSON y recurre a un recorrido
del disco mientras el índice del proveedor no está disponible.
links/document_cache.py se encarga de las cachés persistentes del cuerpo de
Markdown y del frontmatter analizado, cuya clave se basa en mtime. El router solo
proporciona las rutas de caché activas, el analizador y el escritor JSON seguro,
por lo que el comportamiento de las cachés es independiente del proveedor de archivos.
links/relation_sync.py se encarga de las actualizaciones idempotentes del
sistema de archivos y las cachés cuando cambian las relaciones directas y sus
inversas. La correspondencia pura de esquemas permanece en un puerto de reglas
tipado separado: resuelve los campos de relación mediante nombres actuales y
alias normalizados, exige un único campo inverso sin ambigüedades y emite
únicamente operaciones de adición y eliminación sobre ID canónicos de relaciones.
El router de compatibilidad proporciona la entrada y salida de páginas resuelta
en el momento de uso.
El inicio carga primero las instantáneas válidas del disco y después pone en marcha la actualización. Un recorrido parcial del proveedor de archivos se marca como parcial y no puede reemplazar una caché que se sabe completa. Los fallos de cada archivo se aíslan para que un marcador de posición disponible solo en línea o huérfano no elimine el resto del Vault de una respuesta.
pages/index_entries.py se encarga de las lecturas acotadas del frontmatter,
los reintentos ante bloqueos de la nube y la normalización de las entradas de
caché. pages/index_service.py se encarga del descubrimiento, la actualización,
los mapas inversos de ID y las instantáneas deduplicadas. pages/resolver.py se
encarga de la resolución por ID estable, UUID canónico, título indexado y
recorridos acotados sin caché.
pages/tags.py se encarga de agregar las etiquetas del frontmatter y las
etiquetas semánticas de tablas de forma independiente del proveedor, incluida
la deduplicación por página. El router de compatibilidad inyecta los puertos del
Vault activo, el registro de configuración, el calendario y las cachés, de modo
que ninguno de estos servicios importa la fachada HTTP.
El entorno de ejecución del registro referencia los propietarios tipados reales de los callbacks y del estado, utiliza el decorador estándar de gestores de contexto para los ciclos de modificación y trata la falta de un Vault activo como ausencia de una raíz de adjuntos en la nube. El orden de las rutas de registro y tablas, los bloqueos, las cachés y los candidatos a adjuntos específicos de cada proveedor permanecen sin cambios.
La API principal del Vault importa directamente el router y los servicios y
limita sus colaboradores de resolución tardía a CoreVaultPort. La creación de
páginas admite metadatos abiertos sin coerciones; la inserción en el índice
actualiza el propietario existente de la caché. Los nombres de usuario conservan
las alternativas de nombre, correo e identificador. La creación de notas diarias
pasa el usuario del espacio de trabajo ya autorizado al servicio canónico, en
vez de llamar a un manejador HTTP con una dependencia pendiente de resolver.
Los callbacks explícitos de plugins conservan sus dos argumentos históricos.
Los permisos, controles de plugins, recuperación de notas existentes, bloqueo
de creación y esquemas HTTP públicos no cambian.
La composición de formato, búsqueda, catálogo y exportación de citas utiliza
contratos explícitos de registros y callbacks sin conversiones de resultados.
Las propiedades del registro conservan su identidad; los consumidores de lectura
admiten interfaces de mapeo y secuencia. Las referencias importadas reciben el
contexto del usuario autorizado cuando usan el manejador canónico de páginas;
los callbacks tardíos de dos argumentos siguen admitidos. La deduplicación,
formatos, descargas y errores de Pandoc no cambian. Todos los entornos guardan la
designación bibliográfica en GNOSI_DATA_DIR/config/references.json. La configuración
antigua requiere scripts/migrate-reference-config.py: su migración explícita sin
sobrescritura conserva los bytes, campos desconocidos y el original, con diario
privado y reversión recuperable. El arranque comprueba este requisito antes de
migrar bases de datos o iniciar tareas. La validación temporal nunca consulta archivos antiguos.
La consulta de metadatos, el reconocimiento de PDF, la traducción de URL, la promoción de Zotero, las actualizaciones masivas y el registro del catálogo y la búsqueda de citas comparten esa misma capa HTTP de tipo acotado. Los mecanismos alternativos de proveedores, los permisos del editor y la unicidad de las claves de cita siguen resolviéndose en el momento de uso y conservan su comportamiento.
La consulta bibliográfica importa los servicios directamente y declara bajo
TYPE_CHECKING alias comprobados de los propietarios reales de los callbacks,
sin conversiones de módulos ni resultados. La sustitución tardía sigue vigente.
Las pruebas cubren ambos órdenes de importación, los esquemas HTTP exactos,
la sustitución de callbacks durante una consulta y la conservación de los
metadatos desconocidos. citations/title_regex.py conserva
los errores nativos de Python: la única excepción documentada del verificador
valida entradas incorrectas y nunca afecta a los datos devueltos. Los tipos
heredados de proveedores de registro y páginas siguen siendo deuda separada.
La importación de Markdown, los comentarios en línea, los bloques sincronizados, la navegación por enlaces y las menciones sin enlazar comparten un router tipado de sincronización de páginas. Los modelos de solicitud utilizan Pydantic directamente y mantienen la identidad histórica de su módulo, conservando los nombres de los esquemas, el comportamiento SSE y la salida OpenAPI.
El CRUD de anotaciones PDF importa directamente el router compartido y las
dependencias de autorización y persistencia. Los payloads TypedDict con nombre
describen los diccionarios devueltos a los consumidores Python sin conversiones
de tipo ni Any. Los rectángulos guardados conservan la decodificación JSON
original; los modelos HTTP siguen validando su forma. No cambian las identidades
de los esquemas, el filtrado por URI, el orden por página y fecha de creación,
los permisos, las actualizaciones con campos nulos u omitidos ni el esquema
SQLite. Las pruebas aisladas SQLite y HTTP cubren ambos órdenes de importación:
primero la fachada o primero el dominio.
La administración del Vault ahora falla explícitamente con una respuesta de
servicio no disponible cuando falta la ruta principal del Vault, en lugar de
construir una ruta a partir de None. Las anotaciones heredadas de respuesta
permanecen congeladas, y el cambio de nombre lógico cruza la antigua capa de
descriptores del ORM sin cambiar carpetas en disco, slugs, reglas de purga ni
comprobaciones de confinamiento de rutas.
El catálogo de plantillas del Vault, la instalación, la exportación y el envío moderado exponen contratos tipados de solicitud y respuesta. Los manejadores validan cada diccionario antes de devolverlo y deshabilitan la publicación del modelo de respuesta en las rutas de compatibilidad, para que no varíen los esquemas FastAPI congelados ni el contrato de diccionarios de las llamadas directas. Las comprobaciones de firmas, los hallazgos de privacidad, los paquetes deterministas y la reversión ante fallos de registro no cambian.
Proveedores de archivos¶
Al iniciar o cambiar de Vault se solicita la descarga de todos los metadatos de la nube de .gnosi, incluidos habilidades, plugins, sidecars, estado de Wiki, iconos y copias de seguridad. El lector también solicita archivos individuales cuando se necesitan. Dos procesos auxiliares y plazos de diez segundos acotan el trabajo del proveedor; las lecturas concurrentes comparten una petición y los fallos requieren esperar treinta segundos antes de reintentar. Las lecturas asíncronas devuelven un 503 recuperable sin bloquear el bucle de eventos. Los metadatos remotos no se sustituyen por valores predeterminados: los lectores propagan la indisponibilidad y las escrituras atómicas requieren que el archivo original esté disponible localmente. Los archivos inexistentes conservan su creación normal. La descarga en segundo plano excluye árboles con enlaces simbólicos. Si el proveedor falla, no se garantiza la disponibilidad; la configuración crítica permanece cerrada por seguridad.
La abstracción de proveedores selecciona el comportamiento local, genérico de
macOS File Provider, o adaptado a OneDrive, iCloud Drive, Google Drive, Nextcloud
o Dropbox. El código habitual del dominio sigue trabajando con Path; el
adaptador añade detección de marcadores de posición, hidratación, disponibilidad
y correspondencia de rutas. Configurar GNOSI_FILES_PROVIDER explícitamente
cuando la detección automática de rutas sea ambigua.
El entorno de archivos bajo demanda es independiente del proveedor. En macOS se
utiliza por defecto un auxiliar de solo lectura basado en NSFileCoordinator,
compilado y firmado en Contents/Resources/native/gnosi-file-access. Cada llamada
solicita un archivo, lee como máximo 64 KiB y no devuelve contenido del documento.
El límite de dos descargas simultáneas y la unificación de peticiones por ruta
acotan la carga; el tiempo límite o la cancelación terminan el proceso auxiliar.
Este modo no abre aplicaciones gráficas, fija archivos al disco, descarga
recursivamente ni reinicia clientes de nube. Docker conserva el auxiliar
configurado en el host. Los modos antiguos open y direct requieren una
selección explícita; solo el adaptador antiguo de OneDrive puede activar una
recuperación específica del proveedor.
La detección del índice utiliza indicadores de archivos sin datos para conservar marcadores accesibles sin abrir todos los documentos de la nube. Las entradas existentes más completas se conservan si un archivo no se puede leer; la descarga no necesita cambiar el mtime para actualizar un marcador. Una exploración inicial fallida no se marca como inicializada. La navegación lateral y la tabla solicitada obtienen los metadatos necesarios, mientras que las lecturas de páginas y adjuntos materializan el archivo seleccionado. Los archivos no disponibles devuelven 503 con Retry-After en lugar de una lectura vacía correcta. El precalentamiento masivo del árbol crítico está desactivado por defecto con el acceso coordinado nativo.
La validación del código del 14 de septiembre incluye pruebas sintéticas de fallo, cancelación, deduplicación y ausencia de lecturas masivas, además de una lectura real de un marcador de OneDrive. Esa descarga tardó 2,2 segundos; las lecturas directas posteriores del mismo archivo de 3,9 KiB tardaron 0,24–1,03 ms, no el tiempo total de renderización de la página.
El cambio está empaquetado en el DMG local con firma ad hoc
Gnosi-3.0.2-on-demand-arm64.dmg, e instalado desde esa imagen. El backend
empaquetado ha superado una prueba aislada de vaults, páginas y favoritos. Un PDF
real de OneDrive de 428.327 bytes ha pasado de no tener datos ni bloques locales a
estar disponible mediante el endpoint Library de la aplicación instalada. Una
petición HTTP Range de 16 bytes tardó 3,5959 segundos la primera vez y 14,8 ms al
repetirla; ambas devolvieron 206 y una cabecera PDF. No son medidas de renderización
del documento completo. La aplicación instalada enumera los tres vaults existentes;
su barra lateral devuelve 2.672 páginas y 19 favoritos, y la interfaz abre un
favorito con el contenido de la página. También se ha probado correctamente la
opción de expulsar solo la imagen. No es una release publicada ni notarizada por
Apple; otros proveedores de nube y Windows no se han probado en vivo. Mostrar
todos los favoritos inmediatamente en un dispositivo nuevo aún requiere metadatos
portables de navegación: los nombres de archivo no permiten deducir favoritos que
solo constan dentro de un documento no descargado.
Las rutas de Dropbox File Provider se detectan explícitamente. Un servicio
desconocido en ~/Library/CloudStorage de macOS utiliza el adaptador
fileprovider, que no tiene efectos secundarios; cualquier carpeta totalmente
sincronizada o montada de forma ordinaria utiliza local. Solo hace falta un
nuevo adaptador con nombre propio para una señal de marcador de posición distinta
o un mecanismo de hidratación específico del proveedor. GNOSI_DATA_DIR sigue
siendo local independientemente del proveedor del Vault.
Solo el Markdown portátil del Vault y los adjuntos pueden residir en un árbol
sincronizado. Las bases de datos SQLite, los bloqueos, las cachés derivadas, los
secretos y GNOSI_DATA_DIR permanecen en el almacenamiento local de la aplicación.
Una carpeta de Nextcloud totalmente sincronizada se comporta como local; los
despliegues con archivos virtuales utilizan el proveedor correspondiente o el
adaptador genérico fileprovider. WebDAV y las API directas de la nube son
transportes de transferencia o copia de seguridad, no almacenamiento activo
para SQLite. El destino de las copias de seguridad y el proveedor del Vault se
configuran de forma independiente.
Adjuntos y propiedades de tipo archivo¶
Las escrituras eligen un destino permitido dentro del Vault activo, normalizan los nombres, evitan colisiones y devuelven metadatos portátiles. La raíz de los enlaces a archivos se adapta al host actual en el momento de la lectura. Las operaciones de subida y eliminación validan el confinamiento de las rutas; una ruta proporcionada por el cliente nunca constituye autorización suficiente.
Los manejadores de rutas de recursos y archivos son exportaciones canónicas del
dominio. El router heredado del Vault los registra en sus posiciones históricas
e inyecta puertos acotados para consultas al registro de configuración, resolución
de rutas y selección de proveedores. No debe mantener un segundo mapa de tokens
locales, bloqueo de iconos personalizados o semáforo de flujos de archivos. Los
decoradores repetidos de /local-file/{token} conservan su orden original de
rutas de abajo arriba, y cada cambio estructural debe preservar las cabeceras
de transmisión y el documento OpenAPI exacto.
Los metadatos de tipo archivo se normalizan recursivamente sin cambiar su
estructura de lista u objeto. Las rutas Assets/ existentes y las URL HTTP
remotas siguen siendo referencias; las URL de datos y los archivos locales
aprobados se copian de forma atómica al directorio de recursos de la propiedad.
La limpieza física resuelve cada candidato dentro de la raíz Assets del Vault
activo antes de desvincularlo, por lo que una cadena de recorrido de directorios
en el frontmatter no puede salir del Vault.
Papelera y operaciones destructivas¶
drawings/service.py se encarga del descubrimiento de dibujos Tldraw y Excalidraw
heredados, las lecturas, las instantáneas del historial con un intervalo mínimo
entre ellas, las escrituras atómicas y la eliminación recuperable. El trabajo
sobre el sistema de archivos se ejecuta fuera del bucle de eventos, y la
eliminación reutiliza el mismo contrato de archivos auxiliares de la papelera
del Vault que las páginas.
La eliminación ordinaria es recuperable: las páginas y los recursos relacionados
pasan por el modelo de papelera del Vault. La purga es una operación distinta y
elimina el contenido junto con los metadatos derivados y las relaciones inversas.
trash/purge.py se encarga de la fase irreversible sobre el sistema de archivos
y de la limpieza del historial, los archivos auxiliares de metadatos y los
comentarios, mediante puertos de la fachada resueltos en el momento de uso.
La eliminación de un Vault del registro borra por defecto la fila lógica del
registro; eliminar físicamente la carpeta requiere una señal explícita separada
y comprobaciones de confinamiento más estrictas.
Al eliminar una tabla, primero se mueve de forma atómica cada árbol de recursos
perteneciente a la tabla a .gnosi/pending-cleanup/table-assets/in-progress-*
y se escribe un manifiesto dentro de los límites permitidos. Después, la
confirmación de la transacción del registro renombra ese directorio a ready-*
antes de una purga en segundo plano. La recuperación al iniciar restaura una
cuarentena en curso si la tabla todavía existe, la purga cuando el registro
persistente demuestra la eliminación y deja intactas las entradas que no se
pueden leer o son desconocidas. Las revisiones de recursos incluyen los enlaces
simbólicos sin seguir sus destinos e impiden la eliminación tras una vista
previa desactualizada.
Plantillas de Vault¶
El repositorio de plantillas es un catálogo firmado que se utiliza en tiempo de ejecución; los recursos de los paquetes no se versionan en el repositorio Git de la aplicación. La creación a partir de una plantilla verifica la firma separada del índice, el SHA-256 del paquete, la firma del publicador, el manifiesto, el inventario de archivos, los límites del archivo comprimido, las rutas, los tipos de archivo y los enlaces antes de escribir. La extracción se realiza en un directorio de preparación hermano dentro de la raíz de Vaults. El directorio completado se mueve a su ubicación definitiva de forma atómica y solo entonces se registra en la base de datos de gestión, de modo que un fallo no pueda exponer un Vault parcial.
La validación del archivo comprimido se divide en validación acotada de entradas, decodificación del manifiesto, comparación del inventario y comprobaciones de integridad del contenido. Estos pasos puros y tipados conservan el mismo contrato de paquetes que rechaza la operación ante fallos y mantienen cada función auxiliar por debajo del límite de complejidad del backend.
La exportación se basa en una lista de elementos permitidos y es determinista.
Excluye .gnosi, plugins, almacenes de confianza, correo, papelera, historial,
contenido ejecutable, archivos de entorno, enlaces, archivos que no se pueden
leer y contenido de tamaño excesivo. Una vista previa enumera todos los archivos
incluidos y excluidos y analiza archivos de texto con límites definidos para
buscar valores que parezcan credenciales. Los hallazgos requieren una aceptación
explícita. Los plugins recomendados son identificadores en el manifiesto; el
código ejecutable de plugins nunca se incluye en una plantilla de Vault.
El envío público es independiente de la exportación y requiere acceso de administrador. Utiliza un intermediario opcional de moderación en lugar de una credencial de GitHub incorporada en Gnosi. Los campos adicionales del acuse de recibo específicos del intermediario se conservan sin pérdida mediante un modelo de respuesta que permite campos adicionales; los datos de error del catálogo mantienen su estructura heredada para la recuperación sin conexión y ante errores de firma.
Invariantes de concurrencia¶
daily/service.py se encarga, de forma independiente del proveedor, del
descubrimiento de carpetas y tablas, la normalización de fechas, la creación
inicial a partir de plantillas, los listados y el flujo atómico de obtención o
creación de notas diarias. El router de compatibilidad conserva los decoradores
públicos de FastAPI e inyecta comandos de página resueltos en el momento de uso
para que los plugins y las pruebas existentes mantengan sus puntos de sustitución.
- Los ETags desactualizados impiden las sobrescrituras.
- La creación de registros de configuración y notas diarias utiliza nuevas comprobaciones seguras frente a condiciones de carrera.
- Las actualizaciones de páginas, registros de configuración, índices de enlaces y archivos auxiliares mantienen la coherencia tras un cambio de nombre o una eliminación.
- Las rutas absolutas recibidas de un cliente se resuelven dentro de raíces aprobadas.
- Los enlaces simbólicos y el recorrido de directorios no pueden salir de los límites del Vault seleccionado.
- La extracción de plantillas no puede publicar un directorio parcial ni registrarlo antes de tiempo.
- Las exportaciones de plantillas no pueden incluir estado de ejecución ni contenido ejecutable de plugins.
- Las conversiones de ida y vuelta de Markdown conservan el contenido sensible al uso de caracteres de escape y la sintaxis de wikilinks.
Frontend¶
VaultDashboard se encarga del historial de navegación y selecciona las
interfaces de página, tabla, dibujo, galería, tablero, calendario, línea temporal,
feed o lector. VaultShell proporciona la estructura; los componentes
especializados implementan los editores y las vistas. El frontend almacena en
caché el estado de interacción, pero considera autoritativos el contenido de
las páginas y los ETags del backend.
El traslado revisado sitúa VaultDashboard.tsx en la raíz de la feature y
su orquestación en features/vault/dashboard/. La composición de tablas y la
edición de celdas están en features/vault/views/vault-table/; los campos y las
opciones del esquema, en features/vault/schema/schema-config/; y las propiedades
de página, los documentos enriquecidos, los efectos y la persistencia, en
features/vault/editor/block-editor/. El renderizado, los hooks de registros
y los controles de vista reutilizables pertenecen a shared/editor/,
shared/records/ y shared/record-views/; nunca importan UI del Vault.
Un módulo solo es público mediante la raíz de la feature o su entrada exacta
revisada en el manifiesto; ser un archivo de composición no lo hace público.
Los cambios de propiedad conservan rutas API y formatos de almacenamiento;
la verificación de integración sigue siendo un paso separado.
Las transiciones de Markdown al modo visual publican los borradores pendientes antes de montar el editor enriquecido, evitando que el contenido desactualizado del componente padre reemplace una edición sin guardar. Los guardados de solo metadatos omiten el cuerpo; las fórmulas de valores predeterminados conservan los valores anidados de relaciones y plugins. Las pruebas de regresión cubren estos traspasos, además de los identificadores de opciones del esquema, la identidad de las filas de las tablas y las extensiones de metadatos desconocidas.
Continuidad de las pestañas de documentos¶
Las pestañas abiertas de página, tabla y PDF conservan sus vistas montadas tras la primera visita, también cuando un documento pasa entre el panel principal y los paneles divididos. Cerrar una pestaña libera su vista. Volver del PDF de una cita restaura el foco en la tarjeta de origen de la galería y las posiciones de desplazamiento anidadas sin desplazar al dar el foco; la navegación con flechas continúa desde esa tarjeta. Los desplazamientos pendientes del layout no pueden sustituir la posición guardada, y una entrada de teclado o puntero cancela cualquier frame de restauración pendiente.
Los paneles ocultos son inertes. Sus atajos globales de selección, edición y navegación ceden a los paneles visibles, y sus menús renderizados en portales se ocultan con la pestaña propietaria. La expansión de los grupos de galería se guarda por sesión, página y vista de anclaje guardada, en lugar de los identificadores de bloque regenerados del editor. Este estado de interacción no modifica el contenido de las páginas, las definiciones de vista guardadas ni los ETags.
Aspectos que verificar¶
Ejecutar pruebas de concurrencia con ETag, confinamiento de rutas, entrada y salida seguras, condiciones de carrera del registro, cambios de nombre, papelera y purga, numeración de adjuntos, relaciones, actualización de índices y flujos representativos del Vault con Playwright. Los incidentes de proveedores en la nube también requieren leer un marcador de posición real, porque las pruebas con datos locales de prueba no pueden reproducir el comportamiento de File Provider.
Recuperación de metadatos de la barra lateral¶
El resumen y el árbol compacto de la barra lateral recuperan las entradas incompletas de la raíz, Wiki y los tableros mediante el proveedor de archivos activo antes de releer sus metadatos. Esto también se aplica a las instantáneas en caché. La recuperación restaura los identificadores canónicos y de los padres, las marcas de base de datos, los favoritos y los iconos, actualiza la búsqueda por identificador y reconstruye la instantánea versionada para eliminar duplicados. Las entradas completas y los catálogos de tablas no activan esta recuperación. Las lecturas parciales de la cabecera reintentan ambos códigos de error transitorio de la nube (11 y 35), empezando con el búfer y los delimitadores vacíos cada vez que se reabre el archivo. Las lecturas fallidas conservan los metadatos existentes en caché.
Cambiar de biblioteca con una nota abierta cierra su editor. BlockNote puede destruir la vista ProseMirror antes de que React ejecute la limpieza de navegación de los elementos incrustados. Esta limpieza restaura los controladores de teclado anteriores solo si la vista sigue viva y conserva los controladores instalados; nunca llama a setProps sobre una vista destruida. Las pruebas de regresión utilizan una vista ProseMirror real y cubren ambos órdenes de cierre para evitar que una excepción elimine toda la interfaz.
Recuperación de creaciones por ámbito y descargas de manuscritos¶
Las peticiones de creación persisten un recibo privado y checkpoints antes de los efectos recuperables. La recuperación conserva el ID original de la página y las entradas tipadas congeladas, verifica las revisiones del documento y la configuración y comprueba el acceso antes de cada efecto. Solo se repite trabajo no iniciado. Un paso de índice iniciado solo se puede reconciliar tras verificar las entradas registradas y la postcondición actual; los demás efectos inciertos siguen bloqueados. La recuperación no selecciona código ejecutable a partir de datos almacenados. El navegador consulta la petición guardada y abre la página existente, sin crear un duplicado cuando se pierde la respuesta.
El vault efectivo seleccionado por cabecera, query o cookie es también la identidad usada para la autorización. Un vault explícitamente inexistente, sin acceso o sin montar falla sin recurrir a otro vault.
Abrir el canvas de dibujo no descarga ni carga automáticamente un modelo OCR. El estado distingue pesos ausentes, descarga, descargado, carga, cancelación y error. La cancelación queda limitada al propietario y afecta a la transferencia, no a una inferencia activa. Los archivos solo se hacen visibles en la caché tras verificar su tamaño y checksum; los archivos parciales se eliminan. La interfaz muestra el tamaño transferido y el estado traducido. Las pruebas sintéticas de transferencia y canvas verifican estos contratos, pero no la precisión con manuscritos reales ni archivos de nube sin materializar.
Notas generadas, referencias legibles y revisión del Conocimiento¶
El tipo de idea es una asignación por IA predeterminada mediante el ID de propiedad, independiente de los campos de índice. El contrato existente de dimensiones transmite las etiquetas traducidas permitidas y una justificación breve classification_reason; los valores vacíos representan una abstención explicada. Las asignaciones explícitas copiadas, fijas y vacías tienen prioridad. La interpretación, la revisión y el guardado conservan el valor, y llm_wiki_idea_classification registra su versión, método y procedencia. El reprocesamiento preserva los valores modificados manualmente. La clasificación antigua ausente debe completarse, sin aplicar Concepto implícitamente.
El Cerebro ya no crea, vincula ni rellena el estado de verificación o la fecha de última revisión. El estado de trabajo y las fechas del sistema pertenecen a la aplicación; las asignaciones del recurso no pueden sobrescribirlos. Las nuevas notas de lectura son borradores. El estado existente y la fecha de creación se conservan al reprocesar. Se retira la categoría de notas antiguas basada en fechas; la API conserva stale como lista vacía y el recuento a cero por compatibilidad. Las notas generadas sustituidas siguen siendo una incidencia diferente.
La orden explícita de migración es python -m backend.domains.llm_wiki.note_migration --vault /path/vault --output /private/report --action preview. Las acciones classify, apply y rollback separan el trabajo gobernado del modelo de la modificación de datos. La simulación solo lee; la clasificación reutiliza los resultados de notas sin cambios y los presupuestos de lectura existentes sin reiniciar el gasto. La aplicación retira los campos y referencias en vistas y configuración por ID y rol, conserva el cuerpo y las demás propiedades, verifica una copia comprimida, compara cada original antes de escribir y registra el progreso para recuperarse. Las copias e informes deben quedar fuera del vault sincronizado y de Git. La migración de esquema registra note_fields_revision y repetirla es idempotente. La acción opcional retire aplica solo la retirada de campos y la limpieza del estado de trabajo, informa de la clasificación pendiente y no llama a ningún modelo; se debe usar una carpeta de salida diferente para cada fase aplicada.
El procesamiento de recursos crea notas de lectura con el estado Esborrany y las fechas canónicas de creación y modificación de la tabla. El reprocesamiento conserva la fecha de creación y el estado de trabajo existente, actualiza la modificación y mantiene los identificadores estables y el texto del usuario.
Los enlaces internos muestran los títulos actuales en editores, relaciones, tablas, galerías, kanban, búsqueda, grafos, menciones y predecesores de planificación. El resolutor compartido admite identificadores, rutas de página, ambos órdenes de wikilink y citas de recursos gnosi-cite:. Las referencias desconocidas muestran un texto provisional localizado mientras se carga su título. Los índices de relaciones conservan sus claves gestionadas e identificadores guardados con etiquetas legibles.
Las previsualizaciones de lectura ocultan los comentarios HTML de procesamiento, incluidos los límites <!-- ... -->. Los comentarios literales de bloques de código y código en línea siguen visibles; el Markdown guardado se conserva.
Las conexiones del Conocimiento solicitan sus textos en el idioma activo mediante el parámetro de consulta opcional locale. El agente del Conocimiento traduce solo títulos y explicaciones y guarda cada idioma por propuesta. Se conservan las evidencias y los identificadores de las notas. Las lecturas de caché evitan otra llamada al modelo y una propuesta descartada durante la traducción no se restaura.
El diálogo de revisión agrupa notas y recursos afectados en categorías desplegables con enlaces por título y lotes de veinte entradas. Abrir un resultado cierra el diálogo y abre su registro. La comprobación de notas huérfanas reconoce tanto [[id|title]] como [[title|id]]. La revisión no reprocesa recursos automáticamente ni crea notas permanentes.
En el escritorio, la selección de archivos y carpetas utiliza la capacidad IPC fiable y validada pick-filesystem y el diálogo nativo del sistema operativo. Cancelar cierra el selector sin aplicar una selección; las selecciones múltiples conservan el tipo de archivo o carpeta de cada ruta. Windows y Linux muestran primero una elección nativa entre archivos y carpetas para la selección combinada. El navegador interno sigue disponible para clientes web sin el puente de escritorio.
La cabecera de un solo documento muestra Cerrar pestaña únicamente cuando el identificador activo corresponde a una pestaña abierta. Conservar una pestaña inactiva no debe mostrar la acción de cerrar en la pantalla de bienvenida. Los controles de apertura y búsqueda siguen disponibles y cerrar la última pestaña activa elimina su botón de cierre.
Añada --resource-id <id> a preview para clasificar solo un recurso; las acciones posteriores reutilizan esa selección guardada. La clasificación vincula al propietario personal verificado, excluye contextos adjuntos y memorias revisadas, desactiva las delegaciones y reserva como máximo 4096 tokens de salida por lote sin cambiar los límites de gasto.
Los encabezados plegables admiten Alt/Opción+Intro con el cursor dentro del título. El botón nativo se puede enfocar con Tab y activar con Intro o Espacio, con foco visible, etiqueta traducida y estado de despliegue. La activación conserva el cursor o el foco del botón y el estado de despliegue guardado. Intro sin modificadores conserva su comportamiento de edición; los párrafos hijos, los controles incrustados y la composición de texto no pliegan una sección padre.