{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://raw.githubusercontent.com/sadkinglabs/sorcery-registry/main/schema/registry.schema.json",
  "title": "Sorcery Card Registry export",
  "description": "Stable identifiers for every card and printing in Sorcery: Contested Realm. codex_id (C000042) identifies a card across all its reprints; printing_id (P000042) identifies one physical print. Key on those two - every other field is data that can change. The shape mirrors the official API: gameplay data lives on the card, physical facts on the printing. This schema is structural: vocabularies noted in descriptions (product, finish, type, rarity, category) are the values observed today, not closed sets, so new official values never become schema violations.",
  "$defs": {
    "codexId": {
      "type": "string",
      "pattern": "^C[0-9]{6}$",
      "description": "Permanent card-level id, shared by every printing of the card."
    },
    "printingId": {
      "type": "string",
      "pattern": "^P[0-9]{6}$",
      "description": "Permanent id of one physical print (set + product + finish)."
    },
    "date": {
      "type": "string",
      "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
    },
    "threshold": {
      "type": "integer",
      "minimum": 0
    },
    "setCode": {
      "type": "string",
      "pattern": "^([0-9]{3}|[A-Z]{3})$",
      "description": "A set's code: a label, never a number, so nothing about a set is read from it (what a set is, is its kind). The publisher's codes are three digits (001 Alpha, 002 Beta, 004 Arthurian Legends, 005 Dragonlord, 006 Gothic, 999 Promo; 003 is deliberately unused). The registry's own are three capital letters, such as CUR for the curios, so they can never collide with the publisher's. There is no collector number: cards have no official serialisation within a set."
    },
    "face": {
      "type": "object",
      "properties": {
        "type": {
          "type": [
            "string",
            "null"
          ],
          "description": "Observed values: Artifact, Aura, Avatar, Magic, Minion, Site."
        },
        "category": {
          "type": [
            "string",
            "null"
          ],
          "description": "Coarser grouping: Spell (Minion, Magic, Artifact, Aura), Site, Avatar, Token."
        },
        "rarity": {
          "type": [
            "string",
            "null"
          ],
          "description": "The rarity printed on the card. Observed values: Ordinary, Exceptional, Elite, Unique; null on Avatars and some tokens."
        },
        "slot": {
          "type": [
            "string",
            "null"
          ],
          "description": "The rarity slot the card is distributed in. Agrees with rarity except for Avatars (no printed rarity) and tokens."
        },
        "subtypes": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "In upstream's order, e.g. [\"Beast\", \"Spirit\"]."
        },
        "elements": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "In upstream's order. [\"None\"] means colourless (Artifacts, Avatars, most Sites)."
        },
        "keywords": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Ability keywords the card carries (Airborne, Genesis, Spellcaster, ...), as upstream tags them."
        },
        "umbrellas": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Cross-subtype groupings rules text refers to (Evil, Knight, Royalty)."
        },
        "cost": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Mana cost. null for Sites and Avatars, and for X costs."
        },
        "attack": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Printed attack; null when the card has none."
        },
        "defense": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Printed defense; null when the card has none."
        },
        "power": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Derived: Sorcery's power - equal to attack when attack equals defense, otherwise floor((attack + defense) / 2); null when either is null. Published so every consumer computes the same number."
        },
        "life": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Only Avatars have a life value."
        },
        "thr_air": {
          "$ref": "#/$defs/threshold",
          "description": "Air threshold: the number of Air the card needs in play to be cast. 0 when none."
        },
        "thr_earth": {
          "$ref": "#/$defs/threshold",
          "description": "Earth threshold. 0 when none."
        },
        "thr_fire": {
          "$ref": "#/$defs/threshold",
          "description": "Fire threshold. 0 when none."
        },
        "thr_water": {
          "$ref": "#/$defs/threshold",
          "description": "Water threshold. 0 when none."
        },
        "rules_text": {
          "type": "string",
          "description": "Current text the card plays by, applying to every printing. Canonicalised: \\n line endings, no trailing whitespace, one line per ability. Changes are recorded in rules_history."
        }
      },
      "required": [
        "type",
        "category",
        "rarity",
        "slot",
        "subtypes",
        "elements",
        "keywords",
        "umbrellas",
        "cost",
        "attack",
        "defense",
        "power",
        "life",
        "thr_air",
        "thr_earth",
        "thr_fire",
        "thr_water",
        "rules_text"
      ],
      "additionalProperties": false,
      "description": "The gameplay data of one face of a card."
    },
    "printingFace": {
      "type": "object",
      "properties": {
        "artist": {
          "type": [
            "string",
            "null"
          ],
          "description": "The artist as credited; null when unrecorded."
        },
        "artist_slug": {
          "type": [
            "string",
            "null"
          ],
          "description": "Upstream's slug for the artist, e.g. jeff_a_menges."
        },
        "flavour_text": {
          "type": [
            "string",
            "null"
          ],
          "description": "The flavour text as printed; empty or null when the card has none."
        },
        "typeline": {
          "type": [
            "string",
            "null"
          ],
          "description": "The flavour typeline printed under the name, e.g. 'An Ordinary Mortal new to power'."
        },
        "image_urls": {
          "anyOf": [
            {
              "$ref": "#/$defs/imageUrls"
            },
            {
              "type": "null"
            }
          ],
          "description": "Derived: the back face's own image renditions; null while the registry holds none."
        }
      },
      "required": [
        "artist",
        "artist_slug",
        "flavour_text",
        "typeline",
        "image_urls"
      ],
      "additionalProperties": false,
      "description": "The physical facts that differ per face of a double-faced printing."
    },
    "imageUrls": {
      "type": "object",
      "properties": {
        "small": {
          "type": "string",
          "format": "uri",
          "description": "146x204 WebP (aspect preserved, so possibly a pixel narrower): search results, lists."
        },
        "normal": {
          "type": "string",
          "format": "uri",
          "description": "488x680 WebP: card pages."
        },
        "large": {
          "type": "string",
          "format": "uri",
          "description": "672x936 WebP: click-through."
        },
        "original": {
          "type": "string",
          "format": "uri",
          "description": "The publisher's file, untouched."
        }
      },
      "required": [
        "small",
        "normal",
        "large",
        "original"
      ],
      "additionalProperties": false,
      "description": "Addresses of one face's image renditions. Object names are self-describing - {printing_id}.{key}.{rendition}.{ext}, '.back' before the rendition for a back face - where key is the art-version key: it changes when the art or the encoding recipe changes, so the bytes at a name never change. Card content and images are (c) Erik's Curiosa, served for archive, identification and site function."
    },
    "imageStatus": {
      "type": "string",
      "enum": [
        "missing",
        "lowres",
        "ok"
      ],
      "description": "missing: the registry holds no image for this face (image_urls null). lowres: held and served in every rendition, but the publisher's file was too small for the large rendition and was upscaled (the early sets ship at 380x531). ok: held at full size."
    },
    "note": {
      "type": "object",
      "properties": {
        "text": {
          "type": "string",
          "minLength": 1,
          "description": "The fact, in plain words."
        },
        "source": {
          "type": "string",
          "minLength": 1,
          "description": "Where the fact came from: a document, a product, a community report. Names a place, not a person, unless that person asked to be credited."
        },
        "recorded": {
          "$ref": "#/$defs/date",
          "description": "The day the note was written down, not the day the fact became true."
        }
      },
      "required": [
        "text",
        "source",
        "recorded"
      ],
      "additionalProperties": false,
      "description": "Something known about a record that the official API does not say, with where it came from. A note never contradicts a field: a fact that fits a field is a correction and changes the field instead."
    },
    "origin": {
      "type": "string",
      "enum": [
        "api",
        "manual"
      ],
      "description": "Who stands behind the record. 'api': the official API serves it. 'manual': the registry recorded it by hand (data/manual.json) because the API does not serve it. A manual record becomes 'api', keeping its id, once the API serves it and a person confirms the match."
    },
    "manual": {
      "anyOf": [
        {
          "type": "null"
        },
        {
          "type": "object",
          "properties": {
            "source": {
              "type": "string",
              "minLength": 1,
              "description": "Where the hand record came from. Names a place, not a person, unless that person asked to be credited."
            },
            "recorded": {
              "$ref": "#/$defs/date",
              "description": "The day the record was written down."
            },
            "confirmed_at": {
              "anyOf": [
                {
                  "$ref": "#/$defs/date"
                },
                {
                  "type": "null"
                }
              ],
              "description": "The day the official API was confirmed to serve it, after which origin is 'api'; null while it is still manual."
            },
            "withdrawn": {
              "anyOf": [
                {
                  "type": "null"
                },
                {
                  "type": "object",
                  "properties": {
                    "on": {
                      "$ref": "#/$defs/date",
                      "description": "The day the record was withdrawn."
                    },
                    "reason": {
                      "type": "string",
                      "minLength": 1,
                      "description": "Why the record was found to be wrong."
                    }
                  },
                  "required": [
                    "on",
                    "reason"
                  ],
                  "additionalProperties": false
                }
              ],
              "description": "Set when a manual record was found to be wrong. Ids are permanent, so it stays, marked; a withdrawn printing is never a card's default."
            }
          },
          "required": [
            "source",
            "recorded",
            "confirmed_at",
            "withdrawn"
          ],
          "additionalProperties": false
        }
      ],
      "description": "Null for a record the registry only ever observed in the official API. For a record the registry recorded by hand: where it came from and when, kept after the API starts serving it."
    }
  },
  "type": "object",
  "properties": {
    "header": {
      "type": "object",
      "properties": {
        "schema_version": {
          "type": "integer",
          "description": "The shape of the file. Goes up when a field is added; a breaking change is a new major release instead."
        },
        "source": {
          "type": "string",
          "format": "uri",
          "description": "The official API the data was read from."
        },
        "sets": {
          "type": "integer",
          "description": "How many records the sets section holds."
        },
        "cards": {
          "type": "integer",
          "description": "How many records the cards section holds."
        },
        "printings": {
          "type": "integer",
          "description": "How many records the printings section holds."
        },
        "slug_history": {
          "type": "integer",
          "description": "How many rows the slug_history section holds."
        },
        "name_history": {
          "type": "integer",
          "description": "How many rows the name_history section holds."
        },
        "card_history": {
          "type": "integer",
          "description": "How many rows the card_history section holds."
        }
      },
      "required": [
        "schema_version",
        "source",
        "sets",
        "cards",
        "printings",
        "slug_history",
        "name_history",
        "card_history"
      ],
      "additionalProperties": false,
      "description": "Counts match the section lengths. Deliberately no timestamp: an unchanged registry produces a byte-identical file."
    },
    "sets": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "set_code": {
            "anyOf": [
              {
                "$ref": "#/$defs/setCode"
              },
              {
                "type": "null"
              }
            ],
            "description": "The official three-digit code. A label, not an order: 003 is unused."
          },
          "set_name": {
            "type": "string",
            "description": "The official display name."
          },
          "released_at": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ],
            "description": "Derived: the earliest released_at among the set's printings."
          },
          "cards": {
            "type": "integer",
            "description": "Distinct cards in the set."
          },
          "printings": {
            "type": "integer",
            "description": "Printings in the set."
          },
          "kind": {
            "type": "string",
            "enum": [
              "release",
              "promo",
              "registry"
            ],
            "description": "What the set is, as the registry records it (data/sets.json); never read from the code. release: a set release, whose printings came out with it. promo: the publisher's bucket for promos (999), which came out with many releases; each printing's released_with says which. registry: a set of the registry's own for printings the official API will never serve, such as CUR."
          },
          "origin": {
            "$ref": "#/$defs/origin",
            "description": "'manual' when every printing in the set is a manual record, as for a set code of the registry's own; 'api' otherwise."
          },
          "api_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Derived: this set's JSON object on api.kairosarchive.net; null only for a set without a code."
          },
          "kairos_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Derived: this set's page on kairosarchive.net."
          }
        },
        "required": [
          "set_code",
          "set_name",
          "released_at",
          "cards",
          "printings",
          "kind",
          "origin",
          "api_url",
          "kairos_url"
        ],
        "additionalProperties": false,
        "description": "Derived catalogue of the sets themselves, with distinct-card and printing counts."
      }
    },
    "cards": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "codex_id": {
            "$ref": "#/$defs/codexId",
            "description": "The card's permanent ID."
          },
          "name": {
            "type": "string",
            "description": "The current name. Earlier names are in name_history."
          },
          "type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Observed values: Artifact, Aura, Avatar, Magic, Minion, Site."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Coarser grouping: Spell (Minion, Magic, Artifact, Aura), Site, Avatar, Token."
          },
          "rarity": {
            "type": [
              "string",
              "null"
            ],
            "description": "The rarity printed on the card. Observed values: Ordinary, Exceptional, Elite, Unique; null on Avatars and some tokens."
          },
          "slot": {
            "type": [
              "string",
              "null"
            ],
            "description": "The rarity slot the card is distributed in. Agrees with rarity except for Avatars (no printed rarity) and tokens."
          },
          "subtypes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "In upstream's order, e.g. [\"Beast\", \"Spirit\"]."
          },
          "elements": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "In upstream's order. [\"None\"] means colourless (Artifacts, Avatars, most Sites)."
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ability keywords the card carries (Airborne, Genesis, Spellcaster, ...), as upstream tags them."
          },
          "umbrellas": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Cross-subtype groupings rules text refers to (Evil, Knight, Royalty)."
          },
          "cost": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Mana cost. null for Sites and Avatars, and for X costs."
          },
          "attack": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Printed attack; null when the card has none."
          },
          "defense": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Printed defense; null when the card has none."
          },
          "power": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Derived: Sorcery's power - equal to attack when attack equals defense, otherwise floor((attack + defense) / 2); null when either is null. Published so every consumer computes the same number."
          },
          "life": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Only Avatars have a life value."
          },
          "thr_air": {
            "$ref": "#/$defs/threshold",
            "description": "Air threshold: the number of Air the card needs in play to be cast. 0 when none."
          },
          "thr_earth": {
            "$ref": "#/$defs/threshold",
            "description": "Earth threshold. 0 when none."
          },
          "thr_fire": {
            "$ref": "#/$defs/threshold",
            "description": "Fire threshold. 0 when none."
          },
          "thr_water": {
            "$ref": "#/$defs/threshold",
            "description": "Water threshold. 0 when none."
          },
          "rules_text": {
            "type": "string",
            "description": "Current text the card plays by, applying to every printing. Canonicalised: \\n line endings, no trailing whitespace, one line per ability. Changes are recorded in rules_history."
          },
          "back": {
            "anyOf": [
              {
                "$ref": "#/$defs/face"
              },
              {
                "type": "null"
              }
            ],
            "description": "The back face of a double-faced card; null for every other card. The card's own fields are its front."
          },
          "errata": {
            "type": "boolean",
            "description": "Registry-owned: true once any gameplay field of the card (type, elements, cost, attack, defense, life, thresholds, rules_text, back) has changed since it was printed. Seeded from the old upstream UPDATED: marker; set whenever a sync observes such a change (see card_history); corrected only through data/overrides.json. Rarity, slot, subtypes, keywords and umbrellas are classification, so a change to them is not errata."
          },
          "set_codes": {
            "type": "array",
            "items": {
              "$ref": "#/$defs/setCode"
            },
            "description": "Derived, sorted: the sets this card appears in. For products and finishes, follow printing_ids."
          },
          "printing_ids": {
            "type": "array",
            "items": {
              "$ref": "#/$defs/printingId"
            },
            "description": "Derived, sorted, and it only ever grows."
          },
          "default_printing_id": {
            "anyOf": [
              {
                "$ref": "#/$defs/printingId"
              },
              {
                "type": "null"
              }
            ],
            "description": "Derived: the representative printing, by a fixed rule so every consumer picks the same one - not retired, showing the card's current face (printed_as_current) over older values, Booster over other products, Standard over other finishes, most recently released, lowest id. A reprint that changed the card's stats therefore becomes the default even when it is a promo; otherwise a promo never outranks a Booster printing. A registry maintainer can pin a different printing through data/overrides.json, with a reason. null only for a card with no printings."
          },
          "api_url": {
            "type": "string",
            "format": "uri",
            "description": "Derived: this card's JSON object on api.kairosarchive.net, under the moving major alias (/v3/ always redirects to the newest verified v3.x release), so the address leads to the current record from any copy."
          },
          "kairos_url": {
            "type": "string",
            "format": "uri",
            "description": "Derived: this card's page on kairosarchive.net."
          },
          "image_urls": {
            "anyOf": [
              {
                "$ref": "#/$defs/imageUrls"
              },
              {
                "type": "null"
              }
            ],
            "description": "Derived: the image of the card's default_printing_id (its front face), null while that printing has no image."
          },
          "image_status": {
            "$ref": "#/$defs/imageStatus",
            "description": "Derived: the default printing's image_status."
          },
          "origin": {
            "$ref": "#/$defs/origin"
          },
          "manual": {
            "$ref": "#/$defs/manual"
          },
          "notes": {
            "type": "array",
            "items": {
              "$ref": "#/$defs/note"
            },
            "description": "What the registry knows about this card that the official API does not say, oldest first. Empty for most records."
          }
        },
        "required": [
          "codex_id",
          "name",
          "type",
          "category",
          "rarity",
          "slot",
          "subtypes",
          "elements",
          "keywords",
          "umbrellas",
          "cost",
          "attack",
          "defense",
          "power",
          "life",
          "thr_air",
          "thr_earth",
          "thr_fire",
          "thr_water",
          "rules_text",
          "back",
          "errata",
          "set_codes",
          "printing_ids",
          "default_printing_id",
          "api_url",
          "kairos_url",
          "image_urls",
          "image_status",
          "origin",
          "manual",
          "notes"
        ],
        "additionalProperties": false,
        "description": "One record per card as a game piece; reprints share one record."
      }
    },
    "printings": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "printing_id": {
            "$ref": "#/$defs/printingId",
            "description": "This printing's permanent ID."
          },
          "codex_id": {
            "$ref": "#/$defs/codexId",
            "description": "The card this is a printing of."
          },
          "card_name": {
            "type": "string",
            "description": "Derived from the card record so a printing is readable without a join."
          },
          "set_name": {
            "type": "string",
            "description": "The set's official display name."
          },
          "set_code": {
            "anyOf": [
              {
                "$ref": "#/$defs/setCode"
              },
              {
                "type": "null"
              }
            ],
            "description": "The set's official three-digit code."
          },
          "released_at": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ],
            "description": "The date this printing reached the public."
          },
          "released_with": {
            "anyOf": [
              {
                "$ref": "#/$defs/setCode"
              },
              {
                "type": "null"
              }
            ],
            "description": "The set release this printing belongs to: a set whose kind is release. For a printing in a release set it is that set. For a promo (a set of kind promo, 999) or a curio (kind registry, CUR), filed outside any release, it is the release it came out with, recorded by hand; null until recorded."
          },
          "product": {
            "type": [
              "string",
              "null"
            ],
            "description": "Official product line, spelled as upstream spells it. Observed values: Booster, BoxTopper, Dust, OrganizedPlay, PreconstructedDeck, DraftKit, AlphaInvestments, WelcomeKit, Kickstarter, TeamCovenant, StarCityGames."
          },
          "finish": {
            "type": [
              "string",
              "null"
            ],
            "description": "Observed values: Standard, Foil, Rainbow."
          },
          "slug": {
            "type": "string",
            "description": "Current official API slug. Mutable - never key on it; resolve old ones via slug_history."
          },
          "artist": {
            "type": [
              "string",
              "null"
            ],
            "description": "The artist as credited; null when unrecorded."
          },
          "artist_slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Upstream's slug for the artist, e.g. jeff_a_menges."
          },
          "flavour_text": {
            "type": [
              "string",
              "null"
            ],
            "description": "The flavour text as printed; empty or null when the card has none."
          },
          "typeline": {
            "type": [
              "string",
              "null"
            ],
            "description": "The flavour typeline printed under the name, e.g. 'An Ordinary Mortal new to power'."
          },
          "back": {
            "anyOf": [
              {
                "$ref": "#/$defs/printingFace"
              },
              {
                "type": "null"
              }
            ],
            "description": "Back face of a double-faced printing; null otherwise."
          },
          "image_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "The art-version key of this printing's front image - sha256(original bytes || encoding recipe)[:12], recorded in data/images.json by the image pipeline; null while the registry holds none. image_urls is derived from it; a new key means new art or a new recipe, never changed bytes at an old address."
          },
          "printed_as_current": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Derived: whether the values physically printed on this printing equal the card's current gameplay face - true when the printing was released on or after the current face took effect, or entered the registry on or after it (a reprint that arrives in the same sync as the change it carries), or when the card's face has never changed. null when the printing has no release date and predates the current face, or when it shows no face at all (a textless promo listed under unknown_printings in data/errata.json)."
          },
          "retired_at": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ],
            "description": "Set when the printing vanished upstream. Retired printings keep their rows and ids forever."
          },
          "api_url": {
            "type": "string",
            "format": "uri",
            "description": "Derived: this printing's JSON object on api.kairosarchive.net, under the moving major alias."
          },
          "kairos_url": {
            "type": "string",
            "format": "uri",
            "description": "Derived: this printing's page on kairosarchive.net."
          },
          "image_urls": {
            "anyOf": [
              {
                "$ref": "#/$defs/imageUrls"
              },
              {
                "type": "null"
              }
            ],
            "description": "Derived: the front face's image renditions, null while the registry holds none."
          },
          "image_status": {
            "$ref": "#/$defs/imageStatus",
            "description": "How good the source of the front image was: missing, lowres or ok."
          },
          "origin": {
            "$ref": "#/$defs/origin"
          },
          "manual": {
            "$ref": "#/$defs/manual"
          },
          "notes": {
            "type": "array",
            "items": {
              "$ref": "#/$defs/note"
            },
            "description": "What the registry knows about this printing that the official API does not say, oldest first. Empty for most records."
          }
        },
        "required": [
          "printing_id",
          "codex_id",
          "card_name",
          "set_name",
          "set_code",
          "released_at",
          "released_with",
          "product",
          "finish",
          "slug",
          "artist",
          "artist_slug",
          "flavour_text",
          "typeline",
          "back",
          "image_hash",
          "printed_as_current",
          "retired_at",
          "api_url",
          "kairos_url",
          "image_urls",
          "image_status",
          "origin",
          "manual",
          "notes"
        ],
        "additionalProperties": false,
        "description": "One record per physical print: a specific set, product and finish. Gameplay data is on the card it points at."
      }
    },
    "slug_history": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The slug, exactly as the official API issued it."
          },
          "printing_id": {
            "$ref": "#/$defs/printingId",
            "description": "The printing it belonged to. A slug belongs to one printing, ever."
          },
          "valid_from": {
            "$ref": "#/$defs/date",
            "description": "The first date the registry saw this slug."
          },
          "valid_to": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ],
            "description": "The date it was replaced; null while it is the current slug."
          }
        },
        "required": [
          "slug",
          "printing_id",
          "valid_from",
          "valid_to"
        ],
        "additionalProperties": false,
        "description": "Every slug that has ever existed, mapped to its printing. valid_to null = the current slug."
      }
    },
    "name_history": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The name, exactly as the official API gave it."
          },
          "codex_id": {
            "$ref": "#/$defs/codexId",
            "description": "The card that carried the name."
          },
          "valid_from": {
            "$ref": "#/$defs/date",
            "description": "The first date the registry saw this name."
          },
          "valid_to": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ],
            "description": "The date it was replaced; null while it is the current name."
          }
        },
        "required": [
          "name",
          "codex_id",
          "valid_from",
          "valid_to"
        ],
        "additionalProperties": false,
        "description": "Every name a card has ever carried. valid_to null = the current name. Unlike slugs, a name is not owned: two cards may hold the same name at different times."
      }
    },
    "card_history": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "codex_id": {
            "$ref": "#/$defs/codexId",
            "description": "The card this face belongs to."
          },
          "valid_from": {
            "$ref": "#/$defs/date",
            "description": "Start of the range this face was in force. What the date means depends on source."
          },
          "valid_to": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ],
            "description": "End of the range; null for the current face."
          },
          "source": {
            "type": "string",
            "enum": [
              "api",
              "card",
              "manual"
            ],
            "description": "Where the face came from: 'api' when the registry observed it in the official API; 'card' when it was recorded by hand from what is printed on the card (data/errata.json), the case of a card whose printed text differs from the text the API serves; 'manual' for the face of a card the registry recorded by hand because the API does not serve it (data/manual.json)."
          },
          "type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Observed values: Artifact, Aura, Avatar, Magic, Minion, Site."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Coarser grouping: Spell (Minion, Magic, Artifact, Aura), Site, Avatar, Token."
          },
          "rarity": {
            "type": [
              "string",
              "null"
            ],
            "description": "The rarity printed on the card. Observed values: Ordinary, Exceptional, Elite, Unique; null on Avatars and some tokens."
          },
          "slot": {
            "type": [
              "string",
              "null"
            ],
            "description": "The rarity slot the card is distributed in. Agrees with rarity except for Avatars (no printed rarity) and tokens."
          },
          "subtypes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "In upstream's order, e.g. [\"Beast\", \"Spirit\"]."
          },
          "elements": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "In upstream's order. [\"None\"] means colourless (Artifacts, Avatars, most Sites)."
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ability keywords the card carries (Airborne, Genesis, Spellcaster, ...), as upstream tags them."
          },
          "umbrellas": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Cross-subtype groupings rules text refers to (Evil, Knight, Royalty)."
          },
          "cost": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Mana cost. null for Sites and Avatars, and for X costs."
          },
          "attack": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Printed attack; null when the card has none."
          },
          "defense": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Printed defense; null when the card has none."
          },
          "power": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Derived: Sorcery's power - equal to attack when attack equals defense, otherwise floor((attack + defense) / 2); null when either is null. Published so every consumer computes the same number."
          },
          "life": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Only Avatars have a life value."
          },
          "thr_air": {
            "$ref": "#/$defs/threshold",
            "description": "Air threshold: the number of Air the card needs in play to be cast. 0 when none."
          },
          "thr_earth": {
            "$ref": "#/$defs/threshold",
            "description": "Earth threshold. 0 when none."
          },
          "thr_fire": {
            "$ref": "#/$defs/threshold",
            "description": "Fire threshold. 0 when none."
          },
          "thr_water": {
            "$ref": "#/$defs/threshold",
            "description": "Water threshold. 0 when none."
          },
          "rules_text": {
            "type": "string",
            "description": "Current text the card plays by, applying to every printing. Canonicalised: \\n line endings, no trailing whitespace, one line per ability. Changes are recorded in rules_history."
          },
          "back": {
            "anyOf": [
              {
                "$ref": "#/$defs/face"
              },
              {
                "type": "null"
              }
            ],
            "description": "The back face at that time; null unless the card is double-faced."
          }
        },
        "required": [
          "codex_id",
          "valid_from",
          "valid_to",
          "source",
          "type",
          "category",
          "rarity",
          "slot",
          "subtypes",
          "elements",
          "keywords",
          "umbrellas",
          "cost",
          "attack",
          "defense",
          "power",
          "life",
          "thr_air",
          "thr_earth",
          "thr_fire",
          "thr_water",
          "rules_text",
          "back"
        ],
        "additionalProperties": false,
        "description": "Every state a card's gameplay face has been in, as observed by syncs: the face's fields flattened into the row. valid_to null = the current face, which equals the card record. A closed row is how the card played until valid_to; a printing released within a row's dates was printed with that row's values. Seeded with every card's face on 2026-09-09."
      }
    }
  },
  "required": [
    "header",
    "sets",
    "cards",
    "printings",
    "slug_history",
    "name_history",
    "card_history"
  ],
  "additionalProperties": false
}
