{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json",
  "title": "Spectral Ruleset",
  "description": "A Spectral ruleset: a portable, machine-readable set of API governance rules applied to JSON or YAML documents. This schema describes the serialized (JSON/YAML) form of the format. See https://github.com/api-commons/spotlight-spec for the specification.",
  "type": "object",

  "properties": {
    "description": {
      "type": "string",
      "description": "Human-readable description of what this ruleset governs."
    },
    "documentationUrl": {
      "type": "string",
      "format": "uri",
      "description": "URL to documentation for this ruleset. Used as the fallback documentation link for any rule that does not declare its own."
    },
    "formats": {
      "$ref": "#/$defs/formats",
      "description": "Document formats this ruleset applies to. Acts as the default for every rule that does not declare its own formats. When omitted, rules apply to every document."
    },
    "extends": {
      "$ref": "#/$defs/extends",
      "description": "Other rulesets this ruleset inherits from."
    },
    "rules": {
      "type": "object",
      "description": "Map of rule name to rule definition. A rule name is the stable identifier reported in results and used to override the rule downstream.",
      "propertyNames": {
        "minLength": 1
      },
      "additionalProperties": {
        "$ref": "#/$defs/rule"
      }
    },
    "aliases": {
      "$ref": "#/$defs/aliases",
      "description": "Named, reusable path expressions that rules may reference from `given` with a leading `#`."
    },
    "overrides": {
      "$ref": "#/$defs/overrides",
      "description": "Per-file and per-location adjustments applied on top of the ruleset."
    },
    "parserOptions": {
      "$ref": "#/$defs/parserOptions",
      "description": "Severities for problems detected while parsing the document itself, before any rule runs."
    },
    "functions": {
      "type": "array",
      "description": "Names of custom functions to load from `functionsDir`. Serialized rulesets only; JavaScript rulesets import functions directly instead.",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "functionsDir": {
      "type": "string",
      "description": "Directory, relative to the ruleset file, that custom functions named in `functions` are loaded from. Defaults to `functions`.",
      "minLength": 1
    }
  },

  "patternProperties": {
    "^x-": true
  },
  "additionalProperties": false,

  "anyOf": [
    { "required": ["extends"] },
    { "required": ["rules"] },
    { "required": ["overrides"] }
  ],

  "$defs": {
    "numericSeverity": {
      "title": "Numeric severity",
      "description": "0 = error, 1 = warn, 2 = info, 3 = hint, -1 = off.",
      "enum": [-1, 0, 1, 2, 3]
    },

    "humanReadableSeverity": {
      "title": "Human-readable severity",
      "enum": ["error", "warn", "info", "hint", "off"]
    },

    "severity": {
      "title": "Severity",
      "description": "How a rule's results are reported. `off` (-1) disables the rule. Defaults to `warn` when unset.",
      "oneOf": [
        { "$ref": "#/$defs/numericSeverity" },
        { "$ref": "#/$defs/humanReadableSeverity" }
      ]
    },

    "format": {
      "title": "Format",
      "description": "Identifier of a document format. A format is a predicate that decides whether a given document is of that kind, so a rule scoped to a format only runs against matching documents. The listed values are those registered by the reference implementation; any string is permitted so that new and custom formats remain valid.",
      "type": "string",
      "minLength": 1,
      "anyOf": [
        {
          "enum": [
            "oas2",
            "oas3",
            "oas3_0",
            "oas3_1",
            "asyncapi2",
            "aas2",
            "aas2_0",
            "aas2_1",
            "aas2_2",
            "aas2_3",
            "aas2_4",
            "aas2_5",
            "aas2_6",
            "aas3",
            "aas3_0",
            "arazzo1_0",
            "jsonSchema",
            "jsonSchemaLoose",
            "jsonSchemaDraft4",
            "jsonSchemaDraft6",
            "jsonSchemaDraft7",
            "jsonSchemaDraft2019_09",
            "jsonSchemaDraft2020_12"
          ]
        },
        { "type": "string" }
      ]
    },

    "formats": {
      "title": "Formats",
      "type": "array",
      "items": { "$ref": "#/$defs/format" }
    },

    "pathExpression": {
      "title": "Path expression",
      "description": "Either a JSONPath expression beginning with `$`, or an alias reference beginning with `#` optionally followed by a JSONPath subset (for example `#Info.description`).",
      "type": "string",
      "minLength": 1,
      "anyOf": [
        {
          "pattern": "^\\$",
          "title": "JSONPath expression"
        },
        {
          "pattern": "^#",
          "title": "Alias reference"
        }
      ]
    },

    "arrayGiven": {
      "title": "Path expression list",
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/$defs/pathExpression" }
    },

    "given": {
      "title": "Given",
      "description": "The part or parts of the document a rule is applied to. A single path expression, or an array of them.",
      "oneOf": [
        { "$ref": "#/$defs/pathExpression" },
        { "$ref": "#/$defs/arrayGiven" }
      ]
    },

    "functionInvocation": {
      "title": "Function invocation",
      "description": "The assertion half of a rule: the function to run against each node matched by `given`, and the options it takes.",
      "type": "object",
      "properties": {
        "field": {
          "type": "string",
          "description": "Narrow the assertion to a single property of the matched node, rather than the node itself. `@key` targets the node's own key."
        },
        "function": {
          "type": "string",
          "minLength": 1,
          "description": "Name of a core function (`alphabetical`, `casing`, `defined`, `enumeration`, `falsy`, `length`, `pattern`, `schema`, `truthy`, `undefined`, `unreferencedReusableObject`, `xor`, `or`) or of a custom function declared in `functions`."
        },
        "functionOptions": {
          "type": "object",
          "description": "Options passed to the function. The accepted shape is defined by the function itself."
        }
      },
      "required": ["function"]
    },

    "then": {
      "title": "Then",
      "description": "One assertion, or an array of assertions all applied to the nodes matched by `given`.",
      "oneOf": [
        { "$ref": "#/$defs/functionInvocation" },
        {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/functionInvocation" }
        }
      ]
    },

    "ruleDefinition": {
      "title": "Rule definition",
      "description": "A complete rule: where to look (`given`) and what must hold there (`then`).",
      "type": "object",
      "properties": {
        "description": {
          "type": "string",
          "description": "Human-readable statement of what the rule requires."
        },
        "documentationUrl": {
          "type": "string",
          "format": "uri",
          "description": "URL explaining the rule and how to satisfy it. Reported alongside results."
        },
        "message": {
          "type": "string",
          "description": "Template for the reported message. Supports the placeholders `{{error}}`, `{{description}}`, `{{path}}`, `{{property}}`, and `{{value}}`."
        },
        "severity": {
          "$ref": "#/$defs/severity",
          "description": "Severity of this rule's results. Defaults to `warn`."
        },
        "formats": {
          "$ref": "#/$defs/formats",
          "description": "Document formats this rule applies to. Overrides the ruleset-level `formats`."
        },
        "given": {
          "$ref": "#/$defs/given"
        },
        "then": {
          "$ref": "#/$defs/then"
        },
        "recommended": {
          "type": "boolean",
          "description": "Whether the rule is enabled when a consumer extends this ruleset in `recommended` mode. Defaults to true."
        },
        "resolved": {
          "type": "boolean",
          "description": "Whether the rule runs against the document with `$ref`s resolved (true, the default) or against the raw authored document (false). Set to false for rules about references themselves."
        },
        "tags": {
          "type": "array",
          "description": "Free-form labels for grouping and filtering rules.",
          "items": { "type": "string" }
        },
        "type": {
          "description": "Coarse classification of the rule. `validation` rules assert correctness; `style` rules assert convention.",
          "enum": ["style", "validation"]
        },
        "extensions": {
          "type": "object",
          "description": "Arbitrary consumer-defined metadata carried with the rule and ignored by the engine."
        }
      },
      "required": ["given", "then"],
      "patternProperties": {
        "^x-": true
      },
      "additionalProperties": false
    },

    "rule": {
      "title": "Rule",
      "description": "A full rule definition, or — when adjusting a rule inherited via `extends` — a severity string, or a boolean where false disables the rule and true restores its declared severity.",
      "oneOf": [
        { "$ref": "#/$defs/ruleDefinition" },
        { "$ref": "#/$defs/humanReadableSeverity" },
        { "type": "boolean" }
      ]
    },

    "extendsEntry": {
      "title": "Extends entry",
      "description": "A reference to another ruleset: a built-in (`spectral:oas`, `spectral:asyncapi`, `spectral:arazzo`), a file path, an npm module, or a URL.",
      "type": "string",
      "minLength": 1
    },

    "extendsTuple": {
      "title": "Scoped extends entry",
      "description": "A ruleset reference paired with the set of its rules to enable: `recommended` (the default), `all`, or `off`.",
      "type": "array",
      "prefixItems": [
        { "$ref": "#/$defs/extendsEntry" },
        { "enum": ["all", "recommended", "off"] }
      ],
      "minItems": 2,
      "items": false
    },

    "extends": {
      "title": "Extends",
      "oneOf": [
        { "$ref": "#/$defs/extendsEntry" },
        {
          "type": "array",
          "items": {
            "oneOf": [
              { "$ref": "#/$defs/extendsEntry" },
              { "$ref": "#/$defs/extendsTuple" }
            ]
          }
        }
      ]
    },

    "aliasTarget": {
      "title": "Alias target",
      "description": "The path expressions an alias resolves to for one set of formats.",
      "type": "object",
      "properties": {
        "formats": { "$ref": "#/$defs/formats" },
        "given": { "$ref": "#/$defs/arrayGiven" }
      },
      "required": ["formats", "given"]
    },

    "scopedAlias": {
      "title": "Scoped alias",
      "description": "An alias that resolves to different path expressions depending on the format of the document being linted.",
      "type": "object",
      "properties": {
        "description": {
          "type": "string",
          "description": "Human-readable purpose of the alias."
        },
        "targets": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/aliasTarget" }
        }
      },
      "required": ["targets"]
    },

    "alias": {
      "title": "Alias",
      "description": "Either a list of path expressions, or a scoped alias that varies by format.",
      "if": { "type": "object" },
      "then": { "$ref": "#/$defs/scopedAlias" },
      "else": { "$ref": "#/$defs/arrayGiven" }
    },

    "aliases": {
      "title": "Aliases",
      "type": "object",
      "propertyNames": {
        "pattern": "^[A-Za-z][A-Za-z0-9_-]*$"
      },
      "additionalProperties": { "$ref": "#/$defs/alias" }
    },

    "parserOptions": {
      "title": "Parser options",
      "type": "object",
      "properties": {
        "duplicateKeys": {
          "$ref": "#/$defs/severity",
          "description": "Severity for duplicate keys found in the document. Defaults to `error`."
        },
        "incompatibleValues": {
          "$ref": "#/$defs/severity",
          "description": "Severity for YAML values that cannot be represented in JSON. Defaults to `error`."
        }
      },
      "additionalProperties": false
    },

    "overrides": {
      "title": "Overrides",
      "description": "Ordered list of adjustments. Later matching entries win over earlier ones, and every override wins over the base ruleset.",
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/$defs/override" }
    },

    "override": {
      "title": "Override",
      "description": "An adjustment scoped by `files`. When every glob in `files` carries a JSON Pointer fragment (`path/to/file.yaml#/paths/~1users`), the override may only change rule severities. Otherwise it may change anything a ruleset can declare.",
      "type": "object",
      "required": ["files"],

      "if": {
        "type": "object",
        "required": ["files"],
        "properties": {
          "files": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "minLength": 1,
              "pattern": "^[^#]+#"
            }
          }
        }
      },

      "then": {
        "title": "Pointer-scoped override",
        "type": "object",
        "properties": {
          "files": true,
          "rules": {
            "type": "object",
            "additionalProperties": { "$ref": "#/$defs/severity" }
          }
        },
        "required": ["rules"],
        "additionalProperties": false
      },

      "else": {
        "title": "File-scoped override",
        "allOf": [
          {
            "type": "object",
            "required": ["files"],
            "properties": {
              "files": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "pattern": "[^#]"
                }
              }
            }
          },
          {
            "type": "object",
            "properties": {
              "files": true,
              "formats": { "$ref": "#/$defs/formats" },
              "extends": { "$ref": "#/$defs/extends" },
              "rules": {
                "type": "object",
                "additionalProperties": { "$ref": "#/$defs/rule" }
              },
              "parserOptions": { "$ref": "#/$defs/parserOptions" },
              "aliases": { "$ref": "#/$defs/aliases" }
            },
            "anyOf": [
              { "required": ["extends"] },
              { "required": ["rules"] }
            ]
          }
        ]
      }
    }
  }
}
