{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://api.spritefetch.dev/schemas/asset.v1.json",
  "title": "Spritefetch asset record (one asset version)",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id",
    "version",
    "metadata_revision",
    "name",
    "type",
    "category",
    "style",
    "tags",
    "license_spdx",
    "license_url",
    "attribution_required",
    "attribution_text",
    "source_url",
    "author",
    "retrieved_at",
    "commercial_use",
    "redistribution_allowed",
    "verification_status",
    "verified_by",
    "verified_at",
    "license_snapshot_sha256",
    "source_terms_url",
    "source_terms_snapshot",
    "api_mirroring_allowed",
    "thumbnail_url",
    "file_url",
    "filename",
    "file_size_bytes",
    "sha256",
    "format",
    "dimensions",
    "page_url",
    "json_url",
    "markdown_url",
    "published_at",
    "updated_at"
  ],
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^asset_[a-z0-9]{12}$",
      "description": "Immutable asset id."
    },
    "version": {
      "type": "integer",
      "minimum": 1,
      "description": "New version only when file bytes, license, or attribution change."
    },
    "latest_version": {
      "type": "integer",
      "minimum": 1,
      "description": "Informational: highest publicly servable version at response time."
    },
    "metadata_revision": {
      "type": "integer",
      "minimum": 1,
      "description": "Bumped on in-place corrections to descriptive metadata (tags, description, slicing, verification). Never bumped for file/license/attribution changes, which create a new version instead."
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "slug": {
      "type": "string",
      "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
    },
    "description": {
      "type": "string",
      "maxLength": 600
    },
    "type": {
      "enum": [
        "sprite_sheet",
        "tileset",
        "static",
        "ui"
      ]
    },
    "category": {
      "enum": [
        "characters",
        "tilesets",
        "items",
        "ui",
        "effects",
        "structures"
      ]
    },
    "style": {
      "enum": [
        "pixel-art",
        "hand-drawn",
        "flat"
      ]
    },
    "perspective": {
      "enum": [
        "side",
        "top-down",
        "isometric",
        "front"
      ]
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^[a-z0-9][a-z0-9-]*$"
      },
      "maxItems": 30,
      "uniqueItems": true
    },
    "pack_ids": {
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^pack_[a-z0-9]{12}$"
      },
      "uniqueItems": true
    },
    "ai_generated": {
      "type": "boolean",
      "const": false,
      "description": "Reserved. Always false in the MVP (AI assets are cut)."
    },
    "license_spdx": {
      "enum": [
        "CC0-1.0"
      ],
      "description": "SPDX identifier. MVP accepts CC0-1.0 only; CC-BY-4.0 etc. are parked."
    },
    "license_url": {
      "type": "string",
      "format": "uri"
    },
    "attribution_required": {
      "type": "boolean"
    },
    "attribution_text": {
      "type": "string",
      "minLength": 1,
      "description": "Ready-to-paste credit line. Present even for CC0 (credit appreciated)."
    },
    "source_url": {
      "type": "string",
      "format": "uri",
      "description": "Page the file and license were retrieved from."
    },
    "author": {
      "type": "string",
      "minLength": 1
    },
    "retrieved_at": {
      "type": "string",
      "format": "date-time"
    },
    "commercial_use": {
      "type": [
        "boolean",
        "null"
      ],
      "description": "Derived from this asset's license evidence. null = not established (for example, the license text is CC0 but the image may depict a third-party trademark); treat null as 'do not assume allowed'."
    },
    "redistribution_allowed": {
      "type": [
        "boolean",
        "null"
      ],
      "description": "Derived from this asset's license evidence. null = not established; treat null as 'do not assume allowed'."
    },
    "verification_status": {
      "enum": [
        "pending",
        "auto_verified",
        "reviewed",
        "rejected"
      ]
    },
    "verified_by": {
      "type": [
        "string",
        "null"
      ],
      "description": "'ingest-pipeline@<semver>' for auto_verified; a real maintainer handle for reviewed (and for a maintainer rejection); null while pending."
    },
    "verified_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time"
    },
    "license_snapshot_sha256": {
      "type": "string",
      "pattern": "^[a-f0-9]{64}$",
      "description": "SHA-256 of license.txt for this version."
    },
    "source_terms_url": {
      "type": [
        "string",
        "null"
      ],
      "format": "uri",
      "description": "Source site's terms of service URL; null when the source publishes none (the snapshot then records what was checked)."
    },
    "source_terms_snapshot": {
      "type": "string",
      "format": "uri-reference",
      "description": "Path to the stored source-terms.txt for this version."
    },
    "api_mirroring_allowed": {
      "type": [
        "boolean",
        "null"
      ],
      "description": "Whether the source's terms permit serving its files through our API, as signed off by a maintainer. null = sign-off pending (see api_mirroring_status). Must be true for any public status in production."
    },
    "slicing": {
      "description": "Sprite-sheet slicing (type sprite_sheet, static, ui) or Tiled-compatible tileset slicing (type tileset).",
      "oneOf": [
        {
          "$ref": "#/$defs/sheet_slicing"
        },
        {
          "$ref": "#/$defs/tileset_slicing"
        }
      ]
    },
    "thumbnail_url": {
      "type": "string",
      "format": "uri"
    },
    "file_url": {
      "type": "string",
      "format": "uri",
      "description": "Immutable versioned file URL."
    },
    "filename": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9._-]*\\.png$"
    },
    "file_size_bytes": {
      "type": "integer",
      "minimum": 1
    },
    "sha256": {
      "type": "string",
      "pattern": "^[a-f0-9]{64}$",
      "description": "SHA-256 of the exact bytes served at file_url."
    },
    "format": {
      "enum": [
        "png"
      ]
    },
    "dimensions": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "width",
        "height"
      ],
      "properties": {
        "width": {
          "type": "integer",
          "minimum": 1
        },
        "height": {
          "type": "integer",
          "minimum": 1
        }
      },
      "description": "Full image size in pixels."
    },
    "page_url": {
      "type": "string",
      "format": "uri"
    },
    "json_url": {
      "type": "string",
      "format": "uri"
    },
    "markdown_url": {
      "type": "string",
      "format": "uri"
    },
    "license_snapshot_url": {
      "type": "string",
      "format": "uri"
    },
    "published_at": {
      "type": "string",
      "format": "date-time",
      "description": "When this version was first published."
    },
    "updated_at": {
      "type": "string",
      "format": "date-time",
      "description": "Last metadata_revision change."
    },
    "api_mirroring_status": {
      "enum": [
        "allowed",
        "not_allowed",
        "pending_signoff"
      ],
      "description": "allowed = a maintainer signed off on the source's terms; not_allowed = the terms prohibit it; pending_signoff = no real sign-off yet (prototype)."
    },
    "license_evidence": {
      "type": "object",
      "additionalProperties": false,
      "description": "What the license fields were derived from, for this asset.",
      "required": [
        "check_result",
        "signals",
        "scope",
        "license_files"
      ],
      "properties": {
        "check_result": {
          "enum": [
            "pass",
            "pending",
            "rejected"
          ],
          "description": "Automated license check (spec §6.2 step 4)."
        },
        "signals": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "License signals found in the snapshot, e.g. CC0-1.0."
        },
        "exclusion_signals": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "scope": {
          "enum": [
            "file",
            "collection",
            "repository"
          ],
          "description": "What the license statement covers. 'repository' = a repo-wide LICENSE, not a per-file statement."
        },
        "license_files": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "statement_files": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "corroboration": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "caveats": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Known limits of the evidence (third-party re-export, community contributions, trademark flags)."
        }
      }
    },
    "source_file": {
      "type": "object",
      "additionalProperties": false,
      "description": "Present only when the served bytes differ from the source bytes (sanitized re-encode).",
      "required": [
        "sha256",
        "file_size_bytes",
        "transform",
        "pixels_identical"
      ],
      "properties": {
        "sha256": {
          "type": "string",
          "pattern": "^[a-f0-9]{64}$"
        },
        "file_size_bytes": {
          "type": "integer",
          "minimum": 1
        },
        "transform": {
          "type": "string"
        },
        "pixels_identical": {
          "type": "boolean"
        }
      }
    }
  },
  "allOf": [
    {
      "if": {
        "properties": {
          "type": {
            "const": "tileset"
          }
        }
      },
      "then": {
        "required": [
          "slicing"
        ],
        "properties": {
          "slicing": {
            "$ref": "#/$defs/tileset_slicing"
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "sprite_sheet"
          }
        }
      },
      "then": {
        "required": [
          "slicing"
        ],
        "properties": {
          "slicing": {
            "$ref": "#/$defs/sheet_slicing"
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "enum": [
              "static",
              "ui"
            ]
          }
        }
      },
      "then": {
        "properties": {
          "slicing": {
            "$ref": "#/$defs/sheet_slicing"
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "attribution_required": {
            "const": true
          }
        }
      },
      "then": {
        "properties": {
          "attribution_text": {
            "minLength": 1
          }
        }
      }
    }
  ],
  "$defs": {
    "sheet_slicing": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "frame_width",
        "frame_height",
        "grid_cols",
        "grid_rows",
        "pivot",
        "animations"
      ],
      "properties": {
        "frame_width": {
          "type": "integer",
          "minimum": 1
        },
        "frame_height": {
          "type": "integer",
          "minimum": 1
        },
        "grid_cols": {
          "type": "integer",
          "minimum": 1
        },
        "grid_rows": {
          "type": "integer",
          "minimum": 1
        },
        "margin": {
          "type": "integer",
          "minimum": 0,
          "default": 0,
          "description": "Pixels around the whole grid (Tiled semantics). Optional; see Open question 2."
        },
        "spacing": {
          "type": "integer",
          "minimum": 0,
          "default": 0,
          "description": "Pixels between frames (Tiled semantics). Optional; see Open question 2."
        },
        "pivot": {
          "$ref": "#/$defs/point",
          "description": "Sheet-wide pivot in pixels from each frame's top-left."
        },
        "pivot_overrides": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "frame",
              "x",
              "y"
            ],
            "properties": {
              "frame": {
                "type": "integer",
                "minimum": 0
              },
              "x": {
                "type": "number",
                "minimum": 0
              },
              "y": {
                "type": "number",
                "minimum": 0
              }
            }
          }
        },
        "animations": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "name",
              "frames",
              "fps",
              "loop"
            ],
            "properties": {
              "name": {
                "type": "string",
                "pattern": "^[a-z0-9][a-z0-9_-]*$"
              },
              "frames": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": 0
                },
                "minItems": 1
              },
              "fps": {
                "type": "number",
                "exclusiveMinimum": 0,
                "maximum": 60
              },
              "loop": {
                "type": "boolean"
              }
            }
          }
        },
        "frame_count": {
          "type": "integer",
          "minimum": 1,
          "description": "Number of drawn (non-empty) frames. Equals grid_cols*grid_rows minus the empty_frames count."
        },
        "empty_frames": {
          "type": "array",
          "items": {
            "type": "integer",
            "minimum": 0
          },
          "description": "Indices of empty grid cells (no pixels), present only when some exist."
        }
      }
    },
    "tileset_slicing": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "tile_width",
        "tile_height",
        "margin",
        "spacing",
        "tile_count",
        "columns"
      ],
      "properties": {
        "tile_width": {
          "type": "integer",
          "minimum": 1,
          "description": "Tiled: tilewidth"
        },
        "tile_height": {
          "type": "integer",
          "minimum": 1,
          "description": "Tiled: tileheight"
        },
        "margin": {
          "type": "integer",
          "minimum": 0
        },
        "spacing": {
          "type": "integer",
          "minimum": 0
        },
        "tile_count": {
          "type": "integer",
          "minimum": 1,
          "description": "Tiled: tilecount"
        },
        "columns": {
          "type": "integer",
          "minimum": 1
        },
        "tile_properties": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "minimum": 0,
                "description": "Local tile id (0-based, row-major)."
              },
              "collision": {
                "type": "boolean"
              },
              "terrain": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      }
    },
    "point": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "x",
        "y"
      ],
      "properties": {
        "x": {
          "type": "number",
          "minimum": 0
        },
        "y": {
          "type": "number",
          "minimum": 0
        }
      }
    }
  }
}