{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://www.ironscout.ai/ids/schemas/market_snapshot_artifact_v1.schema.json",
  "title": "market_snapshot_artifact_v1",
  "description": "IDS v1 schema for the static snapshot artifacts at /market-snapshots/{windowDays}d/{caliber}.json. Distinct from caliber_market_snapshot_v1 (the API endpoint schema). The artifact uses nested pricePerRound/counts objects, a methodology-as-object shape, the slug form of caliber identifiers, and a legacy schemaVersion VALUE of 'market-snapshot/v1'. The IDS schema NAME (market_snapshot_artifact_v1) and the schemaVersion VALUE (market-snapshot/v1) differ by historical accident; convergence is tracked in /ids/changelog.md Known Divergences.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schemaVersion",
    "caliberSlug",
    "windowDays",
    "statBasis",
    "statLabel",
    "pricePerRound",
    "counts",
    "computedAt",
    "windowStart",
    "windowEnd",
    "dataStatus",
    "methodology"
  ],
  "properties": {
    "schemaVersion": {
      "type": "string",
      "const": "market-snapshot/v1",
      "description": "Legacy schemaVersion VALUE. The IDS schema NAME is market_snapshot_artifact_v1. The two will converge in a future major when the artifact emission is updated; until then the constant matches what the artifact actually emits today."
    },
    "caliberSlug": {
      "type": "string",
      "description": "URL-safe slug form of the caliber (e.g. '9mm', '45-acp'). Distinct from the API endpoint's 'caliber' field, which carries the canonical CaliberValue (e.g. '9mm', '.45 ACP') rather than a slug."
    },
    "windowDays": { "type": "integer", "minimum": 1 },
    "statBasis": { "type": "string", "const": "dailyBestObserved" },
    "statLabel": { "type": "string" },
    "pricePerRound": {
      "type": "object",
      "additionalProperties": false,
      "required": ["median", "p25", "p75", "min", "max"],
      "properties": {
        "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 artifacts, where consumers fall back to max."
        },
        "min": { "type": ["number", "null"] },
        "max": { "type": ["number", "null"] }
      }
    },
    "recentLow": {
      "type": ["number", "null"],
      "description": "Lowest observed price per round in a recent (7-day) sub-window. Distinct from windowed pricePerRound.min."
    },
    "segments": {
      "type": "object",
      "additionalProperties": false,
      "required": ["range", "premium"],
      "description": "Range/practice vs premium/defensive split (ADR-024 amendment, Option B). Absent on pre-Option-B artifacts; per-segment fields are null until a segment has >= 5 products.",
      "properties": {
        "range": { "$ref": "#/$defs/segment" },
        "premium": { "$ref": "#/$defs/segment" }
      }
    },
    "counts": {
      "type": "object",
      "additionalProperties": false,
      "required": ["sampleCount", "daysWithData", "productCount", "retailerCount"],
      "properties": {
        "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", "null"],
      "format": "date-time",
      "description": "Null when dataStatus is UNAVAILABLE (placeholder artifact for a caliber with no qualifying data)."
    },
    "windowStart": { "type": ["string", "null"], "format": "date-time" },
    "windowEnd": { "type": ["string", "null"], "format": "date-time" },
    "dataStatus": {
      "type": "string",
      "enum": ["SUFFICIENT", "INSUFFICIENT_DATA", "UNAVAILABLE"]
    },
    "methodology": {
      "type": "object",
      "additionalProperties": false,
      "required": ["measurementTechnique", "notes"],
      "properties": {
        "measurementTechnique": { "type": "string" },
        "notes": {
          "type": "array",
          "items": { "type": "string" }
        }
      },
      "description": "Object shape (artifact only). Distinct from the API endpoint's methodology field which is a string. See /ids/changelog.md Known Divergences."
    },
    "insufficientSince": {
      "type": ["string", "null"],
      "format": "date-time",
      "description": "ISO timestamp of when this caliber first dropped below the sample-count indexability floor; null once sufficient again. Backs a grace period before dependent pages/sitemap entries are noindexed, so a single thin day doesn't flip indexability. Additive/optional; absent on pre-hysteresis artifacts."
    }
  },
  "$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 }
      }
    }
  }
}
