diff --git a/docs/design/plantare-bilateral.md b/docs/design/plantare-bilateral.md index 157ad1b..42a82ba 100644 --- a/docs/design/plantare-bilateral.md +++ b/docs/design/plantare-bilateral.md @@ -1,8 +1,29 @@ -# Plantaré — the bilateral signed record (design, not yet built) +# Plantaré — the bilateral signed record -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: +Status: **v1 shipped (2026-07-14).** The two-sided signed Plantaré — the "formulario +bilateral firmado" other docs waited on — is now built end-to-end: transport in +`commons_core`, schema v12 + repo, `PlantareService`, and UI (propose from a chat, +accept/decline on the ledger). What's left is noted under "Deferred to a follow-up" +below. This doc keeps the original design for reference; the "Open questions" are now +resolved inline. + +Built (commit series `feat(plantare): …`, 2026-07-14): +- **Transport** — `PlantarePledge` (canonical, byte-stable core + two BIP-340 Schnorr + stubs), `PlantareCrypto` (sign/verify over SHA-256 of the core), `PlantareTransport` + + `NostrPlantareTransport` (propose→accept→counter-sign / decline over NIP-17 gift wrap + carrying a tagged JSON payload; the message inbox skips these and vice-versa). +- **Schema v12** — the deferred columns on `Plantares` (`pledgeId`, `debtorKey`, + `creditorKey`, `debtorSignature`, `creditorSignature`, `movementId`, `remoteState`, + `returnKind`, `workHours`), all nullable/defaulted so v1 local rows are untouched; + guarded migration + schema dump + migration test. +- **`PlantareService`** — signs my stub, sends, and reconciles incoming moves into the + ledger (verifies stubs before storing; pending proposals held in memory keyed by + pledge id, relay redelivers on reconnect). +- **UI** — propose sheet reached from the chat overflow menu; accept/decline + handshake + badges (awaiting signature / signed by both / declined) on the Plantares screen. + +This doc originally specified the two-sided Plantaré so a later session could 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 @@ -147,14 +168,34 @@ An `accepted` Plantaré is the **strong anchor** for a rating (both parties prov 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) +## Resolved in the v1 build (2026-07-14) -- **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. +- **Conflict / divergence:** RESOLVED as anticipated — the two signatures cover the + canonical core (everything except the signatures), and the `pledgeId` is part of that + core. Any edit changes the signing hash, so an accept always closes the *exact* payload + proposed; a changed pledge is a different id and must be re-proposed. `PlantareCrypto` + enforces this; the "editing after signing invalidates the stub" test pins it. +- **Revocation / forgiveness sync:** kept **one-sided** for v1 — `status` + (`open`/`returned`/`forgiven`) stays each party's own view, distinct from the shared + `remoteState` handshake. Propagating a forgive to the counterparty is a follow-up (it'd + need a signed "released" move). +- **Expiry:** `dueBy` passing is a **UI nudge only** (overdue styling), as in Phase 1 — it + does not change `remoteState`. +- **Paper ingest:** the schema already supports it (nullable keys/signatures + free-text + `counterparty`); no UI path yet — deferred. +- **Group/collective party:** deferred — still tied to the collective-identity question in + `sharing-model.md` §6. + +## Deferred to a follow-up + +- **Durable send outbox.** Propose/accept send best-effort over the current session; if + offline the local row is recorded but the move isn't queued for retry. A durable outbox + (like `offer_outbox`) should back it so a proposal made offline goes out on reconnect. +- **Accept after restart before redelivery.** Pending proposals live in memory (refilled by + NIP-17 redelivery on reconnect); accepting one the relay hasn't redelivered yet shows a + gentle "offline / reconnecting" nudge rather than reconstructing the pledge from the row. +- **Rating anchor.** An `accepted` Plantaré is now a provable closed deal, but the rating + flow isn't yet wired to require/prefer it (the "strong anchor" upgrade). +- **Provenance DAG.** `movementId` is stored; building the distributed procedence graph + from accepted pledges is not done. +- **Forgiveness/paper/group** as above.