{
  "$defs": {
    "Algorithm": {
      "enum": [
        "sha256",
        "sha512"
      ],
      "type": "string"
    },
    "Comparator": {
      "oneOf": [
        {
          "const": "lexical",
          "description": "Rust `str` `Ord` — byte-wise over the UTF-8.",
          "type": "string"
        },
        {
          "const": "lexical-ci",
          "description": "ASCII-case-insensitive lexical.",
          "type": "string"
        },
        {
          "const": "numeric",
          "description": "Leading-integer order; entries without a leading integer fall back to `lexical` so a mixed block degrades predictably rather than panicking.",
          "type": "string"
        }
      ]
    },
    "Expect": {
      "enum": [
        "any",
        "file",
        "dir"
      ],
      "type": "string"
    },
    "FilesFrom": {
      "oneOf": [
        {
          "const": "none",
          "description": "One violation for the whole invocation (default).",
          "type": "string"
        },
        {
          "const": "stdout",
          "description": "Parse the checker's stdout for the offender list.",
          "type": "string"
        },
        {
          "const": "stderr",
          "description": "Parse the checker's stderr for the offender list.",
          "type": "string"
        }
      ]
    },
    "Format": {
      "oneOf": [
        {
          "const": "contains",
          "description": "The digest must appear as a substring anywhere in `target`.",
          "type": "string"
        },
        {
          "const": "sums-line",
          "description": "`target` must carry a `sha256sum`-style `<hex> [*]<path>` line whose path token is the source's path.",
          "type": "string"
        }
      ]
    },
    "ImportLanguage": {
      "oneOf": [
        {
          "enum": [
            "go",
            "python",
            "rust",
            "js",
            "scala",
            "java",
            "dart",
            "nix"
          ],
          "type": "string"
        },
        {
          "const": "generic",
          "description": "No preset — an explicit `import_pattern` is required.",
          "type": "string"
        }
      ]
    },
    "Language": {
      "enum": [
        "auto",
        "rust",
        "typescript",
        "javascript",
        "python",
        "go",
        "java",
        "c",
        "cpp",
        "ruby",
        "shell"
      ],
      "type": "string"
    },
    "LinesOpts": {
      "additionalProperties": false,
      "properties": {
        "comment": {
          "default": "#",
          "description": "Lines starting with this (after trim) are skipped.",
          "type": "string"
        }
      },
      "type": "object"
    },
    "Normalize": {
      "oneOf": [
        {
          "const": "none",
          "description": "Exact byte equality.",
          "type": "string"
        },
        {
          "const": "trim",
          "description": "Trim leading/trailing whitespace of the whole output.",
          "type": "string"
        },
        {
          "const": "final-newline",
          "description": "Normalise only a single trailing newline (the most common generator/editor diff).",
          "type": "string"
        }
      ]
    },
    "NormalizeSpec": {
      "anyOf": [
        {
          "$ref": "#/$defs/NormalizeTransform"
        },
        {
          "items": {
            "$ref": "#/$defs/NormalizeTransform"
          },
          "type": "array"
        }
      ],
      "description": "`normalize:` accepts a single transform (`trim`) or an ordered list (`[trim, semver-minor]`), applied left-to-right. A scalar vs a sequence are structurally distinct, so an untagged enum decodes them unambiguously."
    },
    "NormalizeTransform": {
      "oneOf": [
        {
          "enum": [
            "none",
            "trim",
            "lower"
          ],
          "type": "string"
        },
        {
          "const": "semver-major",
          "description": "Compare only the leading `MAJOR` token (the dotnet/runtime SDK-band shape: same feature band, not exact patch).",
          "type": "string"
        },
        {
          "const": "semver-minor",
          "description": "Compare only the leading `MAJOR.MINOR` band — drops patch and pre-release and takes the leading digits of each token, so `4.36-dev`, `4.36.0`, `pnpm@11.3.0` (→ `11.3`) and `>=22.13` all reconcile to one band (the protobuf / pnpm version-format case the v0.12 study surfaced).",
          "type": "string"
        }
      ]
    },
    "OrphansSpec": {
      "additionalProperties": false,
      "description": "Enable the reverse-completeness check: on-disk artefacts under the `space` glob that no entry references (the \"new crate not wired into the workspace\" detector).",
      "properties": {
        "space": {
          "description": "Glob of on-disk artefacts that should each be referenced.",
          "type": "string"
        },
        "unreferenced": {
          "$ref": "#/$defs/Severity",
          "description": "Severity when an on-disk artefact is unreferenced: `warn` (default), `error`, or `off`."
        }
      },
      "required": [
        "space"
      ],
      "type": "object"
    },
    "OutputsSpec": {
      "anyOf": [
        {
          "description": "A single output glob.",
          "type": "string"
        },
        {
          "description": "A non-empty list of output globs.",
          "items": {
            "type": "string"
          },
          "minItems": 1,
          "type": "array"
        }
      ],
      "description": "`outputs:` accepts a single glob or a list (a `Scope`)."
    },
    "Relation": {
      "description": "The relation the source must hold to each target. `equals` is the 1:1 scalar case (the released `cross_file_value_equals`); the set relations compare extracted sets; `identical` compares whole files; `resolves` checks path existence on the filesystem.",
      "oneOf": [
        {
          "const": "equals",
          "description": "Source extracts exactly one value `v`; every target value must equal `v` (after normalize).",
          "type": "string"
        },
        {
          "const": "subset",
          "description": "`S ⊆ T` — every source value appears in the target (singleton `S` = membership).",
          "type": "string"
        },
        {
          "const": "superset",
          "description": "`S ⊇ T` — every target value appears in the source.",
          "type": "string"
        },
        {
          "const": "set_equals",
          "description": "`S == T` — the sets match exactly.",
          "type": "string"
        },
        {
          "const": "identical",
          "description": "Whole-file byte identity (optional `skip_header_lines`).",
          "type": "string"
        },
        {
          "const": "resolves",
          "description": "Each extracted source path must exist on disk (file or dir).",
          "type": "string"
        }
      ]
    },
    "RequireList": {
      "anyOf": [
        {
          "description": "A single basename glob.",
          "type": "string"
        },
        {
          "description": "A non-empty list of basename globs.",
          "items": {
            "type": "string"
          },
          "minItems": 1,
          "type": "array"
        }
      ]
    },
    "SelectSpec": {
      "anyOf": [
        {
          "description": "A single glob.",
          "type": "string"
        },
        {
          "description": "A non-empty list of globs (`!`-prefixed entries are excludes).",
          "items": {
            "type": "string"
          },
          "minItems": 1,
          "type": "array"
        }
      ],
      "description": "`select:` accepts a single glob (`\"src/*\"`) or a list with `!`-prefixed excludes (`[\"packages/*\", \"!packages/internal\"]`). A YAML string vs sequence are structurally distinct, so an untagged enum decodes them unambiguously. Shared by the select-family (`for_each_dir`, `for_each_file`, `every_matching_has`)."
    },
    "Severity": {
      "enum": [
        "warn",
        "error",
        "off"
      ],
      "type": "string"
    },
    "SourceSpec": {
      "additionalProperties": false,
      "description": "The file whose extracted value(s) form the reference side of the relation: a single `{ file, extract }`, or (set relations only) `{ files: <glob>, extract }` whose matches are unioned into one set.",
      "oneOf": [
        {
          "required": [
            "file"
          ]
        },
        {
          "required": [
            "files"
          ]
        }
      ],
      "properties": {
        "extract": {
          "anyOf": [
            {
              "$ref": "#/$defs/extract_spec"
            },
            {
              "type": "null"
            }
          ],
          "description": "The extraction to apply. Absent for `identical` (whole-file); required otherwise."
        },
        "file": {
          "default": null,
          "description": "A single source file.",
          "type": [
            "string",
            "null"
          ]
        },
        "files": {
          "default": null,
          "description": "A glob whose matches are read and whose extracted values are UNIONED into one set, for the set relations only (`subset` / `superset` / `set_equals`).",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "StyleName": {
      "enum": [
        "tabs",
        "spaces"
      ],
      "type": "string"
    },
    "TargetEntrySpec": {
      "additionalProperties": false,
      "properties": {
        "extract": {
          "anyOf": [
            {
              "$ref": "#/$defs/extract_spec"
            },
            {
              "type": "null"
            }
          ],
          "description": "The extraction to apply to this target (absent for `identical`)."
        },
        "file": {
          "description": "A single target file.",
          "type": "string"
        }
      },
      "required": [
        "file"
      ],
      "type": "object"
    },
    "TargetFormat": {
      "description": "The `format:` override values for `json_schema_passes`. Distinct from `structured_path::Format` (which has `xml`, not `yml`): this validator targets json/yaml/toml documents and accepts `yml` as a `yaml` alias.",
      "enum": [
        "json",
        "yaml",
        "yml",
        "toml"
      ],
      "type": "string"
    },
    "TargetName": {
      "enum": [
        "lf",
        "crlf"
      ],
      "type": "string"
    },
    "TargetsSpec": {
      "anyOf": [
        {
          "additionalProperties": false,
          "description": "Form a: one query applied per glob match.",
          "properties": {
            "extract": {
              "anyOf": [
                {
                  "$ref": "#/$defs/extract_spec"
                },
                {
                  "type": "null"
                }
              ]
            },
            "files": {
              "type": "string"
            }
          },
          "required": [
            "files"
          ],
          "type": "object"
        },
        {
          "description": "Form b: a sequence of heterogeneous `{ file, extract }` pins.",
          "items": {
            "$ref": "#/$defs/TargetEntrySpec"
          },
          "type": "array"
        }
      ],
      "description": "`targets:` is either a `{ files: <glob>, extract: … }` map (form a — one query applied per glob match) or a sequence of `{ file, extract }` (form b — heterogeneous pins). A YAML map vs a sequence are structurally distinct, so an untagged enum decodes them unambiguously. `extract` is absent for `identical`."
    },
    "WholeFileOpts": {
      "additionalProperties": false,
      "description": "`whole_file:` carries no options today (an empty `{}` map, like a marker); kept as a struct so options can be added without a breaking change.",
      "type": "object"
    },
    "case_convention": {
      "description": "Case convention. Aliases like PascalCase / pascal / pascal-case / UpperCamelCase all resolve to the same canonical form.",
      "enum": [
        "lower",
        "lowercase",
        "upper",
        "uppercase",
        "pascal",
        "pascalcase",
        "PascalCase",
        "UpperCamelCase",
        "upper_camel",
        "upper_camel_case",
        "camel",
        "camelcase",
        "camelCase",
        "lowerCamelCase",
        "lower_camel",
        "lower_camel_case",
        "snake",
        "snakecase",
        "snake_case",
        "kebab",
        "kebabcase",
        "kebab-case",
        "dash",
        "dashcase",
        "dash-case",
        "screaming-snake",
        "screamingsnake",
        "screamingsnakecase",
        "SCREAMING_SNAKE_CASE",
        "upper_snake",
        "upper_snake_case",
        "flat",
        "flatcase"
      ],
      "type": "string"
    },
    "extends_entry": {
      "description": "One extends entry. Bare string = classic form (url-only). Mapping form lets you filter with `only:` / `except:` (mutually exclusive).",
      "oneOf": [
        {
          "type": "string"
        },
        {
          "additionalProperties": false,
          "properties": {
            "except": {
              "description": "Drop these rule ids from the extended config.",
              "items": {
                "type": "string"
              },
              "minItems": 1,
              "type": "array"
            },
            "only": {
              "description": "Keep only these rule ids from the extended config; drop everything else.",
              "items": {
                "type": "string"
              },
              "minItems": 1,
              "type": "array"
            },
            "url": {
              "description": "Local path, `https://...#sha256-<hex>`, or `alint://bundled/<name>@<rev>`.",
              "type": "string"
            }
          },
          "required": [
            "url"
          ],
          "type": "object"
        }
      ]
    },
    "extract_spec": {
      "additionalProperties": false,
      "description": "Exactly one of: toml/json/yaml (RFC 9535 `JSONPath` string), lines (object; optional `comment` prefix, default `#`), regex (string; capture group 1 is the value), `whole_file` (object `{}`; the entire file content as one value, for byte-level `cross_file` comparison; the non-literal skip does not apply).",
      "maxProperties": 1,
      "minProperties": 1,
      "properties": {
        "json": {
          "default": null,
          "type": [
            "string",
            "null"
          ]
        },
        "lines": {
          "anyOf": [
            {
              "$ref": "#/$defs/LinesOpts"
            },
            {
              "type": "null"
            }
          ]
        },
        "regex": {
          "default": null,
          "type": [
            "string",
            "null"
          ]
        },
        "toml": {
          "default": null,
          "type": [
            "string",
            "null"
          ]
        },
        "whole_file": {
          "anyOf": [
            {
              "$ref": "#/$defs/WholeFileOpts"
            },
            {
              "type": "null"
            }
          ]
        },
        "yaml": {
          "default": null,
          "type": [
            "string",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "fact": {
      "description": "A single fact declaration. Each fact has an `id` plus exactly one kind-specific field.",
      "oneOf": [
        {
          "properties": {
            "any_file_exists": {
              "$ref": "#/$defs/string_or_string_array"
            }
          },
          "required": [
            "any_file_exists"
          ]
        },
        {
          "properties": {
            "all_files_exist": {
              "$ref": "#/$defs/string_or_string_array"
            }
          },
          "required": [
            "all_files_exist"
          ]
        },
        {
          "properties": {
            "count_files": {
              "type": "string"
            }
          },
          "required": [
            "count_files"
          ]
        },
        {
          "properties": {
            "file_content_matches": {
              "additionalProperties": false,
              "properties": {
                "paths": {
                  "$ref": "#/$defs/string_or_string_array"
                },
                "pattern": {
                  "description": "Rust-regex pattern matched against the content of every file in `paths`. The fact is true iff at least one file contains a match. Non-UTF-8 files are skipped.",
                  "type": "string"
                }
              },
              "required": [
                "paths",
                "pattern"
              ],
              "type": "object"
            }
          },
          "required": [
            "file_content_matches"
          ]
        },
        {
          "properties": {
            "git_branch": {
              "additionalProperties": false,
              "description": "Read `<repo>/.git/HEAD` and return the current branch name (string). Detached HEAD, no `.git/` directory, or any unrecognized layout resolves to an empty string (falsy under `when:`).",
              "properties": {},
              "type": "object"
            }
          },
          "required": [
            "git_branch"
          ]
        },
        {
          "properties": {
            "custom": {
              "additionalProperties": false,
              "properties": {
                "argv": {
                  "description": "Program + arguments, passed verbatim to the OS (no shell). Fact value is the process's stdout (trimmed). Non-zero exit, spawn failure, or non-UTF-8 output all resolve to an empty string. SECURITY: `custom:` is only valid in the user's top-level config — alint refuses to load an extended config that declares one.",
                  "items": {
                    "type": "string"
                  },
                  "minItems": 1,
                  "type": "array"
                }
              },
              "required": [
                "argv"
              ],
              "type": "object"
            }
          },
          "required": [
            "custom"
          ]
        }
      ],
      "properties": {
        "id": {
          "$ref": "#/$defs/rule_id"
        }
      },
      "required": [
        "id"
      ],
      "type": "object",
      "unevaluatedProperties": false
    },
    "fix": {
      "description": "Automatic-fix strategy applied by `alint fix`. Each rule kind accepts a specific op — alint errors at load time when the op and kind are incompatible.",
      "oneOf": [
        {
          "additionalProperties": false,
          "properties": {
            "file_create": {
              "additionalProperties": false,
              "oneOf": [
                {
                  "required": [
                    "content"
                  ]
                },
                {
                  "required": [
                    "content_from"
                  ]
                }
              ],
              "properties": {
                "content": {
                  "description": "Literal bytes to write. Mutually exclusive with `content_from`. Pass \"\" for an empty file.",
                  "type": "string"
                },
                "content_from": {
                  "description": "Path to a file whose bytes will be the content, relative to the lint root. Mutually exclusive with `content`. Read at fix-apply time; missing source produces a `Skipped` outcome rather than an error. Useful for LICENSE / NOTICE / boilerplate too long to inline.",
                  "type": "string"
                },
                "create_parents": {
                  "default": true,
                  "description": "Create intermediate directories if missing.",
                  "type": "boolean"
                },
                "path": {
                  "description": "Target path, relative to the repo root. When omitted, the first literal entry from the rule's `paths:` list is used.",
                  "type": "string"
                }
              },
              "type": "object"
            }
          },
          "required": [
            "file_create"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_remove": {
              "additionalProperties": false,
              "properties": {},
              "type": "object"
            }
          },
          "required": [
            "file_remove"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_prepend": {
              "additionalProperties": false,
              "oneOf": [
                {
                  "required": [
                    "content"
                  ]
                },
                {
                  "required": [
                    "content_from"
                  ]
                }
              ],
              "properties": {
                "content": {
                  "description": "Bytes to insert at the start of each violating file. Mutually exclusive with `content_from`. A trailing newline is the caller's responsibility.",
                  "type": "string"
                },
                "content_from": {
                  "description": "Path to a file whose bytes will be prepended, relative to the lint root. Mutually exclusive with `content`. Read at fix-apply time.",
                  "type": "string"
                }
              },
              "type": "object"
            }
          },
          "required": [
            "file_prepend"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_append": {
              "additionalProperties": false,
              "oneOf": [
                {
                  "required": [
                    "content"
                  ]
                },
                {
                  "required": [
                    "content_from"
                  ]
                }
              ],
              "properties": {
                "content": {
                  "description": "Bytes to append to each violating file. Mutually exclusive with `content_from`. A leading newline is the caller's responsibility.",
                  "type": "string"
                },
                "content_from": {
                  "description": "Path to a file whose bytes will be appended, relative to the lint root. Mutually exclusive with `content`. Read at fix-apply time.",
                  "type": "string"
                }
              },
              "type": "object"
            }
          },
          "required": [
            "file_append"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_rename": {
              "additionalProperties": false,
              "description": "Rename the violating file's stem to the parent rule's target convention (e.g. filename_case's `case:`). Extension is preserved; the file stays in the same directory. No parameters.",
              "properties": {},
              "type": "object"
            }
          },
          "required": [
            "file_rename"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_trim_trailing_whitespace": {
              "additionalProperties": false,
              "description": "Read the violating file, strip trailing space/tab on every line, write back. Preserves LF or CRLF endings. Skipped on files larger than `fix_size_limit`.",
              "properties": {},
              "type": "object"
            }
          },
          "required": [
            "file_trim_trailing_whitespace"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_append_final_newline": {
              "additionalProperties": false,
              "description": "Append a single `\\n` byte when the file has content but doesn't end with one. No-op on empty files.",
              "properties": {},
              "type": "object"
            }
          },
          "required": [
            "file_append_final_newline"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_normalize_line_endings": {
              "additionalProperties": false,
              "description": "Rewrite every line ending in the file to the parent rule's `target` (lf or crlf). Skipped on files larger than `fix_size_limit`.",
              "properties": {},
              "type": "object"
            }
          },
          "required": [
            "file_normalize_line_endings"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_strip_bidi": {
              "additionalProperties": false,
              "description": "Remove every Unicode bidi control character (U+202A–202E, U+2066–2069). Skipped on files larger than `fix_size_limit` or on non-UTF-8 content.",
              "properties": {},
              "type": "object"
            }
          },
          "required": [
            "file_strip_bidi"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_strip_zero_width": {
              "additionalProperties": false,
              "description": "Remove every zero-width character (U+200B/C/D, body-internal U+FEFF). A leading BOM is preserved — use `no_bom` to strip that.",
              "properties": {},
              "type": "object"
            }
          },
          "required": [
            "file_strip_zero_width"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_strip_bom": {
              "additionalProperties": false,
              "description": "Remove a leading UTF-8 / UTF-16 / UTF-32 BOM from the file. No-op when no BOM is present.",
              "properties": {},
              "type": "object"
            }
          },
          "required": [
            "file_strip_bom"
          ],
          "type": "object"
        },
        {
          "additionalProperties": false,
          "properties": {
            "file_collapse_blank_lines": {
              "additionalProperties": false,
              "description": "Collapse runs of blank lines longer than the parent rule's `max` down to exactly `max` blank lines. Preserves the file's line endings (LF vs CRLF). Skipped on files larger than `fix_size_limit` or on non-UTF-8 content.",
              "properties": {},
              "type": "object"
            }
          },
          "required": [
            "file_collapse_blank_lines"
          ],
          "type": "object"
        }
      ],
      "type": "object"
    },
    "git_tracked_only": {
      "default": false,
      "description": "When true, restrict the rule to files / directories tracked in git's index. Outside a git repo the rule becomes a silent no-op for absence-style checks. Currently supported on `file_exists`, `file_absent`, `dir_exists`, `dir_absent`; other rule kinds ignore the field. See https://alint.org/docs/concepts/walker-and-gitignore/ for the full semantics.",
      "type": "boolean"
    },
    "if_present": {
      "default": false,
      "description": "When true, a JSONPath query returning zero matches is silently OK — only real matches that fail the op produce violations. Use for predicates conditional on a field's presence (e.g. \"every `uses:` must be SHA-pinned\" where a workflow without `uses:` steps shouldn't be flagged).",
      "type": "boolean"
    },
    "level": {
      "description": "Severity. `off` disables a rule (useful when overriding inherited rules).",
      "enum": [
        "error",
        "warning",
        "info",
        "off"
      ],
      "type": "string"
    },
    "nested_rule": {
      "description": "A nested rule inside `require:`. Unlike top-level rules, `id` and `level` are synthesized from the parent; the user supplies only `kind` plus kind-specific options.",
      "properties": {
        "kind": {
          "type": "string"
        },
        "message": {
          "type": "string"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "policy_url": {
          "format": "uri",
          "type": "string"
        },
        "when": {
          "type": "string"
        }
      },
      "required": [
        "kind"
      ],
      "type": "object"
    },
    "paths_spec": {
      "description": "A single glob, an array of globs (with optional `!negation`), or an explicit include/exclude pair.",
      "oneOf": [
        {
          "type": "string"
        },
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "additionalProperties": false,
          "properties": {
            "exclude": {
              "$ref": "#/$defs/string_or_string_array"
            },
            "include": {
              "$ref": "#/$defs/string_or_string_array"
            }
          },
          "type": "object"
        }
      ]
    },
    "rule": {
      "description": "A rule entry — either a kind-driven rule (with `kind` and the kind-specific fields) or a template instance (with `extends_template` referencing a template id and optional `vars` overrides).",
      "oneOf": [
        {
          "allOf": [
            {
              "$ref": "#/$defs/rule_common"
            },
            {
              "$ref": "#/$defs/rule_kind_dispatch"
            }
          ],
          "unevaluatedProperties": false
        },
        {
          "$ref": "#/$defs/rule_template_instance"
        }
      ]
    },
    "rule_changeset_requires_path": {
      "description": "The `<since>...HEAD` diff must ADD (git status A) at least one path matching `add_glob:` — the 'did you add a changelog entry?' gate (prettier `changelog_unreleased/`, cpython `Misc/NEWS.d/next/`, pnpm `.changeset/*.md`). `since:` (the base ref) is required. Optional `when_changed:` gates the requirement on some other glob having changed (don't demand a changelog for a docs-only PR); with no gate, any non-empty changeset triggers it. Diff-scoped: silent no-op outside a git repo / when nothing relevant changed; a `since:` that fails to resolve hard-fails with a shallow-clone hint. Check-only.",
      "properties": {
        "add_glob": {
          "description": "Glob; the diff must ADD at least one path matching it.",
          "type": "string"
        },
        "kind": {
          "const": "changeset_requires_path"
        },
        "since": {
          "description": "Base ref for the `<since>...HEAD` diff. Use the canonical `{{env.X}}` interpolation, e.g. `since: \"{{env.ALINT_BASE_SHA | default('origin/main')}}\"`.",
          "type": "string"
        },
        "when_changed": {
          "default": null,
          "description": "Optional gate: only require the add when some path matching this glob changed (any status). Omit to require it on any non-empty changeset.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "add_glob",
        "since"
      ],
      "type": "object"
    },
    "rule_command": {
      "description": "Shell out to an external CLI per matched file. argv[0] is the program; remaining tokens support path-template substitution (`{path}`, `{dir}`, `{stem}`, `{ext}`, `{basename}`, `{parent_name}`). Working dir = repo root; stdin = /dev/null. Env: ALINT_PATH, ALINT_ROOT, ALINT_RULE_ID, ALINT_LEVEL, plus ALINT_VAR_<NAME> per `vars:` and ALINT_FACT_<NAME> per resolved fact. Verdict: exit 0 = pass; non-zero = one violation whose message is the (truncated) stdout+stderr. Trust-gated: only allowed in the user's top-level config (rejected at load-time when introduced via `extends:` or a bundled ruleset).",
      "properties": {
        "command": {
          "description": "Argv tokens. The first token is the program (looked up via PATH if it's a bare name); remaining tokens accept `{path}` and friends.",
          "items": {
            "type": "string"
          },
          "minItems": 1,
          "type": "array"
        },
        "kind": {
          "const": "command"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "timeout": {
          "default": null,
          "description": "Per-file timeout in seconds. Default 30. Past this, the child is killed and a violation reports the timeout.",
          "format": "uint64",
          "minimum": 1,
          "type": [
            "integer",
            "null"
          ]
        }
      },
      "required": [
        "paths",
        "command"
      ],
      "type": "object"
    },
    "rule_command_idempotent": {
      "description": "Run a user-declared formatter/checker in its --check (idempotence) mode once: exit 0 = the tree is formatter-clean (silent), non-zero = violation(s). alint never runs a mutating formatter and never writes the tree (the sibling of generated_file_fresh; same trust tier as the command rule — trust-gated to the user's own top-level config). With `files_from` (+ optional `files_pattern`) the tool's offender list is parsed into one violation per file. Single-shot, opt-in.",
      "properties": {
        "command": {
          "description": "Checker argv (no shell), run in its --check / idempotence mode; exit 0 = clean.",
          "items": {
            "type": "string"
          },
          "minItems": 1,
          "type": "array"
        },
        "files_from": {
          "$ref": "#/$defs/FilesFrom",
          "default": "none",
          "description": "On failure, parse this stream into per-file violations (default: none = one violation for the whole invocation)."
        },
        "files_pattern": {
          "default": null,
          "description": "Regex whose capture group 1 is a file path, applied per output line (requires `files_from`; omit for bare-path listers).",
          "type": [
            "string",
            "null"
          ]
        },
        "kind": {
          "const": "command_idempotent"
        },
        "timeout": {
          "default": null,
          "description": "Checker timeout in seconds (default 120). On timeout the child is killed and one violation is emitted.",
          "format": "uint64",
          "minimum": 1,
          "type": [
            "integer",
            "null"
          ]
        },
        "workdir": {
          "default": null,
          "description": "Checker cwd, relative to the lint root (default: lint root).",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "command"
      ],
      "type": "object"
    },
    "rule_commented_out_code": {
      "description": "Heuristic detector for blocks of commented-out source code (as opposed to prose comments, license headers, doc comments, or ASCII banners). For each consecutive run of comment lines (≥ `min_lines`), counts the fraction of non-whitespace characters that are structural punctuation strongly biased toward code (`( ) { } [ ] ; = < > & | ^`). Scores ≥ `threshold` (after a runs-of-5+-identical-chars defang to drop ASCII banners) fire as violations. Doc-comment blocks (`/// `, `/** … */`) are excluded; the file's first `skip_leading_lines` lines are skipped to pass over license headers without false-positive flagging. Severity defaults to `warning`; the rule explicitly does not ship at `error` because heuristics have non-zero FP rate and shouldn't gate commits on first adoption. Check-only — auto-removing commented-out code is destructive.",
      "properties": {
        "kind": {
          "const": "commented_out_code"
        },
        "language": {
          "$ref": "#/$defs/Language",
          "default": "auto",
          "description": "`auto` (default) infers the comment-marker set from each file's extension. Explicit override useful for embedded DSLs or cases where the extension lies."
        },
        "min_lines": {
          "default": 3,
          "description": "Minimum consecutive comment-line count for a block to be considered. 1-2 line comments are almost always prose; 3+ starts looking like dead code. Default 3.",
          "format": "uint",
          "minimum": 2,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "skip_leading_lines": {
          "default": 30,
          "description": "Skip blocks whose first line is at or before this line number. Default 30 - covers typical license headers without false-positive flagging them as commented-out code.",
          "format": "uint",
          "minimum": 0,
          "type": "integer"
        },
        "threshold": {
          "default": 0.5,
          "description": "Density floor for code-shapedness. Higher = stricter. Default 0.5 sits at the midpoint between obvious-prose (0.0) and obvious-code (1.0); lower it to widen the catch (more FPs), raise it to narrow.",
          "format": "double",
          "maximum": 1.0,
          "minimum": 0.0,
          "type": "number"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_common": {
      "properties": {
        "fix": {
          "$ref": "#/$defs/fix"
        },
        "id": {
          "$ref": "#/$defs/rule_id"
        },
        "kind": {
          "description": "The primitive rule kind. See docs/design/ARCHITECTURE.md §4.4 for the full catalog.",
          "type": "string"
        },
        "level": {
          "$ref": "#/$defs/level"
        },
        "message": {
          "description": "Override the default failure message. Supports `{{vars.*}}` and `{{ctx.*}}` substitution.",
          "type": "string"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "policy_url": {
          "description": "Link to a human-readable policy justification.",
          "format": "uri",
          "type": "string"
        },
        "scope_filter": {
          "$ref": "#/$defs/scope_filter"
        },
        "when": {
          "description": "Gate the rule on a fact-based expression (available from v0.2).",
          "type": "string"
        }
      },
      "required": [
        "id",
        "kind",
        "level"
      ],
      "type": "object"
    },
    "rule_cross_file": {
      "description": "A `source` file must hold a `relation` to one or more `targets` (or the filesystem). Value relations extract values: `equals` (default — every target value equals the single source value; the released `cross_file_value_equals`), `subset` / `superset` / `set_equals` (the source's extracted set vs each target's set). `identical` compares whole files byte-for-byte (no `extract`; optional `skip_header_lines`). `resolves` checks that each path the source extracts exists on disk (`source.extract`, no `targets` — the forward half of `registry_paths_resolve`). `targets` is a `{ files: <glob>, extract }` map or a `[{ file, extract }]` list. `normalize` relaxes value comparison. Non-literal values are skipped, not failed. `cross_file_value_equals` is a byte-compatible alias (relation defaults to equals). The per-relation shape (which of `source.extract` / `targets` is present) is validated at load.",
      "properties": {
        "allow_missing_target": {
          "default": false,
          "description": "When true, an absent target file or a missing extracted target value is tolerated instead of reported as drift.",
          "type": "boolean"
        },
        "kind": {
          "enum": [
            "cross_file",
            "cross_file_value_equals"
          ]
        },
        "normalize": {
          "$ref": "#/$defs/NormalizeSpec",
          "description": "A normalize transform, or an ordered list of transforms applied left-to-right (`[trim, semver-minor]`). `semver-major` / `semver-minor` keep only the leading MAJOR / MAJOR.MINOR band."
        },
        "relation": {
          "$ref": "#/$defs/Relation",
          "description": "The assertion checked between the source and each target: `equals` (default), `subset`, `superset`, `set_equals`, `identical` (whole file byte-for-byte), or `resolves` (each path the source extracts exists on disk)."
        },
        "skip_header_lines": {
          "default": null,
          "description": "For the `identical` relation only: drop this many leading lines from both files before comparison, to ignore a differing license or generated header.",
          "format": "uint",
          "minimum": 0,
          "type": [
            "integer",
            "null"
          ]
        },
        "source": {
          "$ref": "#/$defs/SourceSpec",
          "description": "The file whose extracted value(s) form the reference side of the relation: a single `{ file, extract }`, or (set relations only) `{ files: <glob>, extract }` whose matches are unioned into one set."
        },
        "targets": {
          "anyOf": [
            {
              "$ref": "#/$defs/TargetsSpec"
            },
            {
              "type": "null"
            }
          ],
          "description": "The file(s) compared against the source, one relation check per target. Absent for `resolves` (the target is the filesystem)."
        }
      },
      "required": [
        "source"
      ],
      "type": "object"
    },
    "rule_dir_absent": {
      "properties": {
        "git_tracked_only": {
          "default": false,
          "description": "Restrict matches to directories that contain at least one git-tracked file. No effect outside a git repo. Default `false`.",
          "type": "boolean"
        },
        "kind": {
          "const": "dir_absent"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "root_only": {
          "default": false,
          "description": "If true, only a directory directly at the repository root is forbidden; a nested match with the same name is allowed.",
          "type": "boolean"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_dir_contains": {
      "description": "Every directory matching `select` must have at least one direct child basename matching each glob in `require`. Sugar over for_each_dir + file_exists; use for_each_dir for deeper semantics.",
      "properties": {
        "kind": {
          "const": "dir_contains"
        },
        "require": {
          "$ref": "#/$defs/RequireList",
          "description": "Basename glob(s): every dir matching `select` must have at least one child matching each."
        },
        "select": {
          "description": "Glob selecting the directories to check.",
          "type": "string"
        }
      },
      "required": [
        "select",
        "require"
      ],
      "type": "object"
    },
    "rule_dir_exists": {
      "properties": {
        "git_tracked_only": {
          "default": false,
          "description": "Restrict matches to directories that contain at least one git-tracked file. No effect outside a git repo. Default `false`.",
          "type": "boolean"
        },
        "kind": {
          "const": "dir_exists"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "root_only": {
          "default": false,
          "description": "If true, only a directory directly at the repository root satisfies the rule; a nested match does not.",
          "type": "boolean"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_dir_only_contains": {
      "description": "Every direct child *file* of directories matching `select` must match at least one glob in `allow`. Subdirectories of the matched dir are not checked. Allow globs match the child's basename.",
      "properties": {
        "allow": {
          "description": "Basename glob(s) accepted as direct children. Anything else is a violation.",
          "oneOf": [
            {
              "type": "string"
            },
            {
              "items": {
                "type": "string"
              },
              "minItems": 1,
              "type": "array"
            }
          ]
        },
        "kind": {
          "const": "dir_only_contains"
        },
        "select": {
          "description": "Glob selecting the directories to enumerate.",
          "type": "string"
        }
      },
      "required": [
        "select",
        "allow"
      ],
      "type": "object"
    },
    "rule_every_matching_has": {
      "description": "Every file OR directory matching `select` must satisfy every nested rule in `require`. Sugar that combines for_each_file + for_each_dir into one rule. Optional `when_iter:` filters iterations.",
      "properties": {
        "kind": {
          "const": "every_matching_has"
        },
        "require": {
          "description": "One or more nested rules that every file or directory matching `select` must satisfy.",
          "items": {
            "$ref": "#/$defs/nested_rule"
          },
          "minItems": 1,
          "type": "array"
        },
        "select": {
          "$ref": "#/$defs/SelectSpec",
          "description": "Glob(s) selecting the files/dirs to iterate: a single glob, or a list with `!`-prefixed excludes (e.g. `[\"packages/*\", \"!packages/internal\"]`)."
        },
        "when_iter": {
          "default": null,
          "description": "Per-iteration `when:` filter (same semantics as `for_each_dir`'s `when_iter`).",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "select",
        "require"
      ],
      "type": "object"
    },
    "rule_executable_bit": {
      "description": "Assert that every file in scope either has the Unix +x bit set (`require: true`) or does not (`require: false`). No-op on non-Unix platforms. No fix op (chmod auto-apply deferred).",
      "properties": {
        "kind": {
          "const": "executable_bit"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "require": {
          "description": "`true` → +x must be set; `false` → +x must NOT be set.",
          "type": "boolean"
        }
      },
      "required": [
        "paths",
        "require"
      ],
      "type": "object"
    },
    "rule_executable_has_shebang": {
      "description": "Every file with the +x bit set must begin with a shebang (`#!`). Catches scripts that were marked executable but whose content is a plain text file or missing a shebang. No-op on non-Unix.",
      "properties": {
        "kind": {
          "const": "executable_has_shebang"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_file_absent": {
      "properties": {
        "content_prefix_hex": {
          "default": [],
          "description": "When non-empty, a file matching `paths` is reported only if its raw content begins with one of these byte signatures, each given as an even-length hex string (e.g. `\"00051607\"`). This separates genuine binary junk from unrelated files that merely share the name pattern: macOS `AppleDouble` sidecars start with `00 05 16 07` and `.DS_Store` with `00 00 00 01 \"Bud1\"`, whereas Hadoop writes `._<name>.crc` checksum files that begin with `crc\\0` and are not macOS junk. A file that cannot be read, or is shorter than every signature, does not match. Empty (the default) keeps the historical name-only behaviour.",
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        "git_tracked_only": {
          "default": false,
          "description": "Restrict matches to files tracked in git's index: entries present in the walked tree but not in `git ls-files` are skipped. No effect outside a git repo. Default `false`.",
          "type": "boolean"
        },
        "kind": {
          "const": "file_absent"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "root_only": {
          "default": false,
          "description": "If true, only a file matching `paths` directly at the repository root is forbidden; a nested match with the same name is allowed.",
          "type": "boolean"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_file_content_forbidden": {
      "properties": {
        "kind": {
          "enum": [
            "file_content_forbidden",
            "content_forbidden"
          ]
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "pattern": {
          "description": "Rust regex. File contents must NOT match.",
          "type": "string"
        }
      },
      "required": [
        "paths",
        "pattern"
      ],
      "type": "object"
    },
    "rule_file_content_matches": {
      "properties": {
        "kind": {
          "enum": [
            "file_content_matches",
            "content_matches"
          ]
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "pattern": {
          "description": "Rust regex. File contents must match.",
          "type": "string"
        }
      },
      "required": [
        "paths",
        "pattern"
      ],
      "type": "object"
    },
    "rule_file_ends_with": {
      "description": "Every file in scope must end with the given suffix (byte-level). For a simple trailing-newline check, prefer `final_newline`. Check-only; pair with `file_append` if you want auto-append.",
      "properties": {
        "kind": {
          "const": "file_ends_with"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "suffix": {
          "description": "Required suffix, matched byte-for-byte.",
          "minLength": 1,
          "type": "string"
        }
      },
      "required": [
        "paths",
        "suffix"
      ],
      "type": "object"
    },
    "rule_file_exists": {
      "properties": {
        "git_tracked_only": {
          "default": false,
          "description": "Restrict matches to files tracked in git's index: entries present in the walked tree but not in `git ls-files` are skipped. No effect outside a git repo. Default `false`.",
          "type": "boolean"
        },
        "kind": {
          "const": "file_exists"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "respect_gitignore": {
          "default": null,
          "description": "Per-rule override for the workspace `respect_gitignore` setting. When `false`, this rule's literal-path checks also stat the filesystem directly, so it sees files that are tracked AND `.gitignore`-masked (the bazel-style `.bazelversion` pattern — pitfall #18 in `docs/development/CONFIG-AUTHORING.md`). Honoured only by `file_exists` literal paths; glob patterns fall through to the workspace setting. Default: inherit the workspace `respect_gitignore`.",
          "type": [
            "boolean",
            "null"
          ]
        },
        "root_only": {
          "default": false,
          "description": "If true, only files directly at the repository root satisfy the rule.",
          "type": "boolean"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_file_footer": {
      "description": "Last `lines` lines of each file in scope must match `pattern` (Rust regex). Mirror of `file_header` anchored at the END of the file. Use for license footers, signed-off-by trailers, generated-file sentinels.",
      "properties": {
        "kind": {
          "enum": [
            "file_footer",
            "footer"
          ]
        },
        "lines": {
          "default": 20,
          "description": "Number of trailing lines to consider.",
          "format": "uint",
          "minimum": 1,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "pattern": {
          "description": "Rust regex. The last `lines` lines of each file must match.",
          "type": "string"
        }
      },
      "required": [
        "paths",
        "pattern"
      ],
      "type": "object"
    },
    "rule_file_graph": {
      "description": "Assemble the repo's file→file reference graph and assert a global structural property the 1-level cross-file kinds can't. `nodes` (glob) selects the graph's files. `edges` is one of `from_content` (extract one reference per match — the `extract` one-of: toml/json/yaml JSONPath, lines, or regex capture group 1 — and resolve it to a path via `resolve`: relative_to_file default, or relative_to_repo_root; bare module names, absolute paths, URLs, computed/interpolated references, and paths that escape the repo root are dropped, not mis-resolved) or `derive_target` (a `from`→`to` name template deriving the target from the node path). `require` is one of: `acyclic` (no dependency cycle among the nodes); `no_dangling` (every path-shaped edge resolves to an existing path — with `derive_target`, the derived sibling must exist); `no_orphans` (no node unreferenced by another, bare or `{ no_orphans: { roots: [...] } }`); `{ forbidden_edges: [{ from, to }] }` (the layering firewall — no edge whose source matches `from` and resolved target matches `to`); or `{ fresh: { hash, marker } }` (the `derive_target` output embeds the source's current content hash, captured by `marker`). Pure-parse, cross-file (no spawn).",
      "properties": {
        "edges": {
          "additionalProperties": false,
          "description": "Exactly one edge extractor: `from_content` (content references → the reference-graph modes) or `derive_target` (a name template → `fresh` codegen-freshness or `no_dangling` derived-sibling existence).",
          "oneOf": [
            {
              "required": [
                "from_content"
              ]
            },
            {
              "required": [
                "derive_target"
              ]
            }
          ],
          "properties": {
            "derive_target": {
              "additionalProperties": false,
              "description": "Derive the generated target's path from the source node's path: `from` is a regex matched against the node path, `to` is a replacement template using its captures (e.g. `to: $1.pb.go`).",
              "properties": {
                "from": {
                  "type": "string"
                },
                "to": {
                  "type": "string"
                }
              },
              "required": [
                "from",
                "to"
              ],
              "type": "object"
            },
            "from_content": {
              "additionalProperties": false,
              "properties": {
                "extract": {
                  "$ref": "#/$defs/extract_spec"
                },
                "resolve": {
                  "enum": [
                    "relative_to_file",
                    "relative_to_repo_root"
                  ]
                }
              },
              "required": [
                "extract"
              ],
              "type": "object"
            }
          },
          "type": "object"
        },
        "kind": {
          "const": "file_graph"
        },
        "nodes": {
          "description": "Glob selecting the graph's node files.",
          "type": "string"
        },
        "require": {
          "description": "A bare-string mode (`acyclic` | `no_dangling` | `no_orphans`), or a map for a configured mode (`{ forbidden_edges: [{from,to}] }`, `{ no_orphans: { roots: [...] } }`, or `{ fresh: { hash, marker } }`). `no_dangling` = every path-shaped edge resolves to an existing path (with `edges.derive_target`, each node's derived sibling must exist — e.g. every `X-LICENSE.txt` needs an `X-NOTICE.txt`); `no_orphans` = no node is unreferenced except those matching a `roots` glob; `fresh` (needs `edges.derive_target`) = the derived output carries the source's current `hash` digest, captured by `marker` (group 1).",
          "oneOf": [
            {
              "enum": [
                "acyclic",
                "no_dangling",
                "no_orphans"
              ]
            },
            {
              "additionalProperties": false,
              "properties": {
                "forbidden_edges": {
                  "items": {
                    "additionalProperties": false,
                    "properties": {
                      "from": {
                        "type": "string"
                      },
                      "to": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "from",
                      "to"
                    ],
                    "type": "object"
                  },
                  "minItems": 1,
                  "type": "array"
                }
              },
              "required": [
                "forbidden_edges"
              ],
              "type": "object"
            },
            {
              "additionalProperties": false,
              "properties": {
                "no_orphans": {
                  "additionalProperties": false,
                  "properties": {
                    "roots": {
                      "items": {
                        "type": "string"
                      },
                      "type": "array"
                    }
                  },
                  "type": "object"
                }
              },
              "required": [
                "no_orphans"
              ],
              "type": "object"
            },
            {
              "additionalProperties": false,
              "properties": {
                "fresh": {
                  "additionalProperties": false,
                  "properties": {
                    "hash": {
                      "enum": [
                        "sha256",
                        "sha512"
                      ]
                    },
                    "marker": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "marker"
                  ],
                  "type": "object"
                }
              },
              "required": [
                "fresh"
              ],
              "type": "object"
            }
          ]
        }
      },
      "required": [
        "nodes",
        "edges",
        "require"
      ],
      "type": "object"
    },
    "rule_file_hash": {
      "description": "Each file in scope must have content whose SHA-256 equals the declared `sha256`. Accepts a bare 64-char hex string or a `sha256:`-prefixed form.",
      "properties": {
        "kind": {
          "const": "file_hash"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "sha256": {
          "description": "Expected SHA-256 in lowercase hex (64 chars). Accepting uppercase and the `sha256:` prefix keeps the field forgiving.",
          "pattern": "^(sha256:)?[0-9a-fA-F]{64}$",
          "type": "string"
        }
      },
      "required": [
        "paths",
        "sha256"
      ],
      "type": "object"
    },
    "rule_file_header": {
      "properties": {
        "kind": {
          "enum": [
            "file_header",
            "header"
          ]
        },
        "lines": {
          "default": 20,
          "description": "Number of leading lines to consider.",
          "format": "uint",
          "minimum": 1,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "pattern": {
          "description": "Rust regex. The first `lines` lines of each file in scope must match.",
          "type": "string"
        }
      },
      "required": [
        "paths",
        "pattern"
      ],
      "type": "object"
    },
    "rule_file_is_ascii": {
      "description": "Every byte in every file in scope must be < 0x80 (pure ASCII), except codepoints listed in `allow:`. Stricter than file_is_text; check-only — auto-replacement for non-ASCII bytes would silently lose meaning. With `allow:` the file is decoded as UTF-8 and checked per character; without it, the strict byte-level fast path is used.",
      "properties": {
        "allow": {
          "default": [],
          "description": "Permitted non-ASCII codepoints - each a single character (e.g. \"o-umlaut\"), a `U+XXXX` codepoint, or a `U+XXXX-U+YYYY` inclusive range.",
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        "kind": {
          "const": "file_is_ascii"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_file_is_text": {
      "properties": {
        "kind": {
          "enum": [
            "file_is_text",
            "is_text"
          ]
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_file_max_lines": {
      "description": "Files in scope must have at most `max_lines` lines, with the same `wc -l`-style accounting as `file_min_lines`. Use to catch the everything-module anti-pattern.",
      "properties": {
        "kind": {
          "enum": [
            "file_max_lines",
            "max_lines"
          ]
        },
        "max_lines": {
          "description": "Maximum allowed line count.",
          "format": "uint64",
          "minimum": 0,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "max_lines"
      ],
      "type": "object"
    },
    "rule_file_max_size": {
      "properties": {
        "kind": {
          "enum": [
            "file_max_size",
            "max_size"
          ]
        },
        "max_bytes": {
          "description": "Maximum allowed file size in bytes.",
          "format": "uint64",
          "minimum": 0,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "max_bytes"
      ],
      "type": "object"
    },
    "rule_file_min_lines": {
      "description": "Files in scope must have at least `min_lines` lines (where a line is `\\n`-terminated, with an unterminated trailing segment counting as one more, matching `wc -l` semantics). Common use: require README / CHANGELOG / SECURITY.md to be more than a title plus one sentence.",
      "properties": {
        "kind": {
          "enum": [
            "file_min_lines",
            "min_lines"
          ]
        },
        "min_lines": {
          "description": "Minimum allowed line count.",
          "format": "uint64",
          "minimum": 0,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "min_lines"
      ],
      "type": "object"
    },
    "rule_file_min_size": {
      "description": "Files in scope must be at least `min_bytes` bytes. Catches placeholder / stub files that pass existence checks but contain no useful content (e.g. a 20-byte README).",
      "properties": {
        "kind": {
          "enum": [
            "file_min_size",
            "min_size"
          ]
        },
        "min_bytes": {
          "description": "Minimum allowed file size in bytes.",
          "format": "uint64",
          "minimum": 0,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "min_bytes"
      ],
      "type": "object"
    },
    "rule_file_shebang": {
      "description": "First line of each file in scope must match the `shebang` regex. Pairs with `executable_has_shebang` (presence) and `shebang_has_executable` (the inverse) to enforce shebang shape, e.g. require `^#!/usr/bin/env bash$`.",
      "properties": {
        "kind": {
          "enum": [
            "file_shebang",
            "shebang"
          ]
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "shebang": {
          "default": "^#!",
          "description": "Rust regex; the first line of every matched file must match. Default `^#!` only enforces shebang presence.",
          "type": "string"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_file_starts_with": {
      "description": "Every file in scope must begin with the given prefix (byte-level). Useful for SPDX headers, generated-file sentinels, magic bytes. Check-only; pair with `file_prepend` if you want auto-prepend (prevents silent duplication on near-matches).",
      "properties": {
        "kind": {
          "const": "file_starts_with"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "prefix": {
          "description": "Required prefix, matched byte-for-byte.",
          "minLength": 1,
          "type": "string"
        }
      },
      "required": [
        "paths",
        "prefix"
      ],
      "type": "object"
    },
    "rule_filename_case": {
      "properties": {
        "case": {
          "$ref": "#/$defs/case_convention"
        },
        "kind": {
          "const": "filename_case"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "case"
      ],
      "type": "object"
    },
    "rule_filename_regex": {
      "properties": {
        "kind": {
          "const": "filename_regex"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "pattern": {
          "description": "Rust regex, automatically anchored with ^...$ by the engine.",
          "type": "string"
        },
        "stem": {
          "default": false,
          "description": "Match the file stem (no extension) instead of the full basename.",
          "type": "boolean"
        }
      },
      "required": [
        "paths",
        "pattern"
      ],
      "type": "object"
    },
    "rule_final_newline": {
      "description": "Every non-empty file in scope must end with a newline byte. Empty files are considered fine.",
      "properties": {
        "kind": {
          "const": "final_newline"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_for_each_dir": {
      "description": "For every directory matching `select`, every rule in `require` must pass. Path-template tokens in the nested rules are pre-substituted per iteration; use `{path}` to refer to the iterated directory itself. Optional `when_iter:` filters which iterations are evaluated, e.g. `iter.has_file(\"Cargo.toml\")` to scope to workspace members.",
      "properties": {
        "kind": {
          "const": "for_each_dir"
        },
        "require": {
          "description": "Nested rules evaluated against each matched directory.",
          "items": {
            "$ref": "#/$defs/nested_rule"
          },
          "minItems": 1,
          "type": "array"
        },
        "select": {
          "description": "Glob(s) selecting the directories to iterate — a single glob, or a list with `!`-prefixed excludes (e.g. [\"src/*\", \"!src/internal\"]).",
          "oneOf": [
            {
              "type": "string"
            },
            {
              "items": {
                "type": "string"
              },
              "minItems": 1,
              "type": "array"
            }
          ]
        },
        "when_iter": {
          "description": "Per-iteration `when:` filter — evaluated against `iter.*` in the iterated entry's context. Iterations whose verdict is false are skipped before any nested rule is built. Examples: `iter.has_file(\"Cargo.toml\")`, `iter.basename matches \"^pkg-\"`.",
          "type": "string"
        }
      },
      "required": [
        "select",
        "require"
      ],
      "type": "object"
    },
    "rule_for_each_file": {
      "description": "For every file matching `select`, every rule in `require` must pass. Path-template tokens in the nested rules are pre-substituted per iteration. Optional `when_iter:` filters iterations.",
      "properties": {
        "kind": {
          "const": "for_each_file"
        },
        "require": {
          "description": "Nested rules evaluated against each matched file.",
          "items": {
            "$ref": "#/$defs/nested_rule"
          },
          "minItems": 1,
          "type": "array"
        },
        "select": {
          "description": "Glob(s) selecting the files to iterate — a single glob, or a list with `!`-prefixed excludes.",
          "oneOf": [
            {
              "type": "string"
            },
            {
              "items": {
                "type": "string"
              },
              "minItems": 1,
              "type": "array"
            }
          ]
        },
        "when_iter": {
          "description": "Per-iteration `when:` filter — see rule_for_each_dir.when_iter. `iter.has_file(...)` always evaluates to false on file iteration; useful predicates here include `iter.basename`, `iter.ext`, `iter.parent_name`.",
          "type": "string"
        }
      },
      "required": [
        "select",
        "require"
      ],
      "type": "object"
    },
    "rule_for_each_match": {
      "description": "For each line matching `select` (a regex), the line must satisfy the nested `require:` predicates: `matches` (the line matches all listed regexes), `forbid` (matches none), `equal` (the listed named `select` captures are all equal — checked on every `select` match on the line). The in-file line quantifier (the dual of `ordered_block`'s `select:`): one violation per offending line. Closes per-line changelog grammars ('every entry line must ALSO match Q1..Qn', which `file_content_matches` cannot express — its match is existence, not a per-line conjunction) and intra-line capture equality (RE2 has no backreference). Per-file.",
      "properties": {
        "kind": {
          "const": "for_each_match"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "require": {
          "additionalProperties": false,
          "description": "Predicates applied to each selected line; at least one of `matches` / `forbid` / `equal` is required.",
          "properties": {
            "equal": {
              "description": "These named `select` captures must all be equal (the only way to assert intra-line capture equality, since RE2 has no backreference).",
              "items": {
                "type": "string"
              },
              "minItems": 2,
              "type": "array"
            },
            "forbid": {
              "description": "The selected line must match NONE of these regexes.",
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            "matches": {
              "description": "The selected line must match ALL of these regexes.",
              "items": {
                "type": "string"
              },
              "type": "array"
            }
          },
          "type": "object"
        },
        "select": {
          "description": "Regex; a line is a checked element iff it matches. Named captures are available to `require.equal`.",
          "type": "string"
        }
      },
      "required": [
        "paths",
        "select",
        "require"
      ],
      "type": "object"
    },
    "rule_generated_file_fresh": {
      "description": "A committed artefact must equal what a declared `command` generator produces. Two modes, selected by shape (exactly one of `file` / `outputs` required): STDOUT mode (`file`) compares one committed file to the generator's stdout — non-mutating, never writes the tree. MUTATING / in-place mode (`outputs`, a glob or list of globs) runs an in-place generator and asserts the regenerated files are unchanged: alint snapshots `outputs`, runs the generator, diffs, reports any stale / new / removed file, and RESTORES the snapshot — so `alint check` stays pure (no regenerated files left behind); the alint-native form of `make gen && git diff --exit-code`. Either mode runs a process, so the kind is trust-gated to the user's own top-level config (same tier as the `command` rule). Single-shot, opt-in.",
      "oneOf": [
        {
          "required": [
            "file"
          ]
        },
        {
          "required": [
            "outputs"
          ]
        }
      ],
      "properties": {
        "command": {
          "description": "Generator argv (no shell). STDOUT mode: emit the file's contents to stdout. MUTATING mode: write the `outputs` in place.",
          "items": {
            "type": "string"
          },
          "minItems": 1,
          "type": "array"
        },
        "file": {
          "default": null,
          "description": "STDOUT mode: the committed generated file to verify against the generator's stdout.",
          "type": [
            "string",
            "null"
          ]
        },
        "kind": {
          "const": "generated_file_fresh"
        },
        "normalize": {
          "$ref": "#/$defs/Normalize",
          "description": "Normalization applied before comparison to absorb trailing-newline churn: `none`, `trim`, or `final-newline`."
        },
        "outputs": {
          "anyOf": [
            {
              "$ref": "#/$defs/OutputsSpec"
            },
            {
              "type": "null"
            }
          ],
          "description": "MUTATING mode: the glob (or list of globs) the in-place generator rewrites; its presence selects the mutating mode. alint snapshots these, runs the generator, diffs, and restores them."
        },
        "timeout": {
          "default": null,
          "description": "Generator timeout in seconds (default 120). On timeout the child is killed and one violation is emitted.",
          "format": "uint64",
          "minimum": 1,
          "type": [
            "integer",
            "null"
          ]
        },
        "workdir": {
          "default": null,
          "description": "Generator cwd, relative to the lint root (default: lint root).",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "command"
      ],
      "type": "object"
    },
    "rule_git_blame_age": {
      "description": "Fire on lines matching a regex whose `git blame` author-time is older than `max_age_days`. Closes the gap between `level: warning` on every TODO (too noisy) and `level: off` (accepts unbounded debt accumulation). Use cases: stale TODO / FIXME / XXX / HACK markers, abandoned `@deprecated` JSDoc tags, model-attributed TODOs that nobody followed up on. Outside a git repo, on untracked files, or when blame fails for any other reason, the rule silently no-ops per file. Heuristic notes: formatting passes (cargo fmt, prettier) reset blame age unless the formatting commit is listed in `.git-blame-ignore-revs`; vendored / imported code carries the import commit's timestamp; squash-merged PRs collapse to a single date. Check-only — auto-removing matched lines is destructive.",
      "properties": {
        "kind": {
          "const": "git_blame_age"
        },
        "max_age_days": {
          "description": "Minimum line age (in days) for a matching line to fire as a violation. Lines younger than this pass silently. Common values: 90 (one quarter), 180 (half a year), 365 (a full year of debt).",
          "format": "uint64",
          "minimum": 1,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "pattern": {
          "description": "Rust-regex pattern applied to each blame line's content. Capture group 1 (when present) is exposed as `{{ctx.match}}` in the message template; without a capture group, `{{ctx.match}}` substitutes the full match.",
          "type": "string"
        }
      },
      "required": [
        "paths",
        "pattern",
        "max_age_days"
      ],
      "type": "object"
    },
    "rule_git_commit_author_allowlist": {
      "anyOf": [
        {
          "required": [
            "email_pattern"
          ]
        },
        {
          "required": [
            "name_pattern"
          ]
        }
      ],
      "description": "Assert every commit author in scope matches an allowed email and/or name pattern. At least one of `email_pattern:` / `name_pattern:` is required; specifying both means BOTH must match (AND). A commit whose author fails any specified pattern fires one violation. With `since:` unset, only HEAD is checked; with `since:` set to a ref, every commit in `<since>..HEAD` is checked. Demand: enterprise repos enforcing contributor identity against a corporate domain; OSS projects catching sock-puppet / compromised-account commits. Outside a git repo, with no commits, or when `git` is unavailable, silently no-ops. A `since:` ref that fails to resolve hard-fails with a shallow-clone hint.",
      "properties": {
        "email_pattern": {
          "default": null,
          "description": "Rust-regex the author email (`git log %ae`) must match, e.g. `^.+@example\\.com$`.",
          "type": [
            "string",
            "null"
          ]
        },
        "include_merges": {
          "default": false,
          "description": "When validating a range (`since:` set), include merge commits. Has no effect when `since:` is unset; combining `include_merges: true` with no `since:` is a load-time error.",
          "type": "boolean"
        },
        "kind": {
          "const": "git_commit_author_allowlist"
        },
        "name_pattern": {
          "default": null,
          "description": "Rust-regex the author name (`git log %an`) must match.",
          "type": [
            "string",
            "null"
          ]
        },
        "since": {
          "default": null,
          "description": "Git ref to use as the base of the commit range. When set, validates every commit in `<since>..HEAD` instead of just HEAD. Accepts anything `git rev-parse` does. Use the canonical `{{env.X}}` interpolation to pass a SHA via an env var, e.g. `since: \"{{env.ALINT_BASE_SHA | default('origin/main')}}\"`.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "rule_git_commit_gpg_signed": {
      "description": "Assert every commit in scope has a verifying signature (`git verify-commit` exits 0). A commit that is unsigned, or signed with a key that doesn't verify against the local keyring, fires one violation. With `since:` unset, only HEAD is checked; with `since:` set to a ref, every commit in `<since>..HEAD` is checked. Demand: kernel maintainers, security-sensitive OSS, GitHub 'Require signed commits' branch protection. Reflects git's own verdict — does NOT distinguish 'unsigned' from 'signed with an untrusted key' (trust is git's GPG config / `.git/allowed_signers`). Outside a git repo, with no commits, or when `git` is unavailable, silently no-ops. A `since:` ref that fails to resolve hard-fails with a shallow-clone hint.",
      "properties": {
        "include_merges": {
          "default": false,
          "description": "When validating a range (`since:` set), include merge commits. Has no effect when `since:` is unset; combining `include_merges: true` with no `since:` is a load-time error.",
          "type": "boolean"
        },
        "kind": {
          "const": "git_commit_gpg_signed"
        },
        "since": {
          "default": null,
          "description": "Git ref to use as the base of the commit range. When set, validates every commit in `<since>..HEAD` instead of just HEAD. Accepts anything `git rev-parse` does. Use the canonical `{{env.X}}` interpolation to pass a SHA via an env var, e.g. `since: \"{{env.ALINT_BASE_SHA | default('origin/main')}}\"`.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "rule_git_commit_message": {
      "anyOf": [
        {
          "required": [
            "pattern"
          ]
        },
        {
          "required": [
            "subject_max_length"
          ]
        },
        {
          "required": [
            "requires_body"
          ]
        }
      ],
      "description": "Validate one or more commit messages against shape rules: regex, max subject length, body required. With `since:` unset, only HEAD is checked. With `since:` set to a ref, every commit in `<since>..HEAD` is checked (oldest first, merge commits excluded by default); right shape for PR-trigger workflows where HEAD is a synthetic `actions/checkout` merge commit. At least one of `pattern:` / `subject_max_length:` / `requires_body: true` must be set. Outside a git repo, with no commits, or when `git` is unavailable, silently no-ops. A `since:` ref that fails to resolve hard-fails with a shallow-clone hint.",
      "properties": {
        "include_merges": {
          "default": false,
          "description": "When validating a range (`since:` set), include merge commits. Defaults to `false` because merge commits in PR contexts are typically the synthetic merge `actions/checkout` produces (with an auto-generated subject the rule would always flag) or maintainer-resolved merges from the base branch. Has no effect when `since:` is unset; combining `include_merges: true` with no `since:` is a load-time error.",
          "type": "boolean"
        },
        "kind": {
          "const": "git_commit_message"
        },
        "pattern": {
          "default": null,
          "description": "Rust-regex pattern the full message (subject + body, joined with newlines) must match. Use `(?s)` to make `.` match newlines.",
          "type": [
            "string",
            "null"
          ]
        },
        "requires_body": {
          "default": false,
          "description": "When true, the message must have a non-empty body, that is, at least one line of content after the subject's blank-line separator.",
          "type": "boolean"
        },
        "since": {
          "default": null,
          "description": "Git ref to use as the base of the commit range. When set, validates every commit in `<since>..HEAD` instead of just HEAD. Accepts anything `git rev-parse` does: SHA (full or abbreviated), branch (`origin/main`), tag (`v1.2.3`), or relative ref (`HEAD~5`). Supports POSIX `${VAR}` and `${VAR:-default}` env-var interpolation so CI can pass a SHA via an env var (e.g. `since: ${ALINT_BASE_SHA:-origin/main}` with `ALINT_BASE_SHA` exported in a workflow step from `github.event.pull_request.base.sha`). The GitHub Actions double-brace template syntax `${{ ... }}` is NOT interpolated by alint.",
          "type": [
            "string",
            "null"
          ]
        },
        "subject_max_length": {
          "default": null,
          "description": "Maximum number of characters allowed in the subject line. Common values: 50 (Tim Pope's recommendation), 72 (GitHub PR-title cutoff).",
          "format": "uint",
          "minimum": 1,
          "type": [
            "integer",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "rule_git_commit_no_fixup": {
      "description": "Fail on residual `fixup!` / `squash!` / `amend!` commits in scope — the ones `git commit --fixup`/`--squash` produce, meant to be collapsed by `git rebase --autosquash` before merging. With `since:` unset, only HEAD is checked; with `since:` set to a ref, every commit in `<since>..HEAD` is checked. No configuration knobs — the matched prefixes are exactly what `--autosquash` understands. Outside a git repo, with no commits, or when `git` is unavailable, silently no-ops. A `since:` ref that fails to resolve hard-fails with a shallow-clone hint.",
      "properties": {
        "include_merges": {
          "default": false,
          "description": "When validating a range (`since:` set), include merge commits. Has no effect when `since:` is unset; combining `include_merges: true` with no `since:` is a load-time error.",
          "type": "boolean"
        },
        "kind": {
          "const": "git_commit_no_fixup"
        },
        "since": {
          "default": null,
          "description": "Git ref to use as the base of the commit range. When set, validates every commit in `<since>..HEAD` instead of just HEAD. Accepts anything `git rev-parse` does. Use the canonical `{{env.X}}` interpolation to pass a SHA via an env var, e.g. `since: \"{{env.ALINT_BASE_SHA | default('origin/main')}}\"`.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "rule_git_commit_signed_off": {
      "description": "Assert every commit in scope carries a DCO `Signed-off-by:` trailer. With `since:` unset, only HEAD is checked; with `since:` set to a ref, every commit in `<since>..HEAD` is checked (oldest first, merge commits excluded by default) — the right shape for PR-trigger workflows. Required by CNCF / Linux Foundation / kernel-style projects. Outside a git repo, with no commits, or when `git` is unavailable, silently no-ops. A `since:` ref that fails to resolve hard-fails with a shallow-clone hint.",
      "properties": {
        "include_merges": {
          "default": false,
          "description": "When validating a range (`since:` set), include merge commits. Has no effect when `since:` is unset; combining `include_merges: true` with no `since:` is a load-time error.",
          "type": "boolean"
        },
        "kind": {
          "const": "git_commit_signed_off"
        },
        "pattern": {
          "default": null,
          "description": "Trailer pattern each commit message must contain. Defaults to the canonical DCO sign-off shape `(?m)^Signed-off-by: .+ <.+@.+>$`. Override to enforce a stricter form (e.g. a corporate-domain email).",
          "type": [
            "string",
            "null"
          ]
        },
        "since": {
          "default": null,
          "description": "Git ref to use as the base of the commit range. When set, validates every commit in `<since>..HEAD` instead of just HEAD. Accepts anything `git rev-parse` does. Use the canonical `{{env.X}}` interpolation to pass a SHA via an env var, e.g. `since: \"{{env.ALINT_BASE_SHA | default('origin/main')}}\"`.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "rule_git_commit_subject_matches": {
      "description": "Each commit's subject line (the first line of its message) must match the `matches:` regex — the subject-grammar member of the commit family (e.g. `pkg/path: lowercase summary`, conventional-commit types). Anchored to the subject alone, unlike `git_commit_message`'s whole-message `pattern:`; use `git_commit_message`'s `subject_max_length:` for a length cap. With `since:` unset, only HEAD is checked; with `since:` set, every commit in `<since>..HEAD`. Outside a git repo / with no commits / when git is unavailable, silently no-ops; a bad `since:` ref hard-fails with a shallow-clone hint.",
      "properties": {
        "include_merges": {
          "default": false,
          "description": "When validating a range (`since:` set), include merge commits. Has no effect when `since:` is unset; combining `include_merges: true` with no `since:` is a load-time error.",
          "type": "boolean"
        },
        "kind": {
          "const": "git_commit_subject_matches"
        },
        "matches": {
          "description": "Rust-regex the commit subject (first line of the message) must match.",
          "type": "string"
        },
        "since": {
          "default": null,
          "description": "Git ref to use as the base of the commit range. When set, validates every commit in `<since>..HEAD` instead of just HEAD. Use the canonical `{{env.X}}` interpolation to pass a SHA via an env var, e.g. `since: \"{{env.ALINT_BASE_SHA | default('origin/main')}}\"`.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "matches"
      ],
      "type": "object"
    },
    "rule_git_no_denied_paths": {
      "description": "Fire when any tracked path matches a configured glob denylist. Companion of `git_tracked_only` for the absence axis: catches secrets, large binaries, or accidentally-tracked artifacts that would otherwise need a `file_absent` per pattern. With `since:` set, only paths changed in the `<since>...HEAD` diff are flagged (PR-scoped). Outside a git repo, silently no-ops; a `since:` ref that fails to resolve hard-fails with a shallow-clone hint.",
      "properties": {
        "denied": {
          "description": "Globset patterns no tracked path may match. Both whole-path patterns (`secrets/**`) and basename-only patterns (`*.env`) work.",
          "items": {
            "type": "string"
          },
          "minItems": 1,
          "type": "array"
        },
        "kind": {
          "const": "git_no_denied_paths"
        },
        "since": {
          "default": null,
          "description": "Optional git ref. When set, only denied paths that changed in the `<since>...HEAD` diff are flagged, catches a secret added in a PR even if HEAD's tree still tracks an older one. Accepts the `{{env.X}}` interpolation, e.g. `since: \"{{env.ALINT_BASE_SHA | default('origin/main')}}\"`.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "denied"
      ],
      "type": "object"
    },
    "rule_id": {
      "description": "Unique kebab-case identifier for the rule.",
      "pattern": "^[a-z][a-z0-9_-]*$",
      "type": "string"
    },
    "rule_import_gate": {
      "description": "Forbid imports whose extracted target matches `forbid` (a regex), within the `paths` scope — an architectural import firewall. Matches the EXTRACTED import target (not the raw line), the precise low-false-positive specialisation of `file_content_forbidden`. `language` (go/python/rust/js/scala/java/dart/nix) selects a built-in import-line pattern; `import_pattern` overrides it (capture group 1 = target; required for `generic`). `allow` globs exempt files inside the scope. Per-file.",
      "properties": {
        "allow": {
          "default": [],
          "description": "File globs inside the scope that are exempt from the gate.",
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        "forbid": {
          "description": "Regex tested against the extracted import target.",
          "type": "string"
        },
        "import_pattern": {
          "default": null,
          "description": "Explicit import-line regex (capture group 1 = target). Overrides the `language` preset.",
          "type": [
            "string",
            "null"
          ]
        },
        "kind": {
          "const": "import_gate"
        },
        "language": {
          "anyOf": [
            {
              "$ref": "#/$defs/ImportLanguage"
            },
            {
              "type": "null"
            }
          ],
          "description": "Built-in import-line pattern preset (capture group 1 = the imported target). Omit to require an explicit `import_pattern`."
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "forbid"
      ],
      "type": "object"
    },
    "rule_indent_style": {
      "description": "Every non-blank line must indent with the configured style. `tabs` rejects any leading space; `spaces` rejects any leading tab and optionally requires the leading-space count to be a multiple of `width`. Check-only — tab-width-aware reindentation is deferred.",
      "properties": {
        "kind": {
          "const": "indent_style"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "style": {
          "$ref": "#/$defs/StyleName",
          "description": "Required indentation style: `tabs` rejects any leading space; `spaces` rejects any leading tab."
        },
        "width": {
          "default": null,
          "description": "When `style: spaces`, the leading-space count on every non-blank line must be a multiple of this. Ignored for `style: tabs`.",
          "format": "uint32",
          "minimum": 1,
          "type": [
            "integer",
            "null"
          ]
        }
      },
      "required": [
        "paths",
        "style"
      ],
      "type": "object"
    },
    "rule_json_path_equals": {
      "description": "Value at a JSONPath (RFC 9535) expression in every JSON file in `paths` must deep-equal `equals`. Multiple matches all must match; zero matches is a violation (value missing) unless `if_present: true`. Typical use: `$.license == \"MIT\"` on every `packages/*/package.json`.",
      "properties": {
        "equals": {
          "description": "Expected value. Any JSON type (string, number, boolean, null, array, object)."
        },
        "if_present": {
          "default": false,
          "description": "When true, a query returning zero matches is silently OK - only real matches that fail the op produce violations.",
          "type": "boolean"
        },
        "kind": {
          "const": "json_path_equals"
        },
        "path": {
          "description": "`JSONPath` expression rooted at `$`. Supports dot-access (`$.foo.bar`), array index (`$.deps[0]`), wildcards (`$.deps[*]`), filters, and every other RFC 9535 construct.",
          "type": "string"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "path",
        "equals"
      ],
      "type": "object"
    },
    "rule_json_path_matches": {
      "description": "Value at a JSONPath in every JSON file in `paths` must be a string and must match `matches` (a Rust regex). Non-string matches produce a clear 'not a string' violation.",
      "properties": {
        "if_present": {
          "default": false,
          "description": "When true, a query returning zero matches is silently OK - only real matches that fail the op produce violations.",
          "type": "boolean"
        },
        "kind": {
          "const": "json_path_matches"
        },
        "matches": {
          "description": "Rust-regex pattern to match against the value at `path`.",
          "type": "string"
        },
        "path": {
          "description": "`JSONPath` expression rooted at `$`.",
          "type": "string"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "path",
        "matches"
      ],
      "type": "object"
    },
    "rule_json_schema_passes": {
      "description": "Validate every JSON / YAML / TOML file in `paths` against the JSON Schema at `schema_path`. Each schema-violation error becomes one violation; a target that fails to parse produces one parse-error violation. Format is detected from the target's extension; pass `format:` to override.",
      "properties": {
        "format": {
          "anyOf": [
            {
              "$ref": "#/$defs/TargetFormat"
            },
            {
              "type": "null"
            }
          ],
          "description": "Override the auto-detected target format. When omitted, format is inferred from each target file's extension (.json / .yaml / .yml / .toml)."
        },
        "kind": {
          "const": "json_schema_passes"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "schema_path": {
          "description": "Path to a JSON Schema file relative to the lint root. The schema must itself be JSON even when validating YAML / TOML targets.",
          "type": "string"
        }
      },
      "required": [
        "paths",
        "schema_path"
      ],
      "type": "object"
    },
    "rule_kind_dispatch": {
      "description": "Dispatch on the `kind` discriminator. Each branch sets the required kind-specific fields.",
      "oneOf": [
        {
          "$ref": "#/$defs/rule_file_exists"
        },
        {
          "$ref": "#/$defs/rule_file_absent"
        },
        {
          "$ref": "#/$defs/rule_dir_exists"
        },
        {
          "$ref": "#/$defs/rule_dir_absent"
        },
        {
          "$ref": "#/$defs/rule_file_content_matches"
        },
        {
          "$ref": "#/$defs/rule_file_content_forbidden"
        },
        {
          "$ref": "#/$defs/rule_file_header"
        },
        {
          "$ref": "#/$defs/rule_file_max_size"
        },
        {
          "$ref": "#/$defs/rule_file_min_size"
        },
        {
          "$ref": "#/$defs/rule_file_min_lines"
        },
        {
          "$ref": "#/$defs/rule_file_max_lines"
        },
        {
          "$ref": "#/$defs/rule_file_footer"
        },
        {
          "$ref": "#/$defs/rule_file_shebang"
        },
        {
          "$ref": "#/$defs/rule_file_is_text"
        },
        {
          "$ref": "#/$defs/rule_json_path_equals"
        },
        {
          "$ref": "#/$defs/rule_json_path_matches"
        },
        {
          "$ref": "#/$defs/rule_yaml_path_equals"
        },
        {
          "$ref": "#/$defs/rule_yaml_path_matches"
        },
        {
          "$ref": "#/$defs/rule_toml_path_equals"
        },
        {
          "$ref": "#/$defs/rule_toml_path_matches"
        },
        {
          "$ref": "#/$defs/rule_xml_path_equals"
        },
        {
          "$ref": "#/$defs/rule_xml_path_matches"
        },
        {
          "$ref": "#/$defs/rule_json_schema_passes"
        },
        {
          "$ref": "#/$defs/rule_commented_out_code"
        },
        {
          "$ref": "#/$defs/rule_markdown_paths_resolve"
        },
        {
          "$ref": "#/$defs/rule_git_no_denied_paths"
        },
        {
          "$ref": "#/$defs/rule_git_commit_message"
        },
        {
          "$ref": "#/$defs/rule_git_commit_signed_off"
        },
        {
          "$ref": "#/$defs/rule_git_commit_subject_matches"
        },
        {
          "$ref": "#/$defs/rule_git_commit_no_fixup"
        },
        {
          "$ref": "#/$defs/rule_git_commit_author_allowlist"
        },
        {
          "$ref": "#/$defs/rule_git_commit_gpg_signed"
        },
        {
          "$ref": "#/$defs/rule_git_blame_age"
        },
        {
          "$ref": "#/$defs/rule_changeset_requires_path"
        },
        {
          "$ref": "#/$defs/rule_pair_changed_together"
        },
        {
          "$ref": "#/$defs/rule_filename_case"
        },
        {
          "$ref": "#/$defs/rule_filename_regex"
        },
        {
          "$ref": "#/$defs/rule_pair"
        },
        {
          "$ref": "#/$defs/rule_pair_hash"
        },
        {
          "$ref": "#/$defs/rule_registry_paths_resolve"
        },
        {
          "$ref": "#/$defs/rule_cross_file"
        },
        {
          "$ref": "#/$defs/rule_file_graph"
        },
        {
          "$ref": "#/$defs/rule_ordered_block"
        },
        {
          "$ref": "#/$defs/rule_for_each_match"
        },
        {
          "$ref": "#/$defs/rule_generated_file_fresh"
        },
        {
          "$ref": "#/$defs/rule_import_gate"
        },
        {
          "$ref": "#/$defs/rule_command_idempotent"
        },
        {
          "$ref": "#/$defs/rule_for_each_dir"
        },
        {
          "$ref": "#/$defs/rule_for_each_file"
        },
        {
          "$ref": "#/$defs/rule_dir_only_contains"
        },
        {
          "$ref": "#/$defs/rule_unique_by"
        },
        {
          "$ref": "#/$defs/rule_dir_contains"
        },
        {
          "$ref": "#/$defs/rule_every_matching_has"
        },
        {
          "$ref": "#/$defs/rule_no_trailing_whitespace"
        },
        {
          "$ref": "#/$defs/rule_final_newline"
        },
        {
          "$ref": "#/$defs/rule_line_endings"
        },
        {
          "$ref": "#/$defs/rule_line_max_width"
        },
        {
          "$ref": "#/$defs/rule_no_merge_conflict_markers"
        },
        {
          "$ref": "#/$defs/rule_no_bidi_controls"
        },
        {
          "$ref": "#/$defs/rule_no_zero_width_chars"
        },
        {
          "$ref": "#/$defs/rule_file_is_ascii"
        },
        {
          "$ref": "#/$defs/rule_no_bom"
        },
        {
          "$ref": "#/$defs/rule_file_hash"
        },
        {
          "$ref": "#/$defs/rule_max_directory_depth"
        },
        {
          "$ref": "#/$defs/rule_max_files_per_directory"
        },
        {
          "$ref": "#/$defs/rule_no_empty_files"
        },
        {
          "$ref": "#/$defs/rule_no_case_conflicts"
        },
        {
          "$ref": "#/$defs/rule_no_illegal_windows_names"
        },
        {
          "$ref": "#/$defs/rule_no_symlinks"
        },
        {
          "$ref": "#/$defs/rule_executable_bit"
        },
        {
          "$ref": "#/$defs/rule_executable_has_shebang"
        },
        {
          "$ref": "#/$defs/rule_shebang_has_executable"
        },
        {
          "$ref": "#/$defs/rule_no_submodules"
        },
        {
          "$ref": "#/$defs/rule_indent_style"
        },
        {
          "$ref": "#/$defs/rule_max_consecutive_blank_lines"
        },
        {
          "$ref": "#/$defs/rule_file_starts_with"
        },
        {
          "$ref": "#/$defs/rule_file_ends_with"
        },
        {
          "$ref": "#/$defs/rule_command"
        }
      ]
    },
    "rule_line_endings": {
      "description": "Every line ending in every file in scope must use the `target` style (`lf` or `crlf`). Mixed endings within a file also fail.",
      "properties": {
        "kind": {
          "const": "line_endings"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "target": {
          "$ref": "#/$defs/TargetName",
          "description": "Required line ending style: `lf` or `crlf`. Mixed endings within a file also fail."
        }
      },
      "required": [
        "paths",
        "target"
      ],
      "type": "object"
    },
    "rule_line_max_width": {
      "description": "No line in any file in scope exceeds `max_width` Unicode scalar values (chars). Display width is NOT computed — use a formatter for that. Check-only; truncation is not a safe auto-fix.",
      "properties": {
        "kind": {
          "const": "line_max_width"
        },
        "max_width": {
          "description": "Maximum number of Unicode scalar values (chars) allowed per line.",
          "format": "uint",
          "minimum": 1,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "max_width"
      ],
      "type": "object"
    },
    "rule_markdown_paths_resolve": {
      "description": "Validate that backticked workspace paths in markdown files resolve to real files or directories in the repo. Targets the AGENTS.md / CLAUDE.md / .cursorrules staleness problem: agent-context files reference paths in inline backticks, and those paths drift as the codebase evolves. Skips fenced and 4-space-indented code blocks (those contain code samples, not factual claims about the tree). Required `prefixes` field marks which path-shapes to validate (e.g. `[\"src/\", \"crates/\", \"docs/\"]`); backticked tokens not starting with a configured prefix are ignored, eliminating the FP class of `\\`npm\\`` / `\\`function\\`` / etc.",
      "properties": {
        "ignore_template_vars": {
          "default": true,
          "description": "When true (default), skip backticked tokens containing `{{ ... }}`, `${ ... }`, or `<...>` template-variable markers. These are placeholders, not real paths.",
          "type": "boolean"
        },
        "kind": {
          "const": "markdown_paths_resolve"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "prefixes": {
          "description": "Whitelist of path-shape prefixes to validate. A backticked token must start with one of these to be considered a path candidate. No defaults — every project's layout differs and the user must declare which prefixes mark a path.",
          "items": {
            "type": "string"
          },
          "minItems": 1,
          "type": "array"
        }
      },
      "required": [
        "paths",
        "prefixes"
      ],
      "type": "object"
    },
    "rule_max_consecutive_blank_lines": {
      "description": "Cap the number of blank lines that may appear in a row. A blank line is empty or only whitespace. Fixable via `file_collapse_blank_lines`, which collapses over-long runs to exactly `max` blank lines.",
      "properties": {
        "kind": {
          "const": "max_consecutive_blank_lines"
        },
        "max": {
          "description": "Maximum number of blank lines allowed in a row. `0` means no blank lines at all.",
          "format": "uint32",
          "minimum": 0,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "max"
      ],
      "type": "object"
    },
    "rule_max_directory_depth": {
      "description": "Every path in scope must sit at depth ≤ `max_depth` (number of `/`-separated components). Check-only; file relocation is a human decision.",
      "properties": {
        "kind": {
          "const": "max_directory_depth"
        },
        "max_depth": {
          "description": "Maximum allowed path depth (number of `/`-separated components).",
          "format": "uint",
          "minimum": 1,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "max_depth"
      ],
      "type": "object"
    },
    "rule_max_files_per_directory": {
      "description": "Every directory containing at least one in-scope file must hold ≤ `max_files` in-scope files (immediate children, non-recursive). Check-only.",
      "properties": {
        "kind": {
          "const": "max_files_per_directory"
        },
        "max_files": {
          "description": "Maximum number of in-scope files allowed as immediate children of any one directory (non-recursive).",
          "format": "uint",
          "minimum": 1,
          "type": "integer"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "max_files"
      ],
      "type": "object"
    },
    "rule_no_bidi_controls": {
      "description": "Flag Unicode bidirectional control characters (U+202A–202E, U+2066–2069) anywhere in a file. Trojan Source (CVE-2021-42574) defense. Fixable via `file_strip_bidi`.",
      "properties": {
        "kind": {
          "const": "no_bidi_controls"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_no_bom": {
      "description": "Flag files that start with a UTF-8 / UTF-16 / UTF-32 BOM. Fixable via `file_strip_bom`.",
      "properties": {
        "kind": {
          "const": "no_bom"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_no_case_conflicts": {
      "description": "Flag pairs of paths that differ only by case (e.g. README.md + readme.md). These can't coexist on macOS HFS+/APFS or Windows NTFS.",
      "properties": {
        "kind": {
          "const": "no_case_conflicts"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_no_empty_files": {
      "description": "Flag zero-byte files. Fixable via `file_remove`, which deletes the empty file.",
      "properties": {
        "kind": {
          "const": "no_empty_files"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_no_illegal_windows_names": {
      "description": "Reject path components Windows can't represent: reserved device names (CON/PRN/AUX/NUL/COM1-9/LPT1-9 regardless of extension), trailing dots or spaces, and the forbidden chars <>:\"|?*.",
      "properties": {
        "kind": {
          "const": "no_illegal_windows_names"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_no_merge_conflict_markers": {
      "description": "Flag files that still contain git conflict markers at the start of a line (`<<<<<<< `, `=======`, `>>>>>>> `, `||||||| `). Check-only; conflict resolution requires human judgment.",
      "properties": {
        "kind": {
          "const": "no_merge_conflict_markers"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_no_submodules": {
      "description": "Flag the presence of `.gitmodules` at the repo root. Always targets `.gitmodules` — no `paths` field accepted. Fixable via `file_remove`.",
      "not": {
        "required": [
          "paths"
        ]
      },
      "properties": {
        "kind": {
          "const": "no_submodules"
        }
      },
      "required": [
        "kind"
      ],
      "type": "object"
    },
    "rule_no_symlinks": {
      "description": "Flag tracked paths that are symbolic links. Symlinks are a portability headache across Windows/macOS/Linux and CI runners. Fixable via `file_remove`, which deletes the symlink.",
      "properties": {
        "kind": {
          "const": "no_symlinks"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_no_trailing_whitespace": {
      "description": "No line in any file in scope ends with a space or tab. Non-UTF-8 files are skipped silently.",
      "properties": {
        "kind": {
          "const": "no_trailing_whitespace"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_no_zero_width_chars": {
      "description": "Flag zero-width characters (U+200B/C/D, plus body-internal U+FEFF). A leading U+FEFF (BOM) is not flagged here — use `no_bom` for that. Fixable via `file_strip_zero_width`.",
      "properties": {
        "kind": {
          "const": "no_zero_width_chars"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_ordered_block": {
      "description": "The lines between a `start` / `end` marker pair must stay sorted (and, with `unique`, free of duplicates) under `comparator`. Both markers are optional: omit `end` to sort from `start` to EOF, omit both to sort the whole file (the markerless 'this file is one sorted list' form). The generic form of per-project keep-sorted scripts. Per-file: markers match the trimmed line; blank lines inside a block are ignored.",
      "properties": {
        "comparator": {
          "$ref": "#/$defs/Comparator",
          "default": "lexical",
          "description": "Comparator used to order entries: lexical (default), lexical-ci, or numeric."
        },
        "end": {
          "default": null,
          "description": "Marker line closing a block. Optional - omit to run the block to EOF.",
          "type": [
            "string",
            "null"
          ]
        },
        "kind": {
          "const": "ordered_block"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        },
        "select": {
          "default": null,
          "description": "Regex; when set, only lines inside a block matching it are sortable entries (others, such as comments or group headers, pass through). The sectioned / keep-sorted-subset shape.",
          "type": [
            "string",
            "null"
          ]
        },
        "start": {
          "default": null,
          "description": "Marker line opening a block (matched on the trimmed line). Optional - omit to anchor the block at the start of the file.",
          "type": [
            "string",
            "null"
          ]
        },
        "unique": {
          "default": false,
          "description": "When true, also forbid duplicate (equal) entries within a block.",
          "type": "boolean"
        }
      },
      "type": "object"
    },
    "rule_pair": {
      "description": "For every file matching `primary`, a file matching the `partner` template must exist. Template tokens: {dir}, {stem}, {ext}, {basename}, {path}, {parent_name}.",
      "properties": {
        "kind": {
          "const": "pair"
        },
        "partner": {
          "description": "Path template resolved per primary match. Example: \"{dir}/{stem}.h\".",
          "type": "string"
        },
        "primary": {
          "description": "Glob selecting the primary files.",
          "type": "string"
        }
      },
      "required": [
        "primary",
        "partner"
      ],
      "type": "object"
    },
    "rule_pair_changed_together": {
      "description": "If the `<since>...HEAD` diff changes any path matching `if_changed:`, at least one path matching `then_changed:` must change in the same range — the co-change gate (a FORMAT_VERSION must bump when the format struct changes; version.txt and the lockfile change together). Both globs and `since:` (the base ref) are required. Directional: a `then_changed`-only change never triggers it (add a second rule with the globs swapped for a bidirectional pact). Diff-scoped: silent no-op outside a git repo / when `if_changed` didn't change; a `since:` that fails to resolve hard-fails with a shallow-clone hint. Check-only.",
      "properties": {
        "if_changed": {
          "description": "Glob; the trigger. When the diff changes a path matching this, a `then_changed` co-change is required.",
          "type": "string"
        },
        "kind": {
          "const": "pair_changed_together"
        },
        "since": {
          "description": "Base ref for the `<since>...HEAD` diff. Use the canonical `{{env.X}}` interpolation, e.g. `since: \"{{env.ALINT_BASE_SHA | default('origin/main')}}\"`.",
          "type": "string"
        },
        "then_changed": {
          "description": "Glob; the obligation. At least one changed path must match it whenever `if_changed` fired.",
          "type": "string"
        }
      },
      "required": [
        "if_changed",
        "then_changed",
        "since"
      ],
      "type": "object"
    },
    "rule_pair_hash": {
      "description": "Cross-file: the `algorithm` digest of every file matching `source` must appear in the single `target` file — either as an embedded hex substring (`format: contains`, the default) or a coreutils / go-`.sum`-style `<hex> <path>` manifest line (`format: sums-line`). Detection-only (alint never rewrites the manifest). A missing `target` is one violation; one violation per source whose digest is absent or mismatched. golang/go FIPS `fips140.sum` is the canonical use.",
      "properties": {
        "algorithm": {
          "$ref": "#/$defs/Algorithm",
          "default": "sha256",
          "description": "Digest algorithm (default: sha256)."
        },
        "format": {
          "$ref": "#/$defs/Format",
          "default": "contains",
          "description": "How the digest must appear in `target`: `contains` = hex substring anywhere (default); `sums-line` = a `<hex> [*]<path>` line whose path token is the source's path."
        },
        "kind": {
          "const": "pair_hash"
        },
        "source": {
          "description": "Literal path or glob selecting the file(s) whose content is hashed (one check per match).",
          "type": "string"
        },
        "target": {
          "description": "The single file that must carry the digest (a `.sum` / `SHA256SUMS` / a file with an embedded hash).",
          "type": "string"
        }
      },
      "required": [
        "source",
        "target"
      ],
      "type": "object"
    },
    "rule_registry_paths_resolve": {
      "description": "A manifest enumerates path entries; each must resolve to an on-disk artefact. `extract` selects how the entry list is pulled (structured-query toml/json/yaml, a line list, or regex capture group 1). Optional `orphans` adds the reverse check: on-disk artefacts under `space` that no entry references. Non-literal entries (interpolation / antiquotation) are skipped, not failed.",
      "properties": {
        "base": {
          "default": null,
          "description": "Resolve entries relative to: `registry_dir` (default), `lint_root`, or an explicit path.",
          "type": [
            "string",
            "null"
          ]
        },
        "entries_are_globs": {
          "default": false,
          "description": "Expand each extracted entry as a glob rather than treating it as a single literal path; a glob that matches nothing is a violation.",
          "type": "boolean"
        },
        "exclude_query": {
          "default": null,
          "description": "A structured query selecting entries to subtract from the extracted list before resolution is checked.",
          "type": [
            "string",
            "null"
          ]
        },
        "expect": {
          "$ref": "#/$defs/Expect",
          "description": "Constrain the kind each entry must resolve to on disk: `any` (default), `file`, or `dir`."
        },
        "extract": {
          "$ref": "#/$defs/extract_spec",
          "description": "Exactly one of: toml/json/yaml (RFC 9535 JSONPath string), lines (object; optional `comment` prefix, default `#`), regex (string; capture group 1 is the path).",
          "not": {
            "required": [
              "whole_file"
            ]
          }
        },
        "kind": {
          "const": "registry_paths_resolve"
        },
        "must_contain": {
          "default": null,
          "description": "When an entry resolves to a directory, that directory must contain this named child (e.g. `Cargo.toml`), else the entry is a violation.",
          "type": [
            "string",
            "null"
          ]
        },
        "orphans": {
          "anyOf": [
            {
              "$ref": "#/$defs/OrphansSpec"
            },
            {
              "type": "null"
            }
          ],
          "description": "Enable the reverse-completeness check (see `OrphansSpec`)."
        },
        "source": {
          "description": "The manifest/registry file (path, or a glob to run once per matching manifest) that enumerates the path entries.",
          "type": "string"
        }
      },
      "required": [
        "source",
        "extract"
      ],
      "type": "object"
    },
    "rule_shebang_has_executable": {
      "description": "Every file that starts with `#!` must have the Unix +x bit set. The inverse of `executable_has_shebang`. No-op on non-Unix. No fix op (chmod auto-apply deferred).",
      "properties": {
        "kind": {
          "const": "shebang_has_executable"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths"
      ],
      "type": "object"
    },
    "rule_template_instance": {
      "description": "A rule entry that expands a `templates:` body. The instance's `id:` is required; `vars:` provides the substitutions for the template's `{{vars.<name>}}` placeholders. Any other field overrides the corresponding field in the expanded template.",
      "properties": {
        "extends_template": {
          "description": "Id of an entry in the top-level `templates:` block to instantiate.",
          "type": "string"
        },
        "id": {
          "$ref": "#/$defs/rule_id"
        },
        "level": {
          "$ref": "#/$defs/level"
        },
        "message": {
          "description": "Override the default failure message. Supports `{{vars.*}}` and `{{ctx.*}}` substitution.",
          "type": "string"
        },
        "policy_url": {
          "description": "Link to a human-readable policy justification.",
          "format": "uri",
          "type": "string"
        },
        "vars": {
          "additionalProperties": {
            "type": [
              "string",
              "number",
              "boolean"
            ]
          },
          "description": "Map of `name → value` to substitute into `{{vars.<name>}}` placeholders in the template body.",
          "type": "object"
        },
        "when": {
          "description": "Gate the rule on a fact-based expression (available from v0.2).",
          "type": "string"
        }
      },
      "required": [
        "id",
        "extends_template"
      ],
      "type": "object"
    },
    "rule_toml_path_equals": {
      "description": "TOML sibling of `json_path_equals`. Typical use: enforce `[package].edition == \"2024\"` on every `Cargo.toml`.",
      "properties": {
        "equals": {
          "description": "Expected value. Any JSON type (string, number, boolean, null, array, object)."
        },
        "if_present": {
          "default": false,
          "description": "When true, a query returning zero matches is silently OK - only real matches that fail the op produce violations.",
          "type": "boolean"
        },
        "kind": {
          "const": "toml_path_equals"
        },
        "path": {
          "description": "`JSONPath` expression rooted at `$`. Supports dot-access (`$.foo.bar`), array index (`$.deps[0]`), wildcards (`$.deps[*]`), filters, and every other RFC 9535 construct.",
          "type": "string"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "path",
        "equals"
      ],
      "type": "object"
    },
    "rule_toml_path_matches": {
      "description": "TOML sibling of `json_path_matches`.",
      "properties": {
        "if_present": {
          "default": false,
          "description": "When true, a query returning zero matches is silently OK - only real matches that fail the op produce violations.",
          "type": "boolean"
        },
        "kind": {
          "const": "toml_path_matches"
        },
        "matches": {
          "description": "Rust-regex pattern to match against the value at `path`.",
          "type": "string"
        },
        "path": {
          "description": "`JSONPath` expression rooted at `$`.",
          "type": "string"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "path",
        "matches"
      ],
      "type": "object"
    },
    "rule_unique_by": {
      "description": "Flag any group of files (matching `select`) sharing the same rendered `key`. One violation per collision group, anchored on the lexicographically-first file. Path-template tokens ({path}, {dir}, {basename}, {stem}, {ext}, {parent_name}) may appear in `key`. With `case_insensitive: true` the key is folded to lowercase before grouping, catching collisions that only occur on a case-insensitive filesystem (Windows / macOS).",
      "properties": {
        "case_insensitive": {
          "default": false,
          "description": "Fold the key to lowercase before grouping, so keys that collide only under case-folding count as duplicates — the case-insensitive-filesystem hazard (Windows / macOS).",
          "type": "boolean"
        },
        "key": {
          "default": "{basename}",
          "description": "Path-template producing a key per matched file. Default: {basename}.",
          "type": "string"
        },
        "kind": {
          "const": "unique_by"
        },
        "select": {
          "description": "Glob selecting the files to deduplicate.",
          "type": "string"
        }
      },
      "required": [
        "select"
      ],
      "type": "object"
    },
    "rule_xml_path_equals": {
      "description": "XML sibling of `json_path_equals`. XML maps to the same value tree via the xmltodict-style convention: attributes are `@name` keys, repeated child elements become an array, a leaf element collapses to its text string, namespaces flatten to local names, and every leaf value is a string (quote your `equals:`, e.g. `equals: \"4.0.0\"`). Typical use: enforce `$.Project.PropertyGroup.TargetFramework == \"net8.0\"` on every `.csproj`, or a Maven `$.project.parent.version` across `pom.xml`.",
      "properties": {
        "equals": {
          "description": "Expected value. Any JSON type (string, number, boolean, null, array, object)."
        },
        "if_present": {
          "default": false,
          "description": "When true, a query returning zero matches is silently OK - only real matches that fail the op produce violations.",
          "type": "boolean"
        },
        "kind": {
          "const": "xml_path_equals"
        },
        "path": {
          "description": "`JSONPath` expression rooted at `$`. Supports dot-access (`$.foo.bar`), array index (`$.deps[0]`), wildcards (`$.deps[*]`), filters, and every other RFC 9535 construct.",
          "type": "string"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "path",
        "equals"
      ],
      "type": "object"
    },
    "rule_xml_path_matches": {
      "description": "XML sibling of `json_path_matches` (same xmltodict-style mapping as `xml_path_equals`; all XML leaf values are strings). Typical use: `$.Project.ItemGroup.PackageReference[*]['@Version']` matches a version pattern across `.csproj`.",
      "properties": {
        "if_present": {
          "default": false,
          "description": "When true, a query returning zero matches is silently OK - only real matches that fail the op produce violations.",
          "type": "boolean"
        },
        "kind": {
          "const": "xml_path_matches"
        },
        "matches": {
          "description": "Rust-regex pattern to match against the value at `path`.",
          "type": "string"
        },
        "path": {
          "description": "`JSONPath` expression rooted at `$`.",
          "type": "string"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "path",
        "matches"
      ],
      "type": "object"
    },
    "rule_yaml_path_equals": {
      "description": "YAML sibling of `json_path_equals`. Parses the file as YAML before applying the JSONPath query. Use this on GitHub Actions workflows, docker-compose.yml, kubernetes manifests, `.gitlab-ci.yml`, etc.",
      "properties": {
        "equals": {
          "description": "Expected value. Any JSON type (string, number, boolean, null, array, object)."
        },
        "if_present": {
          "default": false,
          "description": "When true, a query returning zero matches is silently OK - only real matches that fail the op produce violations.",
          "type": "boolean"
        },
        "kind": {
          "const": "yaml_path_equals"
        },
        "path": {
          "description": "`JSONPath` expression rooted at `$`. Supports dot-access (`$.foo.bar`), array index (`$.deps[0]`), wildcards (`$.deps[*]`), filters, and every other RFC 9535 construct.",
          "type": "string"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "path",
        "equals"
      ],
      "type": "object"
    },
    "rule_yaml_path_matches": {
      "description": "YAML sibling of `json_path_matches`.",
      "properties": {
        "if_present": {
          "default": false,
          "description": "When true, a query returning zero matches is silently OK - only real matches that fail the op produce violations.",
          "type": "boolean"
        },
        "kind": {
          "const": "yaml_path_matches"
        },
        "matches": {
          "description": "Rust-regex pattern to match against the value at `path`.",
          "type": "string"
        },
        "path": {
          "description": "`JSONPath` expression rooted at `$`.",
          "type": "string"
        },
        "paths": {
          "$ref": "#/$defs/paths_spec"
        }
      },
      "required": [
        "paths",
        "path",
        "matches"
      ],
      "type": "object"
    },
    "scope_filter": {
      "additionalProperties": false,
      "anyOf": [
        {
          "required": [
            "has_ancestor"
          ]
        },
        {
          "required": [
            "changed_since"
          ]
        }
      ],
      "description": "Per-file rule scoping. `has_ancestor` (v0.9.6+) admits files whose ancestor chain contains a named manifest; `changed_since` (v0.11+) admits files in a `<ref>...HEAD` git diff. At least one must be set; setting both is an AND. Supported on per-file rules and on `dir_absent` (since v0.9.18). Cross-file rules (`pair`, `for_each_dir`, `file_exists`, etc.) reject `scope_filter:` at build time. Combine with the rule's existing `paths:` glob — all gates must match for the rule to fire.",
      "properties": {
        "changed_since": {
          "description": "Git ref; the rule only fires on files in the `<ref>...HEAD` diff (the same merge-base diff as `alint check --changed`). Right shape for grandfathering pre-existing files in a PR — e.g. require an SPDX header only on files the PR touched. Accepts the `{{env.X}}` interpolation (e.g. `changed_since: \"{{env.ALINT_BASE_SHA | default('origin/main')}}\"`). Outside a git repo the predicate matches nothing (silent); an unresolvable ref inside a repo is a hard error with a shallow-clone hint.",
          "minLength": 1,
          "type": "string"
        },
        "has_ancestor": {
          "description": "Manifest filename (or list of filenames) whose presence in any ancestor directory of the linted file admits the rule. Each entry is a literal filename like `Cargo.toml` or `package.json` — no path separators, no glob metacharacters; the ancestor walk handles \"anywhere up the tree\" by traversing `Path::parent()` upward.",
          "oneOf": [
            {
              "minLength": 1,
              "type": "string"
            },
            {
              "items": {
                "minLength": 1,
                "type": "string"
              },
              "minItems": 1,
              "type": "array"
            }
          ]
        }
      },
      "type": "object"
    },
    "string_or_string_array": {
      "oneOf": [
        {
          "type": "string"
        },
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        }
      ]
    },
    "template": {
      "additionalProperties": true,
      "description": "A reusable rule shape. Identical to a regular rule except `level` is optional (instances may add it) and the body is matched after `{{vars.<name>}}` substitution.",
      "properties": {
        "id": {
          "$ref": "#/$defs/rule_id"
        }
      },
      "required": [
        "id"
      ],
      "type": "object"
    }
  },
  "$id": "https://raw.githubusercontent.com/asamarts/alint/main/schemas/v1/config.json",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": false,
  "description": "Schema for .alint.yml configuration files. Authoritative reference: docs/design/ARCHITECTURE.md in the alint repository.",
  "properties": {
    "allow_out_of_root": {
      "description": "Top-level-only escape hatch for path confinement: permit rules to read a config-declared path that escapes the repo root. `true` allows every rule; a map `{ kinds: [...], rules: [...] }` allows only the listed rule kinds / ids. Absent or `false` keeps the secure default (every config-derived path read is confined). REJECTED from `extends:`'d rulesets — only the user's own top-level config may set it. Applies to file reads (currently `registry_paths_resolve` source, `json_schema_passes` schema_path, `pair_hash` target); resolve/index checks stay confined. Permitted reads emit an informational note. See docs/design/v0.12/allow_out_of_root.md.",
      "oneOf": [
        {
          "type": "boolean"
        },
        {
          "additionalProperties": false,
          "properties": {
            "kinds": {
              "description": "Rule kinds permitted to read out of the repo root.",
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            "rules": {
              "description": "Rule ids permitted to read out of the repo root.",
              "items": {
                "type": "string"
              },
              "type": "array"
            }
          },
          "type": "object"
        }
      ]
    },
    "baseline": {
      "description": "Path to a committed baseline file (see `alint baseline`), resolved relative to the repo root. When set, `alint check` suppresses every violation recorded in it and gates only on NEW ones, so CI need not pass `--baseline`. The `--baseline` flag overrides this. Honored only from the user's own top-level config: a `baseline:` reaching the loader from an `extends:`'d or nested config is refused, because a ruleset must not choose which findings the gate suppresses. There is no silent auto-detect — suppression is always an explicit opt-in.",
      "type": "string"
    },
    "extends": {
      "description": "Configs to inherit from. Each entry is either a bare string (local path, `https://` URL with SRI, or `alint://bundled/<name>@<rev>`) or a mapping `{url, only?, except?}` that filters the inherited rule set before merging. Entries resolved left-to-right; later entries override earlier ones, and the current file's own definitions override everything it extends. **Rule merging is field-level by `id`.** A child can override just the fields it wants to change — `rules: [{id: inherited, level: warning}]` keeps the parent's `kind`/`paths`/etc. and only downgrades severity. `{id: inherited, level: off}` disables a rule without redeclaring it. **Selective adoption** via `only:` (keep these ids, drop everything else) or `except:` (drop these ids). The two are mutually exclusive on a single entry. Unknown ids are a load-time error so typos surface immediately. Remote and bundled configs cannot themselves declare `extends:` (no principled base for relative resolution in a fetched body); nest extends locally instead.",
      "items": {
        "$ref": "#/$defs/extends_entry"
      },
      "type": "array"
    },
    "facts": {
      "description": "Properties of the repository evaluated once per run; referenced from `when` clauses as facts.<id>.",
      "items": {
        "$ref": "#/$defs/fact"
      },
      "type": "array"
    },
    "fix_size_limit": {
      "default": 1048576,
      "description": "Max bytes a content-editing fix will touch. Files over this limit are reported as Skipped with a stderr warning. Default: 1 MiB. Explicit null disables the cap. Path-only fixes (file_create, file_remove, file_rename) ignore this setting.",
      "minimum": 0,
      "type": [
        "integer",
        "null"
      ]
    },
    "ignore": {
      "description": "Additional glob patterns excluded on top of .gitignore.",
      "items": {
        "type": "string"
      },
      "type": "array"
    },
    "nested_configs": {
      "default": false,
      "description": "Opt in to discovery of nested `.alint.yml` / `.alint.yaml` files in subdirectories. When true, the loader walks the tree from this config's directory (honoring `.gitignore` + `ignore`) and finds any nested config files. Each nested rule's path-like scope fields (`paths`, `select`, `primary`) are automatically prefixed with the nested config's relative directory, so rules declared in `packages/frontend/.alint.yml` apply only under `packages/frontend/**`. Id collisions across configs are rejected with a clear error — per-subtree overrides aren't supported in this release (use `when:` on the root rule for that). Only the root config sets this; nested configs can't spawn further nested discovery.",
      "type": "boolean"
    },
    "respect_gitignore": {
      "default": true,
      "description": "Whether to honor .gitignore files during the walk.",
      "type": "boolean"
    },
    "rules": {
      "description": "Rules evaluated against the repository tree.",
      "items": {
        "$ref": "#/$defs/rule"
      },
      "type": "array"
    },
    "templates": {
      "description": "Reusable rule shapes referenced by `extends_template:` in `rules:` entries. Each template carries an `id:` and any other rule-spec fields; `{{vars.<name>}}` placeholders in those fields substitute from each instance's `vars:` map at expansion time. Templates are leaf-only — a template may not itself `extends_template:` another. Templates merge through the `extends:` chain by id, so a downstream config can override an upstream template's body.",
      "items": {
        "$ref": "#/$defs/template"
      },
      "type": "array"
    },
    "vars": {
      "additionalProperties": {
        "type": "string"
      },
      "description": "Free-form string variables referenced from rule messages as {{vars.<name>}} and from `when` clauses as vars.<name>.",
      "type": "object"
    },
    "version": {
      "const": 1,
      "description": "Config schema version. Always 1 for this schema."
    }
  },
  "required": [
    "version"
  ],
  "title": "alint configuration (v1)",
  "type": "object"
}
