{
"id": "org.cannadb.strain",
"defs": {
"main": {
"key": "tid",
"type": "record",
"record": {
"type": "object",
"required": [
"name",
"kind",
"createdAt"
],
"properties": {
"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"
]
},
"name": {
"type": "string",
"description": "Primary common name of the strain or cut, e.g., \"Wedding Cake\" or \"Forum cut of GSC\"."
},
"yield": {
"ref": "#yieldRange",
"type": "ref",
"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."
},
"height": {
"ref": "#heightRange",
"type": "ref",
"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."
},
"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."
},
"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.)"
},
"effects": {
"type": "array",
"items": {
"type": "string",
"description": "A single recreational effect descriptor."
},
"description": "Claimed recreational effect descriptors. Open vocabulary — see `flavors` for rationale. Distinct from `medicinalEffects`, which is a parallel vocabulary for therapeutic claims."
},
"flavors": {
"type": "array",
"items": {
"type": "string",
"description": "A single flavor descriptor."
},
"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."
},
"parents": {
"type": "array",
"items": {
"type": "string",
"format": "at-uri",
"description": "AT-URI reference to a parent `org.cannadb.strain` record."
},
"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.)"
},
"cbdRange": {
"ref": "#cannabinoidRange",
"type": "ref",
"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."
},
"terpenes": {
"type": "array",
"items": {
"type": "string",
"description": "A single terpene name."
},
"description": "Claimed dominant terpenes. Open vocabulary — see `flavors` for rationale."
},
"thcRange": {
"ref": "#cannabinoidRange",
"type": "ref",
"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."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this record was originally created."
},
"cycleTime": {
"ref": "#dayRange",
"type": "ref",
"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."
},
"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."
},
"versionOf": {
"type": "array",
"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."
},
"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)."
},
"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."
},
"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."
},
"holderName": {
"type": "string",
"description": "Display fallback for the cut holder."
},
"originYear": {
"type": "integer",
"description": "Year the strain or cut was first introduced."
},
"alsoKnownAs": {
"type": "array",
"items": {
"type": "string",
"description": "An alternate display name for this strain or cut."
},
"description": "Alternate names this strain or cut is known by, e.g., [\"Triangle Mints #23\"]."
},
"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."
},
"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."
},
"description": {
"type": "string",
"maxLength": 100000,
"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. Capped at 10000 graphemes (100000 bytes).",
"maxGraphemes": 10000
},
"growthOdour": {
"type": "string",
"description": "Claimed growth odour intensity. Practical for stealth growers. `knownValues` for forward-compat.",
"knownValues": [
"low",
"medium",
"high"
]
},
"parentNames": {
"type": "array",
"items": {
"type": "string",
"description": "Display name of a parent strain."
},
"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."
},
"thcCbdRatio": {
"type": "string",
"description": "Claimed THC:CBD ratio expressed as a string, e.g., \"1:1\", \"20:1\"."
},
"indicaSativa": {
"type": "integer",
"maximum": 100,
"minimum": 0,
"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."
},
"originRegion": {
"type": "string",
"description": "Region of geographic origin, e.g., \"Hindu Kush\", \"California\"."
},
"primaryImage": {
"type": "blob",
"accept": [
"image/png",
"image/jpeg",
"image/webp"
],
"maxSize": 2000000,
"description": "One representative image of the strain or cut. Accepts common web image formats; capped at 2 MB to keep PDS storage burden modest. Higher-resolution photography belongs in review or grow records."
},
"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"
]
},
"versionOfNames": {
"type": "array",
"items": {
"type": "string",
"description": "Display name of a strain this record is a version of."
},
"description": "Display fallbacks for `versionOf` references."
},
"characteristics": {
"type": "array",
"items": {
"type": "string",
"description": "A single plant characteristic descriptor."
},
"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."
},
"cultivationNotes": {
"type": "string",
"maxLength": 100000,
"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. Capped at 10000 graphemes (100000 bytes).",
"maxGraphemes": 10000
},
"medicinalEffects": {
"type": "array",
"items": {
"type": "string",
"description": "A single medicinal effect descriptor."
},
"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."
},
"seedAvailability": {
"type": "array",
"items": {
"type": "string",
"description": "A single seed variant identifier.",
"knownValues": [
"regular",
"feminized",
"autoflower",
"clone_only"
]
},
"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."
},
"shortDescription": {
"type": "string",
"maxLength": 2000,
"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`. Capped at 250 graphemes (2000 bytes) — enforced by the lexicon to keep card meta lines and share previews short.",
"maxGraphemes": 250
},
"parentBreederNames": {
"type": "array",
"items": {
"type": "string",
"description": "Display name of the breeder of the parent at the same index in `parentNames`. Empty string when unknown."
},
"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."
}
}
},
"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."
},
"dayRange": {
"type": "object",
"properties": {
"max": {
"type": "integer",
"maximum": 365,
"minimum": 0,
"description": "Upper bound in days."
},
"min": {
"type": "integer",
"maximum": 365,
"minimum": 0,
"description": "Lower bound in days."
}
},
"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."
},
"yieldRange": {
"type": "object",
"properties": {
"max": {
"type": "integer",
"minimum": 0,
"description": "Upper bound, in the unit specified by `unit`."
},
"min": {
"type": "integer",
"minimum": 0,
"description": "Lower bound, in the unit specified by `unit`."
},
"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"
]
}
},
"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`."
},
"heightRange": {
"type": "object",
"properties": {
"max": {
"type": "integer",
"maximum": 2000,
"minimum": 0,
"description": "Upper bound in centimeters."
},
"min": {
"type": "integer",
"maximum": 2000,
"minimum": 0,
"description": "Lower bound in centimeters."
}
},
"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."
},
"cannabinoidRange": {
"type": "object",
"properties": {
"max": {
"type": "integer",
"maximum": 10000,
"minimum": 0,
"description": "Upper bound, in hundredths of a percent (e.g., 2700 = 27.00%)."
},
"min": {
"type": "integer",
"maximum": 10000,
"minimum": 0,
"description": "Lower bound, in hundredths of a percent (e.g., 2000 = 20.00%)."
}
},
"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."
}
},
"$type": "com.atproto.lexicon.schema",
"lexicon": 1
}