{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://unpkg.com/@7n/rules/schemas/concern.json",
  "title": "Concern descriptor",
  "description": "Маркер підкаталогу правила як concern-а. Кожна з поверхонь (check/policy/lint) опційна, але хоча б одна обов'язкова. Concern id = ім'я каталогу (lower snake_case [a-z0-9_]+).",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "type": "string",
      "format": "uri"
    },
    "fixability": {
      "type": "string",
      "enum": [
        "code",
        "config",
        "structural"
      ],
      "description": "Маршрутизація fix-движка. Усі мітки виконують детерміновану фазу T0; різниця — що після неї. `code` (дефолт, поле можна опускати) — джерельні порушення, які LLM може осмислено переписати → повна pi-agent-ladder (local→cloud) до чистого re-detect. `config` — canon/settings/tooling конфіг з єдиною правильною формою (jscpd/oxfmtrc/tooling/package_json/*_yml): фікс детермінований (T0/regen з канону), LLM лише вгадав би невалідне → після T0 fail-fast без ladder. `structural` — дублікати/розташування/структура (jscpd_duplicates/test-location/package-structure): авто-правка ризикована й потребує людського розсуду → після T0 fail-fast без ladder. config vs structural різняться мотивом пропуску (детермінований канон vs надто ризиковано) — для телеметрії й майбутнього routing."
    },
    "skipLocalTier": {
      "type": "boolean",
      "description": "Дефолт `false` (поле можна опускати). `true` пропускає local-min/local-min-retry rung-и LLM-ladder-а для цього concern-а — перша спроба одразу йде на cloud-min. Для concern-ів, де local-tier (слабка локальна модель, короткий бюджет) емпірично майже завжди лише витрачає час rung-а без результату, перш ніж ladder однаково ескалює далі."
    },
    "cloudTimeoutMs": {
      "type": "integer",
      "minimum": 1,
      "description": "Опційний budget одного cloud rung-а лише для цього concern-а. Замінює timeout cloud-min/cloud-avg у ladder, не змінюючи глобальний `N_CLOUD_FIX_TIMEOUT_MS` або інші concern-и."
    },
    "fixHint": {
      "type": "string",
      "minLength": 1,
      "description": "Опційний рецепт для МОДЕЛІ, коли повідомлення детектора адресоване людині. Повідомлення — елемент UX: воно радить зручну команду (`npx @7n/n ch`), але цикл fix виконати її не може — shell поза його набором інструментів свідомо. Живий прогін показав ціну розбіжності: єдиний наявний рецепт виявився забороненим, іншого не було, і модель вигоріла всю стелю ходів, не спробувавши жодного запису. Сюди кладуть те, що команда РОБИТЬ: який файл створити і якої він форми. Поле потрібне лише concern-ам з `fixability: code`, чиє повідомлення командне — решту гейт до моделі не пускає.",
      "examples": [
        "створи файл `<workspace>/.changes/<будь-яке-імʼя>.md` з YAML-frontmatter `bump:` (major|minor|patch) і `section:` (Added|Changed|Fixed|Removed), а нижче — опис зміни."
      ]
    },
    "check": {
      "type": "boolean",
      "const": true,
      "description": "JS conformance/fix поверхня. main.mjs має експортувати main(cwd)."
    },
    "policy": {
      "type": "object",
      "additionalProperties": false,
      "description": "Rego або template поверхня. Відповідає колишньому target.json. У unified lint surface (spec 2026-06-29) policy-concern генерує detector main.mjs через codegen; engine обирає Rego чи template. `files` опційне: порожній `policy: {}` — маркер policy-поверхні без явного файл-скоупу (скоуп резолвиться правилом).",
      "properties": {
        "engine": {
          "type": "string",
          "enum": [
            "rego",
            "template"
          ],
          "description": "rego → .rego через conftest; template → generic deep-subset. Канонічна заміна поля `check`."
        },
        "namespace": {
          "type": "string",
          "description": "Rego package/namespace concern-а (напр. `k8s.network_policy`) — для fix-hint маршрутизації по rego-повідомленнях."
        },
        "files": {
          "oneOf": [
            {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "single"
              ],
              "properties": {
                "single": {
                  "type": "string",
                  "minLength": 1
                },
                "required": {
                  "type": "boolean"
                }
              }
            },
            {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "walkGlob"
              ],
              "properties": {
                "walkGlob": {
                  "oneOf": [
                    {
                      "type": "string",
                      "minLength": 1
                    },
                    {
                      "type": "array",
                      "minItems": 1,
                      "items": {
                        "type": "string",
                        "minLength": 1
                      }
                    }
                  ]
                }
              }
            },
            {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "glob"
              ],
              "properties": {
                "glob": {
                  "oneOf": [
                    {
                      "type": "string",
                      "minLength": 1
                    },
                    {
                      "type": "array",
                      "minItems": 1,
                      "items": {
                        "type": "string",
                        "minLength": 1
                      }
                    }
                  ]
                }
              }
            }
          ]
        },
        "check": {
          "type": "string",
          "enum": [
            "template"
          ],
          "description": "template → generic deep-subset без .rego; без поля → Rego mode."
        },
        "missingMessage": {
          "type": "string",
          "description": "Override fail-повідомлення при відсутності required:single файла."
        }
      }
    },
    "lint": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "scope"
      ],
      "description": "Lint поверхня. main.mjs має експортувати lint(changed, cwd, opts).",
      "properties": {
        "scope": {
          "type": "string",
          "enum": [
            "per-file",
            "full"
          ],
          "description": "per-file: delta-режим; full: крос-файловий."
        },
        "glob": {
          "oneOf": [
            {
              "type": "string",
              "minLength": 1
            },
            {
              "type": "array",
              "minItems": 1,
              "items": {
                "type": "string",
                "minLength": 1
              }
            }
          ],
          "description": "per-file: фільтр delta-файлів; full: delta-тригер."
        },
        "anchors": {
          "oneOf": [
            {
              "type": "string",
              "minLength": 1
            },
            {
              "type": "array",
              "minItems": 1,
              "items": {
                "type": "string",
                "minLength": 1
              }
            }
          ],
          "description": "per-file: repo-relative шляхи, які planner ЗАВЖДИ додає до непорожнього delta-batch-у цього concern-а, навіть якщо самі не змінювались (напр. pyproject.toml — гейт-маркер, потрібен per-file wasm-концерну для семантичного existsSync-гейта без full-scope ходу по всьому дереву). Порожній збіг лишається порожнім — якір НЕ тригерить concern сам по собі. Матеріалізація мовчки пропускає якір, якого немає на диску (той самий skip-not-crash, що решта batch-читання)."
        },
        "extensionsSlot": {
          "type": "string",
          "minLength": 1,
          "description": "Ім'я slot-а з extension-map contributions (value: мапа '.ext' → мітка, напр. doc-files.extensions). Ефективний glob концерну виводиться з розширень усіх активних contributions цього slot-а (`**/*.{…}`), тож lang-плагіни розширюють delta-тригер без правки concern.json. Статичний `glob` лишається fallback-ом, коли жодний активний плагін не декларує розширень (плагіни не встановлені)."
        }
      }
    },
    "requires": {
      "type": "object",
      "description": "Гейт активації: концерн виконується лише коли задекларована capability доступна (її надає встановлений плагін через блок n-rules.capabilities у своєму package.json). Приклад: {\"capability\": \"ci:github\"} — концерн вимагає GitHub Actions і спить у репозиторіях без плагіна @7n/rules-ci-github.",
      "additionalProperties": false,
      "required": [
        "capability"
      ],
      "properties": {
        "capability": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9:-]*$",
          "description": "Ім'я capability (конвенція <тип>:<назва>, напр. ci:github, ci:azure, lang:rust)"
        }
      }
    }
  }
}
