{
  "$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}$",
      "description": "The set's official code (001 Alpha, 002 Beta, 004 Arthurian Legends, 005 Dragonlord, 006 Gothic, 999 Promo). A code, not a number: 003 is deliberately unused, so codes do not imply an order. 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"
          ]
        },
        "defense": {
          "type": [
            "integer",
            "null"
          ]
        },
        "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"
        },
        "thr_earth": {
          "$ref": "#/$defs/threshold"
        },
        "thr_fire": {
          "$ref": "#/$defs/threshold"
        },
        "thr_water": {
          "$ref": "#/$defs/threshold"
        },
        "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"
          ]
        },
        "artist_slug": {
          "type": [
            "string",
            "null"
          ],
          "description": "Upstream's slug for the artist, e.g. jeff_a_menges."
        },
        "flavour_text": {
          "type": [
            "string",
            "null"
          ]
        },
        "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."
    }
  },
  "type": "object",
  "properties": {
    "header": {
      "type": "object",
      "properties": {
        "schema_version": {
          "type": "integer"
        },
        "source": {
          "type": "string",
          "format": "uri"
        },
        "sets": {
          "type": "integer"
        },
        "cards": {
          "type": "integer"
        },
        "printings": {
          "type": "integer"
        },
        "slug_history": {
          "type": "integer"
        },
        "name_history": {
          "type": "integer"
        },
        "card_history": {
          "type": "integer"
        }
      },
      "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"
              }
            ]
          },
          "set_name": {
            "type": "string"
          },
          "released_at": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ],
            "description": "Derived: the earliest released_at among the set's printings."
          },
          "cards": {
            "type": "integer"
          },
          "printings": {
            "type": "integer"
          },
          "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",
          "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"
          },
          "name": {
            "type": "string"
          },
          "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"
            ]
          },
          "defense": {
            "type": [
              "integer",
              "null"
            ]
          },
          "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"
          },
          "thr_earth": {
            "$ref": "#/$defs/threshold"
          },
          "thr_fire": {
            "$ref": "#/$defs/threshold"
          },
          "thr_water": {
            "$ref": "#/$defs/threshold"
          },
          "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."
          }
        },
        "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"
        ],
        "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"
          },
          "codex_id": {
            "$ref": "#/$defs/codexId"
          },
          "card_name": {
            "type": "string",
            "description": "Derived from the card record so a printing is readable without a join."
          },
          "set_name": {
            "type": "string"
          },
          "set_code": {
            "anyOf": [
              {
                "$ref": "#/$defs/setCode"
              },
              {
                "type": "null"
              }
            ]
          },
          "released_at": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ],
            "description": "The date this printing reached the public."
          },
          "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"
            ]
          },
          "artist_slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Upstream's slug for the artist, e.g. jeff_a_menges."
          },
          "flavour_text": {
            "type": [
              "string",
              "null"
            ]
          },
          "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"
          }
        },
        "required": [
          "printing_id",
          "codex_id",
          "card_name",
          "set_name",
          "set_code",
          "released_at",
          "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"
        ],
        "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"
          },
          "printing_id": {
            "$ref": "#/$defs/printingId"
          },
          "valid_from": {
            "$ref": "#/$defs/date"
          },
          "valid_to": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "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"
          },
          "codex_id": {
            "$ref": "#/$defs/codexId"
          },
          "valid_from": {
            "$ref": "#/$defs/date"
          },
          "valid_to": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "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"
          },
          "valid_from": {
            "$ref": "#/$defs/date"
          },
          "valid_to": {
            "anyOf": [
              {
                "$ref": "#/$defs/date"
              },
              {
                "type": "null"
              }
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "api",
              "card"
            ],
            "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."
          },
          "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"
            ]
          },
          "defense": {
            "type": [
              "integer",
              "null"
            ]
          },
          "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"
          },
          "thr_earth": {
            "$ref": "#/$defs/threshold"
          },
          "thr_fire": {
            "$ref": "#/$defs/threshold"
          },
          "thr_water": {
            "$ref": "#/$defs/threshold"
          },
          "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"
              }
            ]
          }
        },
        "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
}
