# list-rule-ids.mjs

## Огляд

Модуль `list-rule-ids.mjs` — невелика бібліотечна утиліта, призначена для перебору директорій-правил у каталозі `npm/rules/` та повернення відсортованого алфавітно списку ідентифікаторів правил, які реально містять виконуваний модуль `fix.mjs`.

Логіка модуля побудована навколо архітектурної інваріанти проєкту: після так званої «атомарної міграції» кожне валідне правило **зобов'язане** мати файл `fix.mjs` у власній директорії `rules/<id>/`. Будь-яка піддиректорія без `fix.mjs` вважається або «не-правилом» (службова тека), або заглушкою-чернеткою — і ігнорується. Прихованих директорій (імена, що починаються з `.`) також не існує в результаті.

Додатково модуль підтримує опційне точкове фільтрування одним конкретним `id`, що використовується CLI-флагом `--rule abie` для прогону команд у режимі «тільки одне правило».

Модуль повністю асинхронний (використовує `node:fs/promises`), не має зовнішніх npm-залежностей і не виконує жодних побічних ефектів окрім читання директорії та перевірки наявності файлу через `existsSync`.

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

Файл експортує одну іменовану функцію:

| Експорт       | Тип              | Призначення                                                                                                                       |
| ------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `listRuleIds` | `async function` | Повертає `Promise<string[]>` зі списком id правил, відфільтрованим за наявністю `fix.mjs` і опційним рівнянням до конкретного id. |

Експорту за замовчуванням немає.

## Функції

### `listRuleIds(bundledRulesDir, filter)`

#### Сигнатура

```js
export async function listRuleIds(bundledRulesDir, filter)
```

JSDoc-типи:

```
@param {string} bundledRulesDir — абсолютний шлях до `npm/rules/`
@param {string} [filter]       — id одного правила (через `--rule abie`)
@returns {Promise<string[]>}    — відсортовані алфавітно id
```

#### Параметри

- **`bundledRulesDir`** (`string`, обовʼязковий) — абсолютний шлях до кореневої теки правил, найчастіше `npm/rules/` (наприклад, `/path/to/repo/npm/rules`). Передбачається, що шлях існує і є директорією; модуль не валідує його окремо — будь-яка помилка з `fs.readdir` пробрасується далі промісом.
- **`filter`** (`string | undefined`, опційний) — точна назва правила, до якої потрібно звузити список. Якщо параметр не передано (`undefined`), повертаються всі знайдені id. Якщо передано, у результат потрапляють лише ті id, що _точно_ рівні `filter`. Часткові збіги, регулярні вирази або кейс-незалежність не підтримуються.

#### Повертає

`Promise<string[]>` — масив рядків-ідентифікаторів правил, відсортованих алфавітно за допомогою `Array.prototype.toSorted` із компаратором `a.localeCompare(b)`. Сортування _стабільне_ для оригінального методу і коректне для Unicode-локалей.

Можливі сценарії результату:

- Порожній масив `[]` — якщо в `bundledRulesDir` немає жодної директорії з `fix.mjs`, або якщо `filter` не збігається з жодним наявним id.
- Масив з одного елемента — типово при заданому `filter`.
- Повний відсортований список усіх правил — у випадку відсутності `filter`.

#### Алгоритм / послідовність кроків

1. Викликається `readdir(bundledRulesDir, { withFileTypes: true })` — отримуємо масив `Dirent`-обʼєктів (а не просто рядків імен), що дає змогу одразу перевіряти тип запису без додаткових `stat`-викликів.
2. Залишаються лише записи, для яких одночасно:
   - `e.isDirectory()` — це саме директорія;
   - `!e.name.startsWith('.')` — імʼя не починається з крапки (відсікаються приховані теки на кшталт `.git`, `.cache`, тощо).
3. Записи перетворюються на голі імена (`.map(e => e.name)`).
4. Фільтрація на наявність `fix.mjs` — для кожного `id` синхронно перевіряється `existsSync(join(bundledRulesDir, id, 'fix.mjs'))`. Директорії без цього файлу відсіюються як «not-a-rule або заглушка».
5. Фільтрація за `filter` — якщо параметр визначений, залишаються лише id, рівні `filter`; інакше пропускаються всі.
6. Сортування `toSorted((a, b) => a.localeCompare(b))` — повертає новий масив без мутації проміжного.

#### Side effects

- **Дискові читання**: `readdir` (асинхронний обхід директорії) та `existsSync` (синхронні перевірки наявності файлу для кожного кандидата). Винятки I/O пробрасуються відхиленим промісом.
- **Жодних записів, мережевих викликів, мутацій глобального стану, логування** — функція є read-only і чистою щодо побічних ефектів окрім згаданих читань ФС.
- Використання `existsSync` у циклі — це _синхронні_ виклики всередині асинхронної функції; вони виконуються послідовно, але без `await`. Для типового розміру каталогу `npm/rules/` (десятки правил) це непомітна вартість.

