---
docgen:
  source: npm/scripts/lib/check-mdc-template-refs.mjs
  crc: 17b81322
---

# check-mdc-template-refs.mjs

## Огляд

Модуль `check-mdc-template-refs.mjs` — це невелика утиліта, призначена для перевірки цілісності посилань між файлом правила `<id>.mdc` та шаблонами, що лежать у підкаталогах `template/` цього самого правила. Він обходить структуру каталогів правила (`fix/<concern>/template/`, `policy/<concern>/template/`), збирає всі знайдені файли й порівнює їхні відносні шляхи з вмістом основного файлу правила `<id>.mdc`. Результатом є перелік шаблонних файлів, на які у `.mdc` немає жодного markdown-посилання, тобто «осиротілі» (orphaned) шаблони.

Типовий контекст застосування: модуль використовується в утилітах перевірки/лінтингу правил репозиторію (`npm/rules/<id>/`), де `.mdc`-файл описує правило людською мовою й має посилатися на свої шаблони. Якщо шаблон існує, але не згаданий у `.mdc`, цей модуль повідомить про нього, щоб супровідник правила або додав посилання, або видалив зайвий шаблон.

Модуль повністю асинхронний (використовує `node:fs/promises`), стандартної залежності від сторонніх бібліотек не має, працює у середовищі Node.js (а також сумісне з Bun) як ES-модуль (`.mjs`).

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

Модуль експортує єдину функцію:

- `findMissingMdcRefs(ruleDir, ruleId)` — публічний експорт (`export async function`).

Дві внутрішні (не експортовані) допоміжні функції:

- `walkTemplateDirs(ruleDir)` — обходить підкаталоги `fix/*/template/` і `policy/*/template/`.
- `collectFiles(dir)` — рекурсивно збирає всі файли в заданому каталозі.

Імпорти з ядра Node:

- `existsSync` з `node:fs` — синхронна перевірка існування шляху.
- `readdir`, `readFile`, `stat` з `node:fs/promises` — асинхронні операції з файловою системою.
- `join`, `relative` з `node:path` — нормалізація та побудова шляхів.

## Функції

### findMissingMdcRefs

Сигнатура:

```
export async function findMissingMdcRefs(ruleDir, ruleId): Promise<string[]>
```

Параметри:

- `ruleDir` — `string`, абсолютний шлях до каталогу правила (за конвенцією `npm/rules/<id>/`).
- `ruleId` — `string`, basename правила (наприклад, `"security"`). Використовується для побудови імені `.mdc`-файлу: `${ruleId}.mdc`.

Що робить:

1. Будує очікуваний шлях до файлу правила: `mdcPath = join(ruleDir, `${ruleId}.mdc`)`.
2. Якщо файл `.mdc` не існує — повертає порожній масив `[]` (нічого перевіряти).
3. Читає текст `.mdc` у пам'ять як UTF-8 рядок.
4. Викликає `walkTemplateDirs(ruleDir)`, щоб отримати масив усіх файлів із `template/`-каталогів правила (як шляхи, відносні до `ruleDir`).
5. Фільтрує отриманий масив, залишаючи лише ті файли, посилань на які НЕ виявлено в тексті `.mdc`. Перевірка наявності посилання — підрядкова: файл вважається «посиланим», якщо в тексті `.mdc` зустрічається будь-яке з двох вкраплень:
   - `./<rel>` — типова форма markdown-посилання `[label](./path)`.
   - `(<rel>)` — також зловить голу форму `(path)` у круглих дужках.

Повертає:

- `Promise<string[]>` — масив відносних шляхів (відносно `ruleDir`) файлів шаблонів, на які в `.mdc` немає посилання.

Side effects:

- Виключно read-only доступ до файлової системи: `existsSync`, `readFile`, `readdir`, `stat`. Файли або каталоги не створюються, не змінюються, не видаляються.
- Не виводить нічого в `stdout`/`stderr`. Не має побічних ефектів на process state.

Помилки:

- Може кинути помилку, якщо `readFile`/`readdir`/`stat` зазнають невдачі з причин, відмінних від «файл не існує» (наприклад, права доступу). Перед читанням існування `mdcPath` перевіряється явно, тому відсутність `.mdc` помилки не дає.

### walkTemplateDirs (внутрішня)

Сигнатура:

```
async function walkTemplateDirs(ruleDir): Promise<string[]>
```

Параметри:

