{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://raw.githubusercontent.com/rom-weaver/rom-weaver/main/docs/rom-weaver-bundle-v2.schema.json",
  "title": "rom-weaver bundle",
  "description": "Schema revision v2 for the distributable patching-workflow definition. It adds a required shared patchBasis rule. Unknown fields are rejected.",
  "type": "object",
  "additionalProperties": false,
  "required": ["version", "patchBasis", "patches"],
  "properties": {
    "$schema": { "description": "Optional editor JSON Schema reference.", "type": "string" },
    "version": { "description": "Bundle schema version.", "type": "integer", "const": 2 },
    "patchBasis": {
      "description": "Shared input basis for patch entries. Entry basis overrides this rule. auto uses checksum inference.",
      "type": "string",
      "enum": ["auto", "base", "previous"]
    },
    "checkStates": {
      "description": "Named checksum states. References preserve authored state identity without comparing digest values.",
      "type": "array",
      "items": { "$ref": "#/$defs/checkState" }
    },
    "rom": { "description": "The input ROM for the patch chain.", "$ref": "#/$defs/rom" },
    "patches": {
      "description": "Ordered patch chain: array order is the apply order. May be empty only when `cheats` is not.",
      "type": "array",
      "items": { "$ref": "#/$defs/patch" }
    },
    "cheats": {
      "description": "Cheat selections this bundle reproduces, in selection order. They are baked into the ROM after the patch chain. Optional: a bundle without it is a plain patch recipe.",
      "type": "array",
      "items": { "$ref": "#/$defs/cheat" }
    },
    "output": { "description": "Defaults for the final patched output.", "$ref": "#/$defs/output" }
  },
  "anyOf": [
    { "properties": { "patches": { "minItems": 1 } } },
    { "required": ["cheats"], "properties": { "cheats": { "minItems": 1 } } }
  ],
  "$defs": {
    "checks": {
      "description": "Expected checksums (algorithm -> lowercase hex) and/or exact byte size.",
      "type": "object",
      "additionalProperties": false,
      "minProperties": 1,
      "properties": {
        "checksums": {
          "description": "Map of checksum algorithm to expected lowercase hex digest.",
          "type": "object",
          "additionalProperties": { "type": "string" },
          "propertyNames": { "enum": ["crc32", "md5", "sha1"] }
        },
        "size": { "description": "Exact expected byte size.", "type": "integer", "minimum": 0 }
      }
    },
    "checkState": {
      "description": "One named checksum state shared by every field that references its id.",
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "checks"],
      "properties": {
        "id": { "description": "Stable state identity referenced by checksRef fields.", "type": "string", "minLength": 1 },
        "checks": { "description": "The checksums and/or size this state stands for.", "$ref": "#/$defs/checks" }
      }
    },
    "patchInput": {
      "description": "A ROM/member or prior patch/member selector used by input and target fields. This does not change the authored basis declaration.",
      "oneOf": [
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["rom"],
          "properties": {
            "rom": { "description": "Marks the selector as the bundle's ROM source.", "const": true },
            "member": { "description": "Archive member or disc track. When the input is one bare file rather than an archive or disc, that file always stands in for the weave's rom.member; any other member is matched only when its file name equals the member name or the file satisfies the checks declared for it (a patch's input checks, or rom.checks when the weave sets no rom.member). One bare file never stands in for more than one distinct member.", "type": "string", "minLength": 1 }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["patch"],
          "properties": {
            "patch": { "description": "Earlier selected patch id whose real output supplies the bytes.", "type": "string", "minLength": 1 },
            "member": { "description": "Exact member selected from the produced patch output.", "type": "string", "minLength": 1 }
          }
        }
      ]
    },
    "headerMode": {
      "description": "ROM copier-header handling.",
      "type": "string",
      "enum": ["auto", "keep", "strip"]
    },
    "rom": {
      "description": "The one ROM the chain starts from: where to get it and how to recognize it. Omit it for a patch-only bundle whose user supplies the ROM.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "name": { "description": "Expected source ROM filename. A mismatch is advisory; checks and size verify the bytes.", "type": "string" },
        "url": { "description": "Download URL for the ROM. Mutually exclusive with path.", "type": "string" },
        "path": { "description": "Bundle-relative ROM path. Mutually exclusive with url.", "type": "string" },
        "member": { "description": "Archive member or disc track to patch. A single bare input file (not an archive or disc) always stands in for it, whatever its file name; rom.checks describe the ROM that is patched and are not matched against other members.", "type": "string", "minLength": 1 },
        "checks": { "description": "Expected checksums and size for the ROM.", "$ref": "#/$defs/checks" },
        "checksRef": { "description": "Named expected ROM state. Cannot be combined with checks.", "type": "string", "minLength": 1 }
      },
      "not": { "required": ["url", "path"] }
    },
    "patch": {
      "description": "One patch step: its source, display metadata, execution selectors, and declared checks. Exactly one of url or path.",
      "type": "object",
      "additionalProperties": false,
      "oneOf": [{ "required": ["url"] }, { "required": ["path"] }],
      "properties": {
        "id": { "description": "Stable patch-slot identity.", "type": "string", "minLength": 1 },
        "version": { "description": "Patch release version.", "type": "string", "minLength": 1 },
        "author": { "description": "Patch author credit.", "type": "string", "minLength": 1 },
        "name": { "description": "Display name for the patch.", "type": "string" },
        "description": { "description": "Patch description.", "type": "string" },
        "optional": { "description": "When true the patch starts deselected.", "type": "boolean" },
        "label": { "description": "Maturity or display label.", "type": "string" },
        "url": { "description": "Download URL for the patch. Mutually exclusive with path.", "type": "string" },
        "path": { "description": "Bundle-relative patch path. Mutually exclusive with url.", "type": "string" },
        "input": { "description": "Fixed execution input: the exact ROM/member bytes or a named producer's output. Omitted reads the current accumulated result of its target.", "$ref": "#/$defs/patchInput" },
        "target": { "description": "Cumulative execution lane. Omitted uses the legacy ordered lane; a patch target seeds its lane from the producer when first used.", "$ref": "#/$defs/patchInput" },
        "inputChecks": { "description": "Expected pre-apply state when it differs from rom checks.", "$ref": "#/$defs/checks" },
        "inputChecksRef": { "description": "Named authored pre-apply state. Cannot be combined with inputChecks.", "type": "string", "minLength": 1 },
        "outputChecks": { "description": "Expected post-apply state when it differs from final output checks.", "$ref": "#/$defs/checks" },
        "outputChecksRef": { "description": "Named authored post-apply state. Cannot be combined with outputChecks.", "type": "string", "minLength": 1 },
        "header": { "description": "Per-patch header-mode override.", "$ref": "#/$defs/headerMode" },
        "basis": {
          "description": "Per-entry base or previous override of patchBasis. Omit when it equals the shared rule.",
          "type": "string",
          "enum": ["base", "previous"]
        }
      }
    },
    "cheat": {
      "description": "One cheat selection. `id` names the record in the cheat database; `code` is a raw-code snapshot so the entry still applies when the database is absent.",
      "type": "object",
      "additionalProperties": false,
      "required": ["id"],
      "properties": {
        "id": {
          "description": "Cheat database record ID. An exact description is also accepted, so a hand-authored bundle can name a cheat the way `cheat list` prints it. Must not be blank.",
          "type": "string",
          "pattern": "\\S"
        },
        "source": {
          "description": "Which database the ID belongs to, for example `libretro-database`.",
          "type": "string",
          "minLength": 1
        },
        "revision": {
          "description": "The database revision the selection was made against.",
          "type": "string",
          "minLength": 1
        },
        "description": {
          "description": "The cheat's description, as the database records it.",
          "type": "string"
        },
        "code": {
          "description": "Raw code snapshot, so the entry still applies when the local cheat database is absent.",
          "type": "string"
        },
        "codeKind": {
          "description": "Explicit decoder for the raw code snapshot.",
          "type": "string",
          "enum": ["game-genie", "pro-action-replay", "xploder", "pro-action-rocky", "gold-finger", "game-shark-v1", "game-shark-v1-raw", "action-replay-v3", "action-replay-v3-raw"]
        },
        "optional": {
          "description": "When true an unresolvable cheat is skipped and named in the report; omitted/false makes it fail the apply.",
          "type": "boolean"
        }
      }
    },
    "output": {
      "description": "Defaults and expected result for the final patched output. Compression stays an apply-time choice.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "name": { "description": "Default output file name.", "type": "string" },
        "header": { "description": "Default output header mode.", "$ref": "#/$defs/headerMode" },
        "checks": { "description": "Expected checksums and size after the full chain.", "$ref": "#/$defs/checks" },
        "checksRef": { "description": "Named expected final-output state. Cannot be combined with checks.", "type": "string", "minLength": 1 }
      }
    }
  }
}