#### Гранітні випадки

- Якщо `bundledRulesDir` не існує — `readdir` відхиляє проміс з `ENOENT`.
- Якщо в директорії є символічні посилання — поведінка залежить від того, що повертає `Dirent.isDirectory()`; ця функція покладається на штатну семантику Node.js.
- Якщо `filter` переданий як порожній рядок `''` — він _не_ є `undefined`, тому фільтр буде застосовано (`id === ''`), і результат майже напевно буде порожнім.
- Файл `fix.mjs` як директорія: малоймовірний випадок, але `existsSync` поверне `true` для будь-якого існуючого запису з цим імʼям; модуль не розрізняє файл і теку.

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

### Вбудовані модулі Node.js

| Модуль             | Імпортований символ | Використання                                                 |
| ------------------ | ------------------- | ------------------------------------------------------------ |
| `node:fs`          | `existsSync`        | Синхронна перевірка наявності `fix.mjs` у кожному кандидаті. |
| `node:fs/promises` | `readdir`           | Асинхронний обхід `bundledRulesDir` з `withFileTypes: true`. |
| `node:path`        | `join`              | Безпечне зʼєднання сегментів шляху до `fix.mjs`.             |

### Зовнішні npm-залежності

Немає.

### Внутрішні залежності

Файл не імпортує жодних інших модулів проєкту. Сам по собі він використовується як бібліотечна функція з інших скриптів у `npm/scripts/` (CLI-обгортки, які приймають флаг `--rule`).

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

### Контекст у проєкті

`listRuleIds` — це фундамент будь-якого скрипту, який «робить щось для кожного правила»: лінт, авто-фікс, генерація звіту, перевірка покриття, нормалізація ADR тощо. Замість того щоб кожному скрипту самостійно перелічувати теки в `npm/rules/`, всі вони викликають цю функцію та отримують однаковий, відсортований, відфільтрований список — це гарантує детермінованість порядку обробки правил.

Архітектурно файл підкріплює інваріант «`fix.mjs` обовʼязковий»: будь-яка тека без нього автоматично невидима для всіх споживачів. Це дає змогу тримати у `rules/` допоміжні підтеки (наприклад, шаблони, шаред-файли) без ризику, що вони будуть оброблені як правило.

### Приклад використання

```js
import { listRuleIds } from './lib/list-rule-ids.mjs'
import { resolve } from 'node:path'

const bundledRulesDir = resolve(import.meta.dirname, '../../rules')

// 1. Всі правила.
const all = await listRuleIds(bundledRulesDir)
// → ['abie', 'changelog', 'feedback', ...]

// 2. Тільки одне правило (з CLI-флагу --rule abie).
const ruleFlag = process.argv.includes('--rule') ? process.argv[process.argv.indexOf('--rule') + 1] : undefined

const subset = await listRuleIds(bundledRulesDir, ruleFlag)
// → ['abie']  якщо ruleFlag === 'abie' і таке правило існує
// → []        якщо ruleFlag не відповідає жодному правилу
```

### Інтеграція з CLI

Типовий патерн виклику в скриптах проєкту:

1. Скрипт парсить `process.argv` (часто через `mri`/`minimist` або вручну), отримує опційний `--rule <id>`.
2. Викликає `listRuleIds(rulesDir, ruleFromArgv)` і чекає на результат.
3. Ітерується по отриманому масиву і виконує цільову дію (fix/check/коверідж) для кожного id.

### Послідовність виконання при виклику

```
listRuleIds(dir, filter)
   │
   ├─ readdir(dir, withFileTypes:true) ── async ──▶ [Dirent...]
   │
   ├─ .filter(isDirectory && !startsWith('.'))
   ├─ .map(e => e.name)
   ├─ .filter(existsSync(<id>/fix.mjs))      ◀── sync I/O loop
   ├─ .filter(filter === undefined || id === filter)
   ├─ .toSorted(localeCompare)
   │
   ▼
Promise<string[]>  (відсортовані id)
```

### Очікувані помилки

- `ENOENT` / `ENOTDIR` від `readdir` — якщо переданий `bundledRulesDir` неіснуючий або не є директорією. Викликач має або переконатися в коректності шляху перед викликом, або обгорнути виклик у `try/catch`.
- `EACCES` — якщо немає прав на читання теки.

Усі інші помилки (відсутність окремого `fix.mjs`, відсутність відповідності `filter`) семантично не є помилками — вони лише звужують повернений масив.