- `ruleDir` — `string`, абсолютний шлях до каталогу правила.

Що робить:

1. Ітерується по двох жорстко закодованих «kind»-категоріях: `'fix'` та `'policy'`.
2. Для кожного `kind` будує шлях `kindDir = join(ruleDir, kind)`. Якщо такого каталогу немає — пропускає.
3. Для кожного `concern` (підкаталога або файлу всередині `kindDir`, який повертає `readdir`) будує шлях `tpl = join(kindDir, concern, 'template')`.
4. Якщо `tpl` не існує — пропускає. Якщо існує, але не є каталогом (`stat(tpl).isDirectory() === false`) — також пропускає.
5. Викликає `collectFiles(tpl)` для рекурсивного збору всіх файлів і додає їх до результуючого масиву.
6. На виході перетворює абсолютні шляхи у шляхи, відносні до `ruleDir` (`relative(ruleDir, p)`).

Повертає:

- `Promise<string[]>` — відносні (від `ruleDir`) шляхи всіх файлів у `fix/*/template/` і `policy/*/template/`.

Side effects:

- Лише read-only ФС-операції.

Примітка: значення `readdir(kindDir)` (без `withFileTypes`) повертає масив імен і файлів, і каталогів. Перевірка `existsSync(tpl)` та `stat(tpl).isDirectory()` коректно відсіює випадки, коли `concern` — це файл, а не каталог.

### collectFiles (внутрішня)

Сигнатура:

```
async function collectFiles(dir): Promise<string[]>
```

Параметри:

- `dir` — `string`, абсолютний шлях каталогу, який треба обійти.

Що робить:

1. Викликає `readdir(dir, { withFileTypes: true })` і отримує `Dirent[]`.
2. Для кожного запису будує повний шлях `full = join(dir, entry.name)`.
3. Якщо запис — каталог (`entry.isDirectory()`), рекурсивно занурюється в нього й приєднує знайдене до результату.
4. Якщо запис — файл (або symlink/інше, що НЕ каталог), додає `full` як є.

Повертає:

- `Promise<string[]>` — абсолютні шляхи всіх файлів усередині `dir` (з усіх рівнів вкладеності).

Side effects:

- Read-only обхід ФС.

Зауваження щодо поведінки: символьні посилання й сокети не каталогізуються як директорії (`isDirectory()` поверне `false`), отже потрапляють у список як «файли». Гранична глибина рекурсії не обмежена — для зацикленої структури symlinks теоретично можливе нескінченне занурення; на практиці шаблонні каталоги правил такого не мають.

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

Стандартна бібліотека Node.js (ESM):

- `node:fs` — `existsSync` (синхронна перевірка існування).
- `node:fs/promises` — `readdir`, `readFile`, `stat` (асинхронні ФС-операції).
- `node:path` — `join`, `relative` (нормалізація шляхів).

Зовнішніх npm-залежностей немає. Модуль не використовує жодних глобальних змінних, конфігів, env-vars чи CLI-аргументів. Не звертається до мережі.

Передумови оточення:

- Node.js версія, що підтримує ES Modules (`.mjs`) і `node:`-protocol-imports (Node 14+; на практиці Node 18+/Bun).
- Доступ на читання до каталогу `ruleDir` та його підкаталогів.

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

### Очікувана структура каталогу правила

Модуль розрахований на конвенцію розкладки правил:

```
<ruleDir>/
  <ruleId>.mdc                 ← текст правила з markdown-посиланнями
  fix/
    <concern-A>/
      template/
        file1.ext
        nested/file2.ext
    <concern-B>/
      template/
        ...
  policy/
    <concern-C>/
      template/
        ...
```

Перелічуються лише файли всередині `fix/*/template/` і `policy/*/template/`. Будь-який інший контент (`check-*.mjs`, `README`, інші підкаталоги) ігнорується.

### Алгоритм у двох словах

1. Зібрати всі файли з `fix/*/template/` і `policy/*/template/`, перетворити у відносні шляхи від `ruleDir`.
2. Прочитати `<ruleId>.mdc`.
3. Для кожного відносного шляху перевірити, чи зустрічається в тексті `.mdc` хоча б одна з форм `./<rel>` або `(<rel>)`. Якщо НЕ зустрічається — додати у вихідний список.

### Типове використання

