{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://releeze.app/spec/release.schema.json",
  "title": "releeze Release",
  "description": "One post on a product timeline: the first launch or any update after it. A release must be traceable to real code (`source`) and must be written in the language of the person using the product, not the language of the diff.",
  "type": "object",
  "additionalProperties": false,
  "required": ["product_slug", "kind", "title", "summary", "changes", "source"],
  "properties": {
    "product_slug": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$",
      "description": "The profile this release belongs to."
    },
    "kind": {
      "enum": ["launch", "feature", "improvement", "fix"],
      "description": "`launch` is allowed once per product. `feature` and `launch` require screens; `improvement` and `fix` may be text-only."
    },
    "version": {
      "type": "string",
      "pattern": "^v?\\d+\\.\\d+(\\.\\d+)?(-[0-9A-Za-z.-]+)?$",
      "description": "Optional. Whatever the product actually calls this build."
    },
    "title": {
      "type": "string",
      "description": "A headline a stranger would read, at most 60 characters. Not a commit subject: conventional-commit prefixes are rejected.",
      "minLength": 8,
      "maxLength": 60,
      "not": {
        "pattern": "^(feat|fix|chore|refactor|docs|test|build|ci|perf|style|revert)(\\([^)]*\\))?!?:"
      }
    },
    "summary": {
      "type": "string",
      "description": "What is different for the person using the product, in one or two sentences.",
      "minLength": 20,
      "maxLength": 200
    },
    "changes": {
      "type": "array",
      "description": "One to five lines, each phrased as something the person can now do. File paths, symbol names, and bare identifiers are rejected.",
      "minItems": 1,
      "maxItems": 5,
      "items": {
        "type": "string",
        "minLength": 12,
        "maxLength": 120,
        "not": {
          "anyOf": [
            { "pattern": "\\.(ts|tsx|js|jsx|mjs|py|go|rs|rb|java|kt|swift|css|scss|html|json|yml|yaml|sql)\\b" },
            { "pattern": "(^|\\s)[A-Za-z_$][A-Za-z0-9_$]*\\(\\)" },
            { "pattern": "(^|\\s)(src|lib|app|packages|components)/" }
          ]
        }
      }
    },
    "screens": {
      "type": "array",
      "description": "Real screens from the running app, captured by capture_screens, in the order that explains the change. Three to five for a launch or a feature.",
      "minItems": 1,
      "maxItems": 5,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["file", "caption"],
        "properties": {
          "file": {
            "type": "string",
            "minLength": 1,
            "description": "The filename returned by upload_asset. Never a placeholder or a mockup."
          },
          "caption": {
            "type": "string",
            "minLength": 6,
            "maxLength": 60,
            "description": "What this screen shows. Required for every screen — an uncaptioned screen is rejected."
          },
          "order": { "type": "integer", "minimum": 1, "maximum": 5 },
          "sha256": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$",
            "description": "Set by upload_asset. Two screens in one release may not share a hash."
          }
        }
      }
    },
    "source": {
      "type": "object",
      "description": "Where this release came from. Without it a post is unverifiable, so it is required and it is weighted in feed ranking.",
      "additionalProperties": false,
      "required": ["type", "ref"],
      "properties": {
        "type": { "enum": ["commit_range", "pull_request", "tag", "changelog"] },
        "ref": {
          "type": "string",
          "minLength": 1,
          "description": "e.g. \"v2.2..v2.4\", \"#412\", \"v3.0\", \"CHANGELOG.md#L1-L22\"."
        },
        "repo": { "type": "string", "description": "e.g. \"margin/app\"." },
        "commit_count": { "type": "integer", "minimum": 1 }
      }
    },
    "feature_deltas": {
      "type": "object",
      "description": "How this release changes the profile's current_features. Applied on publish, which is how a profile stays current without the maker editing it.",
      "additionalProperties": false,
      "properties": {
        "add": { "type": "array", "items": { "type": "string", "minLength": 6, "maxLength": 80 } },
        "remove": { "type": "array", "items": { "type": "string", "minLength": 6, "maxLength": 80 } }
      }
    },
    "format": {
      "enum": ["carousel", "clip", "text"],
      "default": "carousel",
      "description": "`carousel` is the default and the only format guaranteed at launch. `clip` is reserved. `text` is for releases with no screens."
    },
    "approval": {
      "type": "object",
      "description": "Set only after the maker has seen preview_release. publish_release refuses to run without approved_by_maker true.",
      "additionalProperties": false,
      "required": ["approved_by_maker"],
      "properties": {
        "approved_by_maker": { "type": "boolean" },
        "approved_at": { "type": "string", "format": "date-time" },
        "edited_by_maker": {
          "type": "boolean",
          "description": "Whether the maker changed the draft before approving. Tracked to measure draft acceptance."
        }
      }
    },
    "visibility": { "enum": ["draft", "published"], "default": "draft" }
  },
  "allOf": [
    {
      "if": { "properties": { "kind": { "enum": ["launch", "feature"] } }, "required": ["kind"] },
      "then": {
        "required": ["screens"],
        "properties": { "screens": { "type": "array", "minItems": 3 } }
      }
    },
    {
      "if": { "properties": { "visibility": { "const": "published" } }, "required": ["visibility"] },
      "then": {
        "required": ["approval"],
        "properties": {
          "approval": {
            "type": "object",
            "properties": { "approved_by_maker": { "type": "boolean", "const": true } }
          }
        }
      }
    },
    {
      "if": { "properties": { "format": { "const": "text" } }, "required": ["format"] },
      "then": { "properties": { "kind": { "type": "string", "enum": ["improvement", "fix"] } } }
    }
  ]
}
