{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://sl2-analyzer.darthdemono.com/schema.json",
  "title": "sl2-analyzer save export",
  "description": "One FromSoftware .sl2 save, read into plain data: which game it is, what the machine ran it on (if the caller said), and one object per populated character slot. Produced by sl2-analyzer (https://github.com/darthdemono/sl2-analyzer).\n\nThe governing rule of this format is that ABSENCE IS MEANINGFUL. A field is present only when it was read from the save; a game that does not store something simply has no key for it, so a consumer can always tell \"this game has no death counter\" from \"this character has died zero times\". Nothing is filled in with a default, a zero, or a null to make the shape regular. Progress sections are FLOORS: they report what the save proves, never what it rules out.",
  "type": "object",
  "required": [
    "schema_version",
    "generated",
    "tool",
    "source",
    "characters"
  ],
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "type": "string",
      "format": "uri",
      "description": "This schema's URL."
    },
    "schema_version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
      "description": "Semver for this document format. MINOR adds an optional field; MAJOR would break a reader that trusted the previous shape."
    },
    "generated": {
      "type": "string",
      "format": "date-time",
      "description": "When the export ran, UTC. Not a save timestamp."
    },
    "tool": {
      "type": "object",
      "required": [
        "name",
        "url"
      ],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string"
        },
        "url": {
          "type": "string",
          "format": "uri"
        }
      }
    },
    "source": {
      "type": "object",
      "description": "The save file itself. Everything here is read from the file or decided by which game it turned out to be.",
      "required": [
        "filename",
        "game",
        "game_title",
        "support_tier"
      ],
      "additionalProperties": false,
      "properties": {
        "filename": {
          "type": "string",
          "description": "Basename of the .sl2 that was read."
        },
        "game": {
          "type": "string",
          "enum": [
            "dsr",
            "ptde",
            "ds2vanilla",
            "ds2sotfs",
            "ds3",
            "er",
            "sdt",
            "nr"
          ],
          "description": "Game id. dsr = Dark Souls Remastered, ptde = Prepare to Die Edition, sdt = Sekiro: Shadows Die Twice."
        },
        "game_title": {
          "type": "string",
          "description": "Human-readable game name."
        },
        "support_tier": {
          "type": "string",
          "enum": [
            "full",
            "inventory",
            "roster"
          ],
          "description": "How far this build trusts itself for this save. 'full' is identity, stats, souls, inventory and progress; 'inventory' means the stat block failed to validate (an unrecognised patch or an edited save) and stats were dropped rather than printed wrong. 'roster' is Elden Ring Nightreign, whose container decrypts and self-checks but whose layout past the character list is not pinned, so only the name is claimed. The tier is per character too — see character.tier."
        },
        "save_format_version": {
          "type": "integer",
          "description": "The save's own format version word (DSR 71, DS3 98, ER 220/251). Absent for DS2 and PtDE, which carry no such field. This is NOT the game patch."
        },
        "support_tier_coverage": {
          "type": "string",
          "description": "What the support tier does NOT cover for this game, where a gap would not be guessable from the tier word alone. Sekiro is the only game with one today: it is `full` for identity, stats, currency and inventory, but its event flags are not read, so Sculptor's Idols and boss-defeat flags are absent. Absent for every game with no such gap."
        },
        "game_patch": {
          "type": "string",
          "description": "Elden Ring only: the version of the regulation the save carries, e.g. \"1.16.0\". No other game stores one."
        },
        "steam_account_id": {
          "type": "integer",
          "description": "The Steam account that wrote this save (the low 32 bits of its SteamID64). DS3, Sekiro and Elden Ring record it; DS1 and DS2 do not store it at all, so it is absent for them."
        },
        "steam_id64": {
          "type": "string",
          "description": "The same account as a full SteamID64. A STRING, not a number: the value exceeds the integer a JSON parser is obliged to represent exactly, and a consumer reading it into a double would lose the last digits."
        },
        "save_folder": {
          "type": "string",
          "description": "The folder name the game will look for this save under, derived from the owning account: hex for DS3, decimal for Sekiro. The game loads a save only from this folder, so a save under any other account's folder will not be seen. Absent where the convention is unverified (Elden Ring) or no account is stored."
        }
      }
    },
    "environment": {
      "type": "object",
      "description": "How the game was run — SUPPLIED BY THE CALLER, never read from the save, which cannot know any of it. Present only when metadata was passed. Any key is accepted; the ones below are simply the ones this schema names. A key given more than once on the command line arrives here as an array, in the order given.",
      "additionalProperties": {
        "$ref": "#/$defs/metaValue"
      },
      "properties": {
        "source": {
          "$ref": "#/$defs/metaValue",
          "description": "Where the game came from: Steam, GOG, Epic, a disc, …"
        },
        "version": {
          "$ref": "#/$defs/metaValue",
          "description": "Game version as the player knows it, e.g. \"1.15.2\"."
        },
        "dlc": {
          "$ref": "#/$defs/metaValue",
          "description": "DLC installed. Repeat the flag per DLC to get an array."
        },
        "os": {
          "$ref": "#/$defs/metaValue",
          "description": "Operating system, e.g. \"Nobara 43\" or \"Windows 11\"."
        },
        "launcher": {
          "$ref": "#/$defs/metaValue",
          "description": "What started the game: Steam, Heroic, Lutris, …"
        },
        "proton": {
          "$ref": "#/$defs/metaValue",
          "description": "Proton / Wine build, e.g. \"GE-Proton9-20\"."
        },
        "gamemode": {
          "$ref": "#/$defs/metaValue",
          "description": "Whether Feral GameMode was on."
        },
        "mangohud": {
          "$ref": "#/$defs/metaValue",
          "description": "Whether MangoHud was on."
        },
        "notes": {
          "$ref": "#/$defs/metaValue",
          "description": "Anything else worth recording about the run."
        }
      }
    },
    "characters": {
      "type": "array",
      "description": "One per populated slot. An empty array means the file parsed but held no characters.",
      "items": {
        "$ref": "#/$defs/character"
      }
    },
    "file_validation": {
      "type": "object",
      "description": "Present only when the export was made with --validate. The rules that are about the CONTAINER rather than a character: BND4 entry checksums. A pass proves nothing — any save editor rewrites the checksum — which the finding's own note says.",
      "required": [
        "game",
        "rules_run",
        "findings"
      ],
      "properties": {
        "game": {
          "type": "string",
          "description": "Game key the rules were selected for."
        },
        "rules_run": {
          "type": "integer",
          "description": "How many rules ran. Zero means no rules exist for this game, which is not the same as a clean save."
        },
        "unimplemented": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Checks this game has no data for. The half of the answer that 'no findings' does not cover."
        },
        "findings": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/finding"
          },
          "description": "Worst tier first."
        }
      }
    }
  },
  "$defs": {
    "metaValue": {
      "description": "A caller-supplied value. A string, or an array of strings when the key was given more than once. Other JSON types pass through when supplied via a metadata file.",
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        {
          "type": "number"
        },
        {
          "type": "boolean"
        }
      ]
    },
    "countedItem": {
      "type": "array",
      "description": "An item and how many are held: [name, quantity].\n\nThe quantity is NULL in Elden Ring, whose owned-item list comes from the GaItem array and carries no counts — null means \"not read\", not \"none\". It can also legitimately be 0: a Dark Souls II save stores a record whose count field really is zero, and that is reported rather than tidied away.",
      "minItems": 2,
      "maxItems": 2,
      "prefixItems": [
        {
          "type": "string"
        },
        {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0
        }
      ]
    },
    "character": {
      "type": "object",
      "description": "One character. Only `slot`, `game`, `tier`, `name` and `level` are always present; every other key appears only if this game stores it AND this save yielded it.",
      "required": [
        "slot",
        "game",
        "tier",
        "name"
      ],
      "properties": {
        "slot": {
          "type": "integer",
          "minimum": 1,
          "description": "1-based slot number as shown in game."
        },
        "game": {
          "type": "string",
          "description": "Same id as source.game, repeated so a character object stands alone."
        },
        "tier": {
          "type": "string",
          "enum": [
            "full",
            "inventory",
            "roster"
          ],
          "description": "This character's own tier. A single slot can degrade to 'inventory' while its siblings stay 'full' — an edited or unrecognised stat block drops stats for that slot rather than printing wrong ones. 'roster' means only the name was read."
        },
        "name": {
          "type": "string"
        },
        "level": {
          "type": "integer",
          "description": "Soul level (rune level in Elden Ring). Absent for Sekiro, which has no level."
        },
        "klass": {
          "type": "string",
          "description": "Starting class. DS2 and DS1 only — DS3 and ER do not have a calibrated offset for it, so the key is absent rather than guessed."
        },
        "gender": {
          "type": "string",
          "enum": [
            "Male",
            "Female"
          ],
          "description": "DS1 and DS2 only. Printed as \"Sex\", because the values the games store are Male and Female; the key keeps its old name so a reader written against 1.0.0 does not break."
        },
        "covenant": {
          "type": "string",
          "description": "The covenant currently worn."
        },
        "ng_plus": {
          "type": "integer",
          "minimum": 0,
          "description": "Journey / New Game+ cycle; 0 is the first playthrough."
        },
        "play_time": {
          "type": "integer",
          "minimum": 0,
          "description": "Total play time in SECONDS."
        },
        "deaths": {
          "type": "integer",
          "minimum": 0,
          "description": "Death counter. DS1 and DS2 only; DS3 appears not to store one."
        },
        "souls": {
          "type": "integer",
          "description": "Souls (runes) currently held."
        },
        "soul_memory": {
          "type": "integer",
          "description": "DS2 only: total souls ever earned."
        },
        "humanity": {
          "type": "integer",
          "description": "DS1 only."
        },
        "hollow_lvl": {
          "type": "integer",
          "description": "DS2 only: hollowing level."
        },
        "embered": {
          "type": "boolean",
          "description": "DS3 only. When true the max HP below already includes the ~30% ember bonus."
        },
        "hp": {
          "type": "integer",
          "description": "Max HP as stored. In Sekiro this is the second of the field's two copies, read only where both agree — the offset the published editor labels \"Max HP\" is the CURRENT value."
        },
        "posture": {
          "type": "integer",
          "description": "Sekiro only: max Posture, read the same way as `hp` and from the same four-word shape one group along."
        },
        "fp": {
          "type": "integer",
          "description": "DS3 only: max FP."
        },
        "stamina": {
          "type": "integer"
        },
        "attack": {
          "type": "integer",
          "minimum": 0,
          "maximum": 98,
          "description": "Sekiro only: Attack Power. Also a COUNT — it rises by exactly one per Memory consumed, so `attack - 1` is how many boss Memories have been spent (1 is the value on a character who has spent none)."
        },
        "vitality": {
          "type": "integer",
          "minimum": 1,
          "maximum": 20,
          "description": "Sekiro only: Vitality, the track that raises max HP and Posture. Also a COUNT — it rises by exactly one per Prayer Necklace used, so `vitality - 1` is how many necklaces (four Prayer Beads each) have been consumed, cumulative across journeys. 1 is the value on a character who has used none."
        },
        "gourd": {
          "type": "integer",
          "minimum": 1,
          "maximum": 10,
          "description": "Sekiro only: how many charges the Healing Gourd holds. Also a COUNT — a fresh gourd holds one and each Gourd Seed handed to Emma buys exactly one more, so `gourd - 1` is how many seeds have been consumed, cumulative across journeys. 1 is the value on a character who has used none; the maximum is the game's own maxNum for the item."
        },
        "skill_points": {
          "type": "integer",
          "minimum": 0,
          "description": "Sekiro only: skill points banked and not yet spent, carried in the header rather than the inventory because they are a spendable currency. Points already spent are not stored, so this is not a total earned. A point sitting in the storage box is left as an ordinary item."
        },
        "suppressed_count": {
          "type": "integer",
          "minimum": 0,
          "description": "Sekiro only: item records that resolved to a real name but are engine state rather than inventory — the character's own body models (Sekiro has no armour system) and the `Virtual Weapon:` / `Upgrade Menu:` rows that restate something already listed. Counted rather than hidden, and separate from `internal_count`, which is rows that only had a development name."
        },
        "memories": {
          "type": "object",
          "description": "Sekiro only: the boss count that survives spending the token. Every other game's boss floor goes blind the moment a soul is consumed; here the spent ones are still counted, because Attack Power counts them.",
          "required": [
            "spent",
            "held",
            "cumulative"
          ],
          "additionalProperties": false,
          "properties": {
            "spent": {
              "type": "integer",
              "minimum": 0,
              "description": "Memories already consumed, read back from Attack Power."
            },
            "held": {
              "type": "integer",
              "minimum": 0,
              "description": "Memories still in the inventory."
            },
            "cumulative": {
              "type": "boolean",
              "description": "True past journey 0, where `spent` covers every lap rather than this one: Attack Power carries into New Game+ and the Memories do not."
            }
          }
        },
        "stats": {
          "type": "object",
          "description": "Attributes, keyed by the game's own names (Vigor, Endurance, … / Mind and Arcane in Elden Ring). Absent entirely at inventory tier.",
          "additionalProperties": {
            "type": "integer"
          }
        },
        "inv": {
          "type": "object",
          "description": "Inventory by category (weapons, armors, rings, spells, bolts, upgrade, consumables, online, emotes, bosssouls, talismans, ashes; Sekiro adds arts, prosthetics, skills, beads, memories and storage). Which categories exist depends on the game.",
          "additionalProperties": {
            "type": "array",
            "items": {
              "$ref": "#/$defs/countedItem"
            }
          }
        },
        "key_items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/countedItem"
          },
          "description": "Progression items: keys, tomes, coals, ashes."
        },
        "boss_souls": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/countedItem"
          },
          "description": "Boss souls / remembrances still held. Each one is proof of a kill; a consumed one is invisible."
        },
        "unknown_count": {
          "type": "integer",
          "minimum": 0,
          "description": "Items whose id is in the save but not in the name tables. Reported rather than hidden."
        },
        "internal_count": {
          "type": "integer",
          "minimum": 0,
          "description": "Sekiro only: entries that resolved only to an engine development string — placeholder and debug rows, not items the game hands you. Counted rather than listed, so an uncertain name never sits beside a real one."
        },
        "equipped_weapons": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          },
          "description": "DS3 only, keyed by slot (\"Right Hand\", \"Left Hand 2\", …). Names carry infusion and +N."
        },
        "equipped_armor": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          },
          "description": "DS3 only: Head / Chest / Hands / Legs."
        },
        "equipped_rings": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "DS3 only, up to four."
        },
        "equipped_ammo": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "DS3 only: arrows and bolts."
        },
        "ring_effects": {
          "type": "array",
          "description": "DS3 only: what each worn ring does, as [ring name, effect text]. Reference data about the ITEM (the fextralife Rings table), not a read of this save — present only for rings that table covers.",
          "items": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 2,
            "maxItems": 2
          }
        },
        "ring_mods": {
          "type": "array",
          "description": "DS3 only: the subset of ring_effects that modifies a value this tool derives, as [ring name, {stat: amount}]. Stats are load_pct, discovery, slots, hp_pct, stam_pct.",
          "items": {
            "type": "array",
            "minItems": 2,
            "maxItems": 2,
            "prefixItems": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "additionalProperties": {
                  "type": "number"
                }
              }
            ]
          }
        },
        "bosses": {
          "type": "object",
          "description": "Bosses proven dead, keyed by boss name; the value lists WHY, which is the point — every entry is auditable. A floor, not a roster.",
          "additionalProperties": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "flag",
                "soul",
                "gate",
                "clear"
              ]
            }
          }
        },
        "boss_total": {
          "type": "integer",
          "description": "How many bosses the game's own tables can NAME — the denominator for `bosses`. Not a claim about how many the game ships."
        },
        "bosses_missing": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Tracked bosses with no evidence yet. A boss whose soul was spent can sit here while being long dead."
        },
        "bosses_available": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "DS3 only: missing bosses whose prerequisites are all dead and whose area already has a lit bonfire. From the game's fixed route, not from this save."
        },
        "bonfires": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "DS2 only: the flat discovered-bonfire list. `bonfire_areas` is the grouped view of the same data."
        },
        "bonfire_areas": {
          "type": "array",
          "description": "Bonfires by area. EVERY area is listed, including untouched ones, so an area reading 0/6 is visible.",
          "items": {
            "type": "array",
            "minItems": 5,
            "maxItems": 5,
            "prefixItems": [
              {
                "type": "string",
                "description": "Area name."
              },
              {
                "type": "integer",
                "description": "How many are lit here."
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Their names."
              },
              {
                "type": "integer",
                "description": "How many the area holds in total."
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The ones still unlit."
              }
            ]
          }
        },
        "pickups": {
          "type": "array",
          "description": "DS3 and Sekiro: one-off world items picked up, by area, from that area's pickup flags. Only areas whose flag group has a DERIVED base appear — an absent area is untracked, not empty. Sekiro's item-lot family sits in a second bank of flag categories, 66 along from each map's own, so its nine seated areas cover 589 of the 826 known lots.",
          "items": {
            "type": "array",
            "minItems": 4,
            "maxItems": 4,
            "prefixItems": [
              {
                "type": "string",
                "description": "Area name."
              },
              {
                "type": "integer",
                "description": "How many were picked up."
              },
              {
                "type": "integer",
                "description": "How many the area holds."
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The ones not picked up."
              }
            ]
          }
        },
        "enemies": {
          "type": "array",
          "description": "Dark Souls III only: one-time enemies defeated, by area. Each is read from the enemy's own DEATH event flag, extracted from the game's committed event scripts, so a kill here is exact rather than inferred -- and distinct from the pickup flag the same enemy's drop sets. Unlike the per-map boss flags these survive a new journey. Only enemies whose flag group has a derived base are in the table, so an area absent here is untracked, never empty. Names are enemy types, so repeats within an area are different enemies.",
          "items": {
            "type": "array",
            "minItems": 5,
            "maxItems": 5,
            "prefixItems": [
              {
                "type": "string",
                "description": "Area name."
              },
              {
                "type": "integer",
                "description": "How many are dead."
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The ones killed, by name."
              },
              {
                "type": "integer",
                "description": "How many the area holds."
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The ones still alive, by name."
              }
            ]
          }
        },
        "npc_states": {
          "type": "array",
          "description": "Dark Souls III only: NPC deaths, hostility and questline milestones, by family. Each is its own event flag, read on the common-group base 111 + 128*g. ONLY WHAT FIRED is listed: half of these are mutually exclusive outcomes of one questline, so a missing list would read as a to-do list of things a player cannot all do. Labels are the source's own English where it has one -- and one of those is known to sit on the wrong flag, so an entity id is appended for checking -- otherwise the label is a bare entity id, because inventing a name would be worse.",
          "items": {
            "type": "array",
            "minItems": 4,
            "maxItems": 4,
            "prefixItems": [
              {
                "type": "string",
                "description": "Family name (NPCs killed, Questline states, …)."
              },
              {
                "type": "integer",
                "description": "How many of the family's flags are set."
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The labels of the flags that are set."
              },
              {
                "type": "integer",
                "description": "How many flags the family holds."
              }
            ]
          }
        },
        "world_events": {
          "type": "array",
          "description": "Dark Souls 1 only: world events from the game's own event scripts, counted by kind. SOURCED, NOT VERIFIED against a save here -- it decodes and it discriminates, which is all that is claimed. Some families are transient (the flag is set while the event runs and cleared after), so a finished run can honestly read 0 in one, and 'Boss-fight flags' counts flags rather than bosses. No names: they are FromSoft's own Japanese event names and translating them would be inventing a label.",
          "items": {
            "type": "array",
            "minItems": 3,
            "maxItems": 3,
            "prefixItems": [
              {
                "type": "string",
                "description": "Family name."
              },
              {
                "type": "integer",
                "description": "How many of the family's flags are set."
              },
              {
                "type": "integer",
                "description": "How many flags the family holds."
              }
            ]
          }
        },
        "minibosses": {
          "type": "array",
          "description": "Sekiro only: minibosses defeated, by area. Each is read from the enemy's OWN entity id used as an event flag, so a kill here is exact rather than inferred. Resets on a new journey, like every other Sekiro flag family. Names are the game's own: read from the FMG id its health-bar instruction carries, so a repeat within an area is a second placement of the same character at a different entity id, not a duplicate.",
          "items": {
            "type": "array",
            "minItems": 5,
            "maxItems": 5,
            "prefixItems": [
              {
                "type": "string",
                "description": "Area name."
              },
              {
                "type": "integer",
                "description": "How many are dead."
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The ones killed, by name."
              },
              {
                "type": "integer",
                "description": "How many the area holds."
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The ones still alive, by name."
              }
            ]
          }
        },
        "world_flags": {
          "type": "array",
          "description": "Dark Souls 1 only: one-off world events by category — the Bells of Awakening, the Lordvessel, shortcut doors and levers, non-boss fog gates, NPC states and the covenant joined. Each is read from its own event flag, so it is exact rather than inferred; the denominator is only the flags that have been NAMED, not every flag the game sets.",
          "items": {
            "type": "array",
            "minItems": 5,
            "maxItems": 5,
            "prefixItems": [
              {
                "type": "string",
                "description": "Category name."
              },
              {
                "type": "integer",
                "description": "How many are set."
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The ones set, by name."
              },
              {
                "type": "integer",
                "description": "How many the category tracks."
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The ones not set yet, by name."
              }
            ]
          }
        },
        "covenants": {
          "type": "object",
          "description": "Covenants discovered, keyed by name; the value says what proves it. Distinct from the worn `covenant`.",
          "additionalProperties": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "covenant_total": {
          "type": "integer"
        },
        "covenants_missing": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "questlines": {
          "type": "object",
          "description": "DS3 only: one-off rewards obtained, keyed by the NPC or landmark that gives them.",
          "additionalProperties": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "cinders": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "DS3 only: Lords of Cinder whose cinders are confirmed placed on the throne by a mapped flag."
        },
        "endings": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "DS3 only: endings this character has reached. The flags are cumulative across journeys, so a character can hold more than one."
        },
        "lords": {
          "type": "object",
          "description": "DS3 only: the Lords of Cinder. There are exactly four, so this section has a real denominator.",
          "properties": {
            "placed": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "maximum": 4,
              "description": "How many are on the throne, derived arithmetically as (lords defeated - cinder items still held) — which is why it can be known without every throne flag being mapped. NULL on NG+, where the thrones reset but the defeat flags do not, so the subtraction would over-report; there the `named` list from mapped flags is all there is."
            },
            "total": {
              "type": "integer",
              "const": 4
            },
            "dead": {
              "type": "integer",
              "minimum": 0,
              "maximum": 4,
              "description": "How many of the four are proven dead."
            },
            "held": {
              "type": "integer",
              "minimum": 0,
              "description": "How many sets of Cinders of a Lord are still in the inventory, i.e. killed but not yet offered."
            },
            "named": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "The lords whose placement a mapped throne flag confirms. Shorter than `placed` whenever a lord's flag is not pinned yet — three of the four are."
            }
          }
        },
        "validation": {
          "type": "object",
          "description": "Present only when the export was made with --validate. A separate pass over the parsed character; it never changes what was parsed. No verdict and no score is emitted — findings only.",
          "required": [
            "game",
            "rules_run",
            "findings"
          ],
          "properties": {
            "game": {
              "type": "string",
              "description": "Game key the rules were selected for."
            },
            "rules_run": {
              "type": "integer",
              "description": "How many rules ran. Zero means no rules exist for this game, which is not the same as a clean save."
            },
            "unimplemented": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Checks this game has no data for. The half of the answer that 'no findings' does not cover."
            },
            "findings": {
              "type": "array",
              "items": {
                "$ref": "#/$defs/finding"
              },
              "description": "Worst tier first."
            }
          }
        }
      }
    },
    "finding": {
      "type": "object",
      "description": "One contradiction, stated as the pair a reader can check: what an unmodified game would have to produce, and what the file holds.",
      "required": [
        "rule_id",
        "tier",
        "title",
        "expected",
        "found"
      ],
      "properties": {
        "rule_id": {
          "type": "string",
          "description": "Stable id of the rule that produced this, e.g. 'level-vs-stats'."
        },
        "tier": {
          "type": "string",
          "enum": [
            "impossible",
            "inconsistent",
            "suspicious"
          ],
          "description": "'impossible' — the game cannot produce this state. 'inconsistent' — two fields the game keeps in lockstep disagree. 'suspicious' — odd but legitimately reachable, and always carrying a note naming the legitimate cause."
        },
        "title": {
          "type": "string",
          "description": "Short human-readable name of the check."
        },
        "expected": {
          "type": "string",
          "description": "What an unmodified game would produce."
        },
        "found": {
          "type": "string",
          "description": "What this save actually holds."
        },
        "note": {
          "type": "string",
          "description": "A known legitimate cause of this finding. Required in practice at the 'suspicious' tier."
        }
      }
    }
  }
}