```js
import { findMissingMdcRefs } from './check-mdc-template-refs.mjs'

const missing = await findMissingMdcRefs('/abs/path/to/npm/rules/security', 'security')
if (missing.length > 0) {
  console.error('Orphaned template files (no markdown link in .mdc):')
  for (const rel of missing) console.error(`  - ${rel}`)
  process.exit(1)
}
```

Цей виклик часто є частиною check-скрипта правила (`check-<id>.mjs`) або агрегованого валідатора, який обходить усі правила репозиторію.

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

- `.mdc` відсутній — повертається `[]`. Це навмисно: правило без `.mdc` не валідуємо «на покинуті шаблони».
- Каталоги `fix/`, `policy/`, `template/` відсутні — обходяться silent, результат для них порожній.
- Шаблонів немає взагалі — повертається `[]`.
- `.mdc` посилається на шаблон, якого фізично немає, — цей сценарій модуль НЕ перевіряє (зворотний напрям перевірки тут не реалізовано).
- Перевірка посилань — підрядкова, без парсингу markdown. Якщо файл містить `./foo/bar.md` як випадковий збіг у документації, шаблон `foo/bar.md` буде вважатися посиланим, навіть якщо це не власне markdown-link. На практиці це не проблема, бо шляхи всередині `template/` достатньо унікальні.
- Сепаратор шляхів — платформозалежний (`path.join`/`path.relative` використовують `\` на Windows). Усередині `.mdc` посилання, як правило, пишуть зі слешем `/` — на Windows це може давати хибно-позитивні результати (тобто файл буде вважатися «без посилання»). У цільовому оточенні (macOS/Linux/Docker) проблема не виникає.

### Складність

- Час: O(N + M), де N — сумарна кількість файлів у `template/`-каталогах, а M — довжина тексту `.mdc` (через `String.prototype.includes`, що виконується для кожного з N файлів двічі).
- Пам'ять: лінійна щодо розміру `.mdc` плюс масиву шляхів.

## Rebuild Test

Файл `check-mdc-template-refs.mjs` можна повністю відновити, дотримуючись опису вище, якщо забезпечити такі властивості:

1. ES-модуль (`.mjs`), без `default export`. Єдиний `export` — `async function findMissingMdcRefs(ruleDir, ruleId)`.
2. Імпорти лише з `node:fs` (`existsSync`), `node:fs/promises` (`readdir`, `readFile`, `stat`) і `node:path` (`join`, `relative`).
3. Дві внутрішні async-функції: `walkTemplateDirs(ruleDir)` та `collectFiles(dir)`.
4. `walkTemplateDirs` ітерується по двох категоріях у фіксованому порядку: спочатку `'fix'`, потім `'policy'`. Пропускає неіснуючі `kindDir`. Для кожного `concern` перевіряє існування й те, що `template` — каталог; у разі успіху делегує до `collectFiles`. На виході конвертує абсолютні шляхи у відносні від `ruleDir`.
5. `collectFiles` використовує `readdir(dir, { withFileTypes: true })`, рекурсивно занурюється тільки в каталоги, інші записи додає в результат як абсолютні шляхи.
6. `findMissingMdcRefs` спочатку перевіряє існування `<ruleId>.mdc`, читає його у UTF-8, отримує всі файли через `walkTemplateDirs`, потім фільтрує, залишаючи лише ті `rel`, для яких у тексті `.mdc` ОДНОЧАСНО немає `./<rel>` і немає `(<rel>)` (логічне AND через подвійне заперечення з `||` під `!`).
7. Поведінка строго read-only: жодних запитів у мережу, жодного запису на диск, жодного консольного виводу.
8. Повертає `Promise<string[]>`, відносні шляхи (з POSIX/платформозалежним сепаратором у міру `path.relative`).

Тестові сценарії для smoke-перевірки:

- Каталог без `.mdc` → `[]`.
- `.mdc` є, але `fix/`, `policy/` відсутні → `[]`.
- `fix/x/template/a.txt` існує, у `.mdc` згадано `./fix/x/template/a.txt` → `[]`.
- Той самий шаблон, але `.mdc` посилається як `(fix/x/template/a.txt)` (без `./`) → `[]`.
- Той самий шаблон, але в `.mdc` посилання немає → `['fix/x/template/a.txt']`.
- Вкладений `fix/x/template/sub/b.txt`, посилання немає → містить `'fix/x/template/sub/b.txt'`.
- Запис `concern`, що насправді є файлом, а не каталогом → пропускається без помилки.
