{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://www.ironscout.ai/ids/schemas/caliber_market_snapshot_v1.schema.json",
  "title": "caliber_market_snapshot_v1",
  "description": "IDS v1 response shape for GET /api/market-snapshots/calibers/:caliber and the static artifacts at /market-snapshots/{windowDays}d/{caliber}.json. See https://www.ironscout.ai/.well-known/ids.md.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schemaVersion",
    "caliber",
    "windowDays",
    "windowStart",
    "windowEnd",
    "statBasis",
    "statLabel",
    "methodology",
    "methodologyMeta",
    "median",
    "p25",
    "p75",
    "min",
    "max",
    "sampleCount",
    "daysWithData",
    "productCount",
    "retailerCount",
    "computedAt",
    "computationVersion",
    "dataStatus"
  ],
  "properties": {
    "schemaVersion": {
      "type": "string",
      "const": "caliber_market_snapshot_v1"
    },
    "requestId": {
      "description": "Opaque per-request identifier. May be null when there is no active request context (e.g. cached background invocation).",
      "type": ["string", "null"]
    },
    "caliber": {
      "type": "string",
      "description": "Canonical caliber identifier (e.g. '9mm', '.45 ACP')."
    },
    "windowDays": {
      "type": "integer",
      "minimum": 1
    },
    "windowStart": {
      "type": "string",
      "format": "date-time"
    },
    "windowEnd": {
      "type": "string",
      "format": "date-time"
    },
    "statBasis": {
      "type": "string",
      "const": "dailyBestObserved"
    },
    "statLabel": {
      "type": "string"
    },
    "methodology": {
      "type": "string",
      "description": "Human-readable methodology description for use in citations."
    },
    "methodologyMeta": {
      "type": "object",
      "additionalProperties": false,
      "required": ["methodologyVersion", "computationVersion", "methodologyUrl"],
      "properties": {
        "methodologyVersion": {
          "type": "string",
          "description": "Stable identifier for the abstract methodology (changes when methodology semantics change)."
        },
        "computationVersion": {
          "type": "string",
          "description": "Implementation version (bumped when query logic changes without methodology semantics changing)."
        },
        "methodologyUrl": {
          "type": "string",
          "format": "uri"
        }
      }
    },
    "median": { "type": ["number", "null"] },
    "p25": { "type": ["number", "null"] },
    "p75": { "type": ["number", "null"] },
    "p95": {
      "type": ["number", "null"],
      "description": "95th percentile of daily-best price per round — the public '30-day high' (ADR-024 amendment). Additive/optional; null on pre-p95 computationVersions and when dataStatus is INSUFFICIENT_DATA. Consumers fall back to max when null."
    },
    "min": { "type": ["number", "null"] },
    "max": { "type": ["number", "null"] },
    "sampleCount": {
      "type": "integer",
      "minimum": 0,
      "description": "Count of (product, day) daily-best minima. NOT raw observation count."
    },
    "daysWithData": {
      "type": "integer",
      "minimum": 0
    },
    "productCount": {
      "type": "integer",
      "minimum": 0
    },
    "retailerCount": {
      "type": "integer",
      "minimum": 0
    },
    "computedAt": {
      "type": "string",
      "format": "date-time"
    },
    "computationVersion": {
      "type": "string"
    },
    "dataStatus": {
      "type": "string",
      "enum": ["SUFFICIENT", "INSUFFICIENT_DATA", "UNAVAILABLE"],
      "description": "SUFFICIENT: percentiles computed (sampleCount >= 5). INSUFFICIENT_DATA: percentiles null but min/max may be present. UNAVAILABLE: no qualifying data exists. The API currently 404s instead of returning UNAVAILABLE, but the value is allowed for forward-compatibility with artifact-style responses that prefer placeholder rows."
    },
    "segments": {
      "type": "object",
      "additionalProperties": false,
      "required": ["range", "premium"],
      "description": "Range/practice vs premium/defensive split (ADR-024 amendment, Option B). Per-segment fields are null until computationVersion >= snapshot/2026-06-08.2 and until a segment has >= 5 products.",
      "properties": {
        "range": { "$ref": "#/$defs/segment" },
        "premium": { "$ref": "#/$defs/segment" }
      }
    }
  },
  "$defs": {
    "segment": {
      "type": "object",
      "additionalProperties": false,
      "required": ["median", "p25", "p75", "min", "max", "sampleCount"],
      "properties": {
        "median": { "type": ["number", "null"] },
        "p25": { "type": ["number", "null"] },
        "p75": { "type": ["number", "null"] },
        "min": { "type": ["number", "null"] },
        "max": { "type": ["number", "null"] },
        "sampleCount": { "type": "integer", "minimum": 0 }
      }
    }
  },
  "allOf": [
    {
      "description": "When dataStatus is INSUFFICIENT_DATA, the percentile fields (median, p25, p75, p95) must be null per ADR-025.",
      "if": {
        "properties": { "dataStatus": { "const": "INSUFFICIENT_DATA" } }
      },
      "then": {
        "properties": {
          "median": { "type": "null" },
          "p25": { "type": "null" },
          "p75": { "type": "null" },
          "p95": { "type": "null" }
        }
      }
    }
  ]
}
