tane/docs/design/open-decisions.md
vjrj 6cff0d0b11 feat(chat): usable 1:1 chat — bottom-anchored, Drift-backed, dated
- 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.
2026-07-11 06:40:50 +02:00

62 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 12 (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.