- Anchor the message list to the bottom (`reverse: true`) so new messages stay in view instead of landing below the fold. - Move chat history from the OS keystore (O(n²) JSON blob, silent 200-msg cap) to a separate encrypted Drift/SQLCipher DB (`ChatDatabase`): indexed append, uncapped history, dedup as a unique-key invariant. It's an ephemeral per-device cache, isolated from the inventory schema, its migrations, and its sync. No data migration (pre-release). - Add day separators (Today/Yesterday/locale date) and a per-bubble time, all via ICU (12/24h per locale; Localizations locale maps Asturian → Spanish for intl date symbols). - Add peer avatars (deterministic colour from the pubkey + name initial), surface send failures that were previously silent, and make bubble text selectable (addresses, links). - New i18n keys in en/es/pt/ast; tests for grouping, formatting, avatars, scroll anchoring, storage and send errors. Docs: docs/design/chat-storage.md + open-decisions.md.
62 lines
13 KiB
Markdown
62 lines
13 KiB
Markdown
# Tanemaki — Decisiones abiertas y "qué chirría"
|
||
|
||
*Revisión de conjunto antes de seguir construyendo. Reúne los cabos sueltos repartidos por los docs de diseño, ordenados por cuándo hay que decidirlos, y nombra las tensiones del plan que conviene mirar de frente.*
|
||
|
||
> **Actualización (2026-07-10) — se abre la ronda social (Block 2).** El spike de de-risking ([spike-block2-findings.md](spike-block2-findings.md)) validó el happy-path completo de la capa social: derivación de la clave Nostr desde la semilla Ğ1, `OfferTransport`/NIP-99, mensajería NIP-17 (privada y con metadatos ocultos) y red de confianza estilo Duniter, todo sobre **una `NostrConnection` compartida con tres interfaces**. Con eso, la puerta de "no empezar Block 2" queda **levantada**. Orden de construcción dentro de Block 2: **(1) base de transporte en `commons_core`** (sobre librerías contrastadas — `nostr`, Dart puro, LGPL) → (2) UI de ofertas → (3) endurecer mensajería (entrega offline, vectores NIP-44, multidispositivo) → (4) parámetros WoT + arranque en frío. Los ítems de §B siguen siendo las decisiones a cerrar; §D.3 y §D.7 quedan **resueltos** por el spike.
|
||
|
||
## A) Decidir antes de escribir código de la Fase 1 (inventario)
|
||
|
||
Estas fijan `schemaVersion = 1` y el arranque técnico:
|
||
|
||
- **Formato de exportación/copia → ✅** un **archivo único cifrado**, abierto, documentado y versionado (volcado SQLite/CRDT + fotos + semilla envuelta). → [backup-and-recovery.md](backup-and-recovery.md)
|
||
- **Cifrado en reposo (el "cómo" técnico):** SQLCipher + llave aleatoria en el almacén del sistema + QR de recuperación. Conceptualmente decidido; falta bajar el mecanismo. → [security-privacy.md](security-privacy.md)
|
||
- **Identidad → ✅ una sola clave** raíz (Duniter/Ğ1), de la que se deriva la secp256k1 para Nostr. La clave simétrica que cifra la BD en reposo es un **detalle interno aparte** (no es una identidad), así que no contradice el "una sola identidad". → [g1-integration.md](g1-integration.md), [security-privacy.md](security-privacy.md)
|
||
- **Detalles de modelo que fijan el esquema → ✅** `Quantity` como **tipo compartido** (Lot y Movement); `offer_status` **solo en Lot**; `category` **texto libre** prerrellenado desde `Species.family`. → [data-model.md](data-model.md) §6
|
||
- **Forma del lote (`LotType`) y envase (`Presentation`) → ✅** (schema v4). `LotType` pasa de `{seed, plant}` a **6 formas** append-only: `seed, seedling(plantón), plant, tree, bulb, cutting` — tras repasar el vocabulario de viveros/tiendas ("plantel" era mal nombre: es el semillero, no una unidad). El **envase** (`pot/tray/plug/bareRoot/rootBall`) es un **atributo opcional aparte**, no una cantidad, para no mezclar "cuántas" con "en qué formato". Germinación sigue siendo solo-`seed`. → [data-model.md](data-model.md) §2.3.2
|
||
- **Licencia** del código → ✅ **AGPL-3.0** (GPLv3 + cláusula de red; compatible con GPLv3 y con Duniter/Ğ1nkgo). Falta añadir el fichero `LICENSE` al abrir el repo.
|
||
- **Varilla v1 → ✅ viable** con datos abiertos: nombres/identificación (Wikidata+GBIF, fácil), **conservación desde Kew SID** (dominio público, ~52k taxones), cuidados enlazados de Permapeople. Falta: confirmar acceso/descarga de Kew SID y el subconjunto curado a empaquetar. → [data-notes.md](data-notes.md) §3-ter
|
||
- **Estructura del workspace** (`commons_core` + `app_seeds`, pub workspaces) — decidido; falta materializarlo.
|
||
|
||
## B) Decidir antes de la capa de compartir (Fase 3) — muy interdependientes
|
||
|
||
- **Transporte de la capa social → Nostr (confirmado).** No se renuncia a Nostr: **Duniter no tiene mensajería**, así que Nostr es la vía para mensajes y ofertas. Se mantiene "una sola identidad" derivando la clave **secp256k1** (Nostr) de la semilla raíz Ğ1. Falta: confirmar madurez de los NIPs y la ruta de derivación. (Datapods de Duniter descartados: no están en servicio.) → [g1-integration.md](g1-integration.md)
|
||
- **Estrategia de relays:** app-as-relay oportunista + proximidad física + relays comunitarios. → network-trust §3
|
||
- **Parámetros de la red de confianza — RESUELTO/IMPLEMENTADO (2026-07-10).** Modelo **membresía Duniter completa**: regla pura `WebOfTrust.membersWith(seeds, WotParams)` con `WotParams` (sigQty/stepMax/sigValidity) por defecto Ğ1 (5/5/1año) **configurables** (`WotSettings`, keystore) — una red joven los afloja y aprieta al crecer. Cold-start honesto (sin inventar identidades): referentes "semilla" desde un asset empaquetado (vacío hasta curar fundadores reales) ∪ referentes que el usuario añade por npub/QR (`TrustReferents`). Caducidad ya la ponía el transporte (`certify` con `expiration`). UI: badge por `TrustTier` (miembro de la red > en tu círculo > avalado > desconocido) en el chat, y pantalla "Red de confianza" (gestión de raíces + parámetros) desde el perfil. Certificación por colectivo queda para sharing-model §6. → network-trust §2
|
||
- **Mensajería:** alcance v1 (1:1 atada a oferta), apoyo en NIP-17. → network-trust §4
|
||
- **Almacenamiento del historial de chat — RESUELTO/IMPLEMENTADO (2026-07-11).** El historial 1:1 se guarda en una **BD Drift/SQLCipher aparte** (`tane_chat.sqlite` / `ChatDatabase`), no en el keystore (era un blob JSON por conversación con read-modify-write O(n²), tope silencioso de 200 e índice manual). BD separada a propósito: el chat es **caché de red efímera por dispositivo**, no inventario — sin metadatos CRDT, fuera de la serie de migraciones del inventario, fuera del sync/backup. `append` = un insert indexado (dedup por clave única), historial sin tope. Pre-release: no se migran los blobs viejos del keystore. Mismo antipatrón (a escala trivial) en `OfferOutbox`/`TrustReferents`/`SocialSettings` → keystore aceptable ahí; solo el chat crece con el uso. → [chat-storage.md](chat-storage.md)
|
||
- **Reputación** atada a un trato/oferta cerrada (evitar reseñas falsas). → sharing-model §6
|
||
- **Precio:** ¿monedas comunitarias / de tiempo además de dinero? → sharing-model §6
|
||
- **Integración Ğ1 (moneda libre):** niveles 1–2 (precio en Ğ1 + enlace a cartera Ğecko/Cesium²/Ğ1nkgo) en la capa social; nivel 3 (reusar la WoT de Ğ1 como fuente de confianza) como estudio aparte. → [g1-integration.md](g1-integration.md)
|
||
- **Ofertas de banco colectivo** (publica la entidad, no la persona). → sharing-model §6
|
||
- **Caducidad/revocación** de ofertas ya replicadas en relays. → sharing-model §6
|
||
- **Cambiar de identidad social — RESUELTO/IMPLEMENTADO (2026-07-10).** Se eligió una refinación de la vía (A) que **mantiene "backup UNA semilla"**: identidades seudónimas **derivadas de la misma semilla raíz con un índice de cuenta** (`NostrKeyDerivation.deriveFromSeed(seed, {account})`, HKDF one-way → no vinculables a Ğ1; **cuenta 0 = la identidad actual, byte-for-byte, sin rotar a nadie**). La cuenta activa se persiste en el keystore (`SocialAccountStore`), los stores por-identidad (chats/perfil/nombres) se **namespacian por cuenta** (cuenta 0 = claves legacy, sin migración), y `switchSocialAccount()` re-registra la porción social del DI y reinicia el inbox; la UI (perfil → "Tus identidades", crear/cambiar) aplica el cambio con un `RestartWidget` que reconstruye el árbol leyendo los singletons nuevos. La app NO toca la DB/inventario. Descartada la vía (B) reset total (perdía identidad+recuperación). → [[project-block2-spike]]
|
||
|
||
## C) Puede esperar (no bloquea nada ahora)
|
||
|
||
- Negación plausible / bóveda señuelo; modo discreto. → security-privacy
|
||
- Reconocimiento por foto en la varilla.
|
||
- Respaldo social distribuido entre contactos.
|
||
- Ampliar avisos legales más allá de Europa (empezar por jurisdicción común europea). → PLAN §6
|
||
- Segundo producto (biblioteca de cosas) y extracción de `Item/Holding` al core. → core-domain-boundary
|
||
- Bloqueo de app: ¿activado por defecto o sugerido? → security-privacy
|
||
- **Puente opcional con portales abiertos de biodiversidad (Darwin Core / GBIF / ALA).** Export *opt-in* de agrobiodiversidad (variedades cultivadas, taxonomía folk, nombres vernáculos) como Darwin Core Archive hacia un atlas ALA/GBIF — justo los datos que esos portales tienen más flojos. **No es fase ni código ahora:** solo se apunta como intención. Lo único que exige del diseño actual ya lo hacemos por otros motivos: anclar especies a GBIF key + Wikidata QID (data-model §2.2) y mantener el formato de exportación abierto/versionado (requisito de backup, Fase 1). **Guardas innegociables** para que no choque con el modelo de privacidad: opt-in por ítem, a nivel de banco colectivo (no persona), ubicación difuminada, solo material no sensible, y en un solo sentido (app → portal; nunca escribir de vuelta en un inventario privado). Argumento de interoperabilidad útil para NLnet/NGI. → data-notes §3-bis
|
||
|
||
## D) Qué chirría (tensiones a mirar de frente)
|
||
|
||
1. **La capa social es indivisible y grande.** "MVP = inventario + compartición local" subestima que "compartir" de verdad = **ofertas + mensajería + relays + red de confianza**, todo junto e interdependiente. Propuesta: replantear las fases como **(1) inventario** —entregable, sólido, en solitario— y **(2) el salto social** —un bloque grande, no un incremento—. Ser honestos con esto en el plan y en la financiación.
|
||
|
||
2. **Casi toda la dificultad vive en `commons_core`.** Identidad, sync CRDT, ofertas, pledge, red de confianza, **mensajería**, relays… `app_seeds` es, en comparación, ligero. En el fondo Tanemaki es **infraestructura de procomún** con las semillas como primera aplicación. Es buena noticia para NLnet/NGI Zero (financian infraestructura), pero hay que asumir que lo "sencillo" (semillas) es la punta del iceberg y comunicar el proyecto en consecuencia.
|
||
|
||
3. **Apuesta fuerte por Nostr.** Ofertas, mensajes y confianza dependerían de NIPs en evolución (99 / 17 / 85). La abstracción `OfferTransport` amortigua las ofertas, pero mensajería y confianza también se apoyan en Nostr → más acoplamiento del que sugería el plan. Revisar si un solo protocolo lo cubre bien o si conviene aislar también mensajería y confianza tras interfaces.
|
||
|
||
4. **Cifrado en reposo + sync CRDT + multidispositivo — EN CURSO (base landed 2026-07-10).** El modelo ya está listo para CRDT (HLC, tombstones, LWW, Movement append-only) y el reconciliador de import prueba el merge; faltaba el **transporte de sync**. Primera rebanada en `commons_core`: `SyncTransport` (mueve snapshots opacos y cifrados entre los dispositivos de UNA identidad) + `NostrSyncTransport` (NIP-78 kind 30078 addressable/reemplazable por dispositivo, contenido **NIP-44 cifrado a sí mismo** → el relay solo ve cifrado; filtrado a tu propia clave de autor). Tests con MiniRelay: replica al otro dispositivo, el cable solo ve cifrado, otra identidad no recibe ni podría descifrar, re-push reemplaza. **Confirma la garantía de §4**: no filtra llave ni datos en claro. PENDIENTE (siguiente rebanada, app): `SyncService` que serialice el inventario (reusa `ExportImportService`), empuje en mutaciones, y en recepción corra `importInventory` (LWW idempotente ya existente); id de dispositivo estable por instalación (keystore, NO el nodeId derivado de la semilla, que es común a los dispositivos); integrar `sync` en `SocialSession` (5ª interfaz sobre la conexión compartida); UI de conflictos (raro).
|
||
|
||
5. **La "varilla" y los datos de conservación** necesitan una fuente y una curación que **aún no tenemos**. Puede ser más trabajo del que parece (y es, a la vez, de lo que más valor humano aporta).
|
||
|
||
6. **Alcance vs. sostenibilidad de un mantenedor.** El conjunto (inventario + cripto + P2P + confianza + mensajería + relays, en varias plataformas) es **mucho** para "proyecto personal + IA". Refuerza la necesidad de: fases honestas, comunidad temprana, y financiación (NLnet) para la parte social. La Fase 1 sí es abordable en solitario.
|
||
|
||
7. **Identidad — RESUELTO: una sola identidad, dos claves derivadas.** Una **semilla raíz** estilo Duniter/Ğ1 (perfil, certificaciones, Ğ1) de la que se **deriva** determinísticamente la clave **secp256k1** para Nostr (mensajes, ofertas). Para la persona es *una sola identidad y una sola copia* (el QR de la semilla). **Nostr no se descarta** — es imprescindible porque **Duniter carece de mensajería**. No hay contradicción con "una sola clave": son subclaves de una raíz. → [g1-integration.md](g1-integration.md)
|
||
|
||
## Recomendación de orden
|
||
|
||
Cerrar el bloque **(A)** — son pocas decisiones y desbloquean construir el inventario, que es valioso por sí solo y de bajo riesgo. Dejar **(B)** para una ronda de diseño específica de la capa social (con su propia financiación si llega). Y asumir explícitamente el punto **(D.1)**: replantear las fases para que "lo social" sea un hito grande y no una coletilla del MVP.
|