# rule_meta.mjs

## Огляд

Модуль `rule_meta.mjs` реалізує перевірку метаданих правил пакета `@nitra/cursor`. Він є частиною концерну правила `npm-module` й обслуговує валідацію файлів `npm/rules/<id>/meta.json` у репозиторії пакета.

Логіка перевірки покриває такі інваріанти для кожного підкаталогу `npm/rules/<id>/`:

- наявність валідного `meta.json`;
- коректність поля `auto`, якщо воно присутнє (розпізнаваність специфікації через `parseRuleAutoSpec`: значення `"завжди"`, масив, `{glob}` або `{predicate}`);
- для специфікації `{predicate}` — наявність відповідного імені у реєстрі `RULE_PREDICATES`;
- коректність поля `lint`, якщо воно присутнє (`"quick"` чи `"ci"`), з обовʼязковою наявністю файлу `js/lint.mjs` у каталозі правила;
- відсутність застарілого `auto.md` поряд із `meta.json` (міграція метаданих у `meta.json` вже завершена).

Перевірка застосовна виключно у репо самого пакета `@nitra/cursor`, де існує каталог `npm/rules/`. У споживацьких репо такого каталогу немає, тож модуль м’яко рапортує `pass` і завершується кодом виходу `0`.

Модуль експортує єдину функцію `check`, яка повертає `Promise<number>` із POSIX-кодом виходу: `0` — порушень немає, `1` — є хоча б одне порушення. Звітування реалізоване через інстанс репортера, отриманий від `createCheckReporter`.

## Експорти / API

| Експорт                                | Тип                          | Призначення                                                                                                |
| -------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `check(cwd?: string): Promise<number>` | іменований експорт (функція) | Валідує всі `npm/rules/<id>/meta.json` у переданому корені репозиторію та повертає підсумковий код виходу. |

Інших експортів (default, типи, константи) модуль не має.

## Функції

### `check(cwd = process.cwd())`

Валідує всі `meta.json` у `<cwd>/npm/rules/<id>/`.

- **Сигнатура:** `export function check(cwd?: string): Promise<number>`
- **Параметри:**
  - `cwd` — рядок із абсолютним або відносним шляхом до кореня репозиторію. Необовʼязковий. Якщо не вказано, використовується `process.cwd()` (поточний робочий каталог процесу Node).
- **Повертає:** `Promise<number>`, що резолвиться у код виходу від репортера (`reporter.getExitCode()`). Конвенція:
  - `0` — усі знайдені правила пройшли валідацію, або каталог `npm/rules/` відсутній (у такому випадку додатково логиться `pass`-повідомлення `"npm/rules/ відсутній — немає правил для валідації"`);
  - `1` — зафіксовано принаймні одне порушення (зокрема: залишковий `auto.md`, відсутній/невалідний `meta.json`, нерозпізнаний `auto`, невідомий `predicate`, нерозпізнаний `lint`, або `lint` без `js/lint.mjs`).
- **Side effects:**
  - Виконує синхронне читання файлової системи: `existsSync` для перевірки наявності `npm/rules/`, `npm/rules/<id>/auto.md`, `npm/rules/<id>/js/lint.mjs`; `readdirSync` зі списком підкаталогів `npm/rules/`.
  - Делегує читання та парсинг `meta.json` у `readRuleMetaRaw` (також синхронне читання файла).
  - Викликає методи репортера (`reporter.pass`, `reporter.fail`), які за домовленістю виводять статусні повідомлення (зазвичай у stdout/stderr — залежно від реалізації `createCheckReporter`).
  - Не змінює файли на диску, не запускає дочірніх процесів, не звертається до мережі.
- **Алгоритм:**
  1. Створює репортер: `reporter = createCheckReporter()`.
  2. Обчислює `rulesDir = join(cwd, 'npm', 'rules')`.
  3. Якщо `rulesDir` не існує — рапортує `pass` і резолвить `Promise.resolve(reporter.getExitCode())` (зазвичай `0`).
  4. Інакше перебирає вміст `rulesDir` через `readdirSync(rulesDir, { withFileTypes: true })` і для кожного запису:
     - пропускає, якщо це не каталог (`!entry.isDirectory()`) або імʼя починається з крапки (`entry.name.startsWith('.')`);
     - встановлює локальний прапор `ruleOk = true`;
     - якщо знайдено залишковий `auto.md` у каталозі правила — `reporter.fail(...)`, `ruleOk = false`;
     - читає сирий обʼєкт метаданих через `readRuleMetaRaw(ruleDir)`. Якщо повернуто falsy — `reporter.fail("rules/<id>: відсутній або невалідний meta.json")` і `continue` (наступне правило);
     - якщо `raw.auto !== undefined` — парсить через `parseRuleAutoSpec(raw.auto)`:
       - якщо повернуто `null` — `reporter.fail(...)`, `ruleOk = false`;
       - інакше якщо в `spec` є ключ `'predicate'` і його значення немає у `RULE_PREDICATES` — `reporter.fail(...)`, `ruleOk = false`;
     - якщо `raw.lint !== undefined` — валідує через `parseRuleLintPhase(raw.lint)`:
       - якщо повернуто `null` — `reporter.fail(...)`, `ruleOk = false`;
       - інакше якщо немає файла `<ruleDir>/js/lint.mjs` — `reporter.fail(...)`, `ruleOk = false`;
     - наприкінці ітерації, якщо `ruleOk` усе ще `true` — `reporter.pass("rules/<id>: meta.json валідний")`.
  5. Резолвить `Promise.resolve(reporter.getExitCode())` із підсумковим кодом виходу.
