tane/CLAUDE.md
vjrj a47bd6badd
Some checks failed
ci / analyze (push) Successful in 2m1s
ci / test-commons-core (push) Successful in 1m35s
ci / test-app-seeds (push) Has been cancelled
docs(claude): point git note at git.comunes.org origin; note pre-split backup
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.
2026-07-20 23:16:14 +02:00

11 KiB
Raw Blame History

CLAUDE.md — Tane

Context for AI agents (Claude Code) working in this repo. Read this first, then 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 for the human explanation and 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.
  • 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 1080; 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.
  • 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 §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_seedscommons_core, never the reverse. Boundary rules in 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.
  • 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 3040 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.

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.

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) 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/g1-integration.md, and the spike findings above.

Where things live

  • Live decision log: docs/design/open-decisions.md — check before deciding anything.
  • First code steps: docs/design/first-sprint.md.
  • Prior art / valuable 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.