{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://detect.htora.dev/schema/score.schema.json",
  "title": "Detection score record (score.yml)",
  "description": "One record per detection at detections/<id>/score.yml. CI validates the record, computes score and status, and writes score, status, and scored_on. Hand edits to those three fields fail the build. Status derivation: blocked when telemetry_readiness is 0; mandated when a mandate is due within 90 days; scored at intake; in_progress from hypothesis through validate; live at deploy and tune; retired at retire.",
  "type": "object",
  "additionalProperties": false,
  "required": ["id", "name", "technique", "stage", "preset", "inputs", "evidence"],
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^DET-[0-9]{4}$",
      "description": "DET-0000 is reserved for log source silence."
    },
    "name": {
      "type": "string",
      "minLength": 3,
      "maxLength": 80
    },
    "technique": {
      "type": "array",
      "minItems": 1,
      "uniqueItems": true,
      "items": {
        "type": "string",
        "pattern": "^T[0-9]{4}(\\.[0-9]{3})?$"
      },
      "description": "ATT&CK technique or sub-technique ids. Tactic is derived from the pinned ATT&CK version at build time."
    },
    "stage": {
      "enum": ["intake", "hypothesis", "research", "build", "validate", "deploy", "tune", "retire"]
    },
    "preset": {
      "enum": ["balanced", "threat_led", "asset_led", "telemetry_led"]
    },
    "weights": {
      "type": "object",
      "description": "Optional override of the preset. Any key omitted keeps the preset value.",
      "additionalProperties": false,
      "properties": {
        "wT": {"$ref": "#/$defs/weight"},
        "wI": {"$ref": "#/$defs/weight"},
        "wB": {"$ref": "#/$defs/weight"},
        "wE": {"$ref": "#/$defs/weight"},
        "wC": {"$ref": "#/$defs/weight"},
        "k": {"type": "integer", "minimum": 1, "maximum": 3}
      }
    },
    "inputs": {
      "type": "object",
      "additionalProperties": false,
      "required": ["threat_relevance", "impact", "robustness", "telemetry_readiness", "build_effort", "run_cost"],
      "properties": {
        "threat_relevance": {"$ref": "#/$defs/one_to_five"},
        "impact": {"$ref": "#/$defs/one_to_five"},
        "robustness": {"$ref": "#/$defs/one_to_five"},
        "telemetry_readiness": {"type": "integer", "minimum": 0, "maximum": 5},
        "build_effort": {"$ref": "#/$defs/one_to_five"},
        "run_cost": {"$ref": "#/$defs/one_to_five"}
      }
    },
    "evidence": {
      "type": "object",
      "description": "One line per input naming its source: incident id, report section, query, ticket.",
      "additionalProperties": false,
      "required": ["threat_relevance", "impact", "robustness", "telemetry_readiness", "build_effort", "run_cost"],
      "properties": {
        "threat_relevance": {"$ref": "#/$defs/evidence_line"},
        "impact": {"$ref": "#/$defs/evidence_line"},
        "robustness": {"$ref": "#/$defs/evidence_line"},
        "telemetry_readiness": {"$ref": "#/$defs/evidence_line"},
        "build_effort": {"$ref": "#/$defs/evidence_line"},
        "run_cost": {"$ref": "#/$defs/evidence_line"}
      }
    },
    "mandate": {
      "type": "object",
      "description": "Present only for compliance-driven rules. Due within 90 days sorts the record ahead of the scored backlog.",
      "additionalProperties": false,
      "required": ["framework", "requirement", "due"],
      "properties": {
        "framework": {"type": "string", "minLength": 2},
        "requirement": {"type": "string", "minLength": 1},
        "due": {"type": "string", "format": "date"}
      }
    },
    "alert_volume_threshold_7d": {
      "type": "integer",
      "minimum": 0,
      "description": "Post-deploy 7-day replay fails above this count. Default 1.5 x 7 x expected alerts per day from the research baseline. Required from deploy onward."
    },
    "precision_floor": {
      "type": "number",
      "minimum": 0,
      "maximum": 1,
      "description": "Override. Default derives from median triage minutes divided by the program cap on minutes per true positive."
    },
    "score": {
      "type": "number",
      "minimum": 0,
      "readOnly": true,
      "description": "Written by CI. (value / cost) * (R / 5)^k, 2 decimals."
    },
    "status": {
      "enum": ["blocked", "scored", "mandated", "in_progress", "live", "retired"],
      "readOnly": true,
      "description": "Written by CI."
    },
    "scored_on": {
      "type": "string",
      "format": "date",
      "readOnly": true,
      "description": "Written by CI."
    },
    "retired": {
      "type": "object",
      "additionalProperties": false,
      "required": ["reason", "date"],
      "properties": {
        "reason": {
          "enum": ["precision_below_floor", "no_true_positives_180d", "telemetry_absent_30d", "technique_gone", "other"]
        },
        "date": {"type": "string", "format": "date"},
        "replacement_id": {"type": "string", "pattern": "^DET-[0-9]{4}$"},
        "note": {"type": "string", "maxLength": 300}
      }
    }
  },
  "allOf": [
    {
      "if": {
        "required": ["stage"],
        "properties": {"stage": {"enum": ["deploy", "tune", "retire"]}}
      },
      "then": {"required": ["alert_volume_threshold_7d"]}
    },
    {
      "if": {
        "required": ["stage"],
        "properties": {"stage": {"const": "retire"}}
      },
      "then": {"required": ["retired"]}
    }
  ],
  "$defs": {
    "weight": {"type": "number", "minimum": 0.5, "maximum": 3},
    "one_to_five": {"type": "integer", "minimum": 1, "maximum": 5},
    "evidence_line": {"type": "string", "minLength": 10, "maxLength": 200}
  }
}