- **Особливості реалізації:**
  - Усі гілки порушень для одного правила фіксуються незалежно одна від одної (наприклад, у тому ж проході може бути зафіксовано і нерозпізнаний `auto`, і нерозпізнаний `lint`). Виняток — повністю відсутній/невалідний `meta.json`: тоді решта перевірок для цього правила пропускається через `continue`.
  - Функція синхронно виконує всю роботу, але повертає `Promise<number>` через `Promise.resolve(...)` — для уніфікованої сигнатури з іншими `check`-модулями.
  - Перевірка `'predicate' in spec` дозволяє валідувати лише гілку `{predicate}`-специфікації; інші форми (`"завжди"`, масив, `{glob}`) уже визнані валідними самим `parseRuleAutoSpec`.

## Залежності

### Зовнішні (Node.js standard library)

- `node:fs` — `existsSync` (перевірка існування файлів і каталогів), `readdirSync` (читання вмісту каталогу `npm/rules/`).
- `node:path` — `join` (формування шляхів до каталогів і файлів).

### Внутрішні (репо-локальні)

- `../../../scripts/lib/check-reporter.mjs` — `createCheckReporter()`. Створює репортер з методами `pass(msg)`, `fail(msg)`, `getExitCode()`; останній використовується для отримання підсумкового коду виходу.
- `../../../scripts/lib/rule-meta.mjs`:
  - `readRuleMetaRaw(ruleDir)` — читає та парсить сирий `meta.json` із каталогу правила; повертає обʼєкт або falsy (за відсутності/невалідності файла).
  - `parseRuleAutoSpec(value)` — парсить значення поля `auto`; повертає валідовану специфікацію або `null` за нерозпізнаним вводом.
  - `parseRuleLintPhase(value)` — парсить значення поля `lint`; повертає валідовану фазу (`"quick"`/`"ci"`) або `null`.
- `../../../scripts/lib/rule-predicates.mjs` — `RULE_PREDICATES`: обʼєкт-реєстр відомих імен предикатів; ключі використовуються для перевірки наявності предиката через `Object.hasOwn`.

### Глобальні

- `process.cwd()` — використовується як дефолтний `cwd`.

## Потік виконання / Використання

Модуль є check-функцією конкретного правила і підпорядкований конвенції `check-{id}` (тут — `rule_meta`). Очікуваний сценарій використання:

1. Виклик з вищерівневого скрипта (наприклад, агрегованої перевірки правил пакета):

   ```js
   import { check } from './npm/rules/npm-module/js/rule_meta.mjs'

   const exit = await check(process.cwd())
   process.exit(exit)
   ```

   У такому сценарії функцію викликають із кореня репозиторію пакета `@nitra/cursor`.

2. Запуск у репо-споживачі (де `npm/rules/` відсутній) — функція тихо рапортує `pass` і повертає `0`, не блокуючи pipeline.
3. Сценарій порушення: будь-який із описаних інваріантів спричиняє `fail`-звіт і код виходу `1`, що сигналізує CI зупинку.

Типові точки інтеграції:

- CI lint-фаза пакета `@nitra/cursor` (виклик в одному з кореневих скриптів `bun run lint` / спеціалізованої перевірки правил).
- Локальний запуск через CLI з кореня репозиторію.

Файл не має побічних ефектів під час імпорту: експортована функція не виконується самовільно — її викликає консьюмер.

## Rebuild Test

Контрольні питання, чиї відповіді достатні, щоб реконструювати модуль без перегляду коду:

1. Який каталог сканує функція `check`?
   - `<cwd>/npm/rules/`, де `cwd` — необовʼязковий параметр (дефолт `process.cwd()`).
2. Як обробляється відсутність цього каталогу?
   - Рапортується `pass("npm/rules/ відсутній — немає правил для валідації")`, повертається `0`.
3. Які записи в `npm/rules/` пропускаються?
   - Не-каталоги та елементи, чиє імʼя починається з `.`.
4. За наявності якого файла поряд із `meta.json` фіксується порушення?
   - `auto.md` (повідомлення `"rules/<id>: залишковий auto.md — видали (метадані тепер у meta.json)"`).
5. Що відбувається, якщо `meta.json` відсутній або невалідний?
   - `reporter.fail("rules/<id>: відсутній або невалідний meta.json")` і `continue` до наступного правила.
6. Які гілки валідації поля `auto`?
   - Якщо `parseRuleAutoSpec` повертає `null` — fail (нерозпізнане).
   - Якщо специфікація містить ключ `predicate`, але його значення відсутнє в `RULE_PREDICATES` — fail (`невідомий predicate`).
7. Які гілки валідації поля `lint`?
   - Якщо `parseRuleLintPhase` повертає `null` — fail (`нерозпізнане, очікується "quick"|"ci"`).
   - Якщо валідне, але немає `js/lint.mjs` — fail (`lint:"..." але немає js/lint.mjs`).
8. Як формується підсумковий код виходу?
   - Через `reporter.getExitCode()`, обгорнутий у `Promise.resolve(...)`.
9. Чи синхронна реалізація?
   - Так: усі IO операції — синхронні (`existsSync`, `readdirSync`, `readRuleMetaRaw`); проміс використано лише для уніфікації сигнатури.
10. Які `pass`-повідомлення можливі?
    - `"npm/rules/ відсутній — немає правил для валідації"` (за відсутності каталогу).
    - `"rules/<id>: meta.json валідний"` (за успішної валідації окремого правила).
