{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://uploadcheck.app/schemas/uploadcheck-receipt-v1.json",
  "title": "UploadCheck Signed Media QC Receipt",
  "description": "A tamper-evident record of a media quality-check verdict, bound to the exact file that was checked (by sha256) and the named delivery profile it was checked against. Lets a CI pipeline or an AI agent gate its publish step on media quality the way a signed status check gates a code merge. IMPORTANT: only the fields listed in `signedFields` are covered by the signature — `signature`, `summary`, and `verify` are NOT signed and must never be trusted as assertions.",
  "type": "object",
  "required": ["schema", "verdict", "gates", "blocked", "jobId", "issuedAt", "issuer", "signature"],
  "additionalProperties": false,
  "properties": {
    "schema": {
      "type": "string",
      "const": "uploadcheck-receipt/1",
      "description": "Receipt format identifier and version. A verifier MUST reject a receipt whose schema it does not recognise."
    },
    "verdict": {
      "type": "string",
      "enum": ["PASS", "WATCH", "BLOCK"],
      "description": "The gate outcome. PASS = clear to publish. BLOCK = a gate failed; do not publish. WATCH = findings a human or agent should review. A publish gate SHOULD require PASS; treating WATCH as passing is an explicit caller opt-in."
    },
    "sha256": {
      "type": ["string", "null"],
      "pattern": "^[a-f0-9]{64}$",
      "description": "Lowercase hex SHA-256 of the exact media file that was checked. This is what binds the verdict to specific content: swap the file and the signature no longer matches. Null when the source was not a hashable upload (e.g. a remote URL). A verifier MUST confirm this matches the file in hand before trusting the verdict."
    },
    "profile": {
      "type": ["string", "null"],
      "description": "The named delivery target the file was checked against (e.g. youtube_shorts, meta_reels_ad, tiktok_post, broadcast_r128, podcast_delivery, ai_render_gate, youtube_longform). Null when no named profile was requested and gates were auto-selected.",
      "examples": ["youtube_shorts", "broadcast_r128", "ai_render_gate"]
    },
    "gates": {
      "type": "array",
      "items": { "type": "string" },
      "description": "The quality gates that ran for this check, sorted ascending. Sorting is REQUIRED for signature stability.",
      "examples": [["av_sync", "canvas_fill", "loudness", "shorts_format"]]
    },
    "blocked": {
      "type": "array",
      "items": { "type": "string" },
      "description": "The gates that produced a BLOCK, sorted ascending. Empty on a PASS. Sorting is REQUIRED for signature stability.",
      "examples": [[], ["av_sync"]]
    },
    "jobId": {
      "type": "string",
      "description": "The UploadCheck job this receipt was issued for. Useful for correlating with the full report; not required to verify the receipt."
    },
    "issuedAt": {
      "type": "string",
      "format": "date-time",
      "description": "ISO-8601 timestamp of when the checked job completed. A verifier MAY reject receipts older than its own freshness policy."
    },
    "issuer": {
      "type": "string",
      "description": "Who issued the receipt.",
      "default": "uploadcheck.app"
    },
    "signature": {
      "type": "string",
      "pattern": "^sha256=[a-f0-9]{64}$",
      "description": "HMAC-SHA256 over the canonical JSON of the signed fields, prefixed with 'sha256='. NOT itself part of the signed payload."
    },
    "summary": {
      "type": "string",
      "description": "ADVISORY, NOT SIGNED. A one-line human/agent-readable summary for CI logs. Never parse this for a gating decision — read `verdict` instead."
    },
    "verify": {
      "type": "object",
      "description": "ADVISORY, NOT SIGNED. Self-describing pointer to how to verify this receipt.",
      "properties": {
        "how": { "type": "string" },
        "docs": { "type": "string", "format": "uri" }
      },
      "additionalProperties": true
    }
  },
  "$defs": {
    "signedFields": {
      "description": "The exact fields covered by `signature`, in the order they appear once canonicalized (keys sorted ascending, recursively): blocked, gates, issuedAt, issuer, jobId, profile, schema, sha256, verdict. Everything else in the object is advisory.",
      "const": [
        "blocked",
        "gates",
        "issuedAt",
        "issuer",
        "jobId",
        "profile",
        "schema",
        "sha256",
        "verdict"
      ]
    },
    "verificationProcedure": {
      "description": "To verify: (1) take the receipt object; (2) drop `signature`, `summary`, and `verify`; (3) canonicalize the remainder — serialize as JSON with object keys sorted ascending recursively and no insignificant whitespace; (4) compute HMAC-SHA256 of that canonical string with UploadCheck's signing key; (5) compare, in constant time, to the `signature` value after stripping the 'sha256=' prefix; (6) independently confirm `sha256` equals the SHA-256 of the file you are about to publish. Anyone can perform steps 1-5 without an account by POSTing the receipt to https://api.uploadcheck.app/v1/public/receipts/verify."
    }
  },
  "examples": [
    {
      "schema": "uploadcheck-receipt/1",
      "verdict": "PASS",
      "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
      "profile": "youtube_shorts",
      "gates": ["av_sync", "canvas_fill", "loudness", "shorts_format", "text_safe_area"],
      "blocked": [],
      "jobId": "job_9c1f2ab4",
      "issuedAt": "2026-07-21T09:14:22.000Z",
      "issuer": "uploadcheck.app",
      "signature": "sha256=0000000000000000000000000000000000000000000000000000000000000000",
      "summary": "PASS — youtube_shorts — 9f86d0818844"
    },
    {
      "schema": "uploadcheck-receipt/1",
      "verdict": "BLOCK",
      "sha256": "60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752",
      "profile": "ai_render_gate",
      "gates": ["ai_warp", "av_sync", "black_frames", "flicker", "loop_freeze", "loudness"],
      "blocked": ["loop_freeze"],
      "jobId": "job_4d77e01a",
      "issuedAt": "2026-07-21T09:20:05.000Z",
      "issuer": "uploadcheck.app",
      "signature": "sha256=1111111111111111111111111111111111111111111111111111111111111111",
      "summary": "BLOCK — ai_render_gate — 60303ae22b99"
    }
  ]
}
