feat(seed-saving): per-crop 'how to save this seed' guidance

A bundled, curated knowledge layer — the differentiator of a serious
seed app. For a variety's crop it shows how the plant reproduces and what
it takes to keep the variety true: life cycle, pollination (self/cross +
insect/wind), isolation distance, how many plants to save from, dry/wet
processing, a difficulty pill, and a short tip — all in human words.

- assets/catalog/seed_saving.json: curated public-domain data (Seed to
  Seed / Seed Savers), keyed by botanical FAMILY (defaults) with
  per-species/genus OVERRIDES that win — so e.g. Vicia faba is correctly
  insect-cross-pollinated despite the selfing Fabaceae default, and maize
  is wind-pollinated with a large population. Notes are locale-keyed
  (es/en), extensible to any language — a starter set, not a ceiling.
- domain/seed_saving.dart (pure): enums + SeedSavingGuide (merge, note
  fallback) + SeedSavingData.guideFor (species → genus → family).
- data/seed_saving_catalog.dart: loads the asset once into an in-memory
  singleton (no DB table, no migration); injector loads it at startup.
- VarietyDetail gains  (from the linked species — the reliable
  key, vs the editable category); detail screen shows a _SeedSavingView
  when a guide resolves.
- i18n seedSaving block (labels + enum values + params) in en/es/pt/ast.
- Tests: domain lookup/merge, the real asset's key corrections, and the
  detail section shows/hides. 314 suite tests green; analyze clean.
This commit is contained in:
vjrj 2026-07-11 07:17:12 +02:00
parent 4ebfdca2fd
commit 4bb94db3e0
19 changed files with 954 additions and 2 deletions

View file

@ -436,6 +436,7 @@ class VarietyDetail extends Equatable {
this.notes,
this.speciesId,
this.scientificName,
this.family,
this.gbifKey,
this.wikidataQid,
this.viabilityYears,
@ -459,6 +460,11 @@ class VarietyDetail extends Equatable {
final String? speciesId;
final String? scientificName;
/// Botanical family of the linked species (Latin), the reliable key for
/// seed-saving guidance; null when no species is linked. (The editable
/// [category] is prefilled from this but may be changed by the grower.)
final String? family;
/// Bundled identifiers of the linked species, used to derive verified
/// "learn more" reference links (GBIF/Wikipedia/Wikispecies); null when no
/// species is linked or the catalog entry lacks the id.
@ -506,6 +512,7 @@ class VarietyDetail extends Equatable {
notes,
speciesId,
scientificName,
family,
gbifKey,
wikidataQid,
viabilityYears,
@ -1022,6 +1029,7 @@ class VarietyRepository {
if (v == null) return null;
String? scientificName;
String? family;
int? viabilityYears;
int? gbifKey;
String? wikidataQid;
@ -1030,6 +1038,7 @@ class VarietyRepository {
_db.species,
)..where((s) => s.id.equals(v.speciesId!))).getSingleOrNull();
scientificName = species?.scientificName;
family = species?.family;
viabilityYears = species?.viabilityYears;
gbifKey = species?.gbifKey;
wikidataQid = species?.wikidataQid;
@ -1119,6 +1128,7 @@ class VarietyRepository {
notes: v.notes,
speciesId: v.speciesId,
scientificName: scientificName,
family: family,
gbifKey: gbifKey,
wikidataQid: wikidataQid,
viabilityYears: viabilityYears,