feat(plantare): surface product link + dates; spec the bilateral record
The Plantaré screen stored madeOn/dueBy/varietyId but showed none of them,
so it felt half-built. Surface what's already there (no schema change):
- watchPlantareViews() joins each commitment with its variety label
- tile shows the made-on date, a "Return by {date}" line (amber + "overdue"
when an open promise is past due, via new seedWarning colour), the variety
name, and taps through to /variety/:id
- add sheet gains an optional return-by date picker (createPlantare already
took dueBy)
- variety detail shows a read-only commitments section (hidden when empty,
no add button — honours the single hand-over door)
- i18n en/es/pt/ast; tests for the join, dueBy round-trip, tile, and section
Also specs the deferred two-sided record in docs/design/plantare-bilateral.md
(Nostr pubkey counterparty, PlantareTransport propose->accept->counter-sign,
deferred key/signature/movement columns, provenance DAG, reputation anchor),
with a faithful transcription of the paper Plantaré v0.4 (BAH-Semillero 2009,
CC-BY-SA). Cross-linked from data-model §2.7 and sharing-model §6.
This commit is contained in:
parent
f1e4fe377a
commit
dd36c3ac81
21 changed files with 687 additions and 27 deletions
|
|
@ -151,6 +151,11 @@ The digital version of the paper Plantare. Bilateral, signed, held by both parti
|
|||
| `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). |
|
||||
|
||||
> **Shipped vs. planned.** `app_seeds` ships the local, one-sided version (variety link +
|
||||
> dates + `direction`/`counterparty` free text); the key/signature/`movement_id` columns
|
||||
> and the two-party handshake are **deferred**. Full spec of the bilateral signed record:
|
||||
> [plantare-bilateral.md](plantare-bilateral.md).
|
||||
|
||||
### 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).
|
||||
|
||||
|
|
|
|||
160
docs/design/plantare-bilateral.md
Normal file
160
docs/design/plantare-bilateral.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Plantaré — the bilateral signed record (design, not yet built)
|
||||
|
||||
Status: **design locked, implementation deferred.** This doc specifies the two-sided
|
||||
Plantaré so a later session can build it without re-deciding. It is the "formulario
|
||||
bilateral firmado" that other docs wait on:
|
||||
|
||||
- [`data-model.md` §2.7](data-model.md) — the `Plantare` entity, whose `debtor_key`,
|
||||
`creditor_key`, `debtor_signature`/`creditor_signature` and `movement_id` columns are
|
||||
specified there but **deferred** in the shipped Drift table.
|
||||
- [`sharing-model.md` §1, §6](sharing-model.md) — the Plantaré as the *private bilateral
|
||||
close* of an exchange; and the open question of anchoring reputation to a closed deal.
|
||||
- [`PLAN.md` §5-bis](../../PLAN.md) — the paper original (BAH-Semillero, 2009): "partida
|
||||
doble repartida entre dos bolsillos", device-to-device, provenance DAG.
|
||||
- [`open-decisions.md`](open-decisions.md) — strong rating anchoring "espera al formulario
|
||||
bilateral firmado".
|
||||
|
||||
## What ships today (v1, local-only)
|
||||
|
||||
A private local ledger only. `apps/app_seeds` `Plantares` table stores `varietyId`,
|
||||
`direction` (`iReturn` / `owedToMe`), `counterparty` (**free text**), `owedDescription`,
|
||||
`madeOn`, `dueBy`, `status`, `note`. No identity keys, no signatures, no transport — the
|
||||
other party gets **no record**. The UI now surfaces the linked variety and the dates
|
||||
(Phase 1), but it is still one-sided.
|
||||
|
||||
## The paper original (v0.4, transcribed)
|
||||
|
||||
Source: BAH-Semillero, *Plantaré — moneda comunitaria de intercambio de semilla*, v4.0,
|
||||
© 2009–2010, CC-BY-SA 3.0 (`plantare.ourproject.org`). The printed form is **two matching
|
||||
stubs** ("partida doble") on one sheet:
|
||||
|
||||
**Stub A — "CORTAR Y ENTREGAR A CAMBIO DE LAS SEMILLAS"** (cut off and handed over with the
|
||||
seeds; the **giver keeps it** as proof of what they're owed):
|
||||
- **Yo** ______ (your name)
|
||||
- **localizable en** ______ (*email, tel., etc.* — contact info)
|
||||
- **recibo** ______ (*cantidad de semillas o lo que sea que recibo*)
|
||||
- **y me comprometo a devolver antes de (mes/año):** __ / 20__
|
||||
- return options (*choose those that apply*):
|
||||
- `[ ]` una **cantidad similar\*** de la recibida — *(\*) similar = no hibridada, no
|
||||
transgénica, cultivada en ecológico*
|
||||
- `[ ]` ó un **trabajo de ___ horas** equivalente
|
||||
- `[ ]` ó ______ (free text — anything else)
|
||||
- **Firmado,** ______ **el** __ / __ / 20__ (signature + date)
|
||||
|
||||
**Stub B — "RESGUARDO QUE ME QUEDO"** (the receipt the **receiver keeps** to remember what
|
||||
they owe): person/collective owed, quantity received, and "lo devolveré antes de __/20__".
|
||||
|
||||
What the paper captures that v1 does **not** yet:
|
||||
- **Contact** of the party ("localizable en") — v1 stores only a free-text name. Digitally
|
||||
this becomes the **Nostr pubkey** (reachable by DM); keep a free-text contact for the
|
||||
offline/paper case.
|
||||
- **Structured return options** instead of one free-text line: *similar quantity* (default),
|
||||
*equivalent work in N hours*, or *other*. The work-hours option is time-as-currency — it
|
||||
overlaps Tane's hand-over payment model, which already allows a time/`tiempo` currency
|
||||
([project-sales]; €/Ğ1/time/none).
|
||||
- **"Similar" is a defined term** — open-pollinated, non-GMO, organically grown — which ties
|
||||
to Tane's existing organic/eco self-declaration flag. The default return preset should
|
||||
carry this meaning, not just the word.
|
||||
- **Return-by is month/year**, matched to cultivation cycles (v1 stores a full day; Phase 1's
|
||||
picker is day-precision — display can round to month/year).
|
||||
|
||||
## The goal
|
||||
|
||||
Turn the one-sided note into the paper Plantaré's **two matching signed stubs**: when two
|
||||
people close an exchange, both devices end up holding the *same* commitment, each signed by
|
||||
both, so either can prove "recibí X, devolveré una cantidad similar antes de la fecha Y".
|
||||
|
||||
Non-goals: interest, enforcement, debt framing (microcopy stays "promise, not debt");
|
||||
a central server or global ledger (it stays device-to-device / relay-as-enrichment).
|
||||
|
||||
## Identity — counterparty becomes a real key
|
||||
|
||||
Replace (or rather, *augment*) the free-text `counterparty` with the counterparty's **Nostr
|
||||
pubkey / `Party`**, reusing the identity already present in the social layer (offers, chat).
|
||||
The flow starts from an existing conversation or a closed offer, so the pubkey is in hand.
|
||||
Keep the free-text field as the offline/paper fallback (a Plantaré made face-to-face with
|
||||
someone who has no app, or ingested from a paper form / QR).
|
||||
|
||||
## Schema delta (versioned Drift migration)
|
||||
|
||||
Add the deferred columns to `Plantares`, nullable so v1 rows coexist untouched:
|
||||
|
||||
- `debtorKey` TEXT — pubkey of who received and owes a return.
|
||||
- `creditorKey` TEXT — pubkey of who gave.
|
||||
- `debtorSignature` / `creditorSignature` TEXT — signatures over the canonical payload
|
||||
(both "stubs of the paper").
|
||||
- `movementId` TEXT → `Movement` — the shared `given` event this accompanies (the
|
||||
`Movement.plantareId` link already exists on the other side).
|
||||
- `remoteState` enum — `proposed` / `accepted` / `declined`, distinct from `status`
|
||||
(`open`/`returned`/`forgiven`), which tracks the *promise*, not the *handshake*.
|
||||
|
||||
From the paper form:
|
||||
- ~~`contact` (the paper's "localizable en", email/phone)~~ — **decided NOT needed**
|
||||
(2026-07-13). It's a paper-era artifact: digitally the counterparty is a Nostr pubkey,
|
||||
reachable by DM, so a manual contact field is redundant. Offline/paper Plantarés keep it
|
||||
only inside the free-text `counterparty`.
|
||||
- `returnKind` enum — `similar` / `workHours` / `other`, mirroring the form's checkboxes
|
||||
(default `similar`, meaning open-pollinated · non-GMO · organically grown — surface that
|
||||
definition, not just the word). `workHours` REAL for the "trabajo de N horas" option;
|
||||
`owedDescription` still backs `other`. Note the work-hours option overlaps Tane's hand-over
|
||||
time/`tiempo` currency — reconcile the two rather than duplicating. Kept for the Phase 2
|
||||
build; not added to v1.
|
||||
|
||||
A v1 local row = both keys/signatures null, `remoteState` null. Migration test in CI per
|
||||
[`data-model.md` §5](data-model.md).
|
||||
|
||||
## Transport — `PlantareTransport` in `commons_core`
|
||||
|
||||
A new interface alongside `OfferTransport` / `MessageTransport` / `TrustTransport` /
|
||||
`RatingTransport` / `SyncTransport` / `ReportTransport`
|
||||
(`packages/commons_core/lib/src/social/`), on the shared `NostrConnection`. Unlike ratings
|
||||
(public signed events), a Plantaré is **private to the two parties**, so it is *encrypted*,
|
||||
like DMs/sync. Sketch, mirroring the existing `*Transport` shape:
|
||||
|
||||
```dart
|
||||
abstract interface class PlantareTransport {
|
||||
/// Send a signed proposal (my signature only) to [counterpartyPubkey].
|
||||
Future<void> propose(PlantarePledge pledge);
|
||||
/// Counter-sign a received proposal and send the fully-signed copy back.
|
||||
Future<void> accept(PlantarePledge pledge);
|
||||
/// Decline a received proposal.
|
||||
Future<void> decline({required String pledgeId, String reason});
|
||||
/// Incoming proposals / accepts / declines addressed to this identity.
|
||||
Stream<PlantareEnvelope> incoming();
|
||||
Future<void> close();
|
||||
}
|
||||
```
|
||||
|
||||
**Handshake:** propose → accept → counter-sign. A proposes (signs, encrypts to B); B reviews,
|
||||
accepts (adds B's signature), sends the doubly-signed copy back; both persist the identical
|
||||
row (`remoteState = accepted`). Decline leaves A's row `declined`.
|
||||
|
||||
**Channel:** prefer **NIP-17 encrypted DM** (already used for messaging) carrying a canonical
|
||||
JSON payload — least new surface, rides the existing connection. A dedicated event kind is an
|
||||
alternative if we want first-class filtering. **Offline QR/NFC** device-to-device (PLAN.md's
|
||||
favoured, most-decentralized path) is the same payload exchanged without a relay — the
|
||||
transport abstracts channel from payload. Relays are enrichment only; offline must fully work.
|
||||
|
||||
## Provenance DAG
|
||||
|
||||
Each accepted Plantaré links parent→child (this lot came from that exchange), forming the
|
||||
variety's distributed **procedence DAG** ([PLAN.md §5-bis](../../PLAN.md)) — "un grafo de
|
||||
commits, pero de semillas". `movementId` + the counterparty's prior movement give the edge.
|
||||
|
||||
## Reputation anchor
|
||||
|
||||
An `accepted` Plantaré is the **strong anchor** for a rating (both parties provably closed a
|
||||
real deal), upgrading the current soft, per-conversation anchoring — closing the
|
||||
[`open-decisions.md`](open-decisions.md) / [`network-trust.md`](network-trust.md) item.
|
||||
|
||||
## Open questions (for the build session)
|
||||
|
||||
- **Conflict / divergence:** if A and B end up with mismatched payloads (edited after
|
||||
propose), which wins? Likely: accept is over an immutable hash; any change re-proposes.
|
||||
- **Revocation / forgiveness sync:** `forgiven` is one-sided today; should it propagate to the
|
||||
counterparty's copy, or stay each party's own view?
|
||||
- **Expiry:** does `dueBy` passing change `remoteState`, or only the UI nudge (as in Phase 1)?
|
||||
- **Paper ingest:** signature model for a Plantaré where one side has no key (photo/QR of a
|
||||
paper stub) — store as `creditorSignature` null, `counterparty` free text.
|
||||
- **Group/collective party:** a seed bank (not a person) as creditor — ties to the
|
||||
collective-identity question in `sharing-model.md` §6.
|
||||
|
|
@ -103,7 +103,7 @@ Nueva entidad publicable (se serializa a NIP-99 / ActivityPub). En inglés, para
|
|||
|
||||
## 6. A decidir
|
||||
|
||||
- ¿Las valoraciones (reputación) se atan a un `Movement`/`Offer` cerrado para evitar reseñas falsas? (Probablemente sí, Capa 4.)
|
||||
- ¿Las valoraciones (reputación) se atan a un `Movement`/`Offer` cerrado para evitar reseñas falsas? (Probablemente sí, Capa 4.) El ancla fuerte es el Plantaré bilateral firmado — spec en [plantare-bilateral.md](plantare-bilateral.md).
|
||||
- ¿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 2–3).
|
||||
- ¿Cómo se revoca/expira una Offer publicada en relays que ya la replicaron? (Estado `closed` + caducidad; los relays acaban soltando lo viejo.)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue