tane/docs/design/open-decisions.md
vjrj 1f3c7975eb
All checks were successful
ci / analyze (push) Successful in 1m3s
ci / test-commons-core (push) Successful in 36s
ci / test-app-seeds (push) Successful in 6m1s
site / deploy (push) Successful in 34s
feat(site,docs): Tane is in F-Droid — link it next to Play
The inclusion MR was merged on 2026-07-25 and the app page went live on
2026-07-28 (v0.1.16, no antifeatures), so the landing page can finally
offer both stores.

- site: publish the F-Droid link and give each store link a single-colour
  icon, so the two are told apart by shape (play triangle vs. the
  free-software robot) rather than by brand colour. The badge row is a
  flex row now: left-aligned in the hero, centred under "Get Tane".
- README: an Install section with both stores, and a Status that matches
  reality (Block 1 shipped, Block 2 under way) instead of "early design".
- release.md: record that the app is published, that further releases only
  need a versionCode bump in the fdroiddata recipe, and that publication
  lags the green build by a day or two.
- open-decisions.md: log what unblocked inclusion — reproducible
  developer-signed builds, a Google-free APK, and opt-in networking.
2026-07-28 11:55:54 +02:00

67 lines
16 KiB
Markdown
Raw Permalink 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.

# Tane — 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
- **Confianza egocéntrica sustituye a la WoT global — DECIDIDO/IMPLEMENTADO (2026-07-11), SUPERSEDE la decisión del 2026-07-10.** La membresía Duniter completa (referentes curados + `WotParams` sigQty/stepMax, pantalla de raíces npub y steppers) se retiró de la app: resolvía identidad sybil-proof para una RBU — problema que Tane no tiene —, exigía curar referentes por comunidad/país (inviable internacionalmente; un set de referentes malo es peor que ninguno) y su pantalla violaba "palabras humanas, nunca jerga". **Queda solo la vista egocéntrica**: tú avalas ("Conozco a esta persona", en el chat) / tu círculo (distancia ≤2 desde ti, umbral 1) / avalado por N / desconocido. Cero bootstrap, cero parámetros de usuario; los avales caducan (365 días) y se renuevan. Un racimo sybil queda desconectado de la vista de cada observador — el filtro antispam se conserva sin veredicto global. **Se mantiene sin cambios** el primitivo (`Certification`, kind Nostr 30777, una cert viva por par) y el motor puro `WebOfTrust` en `commons_core` (usado para el círculo; capacidad latente para una futura "confianza por comunidad" o importación opcional de la WoT Ğ1 — sin migración de eventos). **UI:** badge por `TrustTier` (en tu círculo > avalado > desconocido) en el chat; pantalla "Tu gente" (a quién avalas —revocable— / quién te avala) desde el perfil, sustituyendo a "Red de confianza". Eliminados `TrustReferents`, `WotSettings`, `TrustNetworkScreen` y el asset de referentes. Certificación por colectivo sigue en sharing-model §6. → network-trust §2
- **Valoraciones tipo Wallapop v1 — DECIDIDO/IMPLEMENTADO (2026-07-11).** Estrellas 15 + comentario corto, públicas y firmadas (`Rating`, kind Nostr **30778** direccionable, `d` = sujeto): **una valoración viva por par** — re-valorar edita, no acumula; retirable. **Anclaje blando v1:** solo puedes valorar a alguien con quien tienes conversación (el anclaje fuerte a trato cerrado firmado sigue abierto, ver "Reputación" abajo). Se muestran en el detalle de oferta y en el chat **ponderadas por tu círculo** ("N de gente que conoces", mismo cálculo egocéntrico) — inflar reseñas con claves desconocidas no destaca nada; sin valoraciones no se muestra nada (la ausencia nunca es un reproche). Quinto transporte (`RatingTransport`) sobre el canal compartido. → network-trust §2.1
- **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`/`SocialSettings` → keystore aceptable ahí; solo el chat crece con el uso. → [chat-storage.md](chat-storage.md)
- **Reputación: anclaje fuerte** a un trato/oferta cerrada (evitar reseñas falsas del todo) — la v1 con anclaje blando por conversación ya está (ver arriba); el anclaje fuerte espera al formulario bilateral firmado. → 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]]
- **Paquete legal + moderación mínima — DECIDIDO/IMPLEMENTADO (2026-07-13).** Se escribe la capa legal completa en [`docs/legal/`](../legal/README.md): política de privacidad, condiciones de uso, normas de la comunidad y aviso sobre legalidad de semillas (masters en inglés + espejo es), más docs internos de cumplimiento (Play Data Safety/UGC, notas Apple, memoria jurídica con calendario de revisión — el PRM europeo sigue en trílogos). Decisiones que fija: **(a)** consentimiento único **al entrar al mercado por primera vez** (hoja modal con las normas; el inventario local no pide nada — progressive disclosure), con la misma bandera guardando también publicar-oferta y primer DM; **(b)** **bloqueo local** de claves (keystore, sin migración de esquema): filtra ofertas, chats y DMs entrantes; **(c)** **denuncias estándar NIP-56** (kind 1984) publicadas a los relays + ocultación local; en `relay.comunes.org` (nuestro) se actúa sobre ellas — esa es la historia de "moderación con respuesta" para la revisión de Play; **(d)** se reafirma **cero comisiones** sobre semillas y **Ğ1 solo como etiqueta de precio** (sin flujos de pago in-app, precedente Damus/Apple); **(e)** borrado honesto: NIP-09 es best-effort y así se cuenta al usuario. Metadata de tienda en `fastlane/metadata/android/` (la reutiliza F-Droid).
- **Distribución en F-Droid — HECHO (2026-07-28).** Tane está publicada en el repo oficial de F-Droid (<https://f-droid.org/packages/org.comunes.tane/>, v0.1.16) **sin antifeatures**: la MR de inclusión ([43144](https://gitlab.com/fdroid/fdroiddata/-/merge_requests/43144)) se aceptó el 2026-07-25. Lo que lo desbloqueó: **(a)** build **reproducible y firmada por nosotros** (`AllowedAPKSigningKeys` + `binary:` por ABI ⇒ misma firma que Play, se puede cambiar de tienda sin reinstalar); **(b)** APK libre de Google (sin GMS) con splits por ABI; **(c)** **red opt-in** desde v0.1.16 — la app no abre ningún socket hasta que la persona activa compartir, lo que sostuvo el rechazo de `NonFreeNet`/`TetheredNet` (protocolo abierto, relays configurables). Consecuencia operativa: cada versión nueva solo requiere subir los `versionCode` por ABI en la receta de `fdroiddata`. → [release.md](../release.md)
## 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 Tane 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.