{
  "lexicon": 1,
  "id": "org.cannadb.strain",
  "defs": {
    "main": {
      "type": "record",
      "description": "Cannabis genetic identity. The same record type covers two levels of specificity, distinguished by the `kind` field: \"strain\" denotes a broad genetic identity (e.g., \"Wedding Cake\"); \"cut\" denotes a specific phenotype, clone, or keeper pheno (e.g., \"Forum cut of GSC\"). Captures intrinsic identity, structured cultivation profile, and the publisher's contextualized field assertions; subjective experience and measured outcomes live in separate record types (`org.cannadb.review`, future `org.cannadb.grow`) that reference this one. Publishing this record IS the claim — every field below is implicitly contextualized by the publisher's identity, computed by consumers from the publisher DID at display time.",
      "key": "tid",
      "record": {
        "type": "object",
        "required": ["name", "kind", "createdAt"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Primary common name of the strain or cut, e.g., \"Wedding Cake\" or \"Forum cut of GSC\"."
          },
          "kind": {
            "type": "string",
            "description": "Level of specificity. \"strain\" = broad genetic identity; \"cut\" = a specific phenotype, clone, or keeper pheno of a strain. Open vocabulary — clients should accept unknown values gracefully and fall back to generic display.",
            "knownValues": ["strain", "cut"]
          },
          "createdAt": {
            "type": "string",
            "format": "datetime",
            "description": "Client-declared timestamp when this record was originally created."
          },
          "alsoKnownAs": {
            "type": "array",
            "description": "Alternate names this strain or cut is known by, e.g., [\"Triangle Mints #23\"].",
            "items": {
              "type": "string",
              "description": "An alternate display name for this strain or cut."
            }
          },
          "shortDescription": {
            "type": "string",
            "description": "Single-line plain-text summary used in card meta lines, search-result excerpts, and social-share previews. Markdown is disallowed; this is a one-line summary distinct from the full markdown `description`. Soft cap ~200 characters — the lexicon does not enforce this length, but consumers should expect publishers to keep it short."
          },
          "description": {
            "type": "string",
            "description": "Markdown-formatted identity narrative — what this strain is, where it came from, the breeder's intent, the character. Distinct from `cultivationNotes`, which is for cultivators."
          },
          "primaryImage": {
            "type": "blob",
            "description": "One representative image of the strain or cut. Accepts common web image formats; capped at 1 MB to keep PDS storage burden modest. Higher-resolution photography belongs in review or grow records.",
            "accept": ["image/png", "image/jpeg", "image/webp"],
            "maxSize": 1000000
          },
          "sourceUrl": {
            "type": "string",
            "format": "uri",
            "description": "Optional URL pointing at the canonical breeder/origin write-up for this strain — typically the breeder's product page or a strain-specific writeup. Distinct from the breeder record's `website` (the breeder's home page); this is the strain-specific page. No link-rot guarantee; informational only."
          },
          "breeder": {
            "type": "string",
            "format": "at-uri",
            "description": "AT-URI reference to an `org.cannadb.breeder` record. Canonical when present and resolvable; `breederName` is the display fallback. (The lexicon spec does not enforce collection constraints on at-uri; consumers should validate the ref resolves to an `org.cannadb.breeder` record.)"
          },
          "breederName": {
            "type": "string",
            "description": "Display fallback for the breeder name. Used when no `breeder` ref is available, when the ref does not resolve, or for breeders without atproto presence."
          },
          "holder": {
            "type": "string",
            "format": "at-uri",
            "description": "For cut records: AT-URI reference (typically a DID) to the holder of this specific cut. `holderName` is the display fallback."
          },
          "holderName": {
            "type": "string",
            "description": "Display fallback for the cut holder."
          },
          "originYear": {
            "type": "integer",
            "description": "Year the strain or cut was first introduced."
          },
          "originRegion": {
            "type": "string",
            "description": "Region of geographic origin, e.g., \"Hindu Kush\", \"California\"."
          },
          "parents": {
            "type": "array",
            "description": "Genetic ancestry — AT-URI references to parent `org.cannadb.strain` records (typically 1–2 entries). `parentNames` provides display fallbacks; `parentBreederNames` provides per-parent breeder attribution as a parallel array. (The lexicon spec does not enforce collection constraints on at-uri; consumers should validate refs resolve to `org.cannadb.strain` records.)",
            "items": {
              "type": "string",
              "format": "at-uri",
              "description": "AT-URI reference to a parent `org.cannadb.strain` record."
            }
          },
          "parentNames": {
            "type": "array",
            "description": "Display fallbacks for parent strains. Used when refs are absent or unresolved. Phenotype designations, generation markers, and uncertainty markers may be embedded inline in the string (e.g., \"Triangle Mints #23 [pheno: keeper] (F3)\") — clients render them as written.",
            "items": {
              "type": "string",
              "description": "Display name of a parent strain."
            }
          },
          "parentBreederNames": {
            "type": "array",
            "description": "Optional parallel array to `parentNames` capturing per-parent breeder attribution (e.g., [\"Karma Genetics\", \"\"] when the first parent's breeder is known and the second's is not). Index-aligned with `parentNames`: position N in this array attributes the breeder of the parent at position N in `parentNames`. Empty strings or missing trailing entries indicate unknown attribution. Forward-compat note: phenotype designations, generation markers, and uncertainty markers stay inline in the parent name string until evidence shows they need their own structure.",
            "items": {
              "type": "string",
              "description": "Display name of the breeder of the parent at the same index in `parentNames`. Empty string when unknown."
            }
          },
          "versionOf": {
            "type": "array",
            "description": "Derivative or claimed relationships — AT-URI references to `org.cannadb.strain` records this record is sold as, claims to be, or is a take on. Distinct from `parents` (genetic ancestry): a pure relabel has `versionOf` but no `parents`; a derivative breeding take may have both. Includes the autoflower-take-on-a-photo pattern (an autoflower variant of \"the same strain\" is a `versionOf` claim even though its `parents` may differ because a ruderalis line was crossed in).",
            "items": {
              "type": "string",
              "format": "at-uri",
              "description": "AT-URI reference to an `org.cannadb.strain` record this record claims a derivative or equivalence relationship with."
            }
          },
          "versionOfNames": {
            "type": "array",
            "description": "Display fallbacks for `versionOf` references.",
            "items": {
              "type": "string",
              "description": "Display name of a strain this record is a version of."
            }
          },
          "autoflower": {
            "type": "boolean",
            "description": "Whether this strain or cut is autoflowering (i.e., has ruderalis influence). Orthogonal to indica/sativa lean. Also conditions the basis of `cycleTime`: full-run (germination → harvest) for autoflowers, flower-only for photoperiods."
          },
          "indicaSativa": {
            "type": "integer",
            "description": "Indica/sativa lean on a 0–100 scale, where 0 is pure indica and 100 is pure sativa. Pure landraces sit at the extremes; modern hybrids fall between.",
            "minimum": 0,
            "maximum": 100
          },
          "cbdDominant": {
            "type": "boolean",
            "description": "Whether this is a CBD-dominant strain. Orthogonal to the indica/sativa axis (a strain can be both indica-leaning and CBD-dominant). Distinct from `cbdRange` (which is the quantitative CBD percentage); `cbdDominant` is the category signal — cannabis culture treats CBD-dominant strains as a distinct shopping category, not just a high-CBD variant."
          },
          "generation": {
            "type": "string",
            "description": "Free-form generation marker for the strain's own generation (e.g., \"F7\", \"BX3\", \"S1\", \"Selection\"). Free-form because the vocabulary is open and breeders write whatever shorthand fits. Parent strains' generations stay inline in `parentNames` strings."
          },
          "seedAvailability": {
            "type": "array",
            "description": "What variants exist for this strain name. Multi-valued because a strain often ships in multiple variants. Open vocabulary via `knownValues` so additive expansions (e.g., a future \"feminized_autoflower\" composite value) don't require a lexicon revision.",
            "items": {
              "type": "string",
              "description": "A single seed variant identifier.",
              "knownValues": ["regular", "feminized", "autoflower", "clone_only"]
            }
          },
          "thcRange": {
            "type": "ref",
            "ref": "#cannabinoidRange",
            "description": "Claimed THC percentage range. Values are integers in hundredths of a percent (e.g., 2450 = 24.50%). Optional; either bound may be omitted if only one is known."
          },
          "cbdRange": {
            "type": "ref",
            "ref": "#cannabinoidRange",
            "description": "Claimed CBD percentage range. Values are integers in hundredths of a percent (e.g., 75 = 0.75%). Optional; either bound may be omitted if only one is known."
          },
          "thcCbdRatio": {
            "type": "string",
            "description": "Claimed THC:CBD ratio expressed as a string, e.g., \"1:1\", \"20:1\"."
          },
          "cycleTime": {
            "type": "ref",
            "ref": "#dayRange",
            "description": "Cycle time in days, expressed as a min/max range. Basis derives from `autoflower`: full-run (germination → harvest) for autoflowers, flower-only (start of 12/12 → harvest) for photoperiods. Single-value quotes set min == max. The basis is not stored separately because it derives from `autoflower`; documenting the convention here prevents downstream tools from misinterpreting the integer."
          },
          "height": {
            "type": "ref",
            "ref": "#heightRange",
            "description": "Plant height at maturity in centimeters, expressed as a min/max range. Conventional basis is indoor mature (the breeder's typical quote). Outdoor mature heights run substantially larger; outdoor-quoted values should be flagged in `cultivationNotes` rather than smashed into this field."
          },
          "yield": {
            "type": "ref",
            "ref": "#yieldRange",
            "description": "Claimed yield as a min/max integer range plus a `unit` enum so the integer is unambiguous. Single-value quotes set min == max. The unit is required to interpret the integer correctly — a lexicon convention because breeders quote yield in mutually incompatible units (per-plant vs per-m², grams vs ounces) and an unlabeled integer is unsafe."
          },
          "growDifficulty": {
            "type": "string",
            "description": "Claimed grow difficulty. `knownValues` for forward-compat — clients should accept unknown values and fall back to generic display.",
            "knownValues": ["easy", "intermediate", "hard"]
          },
          "growthOdour": {
            "type": "string",
            "description": "Claimed growth odour intensity. Practical for stealth growers. `knownValues` for forward-compat.",
            "knownValues": ["low", "medium", "high"]
          },
          "cultivationNotes": {
            "type": "string",
            "description": "Markdown-formatted cultivation guidance from the publisher — overflow for context that doesn't fit the structured `cycleTime`, `height`, `yield`, `growDifficulty`, `growthOdour` fields. Things like indoor/outdoor recommendations, environmental sensitivities, training notes, and run-specific quirks belong here."
          },
          "flavors": {
            "type": "array",
            "description": "Claimed flavor profile descriptors. Open vocabulary — the lexicon does not enforce a closed set; the AppView normalizes near-duplicates at index time and may ship a curated suggestion list for autocomplete.",
            "items": {
              "type": "string",
              "description": "A single flavor descriptor."
            }
          },
          "terpenes": {
            "type": "array",
            "description": "Claimed dominant terpenes. Open vocabulary — see `flavors` for rationale.",
            "items": {
              "type": "string",
              "description": "A single terpene name."
            }
          },
          "effects": {
            "type": "array",
            "description": "Claimed recreational effect descriptors. Open vocabulary — see `flavors` for rationale. Distinct from `medicinalEffects`, which is a parallel vocabulary for therapeutic claims.",
            "items": {
              "type": "string",
              "description": "A single recreational effect descriptor."
            }
          },
          "medicinalEffects": {
            "type": "array",
            "description": "Claimed medical/therapeutic effect descriptors. Open vocabulary; parallel to `effects` but for medicinal/therapeutic claims distinct from recreational. Examples: \"insomnia relief\", \"appetite stimulation\", \"anxiety relief\". The AppView normalizes near-duplicates at index time.",
            "items": {
              "type": "string",
              "description": "A single medicinal effect descriptor."
            }
          },
          "characteristics": {
            "type": "array",
            "description": "Plant trait descriptors orthogonal to consumption experience. Open vocabulary; parallel to `effects`/`medicinalEffects`/`flavors` but for properties of the plant rather than the experience of consuming it. Examples: \"mold resistant\", \"frost tolerant\", \"high flower-to-leaf ratio\", \"drought tolerant\". The AppView normalizes near-duplicates at index time.",
            "items": {
              "type": "string",
              "description": "A single plant characteristic descriptor."
            }
          }
        }
      }
    },
    "cannabinoidRange": {
      "type": "object",
      "description": "A min/max integer range for a cannabinoid percentage, expressed in hundredths of a percent (e.g., 2450 = 24.50%). Both bounds are independently optional; either may be omitted when only one is known.",
      "properties": {
        "min": {
          "type": "integer",
          "description": "Lower bound, in hundredths of a percent (e.g., 2000 = 20.00%).",
          "minimum": 0,
          "maximum": 10000
        },
        "max": {
          "type": "integer",
          "description": "Upper bound, in hundredths of a percent (e.g., 2700 = 27.00%).",
          "minimum": 0,
          "maximum": 10000
        }
      }
    },
    "dayRange": {
      "type": "object",
      "description": "A min/max integer range expressed in whole days. Both bounds independently optional; single-value quotes set min == max. Atproto's data model has no float type, so all numeric fields use integers; days are a natural integer unit and need no further scaling.",
      "properties": {
        "min": {
          "type": "integer",
          "description": "Lower bound in days.",
          "minimum": 0,
          "maximum": 365
        },
        "max": {
          "type": "integer",
          "description": "Upper bound in days.",
          "minimum": 0,
          "maximum": 365
        }
      }
    },
    "heightRange": {
      "type": "object",
      "description": "A min/max integer range expressed in whole centimeters. Both bounds independently optional; single-value quotes set min == max. Centimeters are a natural integer unit and need no further scaling.",
      "properties": {
        "min": {
          "type": "integer",
          "description": "Lower bound in centimeters.",
          "minimum": 0,
          "maximum": 2000
        },
        "max": {
          "type": "integer",
          "description": "Upper bound in centimeters.",
          "minimum": 0,
          "maximum": 2000
        }
      }
    },
    "yieldRange": {
      "type": "object",
      "description": "A min/max integer range plus a `unit` enum so the integer is unambiguous. Single-value quotes set min == max. The unit is what makes the integer interpretable — breeders quote yield in mutually incompatible units (per-plant vs per-m², grams vs ounces), so consumers must read `unit` before interpreting `min`/`max`.",
      "properties": {
        "min": {
          "type": "integer",
          "description": "Lower bound, in the unit specified by `unit`.",
          "minimum": 0
        },
        "max": {
          "type": "integer",
          "description": "Upper bound, in the unit specified by `unit`.",
          "minimum": 0
        },
        "unit": {
          "type": "string",
          "description": "Unit of `min`/`max`. `g_per_plant` = grams per plant; `g_per_m2` = grams per square meter; `oz_per_plant` = ounces per plant; `oz_per_m2` = ounces per square meter. `knownValues` rather than a closed enum so additive unit expansions don't require a lexicon revision.",
          "knownValues": ["g_per_plant", "g_per_m2", "oz_per_plant", "oz_per_m2"]
        }
      }
    }
  }
}
