{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://opensecuritymetrics.org/schema/0.9/osms-card.schema.json",
  "title": "OSMS Metric Card Contract",
  "description": "Open Security Metrics Standard (OSMS) v0.9 — machine-readable contract for a single metric card. Licensed CC BY 4.0. Field names marked ADAPT may need mapping to the production 327-card catalog format (see tools/osms_validate.py FIELD_MAP).",
  "type": "object",
  "required": [
    "id", "name", "domain", "card_type", "priority", "lifecycle",
    "management_question", "formula", "direction", "roles",
    "data", "data_confidence", "decision", "version"
  ],
  "additionalProperties": true,
  "properties": {
    "id": {
      "type": "string",
      "minLength": 3,
      "pattern": "^[A-Za-z][A-Za-z0-9._-]+$",
      "description": "Unique, stable card identifier. ADAPT pattern to the real ID scheme."
    },
    "name": { "type": "string", "minLength": 3 },
    "domain": { "type": "string", "minLength": 2, "description": "Must match an entry in domains.yaml (checked semantically)." },
    "card_type": {
      "type": "string",
      "enum": ["KPI", "KRI", "KCI", "Outcome", "Confidence", "Maturity", "Metric"]
    },
    "priority": { "type": "string", "enum": ["P0", "P1", "P2"] },
    "lifecycle": {
      "type": "string",
      "enum": ["stable", "draft", "deprecated"],
      "description": "Freeze rule: stable cards must be complete; incomplete cards are demoted to draft instead of blocking v1.0."
    },
    "management_question": { "type": "string", "minLength": 5 },
    "measurement_object": { "type": "string" },
    "scope": { "type": "string" },
    "formula": {
      "type": "object",
      "required": ["expression", "calculation_type"],
      "properties": {
        "expression": { "type": "string", "minLength": 1 },
        "numerator": { "type": "string" },
        "denominator": { "type": "string", "description": "Required for ratio/percentage calculation types (semantic check: missing denominator must never render as 0)." },
        "unit": { "type": "string" },
        "calculation_type": {
          "type": "string",
          "enum": ["ratio", "percentage", "count", "duration", "score", "index", "band", "boolean", "composite", "qualitative"]
        }
      }
    },
    "direction": {
      "type": "string",
      "enum": ["higher_is_better", "lower_is_better", "zero_is_target", "band", "context"]
    },
    "thresholds": {
      "type": "object",
      "description": "RAG thresholds. Omit for qualitative/context cards — a missing hard threshold must never be replaced by an invented RAG colour.",
      "properties": {
        "green": { "type": ["number", "string"] },
        "amber": { "type": ["number", "string"] },
        "red": { "type": ["number", "string"] },
        "band_min": { "type": "number" },
        "band_max": { "type": "number" }
      }
    },
    "roles": {
      "type": "object",
      "required": ["owner"],
      "properties": {
        "owner": { "type": "string", "minLength": 2 },
        "escalation": { "type": "string" }
      }
    },
    "data": {
      "type": "object",
      "required": ["source_system", "minimum_fields"],
      "properties": {
        "source_system": { "type": "string", "minLength": 2 },
        "minimum_fields": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
        "freshness_max_hours": { "type": "number" }
      }
    },
    "data_confidence": {
      "type": "object",
      "required": ["minimum_required"],
      "properties": {
        "method": { "type": "string" },
        "minimum_required": { "type": "number", "minimum": 0, "maximum": 100 },
        "gates": {
          "type": "object",
          "properties": {
            "operational_green": { "type": "number", "minimum": 0, "maximum": 100 },
            "management_green": { "type": "number", "minimum": 0, "maximum": 100 }
          }
        }
      }
    },
    "evidence": {
      "type": "object",
      "required": ["evidence_ref", "record_id_field", "source_system"],
      "properties": {
        "evidence_ref": { "type": "string", "minLength": 1 },
        "record_id_field": { "type": "string", "minLength": 1 },
        "source_system": { "type": "string", "minLength": 1 }
      }
    },
    "drilldown_path": {
      "type": "array",
      "items": { "type": "string" },
      "maxItems": 4,
      "description": "Board value → driver → contributing objects → evidence pointer. Canonical rule: at most 4 steps."
    },
    "guardrails": { "type": "array", "items": { "type": "string" } },
    "decision": {
      "type": "object",
      "required": ["triggered_decision"],
      "properties": {
        "triggered_decision": { "type": "string", "minLength": 5, "description": "No metric without a decision." },
        "decision_owner": { "type": "string" }
      }
    },
    "hierarchy": {
      "type": "object",
      "properties": {
        "parent": { "type": ["string", "null"] },
        "children": { "type": "array", "items": { "type": "string" } },
        "is_helper": { "type": "boolean", "default": false },
        "board_reportable": { "type": "boolean", "description": "Helper cards must never be board_reportable (semantic check)." },
        "rollup_method": { "type": "string" }
      }
    },
    "version": {
      "type": "object",
      "required": ["number", "version_break_rule"],
      "properties": {
        "number": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$" },
        "version_break_rule": { "type": "string", "minLength": 5, "description": "No change to ID, formula, scope, direction or minimum data fields without a documented version break." }
      }
    }
  }
}
