Add design docs; update README/PLAN

This commit is contained in:
vjrj 2026-07-07 01:28:54 +02:00
parent ce3a3da81f
commit 86b0c4d251
5 changed files with 553 additions and 0 deletions

207
docs/design/data-model.md Normal file
View file

@ -0,0 +1,207 @@
# Tanemaki — Data model (v0 draft)
*Technical spec feeding the Flutter/Drift implementation. Written in English (project convention). Implements the concepts discussed in [data-notes.md](data-notes.md) and PLAN §3 / §5-bis.*
> Status: draft for discussion. Nothing here is frozen; it is the starting point for the first Drift schema (`schemaVersion = 1`).
## 0. Design constraints (why the model looks like this)
- **Local-first & offline.** The whole model lives in local SQLite. No server is required. Online only enriches.
- **CRDT-ready from day one.** Every mutable row carries the metadata needed to merge across devices/peers later, even before sync is built. See §4.
- **Progressive disclosure.** Only `Variety.label` is mandatory. Everything else is optional. The schema must never force a field the UI hides.
- **Three levels: identity / batch / event.** `Variety` (what it is) → `Lot` (a batch you hold) → `Movement` (append-only log). This makes year, in/out, dates and germination fall into place.
- **Append-only ledger.** `Movement` rows are immutable events. This is both the bank history (mockup HISTORY tab) and the provenance/Plantare chain (§5-bis).
## 1. Conventions for every table
All identifiers, column names, comments and commit messages are in **English**. Shared columns on every mutable entity:
| Column | Type | Purpose |
|---|---|---|
| `id` | TEXT (UUIDv7) | **Client-generated** primary key. Never auto-increment (would collide across peers). UUIDv7 sorts by time. |
| `created_at` | INTEGER (ms) | Creation timestamp. |
| `updated_at` | TEXT (HLC) | Hybrid Logical Clock stamp of last change — drives last-writer-wins merge. |
| `last_author` | TEXT | Public key / device id of who last wrote (for CRDT + trust). |
| `is_deleted` | BOOLEAN | **Soft delete only.** Never `DELETE FROM`; tombstones must survive to merge correctly. |
| `schema_row_version` | INTEGER | Per-record format version for distributed migration (§5). |
`Movement` is the exception: it is append-only and immutable, so it needs no `updated_at`/`is_deleted` (a correction is a new compensating event).
## 2. Entities
### 2.1 `Variety` — the identity (accession)
What you saved; one row per distinct thing in your inventory.
| Column | Type | Notes |
|---|---|---|
| `id` | UUID | |
| `label` | TEXT **(required)** | Your own name: "el tomate de la abuela". The only mandatory field. |
| `species_id` | UUID → `Species` | Nullable. Links to the bundled catalog; unlocks scientific name + Wikidata. |
| `cultivar_name` | TEXT | Nullable. 'Marmande', 'Cherokee Purple'. Free text — landraces aren't in any authority. |
| `category` | TEXT/enum | Nullable. Botanical family or user group, for the inventory sections. |
| `notes` | TEXT (markdown) | Nullable. Free personal notes. |
| *(+ common columns)* | | |
Related (separate tables so concurrent edits merge as sets):
- `VarietyVernacularName(id, variety_id, name, language, region)` — the multiple common names.
- `Attachment(id, parent_type, parent_id, uri, kind)` — photos/docs (kind: photo, doc). Polymorphic parent.
- `ExternalLink(id, parent_type, parent_id, url, title)` — any pasted URL (Wikipedia, forum…).
### 2.2 `Species` — bundled name catalog (mostly read-only)
The lightweight offline catalog (Wikidata CC0 + GBIF CC-BY). Users may add local entries.
| Column | Type | Notes |
|---|---|---|
| `id` | UUID | |
| `scientific_name` | TEXT | *Solanum lycopersicum*. |
| `wikidata_qid` | TEXT | e.g. `Q23501`. Anchor to derive Wikipedia/image/GBIF online. |
| `gbif_key` | INTEGER | Nullable. GBIF taxon key. |
| `family` | TEXT | Nullable. |
| `is_bundled` | BOOLEAN | True = shipped with app (don't sync); false = user-added (syncs). |
Localized common names for the catalog live in `SpeciesCommonName(species_id, name, language)` and are bundled too.
### 2.3 `Lot` — a batch you hold
Zero or more per `Variety` (the 2023 batch and the 2024 batch are different lots).
| Column | Type | Notes |
|---|---|---|
| `id` | UUID | |
| `variety_id` | UUID → `Variety` | |
| `harvest_year` | INTEGER | Nullable. **This is where "year" lives** (germination decays with lot age). |
| `quantity_label` | TEXT + `quantity_kind` | Qualitative by default. **Not only kitchen units** — see §2.3.1. |
| `quantity_precise` | REAL + `quantity_unit` | Optional precise amount (grams / seed count). |
| `storage_location` | TEXT | Nullable. Free text with suggestions ("fridge", "village bank"). |
| `offer_status` | enum | `private` / `shared` / `exchange` / `sell` — the visibility from PLAN §3 Layer 2. Per lot. |
| `seedbank_id` | UUID → `SeedBank` | Nullable. If this lot belongs to a collective bank. |
| *(+ common columns)* | | |
`GerminationTest(id, lot_id, date, sample_size, germinated_count, notes)` — optional; `percent` is derived (`germinated_count / sample_size`). One-to-many so history of tests is kept.
#### 2.3.1 Quantity units — qualitative and plant-aware (design note)
Quantity is deliberately *rough and human*, not precise. But the informal vocabulary must go **beyond kitchen measures** ("cup", "pinch", "packet") — those feel too "andar por casa" and don't fit many seeds. Seeds are naturally counted in the **forms the plant gives them**. So `quantity_kind` is an **extensible, i18n, grouped list**, and the app *suggests* the relevant units from the linked `Species`/`category` (never forces them):
- **Informal / generic:** a few, some, plenty, a handful, a pinch, a jar, a **packet/envelope** ("sobre").
- **Plant-form natural units:** a **cob** (mazorca, maize), a **flower head** (cabezuela, sunflower), a **pod** (vaina, legumes), an **ear/spike** (espiga, cereals), a **fruit's worth** (tomato, pepper, squash), a **bulb**, a **tuber**, a **seed head**, a **bunch**.
- **Precise (optional):** grams or seed count (`quantity_precise` + `quantity_unit`).
Modelling: store `quantity_kind` (a stable enum key like `pod`, `cob`, `head`, `packet`, `handful`, `grams`, `count`) + an optional numeric `quantity_label`/`quantity_precise`. The **display label is localized** from the key, so "pod"→"vaina"/"beina"… Units are *labels for a rough amount*, generally **not convertible** between each other or to grams — and that's fine. Suggestion logic (e.g. Poaceae → ear/cob, Fabaceae → pod, Helianthus → flower head) lives in the catalog mapping, not hardcoded, so it stays extensible and translatable. **To think through further:** the full starter list per family, and whether users can add their own unit keys (probably yes, as free `quantity_label` text when no key fits).
### 2.4 `Movement` — the append-only event log
Immutable events on a `Lot`. Entries/exits are just types. This *is* the history tab and the provenance chain.
| Column | Type | Notes |
|---|---|---|
| `id` | UUID | |
| `lot_id` | UUID → `Lot` | |
| `type` | enum | `received`, `given`, `sown`, `harvested`, `germination_test`, `split`, `discarded`… |
| `occurred_on` | DATE | The date (in/out/sowing…). |
| `counterparty_id` | UUID → `Party` | Nullable. From/to whom (for received/given). |
| `quantity_label` / `quantity_precise` | | How much moved. |
| `parent_movement_id` | UUID → `Movement` | Nullable. Links a lot to the exchange it came from → **provenance DAG**. |
| `plantare_id` | UUID → `Plantare` | Nullable. A `given` may carry a Plantare. |
| `notes` | TEXT | |
| `created_at`, `last_author`, `schema_row_version` | | (no update/delete — append-only) |
### 2.5 `Party` — a person or collective you exchange with
| Column | Type | Notes |
|---|---|---|
| `id` | UUID | |
| `display_name` | TEXT | |
| `public_key` | TEXT | Nullable. Nostr/keypair identity (Layer 3+). |
| `kind` | enum | `person` / `collective`. |
| `note` | TEXT | |
### 2.6 `SeedBank` — a collective bank (optional, Layer 23)
`SeedBank(id, name, note)` + `SeedBankMember(seedbank_id, party_id, role)`. A `Lot` may point to a `SeedBank`. Kept minimal now; expands with sharing.
### 2.7 `Plantare` — the signed seed-IOU (§5-bis)
The digital version of the paper Plantare. Bilateral, signed, held by both parties.
| Column | Type | Notes |
|---|---|---|
| `id` | UUID | |
| `movement_id` | UUID → `Movement` | The `given` event it accompanies. |
| `debtor_key` | TEXT | Who received and owes a return. |
| `creditor_key` | TEXT | Who gave. |
| `owed_description` | TEXT | "a similar amount of free seed". |
| `due_date` | DATE | Return-by date. |
| `debtor_signature` / `creditor_signature` | TEXT | Cryptographic signatures (both stubs of the paper). |
| `status` | enum | `open` / `returned` / `forgiven`. Framed as a promise, not a debt (microcopy). |
### 2.8 `Offer` — a published, discoverable listing
The *shop window*: a signed statement published to the network so others can discover what you share. Distinct from `Lot` on purpose — publishing reveals only what you choose (privacy). Serializes to Nostr NIP-99 (`kind:30402`) or ActivityPub. Full rationale in [sharing-model.md](sharing-model.md).
| Column | Type | Notes |
|---|---|---|
| `id` | UUID | |
| `lot_id` | UUID → `Lot` | Nullable (offer without exposing a lot; `wanted` needs none). |
| `variety_summary` | TEXT/JSON | Only the name + photo ref you choose to publish. |
| `offer_type` | enum | `gift` / `exchange` / `sale` / `wanted`. |
| `price_amount` | DECIMAL | Nullable (only `sale`). **No in-app payment** — price is informational; money changes hands off-platform. |
| `price_currency` | TEXT | Nullable. ISO 4217 or a local/community currency. |
| `price_negotiable` | BOOLEAN | |
| `exchange_terms` | TEXT | Nullable (for `exchange`). |
| `approx_geohash` | TEXT | Low precision only. |
| `radius_km` | INTEGER | |
| `status` | enum | `active` / `reserved` / `closed`. |
| `published_at` / `expires_at` | | Seasonality / freshness. |
| `transport_ref` | TEXT | Relay event id (NIP-99) / AP object id. |
| `author_key` | TEXT | Publishing public key. |
| *(+ common columns)* | | |
A closed deal may produce a `Movement` (`given`) and, if it is an exchange with a return promise, a `Plantare`. The app never processes payments, takes commissions, or holds funds (keeps Tanemaki neutral infrastructure, not a central market operator).
## 3. Entity map
```
Species (bundled) ──< Variety < Lot < Movement (append-only) >── Party
│ │ │ │
VernacularName │ Offer └── Plantare
Attachment/Link GerminationTest
SeedBank
```
`Movement.parent_movement_id` forms the cross-device provenance DAG. `Offer` is published (NIP-99/AP) and, when a deal closes, spawns a `Movement` (and optionally a `Plantare`).
## 4. CRDT strategy (merge semantics)
Chosen libraries (PLAN §3): `crdt_lf` / `crdt_sync` on top of Drift. Merge rules by data shape:
- **Scalar fields** (label, harvest_year, offer_status…) → **LWW register** keyed by HLC `updated_at`. Simple, good enough for personal edits.
- **Collections** (vernacular names, attachments, links, seedbank members) → **OR-Set** (observed-remove set) so concurrent adds on two phones both survive.
- **`Movement` log** → **grow-only set** (append-only). No conflicts possible; corrections are new events. This is why the ledger is the safest part to sync first.
- **Identity:** client-generated UUIDv7; **deletes are tombstones** (`is_deleted`), never physical.
Adoption order (avoid CRDT-everything at once): sync `Movement` first (append-only, trivial), then `Lot`/`Variety` (LWW), then the set-typed relations.
## 5. Migration — two axes (this is the Liquibase/Flyway question)
### 5.1 Local schema migration → Drift, natively (the Flyway/Liquibase equivalent)
Drift ships versioned, testable migrations — exactly the requested capability:
- `schemaVersion` integer on the database; bump on every schema change.
- **Step-by-step migrations**: `dart run drift_dev schema steps drift_schemas/ lib/db/schema_versions.dart` generates `from1To2`, `from2To3`… functions.
- **Versioned schema exports** stored in `drift_schemas/` (`drift_schema_v1.json`, `v2.json`…) — the migration history, like Flyway's versioned scripts, committed to git.
- **Migration tests**: `dart run drift_dev schema generate` builds old-version DBs; `validateDatabaseSchema` asserts the migrated schema matches a fresh one. Run in CI.
Rule: never edit a released schema version; always add a new one with a forward migration. Keep every `drift_schema_vN.json` in the repo.
### 5.2 Distributed / wire migration → compatibility discipline (no tool can do this for you)
Because peers run **different app versions**, the sync payload must stay backward+forward compatible. This is the harder axis and is a *convention*:
- **Additive, optional-only changes** to synced records → both forward and backward compatible. Never repurpose a field's meaning.
- **`schema_row_version` per record**; readers **ignore unknown fields** and fill missing ones with defaults.
- **Soft deletes / tombstones** always (already in the model).
- **Never reuse or renumber** enum values; only append new ones, and tolerate unknown enum values on read (map to a safe default).
- A new required concept ships as a **new optional table/field**, promoted to "expected" only after most peers have upgraded.
Together: Drift handles the *local database* migration; the compatibility rules handle the *distributed data* migration. Both are needed for a P2P local-first app.
## 6. Open questions (decide before freezing `schemaVersion = 1`)
- `quantity` modelled once and reused on Lot and Movement, or duplicated? (Lean: a shared embedded type.)
- Is `offer_status` on `Lot` enough, or does a `Variety`-level default help? (Lean: Lot only.)
- Do we need `Variety`-level provenance, or is per-Lot/Movement provenance sufficient? (Lean: Movement DAG is enough.)
- `category` as free enum vs linked to `Species.family` when a species is set. (Lean: free, prefilled from family when available.)

124
docs/design/data-notes.md Normal file
View file

@ -0,0 +1,124 @@
# Tanemaki — Notas de diseño: nombres, ciencia, notas y registro de banco
*Documento de reflexión previo al modelo de datos formal. Lengua: español (el código irá en inglés).*
*Refina y expande la Capa 1 del [PLAN.md](../../PLAN.md) §3.*
El objetivo de estas notas es cuadrar una tensión concreta: que la app permita guardar datos ricos (nombre científico, enlaces a Wikipedia, germinación, notas personales, historial de banco) **sin volverse pesada ni difícil de usar**. La respuesta corta es un principio + una distinción de modelado.
---
## 1. Principio rector: simple por defecto, profundidad a demanda
**No construir dos apps (un "modo simple" y un "modo avanzado" como mundos separados).** Los modos globales confunden (¿en cuál estoy?, ¿dónde quedó ese campo?) y parten la comunidad de usuarios. En su lugar, **una sola app que parece simple para todo el mundo y revela profundidad cuando se pide** (progressive disclosure).
Cómo se traduce:
- **El alta en 20 segundos.** Añadir una semilla pide lo mínimo: una **etiqueta** (el nombre que tú usas) y, si quieres, una **foto** y una **cantidad**. Nada más es obligatorio. Alguien puede inventariar su cajón entero a base de nombres y fotos, y ya tiene una app útil.
- **"Añadir más…"** despliega, por secciones plegadas, todo lo demás: identificación científica, germinación, procedencia, ubicación de almacén, notas largas, enlaces. Quien no lo toca, no lo ve.
- **Preferencia global "mostrar campos avanzados"** (opcional): solo cambia *qué secciones vienen desplegadas por defecto*, nunca esconde funciones ni obliga a nada. Una persona novata nunca ve ruido; quien gestiona un banco lo activa una vez y se acuerda.
Regla de oro: **cualquier campo más allá de la etiqueta es opcional y, a poder ser, autorrellenable.** El trabajo lo hace la app, no la persona.
---
## 2. Los nombres (modelo realista, en capas)
Cómo nombra la gente de verdad una semilla, de lo más personal a lo más formal:
1. **Etiqueta propia** *(lo único obligatorio)* — texto libre, en tu idioma: "el tomate de la abuela", "judía del huerto de arriba 2024". Es lo que tecleas y lo que ves en la lista.
2. **Nombres comunes / vernáculos** *(opcional, lista)* — "tomate de colgar", "tomàquet de penjar", con idioma/región. Pueden venir del catálogo (§3) o añadirlos tú. Son los que la gente busca.
3. **Variedad / cultivar** *(opcional)* — 'Marmande', 'Cherokee Purple'. **Ojo: para quien guarda semillas, esto suele ser la identidad que más importa**, y NO es el nombre científico. Merece su propio campo.
4. **Nombre científico (especie)** *(opcional)**Solanum lycopersicum*. **Nunca se teclea en frío**: autocompletado desde un catálogo ligero empaquetado (§3). Es una etiqueta que la app te ofrece, no un deber.
La clave: solo la capa 1 es obligatoria. Las capas 24 son opcionales y, en su mayoría, se rellenan solas al vincular con el catálogo.
---
## 3. La "parte científica" y los enlaces, sin peso
El truco para tener conocimiento botánico sin embeber una base de datos enorme: **anclar cada especie a un identificador estable y derivar el resto bajo demanda.**
- El catálogo ligero empaquetado guarda, por especie, un **Wikidata QID** (p.ej. `Q23501` para el tomate). Con ese único identificador, **cuando hay red**, la app deriva gratis: el enlace a la **Wikipedia en tu idioma**, una foto, el taxón en **GBIF**, sinónimos. **Sin red**, sigues teniendo el nombre común y el científico guardados localmente. La "parte científica" es, por tanto, un dato minúsculo (un QID) que abre todo el conocimiento de la web solo si te interesa mirarlo.
- **Notas personales:** un campo de texto libre (markdown) + una **lista de enlaces** (pega cualquier URL: Wikipedia, un hilo de foro, tu blog) + **adjuntos** (fotos, un PDF). Simple y abierto. Aquí caben lo científico, lo enciclopédico y lo personal como añadidos opcionales.
- Encaja con los mockups: la ficha de ítem (`07_inventory_item`) ya tiene pestañas **DOCS** y **COMMENTS** → notas, enlaces y adjuntos.
Así, "datos científicos / Wikipedia / notas propias" son todo capas opcionales colgadas de un ancla mínima, no un formulario que agobia.
---
## 3-bis. Fuentes de nombres y datos: qué va offline, qué online, con qué licencia
La pregunta clave: *¿qué banco de nombres es realista usar, qué se empaqueta (offline) y qué se consulta bajo demanda (online), y qué licencia lo permite?* Porque empaquetar datos ajenos en la app es redistribuirlos, y ahí manda la licencia.
### Lo que SÍ es realista
**Empaquetar offline (núcleo, tiene que ser ligero):** un catálogo **curado** de las especies hortícolas relevantes en tu contexto (península / Mediterráneo primero, ampliable), NO una taxonomía entera. Por especie: nombre científico + unos pocos nombres comunes por idioma + **Wikidata QID** + clave GBIF. Son del orden de cientos a pocos miles de especies → caben en pocos MB. Se construye extrayendo de:
- **Wikidata** — licencia **CC0** (dominio público): *la mejor base para empaquetar*, porque redistribuir es libre y sin ataduras. Tiene ítems de taxón, nombres vernáculos en muchos idiomas y enlaces a Wikipedia. Cobertura de nombres comunes irregular, pero suficiente para un catálogo curado. **Es nuestro ancla.**
- **GBIF Backbone Taxonomy** — licencia **CC-BY 4.0** (solo pide atribución). Nombres científicos y sinónimos autoritativos, con nombres vernáculos, y **dumps descargables** (Darwin Core Archive). Se usa para validar/completar el científico. Atribución en el "Acerca de".
Con eso, el **catálogo empaquetado se licencia CC-BY** (Wikidata CC0 + GBIF CC-BY), limpio y redistribuible.
**Consultar online (opcional, derivado del QID/clave, siempre cacheado):**
- **Wikipedia** en tu idioma (resumen + enlace) y **Wikimedia Commons** (foto) — del QID, gratis.
- **GBIF** — ficha del taxón, sinónimos, distribución/mapa.
- **Permapeople** — base comunitaria de cultivo (cómo sembrar, asociaciones…), **CC-BY-SA 4.0**, con API (requiere alta y pedir acceso). Útil como *referencia online enlazada*, no para empaquetar (ver abajo).
### Lo que NO es realista (o hay que evitar)
- **Empaquetar la taxonomía completa** (GBIF backbone entero = millones de nombres; Wikidata entero): pesado e innecesario. Curar, no volcar.
- **PFAF (Plants For A Future):** datos valiosos de plantas útiles, pero **licencia restrictiva** → no empaquetar.
- **Dejar que la licencia CC-BY-SA "contamine" el catálogo:** si mezclas datos **share-alike** (Permapeople, Practical Plants) *dentro* del bundle, el catálogo entero queda obligado a CC-BY-SA. No es malo (encaja con el ethos copyleft), pero para mantener el núcleo simple y CC-BY, **trata lo SA como consulta online enlazada**, no como dato embebido. Decisión deliberada, no accidental.
- **Esperar que cualquier base autoritativa conozca tus variedades tradicionales / cultivares.** No las tiene, por definición: las landraces no están en catálogos oficiales (y ahí está justo el sentido político, §6 del PLAN). Los catálogos oficiales (registro UE/España, CPVO) son *las variedades registradas* — lo contrario de lo que cuidas.
### Consecuencia de diseño: los nombres de variedad los pone la comunidad
El **banco de nombres de especie** (capa 4 de §2) viene de Wikidata/GBIF. Pero la **variedad/cultivar** (capa 3, la que más importa a quien guarda semillas) **la teclea la gente**, no una autoridad. Con el tiempo, los nombres de variedad compartidos entre usuarios de Tanemaki (de forma agregada/opcional) van formando **el propio banco folclórico de nombres de Tanemaki** — descentralizado, vivo, y que ninguna base oficial puede darte. Es coherente con todo el proyecto: la ciencia formal se toma prestada (CC0/CC-BY), pero el conocimiento de las variedades tradicionales es de la red, no de un registro.
### Regla transversal: online siempre opcional
Nada del núcleo depende de la red. Offline tienes inventario completo, nombres del catálogo empaquetado, notas y registro de banco. Online solo *enriquece* (Wikipedia, fotos, GBIF, cultivo Permapeople, y más adelante los mapas de la Capa 3) y todo **degrada con elegancia**: si no hay red, simplemente no aparece esa capa extra, sin errores ni bloqueos.
---
## 4. El registro de banco: variedad / lote / movimiento
Esta es la distinción de modelado que hace que **fecha, entrada/salida, año y germinación** —lo que subrayas como importante— caigan en su sitio sin campos sueltos y confusos. Tres niveles:
- **Variedad (accession)** — la *identidad*: nombres, especie, cultivar, notas, enlaces. Permanente. Es lo que ves en el inventario.
- **Lote (lot / batch)** — una *hornada concreta* que tienes de esa variedad: **año de cosecha**, cantidad, ubicación de almacén, resultado de germinación. Puedes tener **varios lotes de la misma variedad** (el de 2023 y el de 2024) y no mezclarlos. El año vive aquí, que es donde importa (la germinación decae con la edad del lote).
- **Movimiento (event)** — el *diario* de un lote: **recibido de** X (fecha), **entregado a** Y (fecha) = *un Plantare*, **sembrado** (fecha), **test de germinación** (fecha, %). Entradas y salidas son, simplemente, tipos de movimiento.
Qué te da esta separación, "gratis":
- **Entrada/salida, fecha, año, germinación** dejan de ser campos ambiguos: cada uno pertenece a su nivel (año→lote, fechas de intercambio→movimiento, identidad→variedad).
- Conecta con la pestaña **HISTORY** del mockup: es la lista de movimientos del ítem.
- Conecta con §5-bis: **cada movimiento de salida ES un Plantare**, y la cadena de movimientos entre personas es el **DAG de procedencia** de la variedad.
- Habilita el **banco colectivo**: se sabe quién aportó qué lote y cuándo, sin ensuciar la identidad de la variedad.
**Germinación como evento** (opcional, avanzado): fecha, tamaño de muestra, nº germinadas, % (calculado), notas. Con el año del lote, permite avisos amables tipo "este lote es de 2019; su germinación puede haber bajado, ¿lo pruebas?".
---
## 5. Qué se ve en "simple" vs. qué revela "avanzado"
| Nivel | Campos | Cuándo aparece |
|---|---|---|
| **Simple (siempre)** | etiqueta, foto, cantidad cualitativa, categoría, ¿se comparte? | alta básica, 20 s |
| **Un toque más** (habitual en banco) | año del lote, procedencia (de quién), fecha de entrada | un despliegue |
| **Avanzado (a demanda)** | nombre científico + Wikidata/Wikipedia, cultivar, nombres vernáculos múltiples, varios lotes, germinación, ubicación de almacén, notas largas, enlaces, historial completo | secciones plegadas / preferencia |
---
## 6. Riesgo a vigilar (banco colectivo)
En un banco colectivo la tentación es **exigir muchos datos** por cada aporte, y eso mata la participación. El diseño debe garantizar que **un aporte mínimo sea siempre válido** (nombre + año + quién lo aporta), y que el resto se rellene *con el tiempo* o *entre varias personas* (colaborativo: alguien añade el nombre científico, otra persona sube una foto, otra registra una germinación). La app acompaña; nunca hace de aduana.
---
## 7. Qué falta decidir antes del modelo formal
- ¿El catálogo ligero empaquetado se centra primero en hortícolas de la península (para que el autocompletado sea realista en tu contexto) y se amplía luego? Probablemente sí.
- ¿"Cantidad" a nivel de lote con unidades cualitativas por defecto (§3) y precisión opcional? Sí, coherente con lo acordado.
- ¿Ubicación de almacén como texto libre o como lista reutilizable ("nevera", "trastero", "banco del pueblo")? Empezar por texto libre con sugerencias.
- ¿Un lote puede pertenecer a un banco colectivo *y* a tu inventario personal a la vez? Afecta al modelo de permisos/compartición; decidir al abordar la Capa 23.

View file

@ -0,0 +1,109 @@
# Tanemaki — Modelo de compartición y mercado (nota de diseño)
*Documento de reflexión. Lengua: español (discusión). Desarrolla la Capa 3 del [PLAN.md](../../PLAN.md) §3§4 y §6, y conecta con el modelo de datos ([data-model.md](data-model.md)).*
Dos cosas a resolver aquí: **(a)** cómo se *anuncia* lo que alguien comparte, de forma descentralizada, más allá del pagaré bilateral (que es el cierre, no el escaparate); y **(b)** permitir que quien quiera pueda **pedir dinero** —mercado— sin traicionar el alma del proyecto ni aumentar el riesgo legal de la gente.
---
## 1. El Plantare no es el anuncio
Aclaración conceptual, porque es fácil mezclarlos:
- El **Plantare** (§5-bis) es el *cierre bilateral* de un intercambio: el compromiso firmado entre dos personas cuando ya se han encontrado. Es privado, entre las dos partes.
- Lo que falta es el **escaparate**: cómo anuncia alguien "tengo esto para dar/cambiar/vender" para que otra persona *lo descubra* antes de que exista trato alguno. Eso es la **Oferta (Offer)**.
Oferta → contacto → trato → (opcional) Plantare. El pagaré es el último paso, no el primero.
---
## 2. La Oferta (Offer): anunciar sin exponerse
Una **Offer** es una declaración *firmada y publicada* a la red: "yo (clave pública) ofrezco [resumen de variedad] cerca de [ubicación aproximada], como [regalo/trueque/venta], [condiciones]". Principios:
- **Revela solo lo que elijas.** La Offer es un objeto distinto del `Lot`/inventario: publicas un resumen (nombre, foto, condiciones), **nunca tu inventario completo ni tu dirección exacta**. La separación Offer↔Lot es una decisión de privacidad, no solo técnica.
- **Ubicación aproximada siempre.** Geohash de baja precisión (el "10 km near you" de los mockups), nunca coordenadas exactas. La precisión la sube la persona solo al cerrar trato, en privado.
- **Identidad = clave pública** que controlas (Nostr). Sin registro central, sin email obligatorio.
- **La app presenta; la gente cierra.** La Offer enlaza a un canal de contacto (mensaje cifrado). El trato se cierra entre personas. Es el "efecto Wallapop" sin el intermediario Wallapop.
- **Estacionalidad y caducidad.** Las semillas son de temporada: la Offer tiene estado (`active`/`reserved`/`closed`) y caducidad (`expires_at`); hay que refrescarla. Una oferta vieja no debe engañar.
- **Descubrimiento** = consulta a relays/instancias por geohash + categoría → es la pantalla de búsqueda de los mockups (`04_search_page`, `05_search_item`).
### Transporte concreto: NIP-99
Detrás de la abstracción `OfferTransport` (PLAN §4), el primer backend es **Nostr NIP-99 ("Classified Listings")**: evento `kind:30402` (activo) / `30403` (borrador), con campos estructurados de **título, resumen, precio (importe + moneda), ubicación y estado**. Es decir: **el estándar ya contempla precio y moneda de serie**, así que regalo, trueque y venta caben en el mismo mecanismo. Existe ecosistema real (p.ej. Shopstr) del que aprender. La ubicación puede acompañarse de un tag geohash. ActivityPub / FEP-0837 queda como segundo backend posible.
---
## 3. Los cuatro modos de una Offer
`offer_type`:
- **Regalo (gift)** — gratis, sin retorno esperado. El don puro.
- **Trueque (exchange)** — a cambio de otras semillas, trabajo, o el compromiso Plantare (devolver después). Es el modo de reciprocidad.
- **Venta (sale)** — dinero. Precio + moneda. *(la novedad que pides)*
- **Busco (wanted)** — el inverso: anunciar *demanda*, no oferta ("busco semilla de tomate rosa de Barbastro"). Barato de añadir y muy útil en bancos y ferias.
---
## 4. El mercado (dinero): cómo hacerlo sin traicionar el proyecto
Permitir vender es legítimo y realista —hay quien produce semilla artesanal, quien vende excedente— pero toca el alma anti-monopolio del proyecto y la ley. Cómo cuadrarlo, con las tensiones **nombradas**, no escondidas:
### 4.1 Valores: el don es de primera clase; la venta, permitida, no promovida
- En la interfaz, **regalo y trueque se muestran primero**; la venta es un modo que *eliges*, nunca el defecto. Preservas el ethos sin imponer tu criterio a nadie (autonomía de la persona).
- Sin gamificación de la venta, sin "destacados de pago", sin ránkings de vendedores. Vender es una etiqueta más, no el eje.
### 4.2 Legal: vender reengancha justo el problema Kokopelli
Esto es lo delicado. La **venta** de variedades tradicionales no registradas es exactamente lo que multó a Kokopelli; el regalo/trueque entre aficionados o agricultores está mucho más protegido (PLAN §6). Al añadir "venta" aumentas la exposición legal *de tus usuarios*. Mitigaciones de diseño:
- **Tanemaki no es un mercado, es infraestructura neutral.** La app no opera el mercado, no publica un catálogo central, no pone en venta nada: son las personas, entre iguales (como flohmarkt). Esta neutralidad es la mejor defensa (replica por qué Kokopelli fue vulnerable —venta + operador central identificable— y evita ambas).
- **Aviso contextual por región, no policía.** Al marcar una Offer como "venta", la app puede mostrar una nota suave y dependiente del país ("vender semilla de variedades no registradas puede estar restringido en tu país"). Informar, no bloquear, no vigilar.
- **La venta prioriza el marco de conservación/compartición** en el discurso y el diseño; el dinero es una opción, no la portada.
### 4.3 Nada de rieles de pago (esto es innegociable)
- **La app NO procesa pagos, NO cobra comisión, NO retiene dinero, NO muestra publicidad.** El pago se acuerda **directamente entre las personas** (efectivo, su propia transferencia, lo que sea), *fuera* de la app.
- Meter pagos dentro convertiría a Tanemaki en operador central: obligaciones KYC/AML, custodia de fondos, y reintroduce justo la centralización que se quiere evitar. Además mataría la sostenibilidad (§7: financiación por subvenciones/donaciones, no por comisiones).
- Resultado: es el "efecto Wallapop sin Wallapop" también para la venta — anuncia y conecta; el dinero cambia de manos entre personas.
### 4.4 Representación del precio
`price_amount` (decimal) + `price_currency` (ISO 4217, admitiendo también monedas locales/comunitarias) + `price_negotiable` (bool). Para trueque, `exchange_terms` (texto: "a cambio de semilla de legumbre" / "o X horas"). El precio puede ser "a convenir".
### 4.5 Confianza y abuso
- Un mercado abierto y descentralizado atrae **spam y estafas**. La red de confianza (Capa 4) es el filtro: mostrar primero ofertas de gente dentro de tu grafo de confianza; el ámbito local/geohash ya acota mucho.
- Tras un trato, valoraciones/comentarios (los del mockup de perfil `09_public_profile`) — descentralizados, sin bloquear a nadie.
---
## 5. Añadidos al modelo de datos
Nueva entidad publicable (se serializa a NIP-99 / ActivityPub). En inglés, para [data-model.md](data-model.md):
**`Offer`**
| Columna | Tipo | Notas |
|---|---|---|
| `id` | UUID | |
| `lot_id` | UUID → `Lot` | Nullable (puedes ofrecer sin exponer un lote concreto; `wanted` no lo usa). |
| `variety_summary` | TEXT/JSON | Desnormalizado: nombre + ref de foto que *eliges* publicar (privacidad). |
| `offer_type` | enum | `gift` / `exchange` / `sale` / `wanted`. |
| `price_amount` | DECIMAL | Nullable (solo `sale`). |
| `price_currency` | TEXT | Nullable. ISO 4217 o moneda local. |
| `price_negotiable` | BOOLEAN | |
| `exchange_terms` | TEXT | Nullable (para `exchange`). |
| `approx_geohash` | TEXT | Baja precisión. |
| `radius_km` | INTEGER | Ámbito. |
| `status` | enum | `active` / `reserved` / `closed`. |
| `published_at` / `expires_at` | | Estacionalidad. |
| `transport_ref` | TEXT | id del evento en el relay (NIP-99) / objeto AP. |
| `author_key` | TEXT | Clave pública que publica. |
| *(+ columnas comunes)* | | |
`Offer` es distinta de `Lot` **a propósito**: publicar revela solo lo elegido. Un trato cerrado puede generar un `Movement` (`given`) y, si es trueque con retorno, un `Plantare`.
---
## 6. A decidir
- ¿Las valoraciones (reputación) se atan a un `Movement`/`Offer` cerrado para evitar reseñas falsas? (Probablemente sí, Capa 4.)
- ¿Permitir monedas comunitarias/tiempo explícitamente en `price_currency` (guiño al espíritu Plantare como "moneda de semillas")? Interesante.
- ¿Ofertas de banco colectivo (publica el banco, no la persona)? Afecta a identidad/permisos (Capa 23).
- ¿Cómo se revoca/expira una Offer publicada en relays que ya la replicaron? (Estado `closed` + caducidad; los relays acaban soltando lo viejo.)

View file

@ -0,0 +1,105 @@
# Tanemaki — Usabilidad y generalización (nota de diseño)
*Documento de reflexión. Lengua: español (discusión). Afecta a la estructura del primer commit de código, por eso conviene decidirlo ahora.*
Dos temas que, resulta, se responden juntos: **quién usa la app** condiciona **cómo se arquitectura**.
---
## Parte A — Usabilidad: de los 10 a los 80, sin dejar a nadie fuera
Principio de primer nivel, por encima de casi todo lo demás: **la app debe cubrir todos los públicos a la vez** — la gente del campo y la persona mayor con poca familiaridad digital *y también* quien gestiona un banco de semillas. No se trata de excluir a nadie, sino de que **nadie se quede fuera**. Personas de diseño concretas, como en **g1nkgo** (pensado para una hija de 10 y una madre de 80): si funciona para las dos, funciona para todos.
Cómo se reconcilia "para la niña de 10 y la abuela de 80" con "para quien lleva un banco": **la superficie de entrada tiene que ser legible y amable para el que menos sabe**, y la profundidad estar disponible, plegada, para el que más necesita. Eso es exactamente el *progressive disclosure* (§1 de [data-notes.md](data-notes.md)): una sola app, simple por fuera, potente por dentro. El gestor de banco **no queda fuera** — obtiene toda la potencia; simplemente no se le impone al resto. Tiene que ser **muy sencilla y a la vez potente**, y esto no es un "nice to have": es un requisito que condiciona la arquitectura (ver Parte B).
Cómo se traduce en decisiones concretas:
- **Objetivos táctiles grandes, tipografía grande y legible, alto contraste.** Pensado para vista cansada y dedos de trabajo. Accesibilidad real, no de adorno.
- **Icono + palabra, siempre.** Nunca solo iconos (se malinterpretan), nunca muros de texto. Cada acción, un dibujo y una palabra clara.
- **Cero jerga.** El usuario nunca ve "accession", "CRDT", "geohash", "relay", "clave pública". Se dicen las cosas como en el campo: "mi cajón de semillas", "dar", "pedir", "cerca de ti". La complejidad técnica existe, pero es invisible.
- **Foto primero.** La gente mayor y del campo **fotografía mejor que teclea**. Una foto + un nombre hablado o corto vale más que un formulario. La cámara es la vía de entrada principal.
- **El alta en 20 segundos** (ya en [data-notes.md](data-notes.md) §1): nombre + foto y ya. Todo lo demás, opcional y plegado.
- **Sin muro de registro.** La identidad se genera sola, en silencio; nada de crear cuenta, email, contraseña para empezar.
- **Funciona sin conexión**, sin penalización ni errores si no hay red (mucha huerta no tiene cobertura).
- **Indulgente.** Deshacer siempre; borrar es una "papelera" reversible (encaja con los tombstones del modelo, §4); ningún callejón sin salida destructivo.
- **La navaja que reconcilia "sencilla y potente": progressive disclosure** (ya decidido). La app parece simple para la abuela y despliega profundidad para quien gestiona un banco — sin ser dos apps ni dos modos visibles.
- **Probar con el público real, pronto.** Test con personas mayores y en una feria de intercambio, no con informáticos. La "prueba de la abuela" manda.
Consecuencia para la Parte B: como la superficie de entrada tiene que ser legible para *todo el mundo, de los 10 a los 80*, **una app genérica de "comparte cualquier cosa" es una mala idea de producto** — es menos legible que "la app de las semillas". La inclusión del público más amplio favorece productos monotema sobre un motor compartido. Eso orienta la arquitectura.
---
## Parte B — Generalización: app de semillas + biblioteca distribuida de cosas
La visión de fondo: un motor genérico que permita implementar **(1)** esta app de semillas y **(2)** una **biblioteca distribuida de cosas/herramientas** — qué tengo, qué comparto, con costes autogestionados y red de confianza para saber si se devuelve o no. La pregunta: ¿motor común, o cada app por su lado?
### El solape es enorme, y es justo la parte difícil
Lo que comparten semillas y biblioteca-de-cosas:
- Identidad descentralizada (par de claves), sincronización P2P local-first, descubrimiento por cercanía (geohash), red de confianza / reputación.
- Inventario de "lo que tengo".
- Oferta / visibilidad de "lo que comparto".
- **Préstamo con devolución esperada + confianza sobre si se devuelve.** Aquí está la clave: *"¿me devolvió la escalera?"* de la biblioteca de cosas **es exactamente** el *"¿devolvió semilla similar?"* del Plantare. El mecanismo es el mismo pagaré.
Lo que **difiere** es poco: el *objeto de dominio* (una variedad con germinación/especie vs. una herramienta con estado/depósito) y un puñado de campos. **El 80% difícil es común; el 20% fácil es específico.**
Prior art que lo confirma: el software de "Library of Things" (Lend Engine, myTurn, PVD Things) existe pero es **todo centralizado** — el hueco descentralizado es el mismo que en semillas. La segunda app también es un nicho real sin cubrir.
### Tres opciones de arquitectura
| Opción | Qué es | Veredicto |
|---|---|---|
| **A. Dos apps independientes** | Cada una reimplementa identidad, sync, confianza, préstamo… | ❌ Duplica la parte difícil. Insostenible para un mantenedor. |
| **B. Una app genérica "comparte lo que sea" con modos** | Un solo producto que sirve para semillas, herramientas, lo que quieras | ❌ **Choca con la Parte A.** La abuela quiere "la app de las semillas", no una plataforma genérica. Hinchazón, identidad confusa, onboarding peor. |
| **C. Núcleo común + apps finas monotema** | Un paquete reutilizable `core` (identidad, sync, inventario, oferta, préstamo/pagaré, confianza, descubrimiento) + apps delgadas y simples encima, cada una con su nombre e identidad | ✅ **Recomendada.** Sin duplicar lo difícil; cada app queda legible y de un solo propósito. |
### La reconciliación: generaliza el MOTOR, no el PRODUCTO
Usabilidad y reutilización apuntan **a la misma respuesta**. Tanemaki (semillas) y la futura app de cosas son **productos separados, simples, monotema**, cada uno con su nombre y su cara; por debajo comparten un **motor común** que resuelve una sola vez la fontanería P2P/confianza/préstamo. Generalizas donde nadie lo ve (el engine), no donde todo el mundo lo sufre (el producto).
### ¿Ahora o después? "Diseña para generalizar, implementa uno"
- **Construye solo Tanemaki ahora.** No hagas la plataforma genérica de forma especulativa (riesgo de abstracción prematura: te sale la abstracción equivocada).
- **Pero estructura el repo desde el primer commit como workspace** con un paquete `core/` + `app_seeds/`, respetando una **costura de dominio limpia**. Extraer más adelante la app de cosas es barato si la costura se respeta; retrofitear un motor genérico dentro de un monolito es caro.
- **En `core` va solo lo obviamente compartido** (identidad, sync, `Offer`, `Party`, confianza, `Pledge`/registro de préstamo). Lo específico de semillas (`Species`, germinación, cosecha) se queda en el dominio de semillas. Se sube más a `core` solo cuando la segunda app revele la forma real compartida (regla del tres — pero el núcleo P2P/confianza/pagaré es conocido-compartido de antemano, así que extraerlo ya está justificado).
### Cómo generalizan las entidades (mapa)
| Genérico (`core`) | Semillas (Tanemaki) | Cosas (futura) |
|---|---|---|
| `Item` / holding | `Variety` + `Lot` | herramienta / objeto |
| `Offer` (+ tipo **`lend`**: devolver *el mismo* objeto) | gift/exchange/sale/wanted | lend/gift/sale/wanted |
| `Pledge` (promesa de devolución) | Plantare (devolver *similar*) | préstamo (devolver *esto*, con fecha) |
| `Movement` / ledger, `Party`, `Group`, confianza | igual | igual |
| *(módulo de dominio)* | Species, germinación, cosecha, cultivar | estado, depósito, coste |
Nota: el `Plantare` es el caso semillas de un concepto genérico **`Pledge`** (promesa de devolución). Para cosas, "devolver *el mismo* objeto por una fecha"; para semillas, "devolver *algo similar*". Misma estructura firmada y bilateral.
### Nombres
- **Tanemaki** = el producto de semillas.
- El **núcleo** conviene que tenga nombre neutro. Tu organización **Comunes** encaja de libro ("motor del procomún"): p.ej. paquete `commons_core` o similar. La app de cosas tendrá su propio nombre cuando llegue.
### Consecuencia técnica inmediata (por eso se decide ahora)
El **primer commit de código** no es "una app Flutter", sino un **workspace** (pub workspaces / melos) con:
```
tanemaki/ (repo)
packages/
commons_core/ ← identidad, sync, Offer, Party, Pledge, confianza, discovery
apps/
app_seeds/ ← Tanemaki: Species, germinación, cosecha, UI de semillas, i18n
```
Drift: tablas de `core` + tablas de dominio de semillas, con las migraciones versionadas (§5 del modelo) por paquete. Así, el día que exista `app_things`, reusa `commons_core` sin tocar Tanemaki.
---
## A decidir
- ¿Confirmamos la estructura workspace (`commons_core` + `app_seeds`) para el primer commit? (Recomendado.)
- ¿Nombre del paquete núcleo: `commons_core`, `comunes_core`, otro?
- ¿El tipo `lend` (devolver el mismo objeto) entra ya en el enum de `Offer` aunque en semillas casi no se use, para no migrar luego? (Probablemente sí: es aditivo y barato.)
- La "prueba de la abuela": ¿tienes a mano personas mayores / una feria donde testear los primeros prototipos?