{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://devcannabis.com/static/spec/metric_observation.schema.json",
  "$comment": "License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/) — DevCannabis Normalization Spec. Free to use/implement/cite with attribution.",
  "title": "metric_observation",
  "description": "One observed number with full dimensional context and provenance. Append-only canonical fact record (table metric_observations) per DevCannabis Normalization Spec v0.1. Enum values are persisted as lowercase strings.",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "integer",
      "description": "Surrogate primary key (autoincrement)."
    },
    "jurisdiction_code": {
      "type": "string",
      "maxLength": 20,
      "description": "State/jurisdiction code (e.g. FL, MA, CO). Not an FK."
    },
    "source_snapshot_id": {
      "type": ["integer", "null"],
      "description": "FK -> source_snapshots.id. Immutable raw capture this number was parsed from."
    },
    "citation_id": {
      "type": ["integer", "null"],
      "description": "FK -> citations.id. Canonical citation supporting the claim."
    },
    "period_start": {
      "type": "string",
      "format": "date",
      "description": "Inclusive start of the coverage window."
    },
    "period_end": {
      "type": "string",
      "format": "date",
      "description": "Inclusive end of the coverage window."
    },
    "cadence": {
      "type": "string",
      "enum": ["daily", "weekly", "monthly", "quarterly", "annual"],
      "description": "Reporting cadence."
    },
    "period_label": {
      "type": ["string", "null"],
      "maxLength": 40,
      "description": "Optional human label for the period."
    },
    "published_at": {
      "type": ["string", "null"],
      "format": "date",
      "description": "Date the source published this number."
    },
    "geo_level": {
      "type": "string",
      "enum": ["state", "county", "province", "municipality"],
      "default": "state",
      "description": "Geographic granularity. Operator is a separate dimension."
    },
    "geo_code": {
      "type": ["string", "null"],
      "maxLength": 20,
      "description": "Geographic code (county FIPS). NULL for state-level. Coalesced to '' in the unique key."
    },
    "geo_name_raw": {
      "type": ["string", "null"],
      "maxLength": 120,
      "description": "Verbatim geography name as published. Descriptive provenance only; not keyed."
    },
    "operator_id": {
      "type": ["integer", "null"],
      "description": "FK -> operators.id. NULL on failed alias resolution or aggregate rows. Not part of the unique key."
    },
    "operator_name_raw": {
      "type": ["string", "null"],
      "maxLength": 200,
      "description": "Verbatim operator name before resolution. Coalesced to '', this (not operator_id) is the operator dimension in the unique key."
    },
    "is_aggregate": {
      "type": "boolean",
      "default": false,
      "description": "True = state/market rollup rather than a single operator's row. Part of the unique key."
    },
    "market_segment": {
      "type": "string",
      "enum": ["medical", "adult_use", "combined", "delivery", "wholesale", "unknown"],
      "default": "combined",
      "description": "Market channel segmentation. Part of the unique key."
    },
    "buyer_residency": {
      "type": "string",
      "enum": ["resident", "non_resident", "all"],
      "default": "all",
      "description": "Resident/non-resident split (exists for IL). Part of the unique key."
    },
    "product_category": {
      "type": "string",
      "x-known-values": [
        "all", "flower", "shake_trim", "kief", "pre_roll", "infused_pre_roll",
        "concentrate", "vape", "edible", "beverage", "tincture", "topical",
        "capsule", "suppository", "seed_clone", "biomass", "other", "unknown"
      ],
      "default": "all",
      "description": "Canonical cross-state product category. OPEN vocabulary (forward-compatible per §2): x-known-values lists the current catalog, but new values validate without a schema bump. Unmapped native strings must be stored as 'unknown' (fail-loud). Part of the unique key."
    },
    "product_category_raw": {
      "type": ["string", "null"],
      "maxLength": 120,
      "description": "Verbatim source category label. Coalesced to '' in the unique key (added by migration 015)."
    },
    "metric": {
      "type": "string",
      "maxLength": 60,
      "x-known-values": [
        "sales", "units_sold", "transactions", "weight_dispensed",
        "thc_dispensed", "cbd_dispensed", "median_price",
        "avg_price", "wholesale_market_rate", "excise_tax",
        "harvest_weight", "patient_count", "physician_count", "location_count"
      ],
      "description": "FK -> metric_definitions.code. OPEN vocabulary (forward-compatible per §2): x-known-values lists the current catalog; new codes are added as MetricDefinition rows without a schema bump. Aggregation safety is governed by the linked MetricDefinition.value_kind, not stored here."
    },
    "value": {
      "type": ["number", "null"],
      "description": "Observed number, native value as published. Fixed precision Numeric(20,4). NULL ONLY when suppressed=true (CHECK ck_observation_value_or_suppressed)."
    },
    "unit": {
      "type": "string",
      "x-known-values": ["usd", "count", "receipts", "mg", "g", "oz", "lb", "usd_per_g", "usd_per_lb"],
      "description": "Native source unit of value. OPEN vocabulary (forward-compatible per §2): x-known-values lists the current set; new units validate without a schema bump. No default; must be supplied. Conversions live in the query layer."
    },
    "suppressed": {
      "type": "boolean",
      "default": false,
      "description": "True = source withheld this number (e.g. CO 'NR'). Suppression is data, not a missing row."
    },
    "suppression_reason": {
      "type": ["string", "null"],
      "maxLength": 80,
      "description": "Optional free-text reason for suppression."
    },
    "is_latest": {
      "type": "boolean",
      "default": true,
      "description": "Current-vintage flag. The dimensional unique index is partial on WHERE is_latest."
    },
    "restated": {
      "type": "boolean",
      "default": false,
      "description": "True = this row has been superseded by a later observation."
    },
    "restates_observation_id": {
      "type": ["integer", "null"],
      "description": "Self FK -> metric_observations.id. On a revision row, points at the older observation it supersedes."
    },
    "created_at": {
      "type": "string",
      "format": "date-time",
      "description": "DB insert timestamp (app default utcnow). Distinct from published_at and the period bounds."
    }
  },
  "required": [
    "jurisdiction_code",
    "period_start",
    "period_end",
    "cadence",
    "geo_level",
    "is_aggregate",
    "market_segment",
    "buyer_residency",
    "product_category",
    "metric",
    "unit",
    "suppressed",
    "is_latest",
    "restated"
  ],
  "allOf": [
    {
      "description": "value may be NULL only when suppressed (CHECK ck_observation_value_or_suppressed).",
      "anyOf": [
        { "properties": { "value": { "type": "number" } }, "required": ["value"] },
        { "properties": { "suppressed": { "const": true } }, "required": ["suppressed"] }
      ]
    }
  ]
}
