Wires the 'coming soon' market card to a real discovery screen (mockups 04/05):
- MarketScreen: opens an OfferTransport lazily; local-first, so it degrades to a
human 'set up sharing' prompt when no relay/area is set or the net is down.
- Discovers offers by the saved coarse area, lists them with human reciprocity
labels (gift/swap/for sale), 'near you' (never exact location), price only for
sales. Config sheet to set area + community servers (no bundled public relays).
- Threaded via TaneApp(social, socialSettings) — null keeps the card 'coming
soon' (so Block 1 tests pass unchanged); main wires it live.
- i18n en/es/pt (market.*), reuses share.* for type labels.
3 market widget tests + home tests green; app_seeds analyzes clean.
Persistence + wiring the offers screen needs, per the product decisions:
- SocialSettings (keystore-backed, no plaintext): coarse area geohash +
configurable relay URLs, both empty by default (no bundled public relays —
don't leak metadata to third parties). Registered in DI.
- createOffersCubit(): opens a SocialSession on the first configured relay when
online, else a null-transport cubit that degrades gracefully. OffersCubit now
closes the owning session on dispose.
4 settings tests green; offers/social suites still green.
The testable core of the offers UI slice, transport-agnostic:
- OfferMapper: pure app-sharing-intent (OfferStatus) -> network Offer/OfferType
mapping; keeps price only for sales, refuses to publish private lots.
- OffersCubit: discover(geohash) streams results; publish() reports the verdict;
depends on the OfferTransport interface (fake in tests) and degrades
gracefully offline (null transport = social layer unavailable, never throws).
8 tests green (fake in-memory transport, no relay). Screen/routing/i18n come
next — they need two product decisions (coarse-area capture, relay list).
Third slice: bridge Block 1's root seed to the Block 2 transport foundation.
- SocialService: derives the secp256k1 Nostr identity from the SAME root seed
the recovery QR backs up (no extra backup), exposes npub/pubkey, and opens a
SocialSession = one shared connection carrying all three transports.
- injector.dart: registers SocialService in get_it. Local-first — derivation is
cheap + offline; no relay is contacted at startup. Replaces the old
"nodeId slice of the seed is the social pubkey" placeholder; keeps the CRDT
author id distinct (unifying it would rewrite authorship — a later decision).
- Test: deterministic identity from the seed hex, matches commons_core, rejects
malformed hex. 5 tests green; app_seeds analyzes clean (0 errors).
Infer the catalog species a free-text variety label names ("Maiz de la
abuela" -> Zea mays) and prefill the category from the species family.
- Pure, testable matcher (domain/species_autoclassify.dart): whole-word,
accent/case-insensitive, Unicode-aware (any script), longest-name-wins,
ambiguous names left unclassified, light plural fold.
- Quick-add and draft naming auto-link the species when the field is empty
(non-destructive; an explicit category is kept).
- Edit sheet offers a one-tap suggestion from the typed name.
Tests: matcher unit, repository integration, SpeciesRepository.classifyLabel,
and an edit-sheet widget test.
The repository clock started fresh at Hlc.zero on every launch and was
never re-seeded from the database. After a restart, if the device wall
clock lagged behind stamps already stored (e.g. rows imported from a
peer whose clock ran ahead), a local edit was stamped *older* than the
row it edited and silently lost last-writer-wins — so lot changes made
on one device did not update when the backup was imported on another.
Re-seed the clock lazily from the newest stored `updatedAt` before the
first local stamp of the session, mirroring what importInventory already
does with the incoming max. _stamp() is now async.
Tests: add a lot-level LWW re-import test (the variety case was covered,
the lot case was not) and a restart-regression test that reproduces the
lagging-wall-clock scenario.
Grow the bundled species catalog from 14 hand-curated entries to ~1200
edible/cultivated species, internationalized in 13 languages (es, en, fr,
de, it, pt, ca, gl, eu, ar, zh, ja, ru — Latin + Arabic RTL + CJK + Cyrillic).
- Add a reproducible generator (tool/gen_species_catalog.dart) that queries
Wikidata (CC0, no attribution burden) in two phases, filters out
non-vernacular noise (author citations, ranks, initials) and applies a
relevance floor, then merges hand-curated, authoritative core-crop data
(tool/curated_overrides.json: names, family, viability_years). GBIF is used
only as an identifier. The generated species.json (v3) is committed.
- Carry wikidata_qid and gbif_key through the parse/seed pipeline; the columns
already existed, so no DB migration.
- Rewrite seedBundled to one read + one batch (was a SELECT per species on
every startup — a real cost at ~1200 rows) and keep it idempotent with
backfill of the new reference fields.
- Move species search filtering to SQL (LIKE) so a large catalog is not pulled
into memory on every keystroke.
- Cover the generator transform, the generated asset, and the new fields with
tests.
- Android release signing reads a gitignored key.properties, falling back
to debug keys so contributors can still build --release.
- CHANGELOG.md (human, newest-first) + Fastlane/F-Droid metadata (en/es)
with descriptions derived from docs/intro.md.
- docs/release.md: the by-hand build + manual smoke checklist (no CI yet).
- inventory_scale_test guards the list query against N+1 at 3000 varieties.
- Add rtl_smoke_test covering inventory, detail (lot sheet) and settings
under Directionality.rtl, with an Arabic label and vernacular name.
- Ship pt.i18n.json (Portuguese) as proof the locale pipeline scales beyond
es/en; wire its language tile in Settings.
- docs/i18n.md documents the Weblate-ready flow for adding locales.
Add a read-only "Learn more" section on the variety detail that surfaces
verified external references for the linked species, built deterministically
from bundled identifiers — no titles bundled, no network, offline-first:
- GBIF taxon page from the backbone key
- Wikipedia via Wikidata Special:GoToLinkedPage (locale-aware, RTL-safe,
falls back to the Wikidata item when a language lacks the article)
- Wikispecies from the scientific name
Enrich the bundled catalog (species.json v3) with gbif_key + wikidata_qid for
all 14 species, cross-checked (each QID carries GBIF ID P846 = the key).
Schema already had the columns; seedBundled now persists and backfills them.
The grower's own manual links stay but are demoted (progressive disclosure):
the section only shows when links exist and "Add link" is a low-emphasis
button, since hand-adding a link is the rarer power move.
Pure-Dart link builder is unit-tested; repo persistence/backfill and the
detail-screen section are covered too.
'Save a backup' now writes a sealed .tanemaki file (AES-256-GCM under a
key HKDF-derived from the root seed; format TANEBK v1, open and stable).
Restoring on the same identity is silent; a copy sealed by another
identity asks for the printed recovery code (TANE1 base32, typo-tolerant)
and adopts that seed — recovering your bank recovers you. Legacy plain
exports still restore. Settings gains a recovery sheet (QR + code PDF).
BackupBox/RecoveryCode live in commons_core, pure Dart, TDD.
The local half of the original Fase 2 (no network, no Offer entity yet):
- Lot sheet gains a 'do you share it?' choice (gift/swap first, sale last,
never the default); declaring plenty of abundance reveals it with a nudge.
- Lot lines and list tiles badge shared batches; an 'I share' filter and
a printable PDF catalog (paper bridge for fairs) round out the view.
- Catalog PDF embeds DejaVu (Latin-ext/Cyrillic/Greek); fonts are a
per-locale resource like the OCR packs.
Lower the bulk-digitization cliff with two more routes on top of the
already-landed CSV import and "save and add another":
- Photo-first drafts (capture now, catalogue later): burst-capture
photos (camera or multi-gallery) into unnamed draft varieties, shown
in a "to catalogue" tray, hidden from the main list until named.
Adds Variety.isDraft (schema), addDraftVariety/watchDrafts/nameDraft,
the triage sheet and the inventory banner.
- On-device OCR label suggestion (Tesseract, offline, no Google): a
"Suggest name from photo" button in the naming dialog behind a
LabelTextExtractor interface (Tesseract on Android/iOS, no-op
elsewhere). Reads the largest print via hOCR bounding boxes, drops
boilerplate/low-confidence noise, preprocesses (grayscale, contrast,
upscale) and sweeps rotations (0-315 deg) so tilted packets still
read. Bundles tessdata_fast eng+spa; validated on-device against real
packets. The photo is written to a temp file deleted immediately in a
finally block (the plugin needs a path) - a bounded, documented
exception to no-plaintext-at-rest.
This commit also carries the co-developed schema evolution v5 to v8 that
shares these files (organic flag, species viability years, crop
calendar, lot provenance/abundance/preservation format, condition
checks) plus their exports/migrations and i18n.
Tests: CSV/draft/OCR unit + widget + migration green in isolation.
Note: the full widget suite currently hangs (>10 min) - under investigation.
The quick-add sheet no longer closes on every save: a new 'Save & add
another' action saves the seed, clears the form (keeping the chosen lot
type), refocuses the name field and shows an 'N added' running count, so
a whole shelf can be entered without reopening the sheet each time.
Plain Save still closes it. Adds a dedicated label field with its own
controller, submitAndAddAnother() on the cubit, and cubit + widget tests.
Import a spreadsheet list (the same 21-column schema the CSV export
already produces) as new varieties/lots. Tolerant reader: columns
matched by header name in any order, only variety_label required,
unknown enums fall back to a safe default, rows sharing variety_id
collapse into one variety with several lots. Species re-linked by
scientific name. Additive by design (fresh ids + HLC), so it never
overwrites existing rows; JSON stays the canonical id-reconciling import.
Adds a hand-rolled RFC 4180 parser (no new dependency), an importCsv
path in VarietyRepository/ExportImportService, an 'Import from CSV'
tile in the backup section, and en/es strings.
Note: i18n .g.dart also carries pre-existing About/intro strings from
the working tree (regenerated by slang).
seedMuted (hints, card subtitles, search placeholder) was #8A9880 —
2.62:1 on the canvas and 3.05:1 on white, failing AA for the 13px text
it colours. Darkened to #617057: 5.30:1 on white, 4.56:1 on canvas,
4.88:1 on the field fill, same muted green-grey hue.
The new contrast test also caught the app-bar green: white titles on
#3E8C38 were 4.19:1, so it is nudged to #3A8434 (4.64:1) — a barely
visible change.
test/ui/theme_contrast_test.dart pins the WCAG relative-luminance math
and asserts every text/background pair the palette actually uses.
Interchange export/import for Phase 1 (data-model §7):
- JSON: canonical, versioned envelope (formatVersion 1) with all sync
metadata verbatim, photos embedded as base64, tombstones excluded.
Species are re-resolved on import by scientific name (catalog ids are
per-install). Reader tolerates unknown fields/enum values (§5.2) and
rejects newer format versions with a clear error.
- CSV: export-only spreadsheet flatten, one row per lot, RFC 4180
escaping, never any photo bytes.
- Import merges by UUIDv7 id in one transaction: insert-if-unknown
preserving original stamps, last-writer-wins by packed HLC for known
mutable rows, append-only movements; afterwards the local clock
receiveEvent()s the newest imported stamp so it never runs behind.
- Settings gains a Backup section (export CSV/JSON, import JSON with
confirmation); file dialogs behind a FileService interface backed by
file_picker (MIT).
- Tests: codec round-trip and tolerance, reconciler LWW, repository
export→import round-trip (fresh DB, idempotent re-import, newer-local
wins, clock monotonicity, species re-resolution), backup widget flows.
Add an "About" entry to the navigation drawer (below Settings) so the
About screen is reachable without opening Settings first.
Recolour the launcher icon and native splash to a white sprout on brand
green (#2F7D34) — the previous dark-green-on-light-green had almost no
contrast. Regenerate Android, iOS and web icons and the light/dark splash.
Add filter chips above the inventory list, one per category in use and
one per lot form actually held; results apply search ∧ category ∧ form.
An active filter that empties the list shows a distinct 'no matches'
state and a clear-filters button. VarietyListItem now carries the set of
lot forms it holds (one aggregate query), and the stream re-emits on lot
changes.
Accessibility: category headers use the text theme so they honour the
system font scale; the list avatar is excluded from semantics (the tile
title already announces the name); the edit action uses the green action
colour (≥3:1 on the canvas, up from 2.62:1). Add category/form/clear
filter tests and a tap-target-size guideline test.
Apply a Material 3 seed-green palette and rounded components across the
home, inventory, quick-add, quantity picker and variety-detail screens.
The full-screen photo viewer can now set any photo as the cover (via
attachment sort order) and delete after confirmation. Lot forms and
packaging surface as choice chips.
Extract About out of Settings into its own screen (app version via
package_info_plus, website link). Add es/en strings for the new lot
types, presentation, home tagline and About. Update Seedees v2 mockups.
Expand LotType from {seed, plant} to six append-only forms (seed,
seedling, plant, tree, bulb, cutting) and add an optional Presentation
attribute (pot/tray/plug/bareRoot/rootBall) kept separate from quantity
so 'how many' and 'in what packaging' never conflate.
Add attachments.sortOrder so the cover photo is the lowest-ordered one,
enabling reordering from the full-screen viewer. Expand QuantityKind
with the new plant-aware forms.
Migrations are additive (v1→v5 all covered by migration_test); enum
values are stored by name and appended only, keeping CRDT sync safe.
- Bundle the real sprout logo (docs/logo.png → assets/logo.png) and show it on
the home menu instead of a Material icon.
- Settings screen: switch language (Español / English / system) via slang, plus
an About tile (logo, app name, one-line description). Drawer "Settings" is now
active and routes to /settings.
Tests: drawer Settings opens the screen and shows the language options. 54 green.
- Each lot shows a Seeds/Plantel chip (green vs teal, with icon) so mixed
seed-and-plant varieties read clearly at a glance.
- Germination is hidden on plant lots (it only applies to seeds).
- Tapping a photo in the detail carousel opens a full-screen, swipeable,
pinch-to-zoom viewer.
Tests: plant lot shows the Plants chip and hides germination. 53 green; Linux runs.
Add Wikipedia/forum-style URLs to a variety:
- Repository: addExternalLink/removeExternalLink (soft delete); watchVariety
loads links and re-emits on external_links changes; ExternalLinkItem model
(title preferred over URL for display).
- Cubit: addExternalLink/removeExternalLink.
- Detail: a "Links" section with tappable link chips (open via url_launcher,
http(s):// prepended if missing), delete per chip, and an "Add link" dialog
(URL + optional title).
Tests: repo add/remove + widget add/remove. 52 green; Linux runs.
The app now opens on a main menu instead of straight into the list:
- HomeScreen (mockup 02): sprout logo over a faint seed-glyph watermark, with
big "Your inventory" (→ /inventory) and "Open market" (Block 2, "coming soon")
destinations.
- AppDrawer (mockup 03): Inventory is live; the social items (market, profile,
chat, wishlist, following, settings) are shown disabled with a "coming soon"
tag so the roadmap is visible. Drawer is available on home and inventory.
- Routing: / → home, /inventory → list, /variety/:id → detail.
Tests: home shows the menu + navigates; drawer opens + navigates. quick-add flow
updated to go home → inventory. 50 green; Linux runs.
Photos:
- Repository addPhoto/removePhoto; VarietyDetail.photos (list); loads all photos.
- Detail: a 140×140 photo carousel with page dots, delete-current, and add-photo
(gallery); empty state shows an add-photo button.
- watchInventory now re-emits on attachment + species changes (StreamGroup), so
a photo added on the detail screen refreshes the inventory avatar immediately
(fixes avatars not updating).
Also carries the concurrent harvest-date work: lots gain an optional harvest
month (schemaVersion 3 + from2To3 migration, drift_schema_v3 exported), a
month/year harvest picker, and localized month names.
48 tests green (add/remove photos, avatar reactivity, v1→v3 migration); Linux runs.
Replace the free-text harvest year with a themed month/year picker:
a tappable field opens a green-styled dialog with month and year grids
(navigable, paginated years), month optional. Lots now read e.g.
"September 2021" or just "Year 2021".
- schema v3: add nullable Lot.harvest_month (1..12) + migration + tests
- repo/cubit propagate harvestMonth; localized month names (en/es) via slang
The "also known as" names were read-only; now they can be added and removed:
- Repository: addVernacularName + removeVernacularName (soft delete);
VarietyDetail.vernacularNames carries ids (new VernacularName model).
- Cubit: addVernacularName / removeVernacularName.
- Detail: deletable name chips + an "Add name" chip opening a small dialog;
the section is always visible so names can be added when empty.
Tests: repo add/remove + widget add/remove. 44 green.
Lots could only be created; now they can be edited and removed:
- Repository: updateLot (LWW) + softDeleteLot (tombstone).
- Cubit: updateLot / deleteLot.
- The add-lot sheet is now a shared add/edit sheet — tapping a lot opens it
prefilled (type, year, quantity), with a trash action to delete. The reactive
detail view refreshes on save/delete.
Tests: repo updateLot/softDeleteLot + widget edit-lot and delete-lot. 42 green.
Feedback from the running app:
- Remove "handful" from the scale (redundant with spoon); keep the enum value.
- Reorder so "packet" (sobre) follows "spoon" (cuchara).
- Use the Material Symbols potted-plant for the plant lot type and plant unit
(adds material_symbols_icons).
- Harvest year is now a dropdown of recent years instead of free text.
38 tests green; Linux build runs with the new icon font bundled.
Rework quantities the way seeds are actually described — a number of an informal
unit — and let a lot hold seeds or plants (plantel).
Quantity model (commons_core):
- Quantity is now { kind, count?, label } — "3 cobs", "2 packets", "a few".
Count is optional; uncountable vibes (a few, some, pinch) carry no number.
- QuantityKind reorganised into an ascending scale: vibes → containers
(teaspoon/spoon/cup/jar/sack) → seed & fruit forms → plant units, each tagged
countable/not. Stored by name; count reuses the existing numeric column.
Lot type (schema v2):
- Lots gain a `type` (seed | plant); germination stays meaningful for seeds.
schemaVersion 2 with a from1To2 migration (adds the column), drift_schema_v2
exported, migration test covers v1→v2.
UI:
- New QuantityPicker: a horizontal scale of large seed-glyph icons + a number
stepper (shown only for countable units), plus a seed/plant SegmentedButton.
Used in quick-add and add-lot. Lot lines render "3 cobs" / "a few".
- i18n: unit singular/plural (ES/EN, Weblate-friendly), lot-type labels.
Build: slang moved to its CLI (disabled in build_runner to avoid a multi-file
collision); CI runs `dart run slang` before build_runner. 38 tests green,
Linux migrates the existing v1 DB and runs.
Visual pass toward docs/mockups (06 inventory, 07 item):
- Bundle the seedks icon font (chars a–k → seed pictograms: jar, packet, spoon…)
and a SeedGlyph/QuantityKindIcon helper.
- Green app bar + pale-green canvas theme (buildTaneTheme).
- Inventory list: rounded-square photo thumbnail or green initial avatar, bold
title, scientific-name subtitle, trailing edit pencil, styled category
headers, rounded search field, seed-glyph empty state.
- Quantity selectors (quick-add + add-lot) show the seed pictogram + word;
detail lot lines use the kind's glyph.
- Detail header restyled to mockup 07: category link + scientific name on the
left, photo on the right.
watchInventory now also returns the linked species' scientific name (subtitle).
37 app tests green; seedks font verified in the Linux asset bundle.
The list showed only initials even when a variety had a photo (quick-add stores
one as an encrypted BLOB). Now the avatar shows the first photo, falling back to
the initial — matching the "foto/inicial" list spec.
- VarietyListItem carries the first photo bytes; watchInventory loads them in a
single query per emission.
- List tile renders a MemoryImage avatar when present.
Tests: repository (photo bytes surfaced, null when none) and a widget test
(avatar replaces the initial). Full suite: 37 passing, 0 skipped.
Record germination tests on a lot and surface the result.
- GerminationEntry model with a derived rate (germinated / sample). VarietyLot
carries its tests (most-recent first) and exposes latestGerminationRate.
- VarietyRepository.addGerminationTest; watchVariety now also re-emits on
germination-test changes, so the detail refreshes live.
- Detail UI: each lot shows a germination % badge and a grass action that opens
a sheet listing past tests and adding a new one (germinated + sample size).
i18n strings (ES/EN).
Tests: derived-rate + reactivity at the repository, and a widget test for the
record-test → badge flow. Full suite: 35 passing, 0 skipped.
Add a small curated catalog of Iberian horticultural species and let a variety
be linked to it from the edit sheet.
- assets/catalog/species.json: 14 species with botanical family and ES/EN
common names (wikidata_qid/gbif_key deferred to the varilla enrichment).
- SpeciesRepository: idempotent seedBundled (is_bundled rows, keyed by
scientific name) + search by scientific/common name with a locale-best label.
Seeded on startup from DI.
- VarietyRepository.linkSpecies: sets species_id and prefills category from the
species' family when empty (never overwrites an existing category).
VarietyDetail now carries the scientific name.
- Edit sheet gains a live species-search field; the detail view shows the
scientific name (italic). i18n strings added (ES/EN).
Tests: catalog parse, idempotent seeding, search by scientific/common name,
linkSpecies prefill semantics, and a widget test for the autocomplete → link →
scientific-name-shown flow. Full suite: 32 passing, 0 skipped.
Tapping an inventory item now opens its detail (the /variety/:id route was a
placeholder). Read + edit one variety end-to-end:
- Detail view: photo, category, "also known as" names, notes and lots, driven
by a reactive VarietyDetailCubit.
- Edit core fields (label/category/notes, LWW) via a bottom sheet.
- Add a lot (harvest year + quantity) — the list refreshes reactively.
- Soft-delete (tombstone) with a confirm dialog.
Repository:
- watchVariety(id): merges Drift table-change streams (variety, lots, names,
attachments) via StreamGroup and reloads the full detail on any change, so a
lot added to a *different* table still refreshes the view.
- updateVariety (LWW), addLot, softDeleteVariety.
i18n: detail/edit/addLot strings (ES/EN); switched slang to {param} braces
interpolation (translator- and Weblate-friendly).
Tests: repository (reactive watchVariety, update, soft-delete) and widget tests
(render, edit updates title, add-lot appears, delete shows not-found). Full
suite green: 23 passing, 1 skipped (SQLCipher-only encryption guard).
Stand up the Tanemaki monorepo and the first end-to-end vertical slice of
Block 1 (offline, encrypted inventory): add a seed → see it in a categorized,
searchable list → it persists → reopen and it's still there.
Architecture (state management like G1nkgo, adapted to Tane's reality):
- flutter_bloc (Cubit-first), but the encrypted Drift DB is the single source
of truth; cubits stream from repositories (no hydrated_bloc/Hive, which would
write plaintext at rest).
- get_it composition root; go_router; slang i18n (ES/EN, Weblate-friendly JSON).
Workspace & core:
- pub workspace: packages/commons_core (pure Dart) + apps/app_seeds (Flutter).
- commons_core primitives: UUIDv7 IdGen, Hybrid Logical Clock, Quantity value
type (+ plant-aware QuantityKind), IdentityService root-seed stub.
Data & security:
- Drift schemaVersion=1 with all 10 Block-1 tables + common CRDT columns
(HLC updated_at, last_author, tombstones); Movement append-only.
- SQLCipher via an injectable executor that refuses to open a plaintext DB;
DB key + root seed in the OS keystore (separate secrets).
- Exported drift_schema_v1.json + migration scaffold.
Tests (near-TDD; nothing merges without tests):
- commons_core units (24), Drift migration test, "no plaintext at rest"
security guard (runs where SQLCipher is present, skips otherwise),
repository, widget, full quick-add flow, file-reopen persistence, and a
no-hardcoded-strings i18n guard. GitLab CI: format + analyze + test + coverage.
Follow-on Block-1 stories (item detail/edit, species catalog, germination UI)
and all of Block 2 remain out of scope.