The bare ~/repos/tane.git was renamed to ~/repos/tane-pre-split-2026-07-15.git (archived pre-filter-repo history) and detached as a remote; origin is now the forge at git.comunes.org.
77 lines
11 KiB
Markdown
77 lines
11 KiB
Markdown
# CLAUDE.md — Tane
|
||
|
||
Context for AI agents (Claude Code) working in this repo. Read this first, then [`docs/design/open-decisions.md`](docs/design/open-decisions.md) (the **live decision log**).
|
||
|
||
## What this is
|
||
|
||
**Tane** (種, "seed"; full name **Tanemaki** 種まき, "sow/scatter seeds") — a **local-first, decentralized** app to manage and share traditional seeds. First app on a generic commons engine. Web: `tane.comunes.org`. Package id: `org.comunes.tane`. See [`VISION.md`](VISION.md) for the human explanation and [`PLAN.md`](PLAN.md) for the origin analysis (with a "Nota de vigencia" reconciling evolved decisions).
|
||
|
||
## Golden rules (do not violate)
|
||
|
||
- **English for ALL code**: identifiers, class/field/column names, comments, commit messages, branch names, and technical specs. No exceptions.
|
||
- **User-facing strings are NEVER hardcoded** — always via i18n. The app is multilingual by design.
|
||
- **International by design, not Spain-first.** Tane targets the whole world, not one country or region. This is not just UI strings: it constrains **bundled data and assets** too.
|
||
- **Locale-keyed data, never mono-locale**: any catalog/reference data with human-readable names (species common names, vernacular names, units, categories) MUST be a locale-keyed map (`{ "es": [...], "en": [...], "ar": [...] }`), extensible to any language — never a single hardcoded language. Starter datasets may ship a small locale set, but the *schema*, code paths, and copy must never assume es/en only, and comments/notes must not brand the data as "Iberian/Spanish".
|
||
- **RTL is a first-class requirement**: layouts, widgets, and tests must work under RTL (Arabic, Hebrew, Persian). Use directional (`start`/`end`) not physical (`left`/`right`) insets/alignment; verify with `Directionality`.
|
||
- **Scripts & fonts**: bundled fonts must cover the scripts we claim to support (Latin + at least RTL/Arabic and CJK as they land); don't hardcode a Latin-only font stack.
|
||
- **OCR / language models**: bundled OCR traineddata and any language resource are pluggable per locale, not fixed to `eng`/`spa`. Match the data pack to the user's locale(s); the current es/en starter set is a seed, not the ceiling.
|
||
- **No locale-specific assumptions** in formatting, sorting, dates, numbers, name order — go through `intl`/ICU, never ad-hoc.
|
||
- **No plaintext at rest, ever** — the inventory DB is encrypted (SQLCipher). No plaintext logs/temp/caches. See [`docs/design/security-privacy.md`](docs/design/security-privacy.md).
|
||
- **Local-first**: everything works offline, no account, no central server. Online only enriches (degrades gracefully).
|
||
- **Progressive disclosure**: only `Variety.label` is mandatory; every other field optional. Audience is everyone 10–80; simple by default, depth on demand. Never build "two modes".
|
||
- **Human words in the UI, never tech jargon.** User-facing copy speaks by *intent* (what it does for the person), not by *mechanism*. File formats and internals (CSV, JSON, HLC, CRDT, LWW, UUID…) are implementation details and MUST NOT appear in labels, subtitles, dialogs or messages. E.g. "Save a backup" / "Restore a backup" (not "Export JSON"); "Export to a spreadsheet" (a spreadsheet is a human concept; "CSV" is not). Format-named i18n *keys* (`exportJson`) are fine — they're code; only their *values* must be human. When a feature is genuinely for power users, reframe it or bury it under progressive disclosure — don't leak the jargon.
|
||
- **License: AGPL-3.0.** Keep new deps compatible.
|
||
- **Tests, near-TDD.** Every behavior is covered by automated tests (unit / widget / integration); **do NOT rely on manual testing.** Write tests first for domain logic (`commons_core` is pure Dart — ideal for TDD). Nothing merges without tests covering the new behavior; CI gates it. See [`docs/design/testing.md`](docs/design/testing.md).
|
||
- Discussion/design docs may be in Spanish; code and specs in English.
|
||
|
||
## Stack & structure
|
||
|
||
- **Flutter/Dart**, monorepo via **pub workspaces**. Target Android/iOS/desktop.
|
||
- **Drift** (SQLite) + **SQLCipher** for the encrypted local DB. Versioned step-by-step migrations, schemas exported to `drift_schemas/`, migration tests in CI. See [`docs/design/data-model.md`](docs/design/data-model.md) §5.
|
||
- i18n from day one (e.g. `flutter_localizations` + `intl`/`slang`).
|
||
|
||
```
|
||
tane/
|
||
pubspec.yaml # pub workspace root
|
||
packages/
|
||
commons_core/ # identity, Party/Group, TrustEdge, Offer, Pledge, transport (Nostr)+CRDT+sync, geohash discovery. NO seed specifics.
|
||
apps/
|
||
app_seeds/ # Tane: Variety/Lot/Movement/Species, germination, plant-family units, UI, i18n, commons_ui (embedded for now)
|
||
```
|
||
Dependency direction: **`app_seeds` → `commons_core`, never the reverse.** Boundary rules in [`docs/design/core-domain-boundary.md`](docs/design/core-domain-boundary.md).
|
||
|
||
**Prefer workspace-level commands from the repo root** — one lockfile, one `.dart_tool`, both members at once. Verified working:
|
||
- `dart pub get` (root) — resolves the whole workspace; don't `pub get` per package.
|
||
- `dart analyze` (root) — analyzes `commons_core` **and** `app_seeds` together. The default gate.
|
||
|
||
**Testing — split by layer; NEVER run the whole `flutter test` blind (it's what "hangs").** Diagnosed 2026-07-11: logic tests are fast and reliable; only *widget* tests hang, because `pumpAndSettle()` on a screen with a never-settling source (live Drift/Nostr stream, periodic `Timer`, connectivity/plugin) waits at the 10-min default. The everyday gate:
|
||
- `dart test packages/commons_core` — pure Dart, ~1s. The TDD workhorse. (`dart test` can't run `app_seeds` — it needs `flutter`.)
|
||
- `flutter test test/services` — app logic + in-memory DB, no widgets, ~10s, reliable. Put new app logic in plain `test()` here.
|
||
- Widget tests: run targeted (`flutter test test/ui/<file>`), always with a timeout so a hang FAILS fast — a global `apps/app_seeds/dart_test.yaml` sets `timeout: 90s`; add `--timeout 30s` when in doubt. Never `pumpAndSettle` a live screen; use bounded `pump(Duration)`. See the testing-gotchas memory + [testing.md](docs/design/testing.md).
|
||
- **ALWAYS wrap a widget-test run in an OS hard-kill: `timeout -k 10 200 flutter test test/ui/<file> --timeout 45s`.** Recurring pain: some hangs sit for 30–40 min because neither `dart_test.yaml`'s `timeout` nor `--timeout` can bound them — a bare `await someStream.first` (outside the per-test clock) or the slow whole-app compile. The OS `timeout` is the only thing that reliably cuts them; without it a hung run wastes half an hour.
|
||
- **The #1 hang cause: a live Drift stream where a one-shot Future belongs.** Never `await watchX().first` — not in a `testWidgets` body AND not in production widget code (e.g. a transient sheet's `initState` filling a picker). A guard (`test/no_stream_first_in_widget_tests_test.dart`) scans `test/ui/` for it, but NOT `lib/ui/`. When a widget test hangs, grep BOTH `test/ui` and the `lib/ui` widget under test for `watch*().first` / a `StreamBuilder` on a never-closing source; replace with a repo one-shot (`…get()` Future). A transient sheet/dialog must never hold a live subscription for one-time data.
|
||
|
||
## Data model (decided, `schemaVersion = 1`)
|
||
|
||
Three levels: **`Variety`** (identity/accession) → **`Lot`** (a batch you hold: `harvest_year`, quantity, `offer_status`, germination) → **`Movement`** (append-only event log = history + provenance DAG + Plantare). Plus `Species` (bundled Wikidata/GBIF catalog), `VernacularName`, `Attachment`, `ExternalLink`, `GerminationTest`, `Party`, `SeedBank`, `Offer`, `Plantare`. Every mutable row: client `UUIDv7`, `created_at`, HLC `updated_at`, `last_author`, `is_deleted` (soft delete/tombstone), `schema_row_version`. CRDT: LWW scalars, OR-Set collections, grow-only `Movement`. Full spec: [`docs/design/data-model.md`](docs/design/data-model.md).
|
||
|
||
Decided schema details: `Quantity` is a shared value type (Lot + Movement); `offer_status` only on Lot; `category` free text prefilled from `Species.family`. Quantity units are **plant-aware** (cob/pod/ear/head…), not just kitchen units (§2.3.1). A Variety can hold multiple Lots with different years AND units at once.
|
||
|
||
## Identity & crypto (decided)
|
||
|
||
- **One identity** = a **Duniter/Ğ1-style root seed** (does profile, certifications, Ğ1). From it, **deterministically derive** a **secp256k1** subkey for **Nostr** (messaging/offers) — because Duniter has no messaging. User backs up ONE thing (the seed, as a printable recovery **QR**).
|
||
- At-rest DB key = a separate internal symmetric key in the OS keystore (unlocked by device biometric/PIN); NOT a user password. Backups also encrypted; key travels via the QR. See [`docs/design/backup-and-recovery.md`](docs/design/backup-and-recovery.md).
|
||
|
||
## Phasing — build order
|
||
|
||
**Block 1: Inventory.** Shippable alone, useful with zero network. Delivered (beta).
|
||
**Block 2 (STARTED — the social leap):** offers + messaging + relays + trust — big and indivisible. The de-risking spike ([`docs/design/spike-block2-findings.md`](docs/design/spike-block2-findings.md)) validated the whole happy path, so the **social round is now open**. Build order within Block 2: **(1) the transport foundation in `commons_core`** — one `NostrConnection` + N interfaces (`OfferTransport`/`MessageTransport`/`TrustTransport`/`RatingTransport`) + the pure `WebOfTrust` engine, on vetted libraries — then (2) offers UI, (3) messaging hardening, (4) trust & reputation polish. It is still large: scope honestly, keep it behind the transport interfaces, and don't let it regress Block 1.
|
||
|
||
Social layer uses: Nostr (offers NIP-99, DMs NIP-17, via the pure-Dart `nostr` package), **ego-centric trust** (you vouch for people you've met — kind 30777, one live cert per pair, expiring; your circle = friends-of-friends from YOUR key; **no global membership, no bootstrap referents, no user-facing parameters** — the global Duniter rule was dropped 2026-07-11 as not internationally viable, see open-decisions.md), **Wallapop-style ratings** (kind 30778, one live rating per pair, circle-weighted display), optional **Ğ1** currency (levels: price → wallet deep-link → optional WoT import remains a possible future enrichment). Identity: a **secp256k1 Nostr key derived (HKDF, domain-separated, one-way) from the Ğ1 root seed** — user backs up ONE seed. Cold-start via real Ğ1 seed groups + fairs. See [`docs/design/network-trust.md`](docs/design/network-trust.md), [`docs/design/g1-integration.md`](docs/design/g1-integration.md), and the spike findings above.
|
||
|
||
## Where things live
|
||
|
||
- Live decision log: [`docs/design/open-decisions.md`](docs/design/open-decisions.md) — check before deciding anything.
|
||
- First code steps: [`docs/design/first-sprint.md`](docs/design/first-sprint.md).
|
||
- Prior art / valuable mockups: [`docs/mockups/`](docs/mockups/) (inventory, item, search, profile, chat — the UI spec).
|
||
- Git: `origin` is the forge at `git.comunes.org/comunes/tane.git` (authoritative; push here). Working clone here. A pre-split backup of the old (pre-`filter-repo`) history is archived at `~/repos/tane-pre-split-2026-07-15.git` — not a remote, do not push to it. Commit messages in English.
|