---
docgen:
  source: npm/scripts/lib/mirror-parity.mjs
  crc: 15dc336a
---

## Огляд

Модуль `mirror-parity.mjs` забезпечує перевірку парності (parity) між «дзеркальними» файлами правил Cursor у `.cursor/rules/n-<id>.mdc` та їх канонічними джерелами у `npm/rules/<id>/<id>.mdc`. Ідея така: репозиторій містить два представлення одного й того ж правила:

- канонічне джерело правила лежить у `npm/rules/<id>/<id>.mdc` (з можливими посиланнями на шаблони у тій самій теці);
- дзеркало лежить у `.cursor/rules/n-<id>.mdc` і повинно бути результатом застосування трансформу `inlineTemplateLinks` до канону (тобто посилання на шаблони мають бути вставлені «інлайном»).

Коли канонічний `.mdc` змінюють і забувають регенерувати дзеркало, виникає «дрейф» (drift). Цей модуль:

- перелічує керовані дзеркала (ті, для яких знайдено канон);
- обчислює очікуваний вміст дзеркала через ту саму трансформацію, що і синхронізатор;
- виявляє ідентифікатори дрейфних правил (де `actual ≠ expected`).

Модуль використовується одночасно і як гард у тестах (очікувано `drift === []`), і для разової регенерації дзеркал. Контекст беклогу — адаптація flow #10.

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

Модуль експортує три іменовані функції:

- `listManagedMirrors(repoRoot)` — синхронна; повертає опис керованих дзеркал.
- `expectedMirrorContent(canonicalPath)` — асинхронна (повертає `Promise<string>`); обчислює очікуваний вміст дзеркала.
- `findMirrorDrift(repoRoot)` — асинхронна; повертає масив id правил із дрейфом, відсортований за зростанням.

Жодних дефолтних експортів, побічних ефектів на рівні модуля немає (лише імпорти стандартних модулів і одного локального).

## Функції

### `listManagedMirrors(repoRoot)`

Сигнатура: `function listManagedMirrors(repoRoot: string): { id: string, mirrorPath: string, canonicalPath: string }[]`

Параметри:

- `repoRoot` — абсолютний шлях до кореня репозиторію (директорія, у якій лежать `.cursor/` та `npm/`).

Повертає масив об'єктів-описів дзеркал, де:

- `id` — ідентифікатор правила без префіксу `n-` і без розширення `.mdc` (наприклад, для файлу `n-bun.mdc` → `bun`);
- `mirrorPath` — абсолютний шлях до файлу дзеркала всередині `.cursor/rules/`;
- `canonicalPath` — абсолютний шлях до канонічного `.mdc` всередині `npm/rules/<id>/`.

Алгоритм:

1. Будує шлях до `.cursor/rules`.
2. Якщо такої директорії не існує (`existsSync` false) — повертає порожній масив (раннє повернення, безпечне для свіжих репо).
3. Зчитує перелік файлів директорії синхронно (`readdirSync`).
4. Фільтрує файли: повинні починатися з префіксу `'n-'` (константа `MIRROR_PREFIX`) і завершуватися розширенням `'.mdc'` (константа `MDC_EXT`).
5. Мапить кожен файл у об'єкт із обчисленими шляхами: `id` отримується вирізанням префіксу і розширення (`f.slice(MIRROR_PREFIX.length, -MDC_EXT.length)`).
6. Залишає лише ті записи, для яких реально існує канонічне джерело `canonicalPath` (друга фільтрація через `existsSync`). Це дозволяє пропускати «зовнішні» дзеркала, які не мають канону в монорепо.

Side effects: лише читання директорії з диску (синхронні `existsSync`, `readdirSync`). Жодних записів, мережевих викликів, мутацій глобального стану.

Помилки: якщо файлова система кине помилку під час `readdirSync` (наприклад, права доступу) — вона прокинеться вгору; для типового сценарію відсутності директорії `.cursor/rules` функція не падає (гард через `existsSync`).

### `expectedMirrorContent(canonicalPath)`

Сигнатура: `function expectedMirrorContent(canonicalPath: string): Promise<string>`

Параметри:

- `canonicalPath` — абсолютний шлях до канонічного `.mdc`-файлу (`npm/rules/<id>/<id>.mdc`).

Повертає `Promise<string>` — очікуваний (нормалізований) текст дзеркала після інлайнінгу шаблонів. Хоча сама функція не використовує `async`/`await`, її результат типізовано як `Promise`, оскільки делегує виклик до `inlineTemplateLinks`, яка є асинхронною (її результат може бути thenable). Це означає, що споживач має `await`-нути повернене значення.

Алгоритм:

1. Синхронно зчитує вміст канонічного файлу як UTF-8 (`readFileSync(canonicalPath, 'utf8')`).
2. Викликає `inlineTemplateLinks(content, dirname(canonicalPath))`, передаючи прочитаний текст та теку канону як base для розв'язання шляхів до шаблонів.
3. Повертає результат трансформації.

Side effects: одне читання з диску. Подальша поведінка щодо шаблонів інкапсульована в `inlineTemplateLinks` (див. її документацію). Контракт: цей же самий трансформ застосовує синк, тому очікуваний вміст детермінований по канону.

Помилки: якщо файл за `canonicalPath` не існує або недоступний — `readFileSync` кине помилку. Викличні сторони мають гарантувати існування (`listManagedMirrors` уже фільтрує неіснуючі канони).

### `findMirrorDrift(repoRoot)`

Сигнатура: `async function findMirrorDrift(repoRoot: string): Promise<string[]>`

Параметри:

- `repoRoot` — абсолютний шлях до кореня репозиторію.

Повертає `Promise<string[]>` — відсортований за зростанням масив `id` правил, де дзеркало розійшлося з очікуваним вмістом.

Алгоритм:

1. Заводить порожній масив `drift`.
2. Послідовно (через `for...of`) обходить усі керовані дзеркала, отримані з `listManagedMirrors(repoRoot)`.
3. Для кожного дзеркала `m`:
   - `await`-ить очікуваний вміст через `expectedMirrorContent(m.canonicalPath)`;
   - синхронно зчитує фактичний вміст файлу дзеркала `readFileSync(m.mirrorPath, 'utf8')`;
   - якщо стрічки не дорівнюють — додає `m.id` до `drift`.
4. Повертає `drift.sort()` (лексикографічне сортування).

Side effects: тільки читання з диску. Не записує жодних файлів, не пише в лог.

Зауваження щодо порівняння: рівність стрічок є точною (строге `!==` по UTF-8 тексту). Будь-яка різниця в пробілах, символах кінця рядка чи порядку інлайнених шаблонів буде розцінена як дрейф.

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

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

- `node:fs` — `existsSync`, `readdirSync`, `readFileSync`. Усі виклики синхронні. Імпорт зроблено через `node:`-префікс для явної прив'язки до built-in модуля.
- `node:path` — `dirname`, `join`. Використовуються для безпечного складання абсолютних шляхів і отримання батьківської теки канону.

Локальні модулі:

- `./inline-template-links.mjs` — імпорт іменованої функції `inlineTemplateLinks(content, baseDir)`. Це той самий трансформ, що його використовує синк дзеркал; саме завдяки спільному використанню досягається консистентність між «гардом дрейфу» і «генератором дзеркал».

Зовнішніх npm-залежностей немає.

Константи на рівні модуля:

- `MIRROR_PREFIX = 'n-'` — префікс імені файлів дзеркал.
- `MDC_EXT = '.mdc'` — розширення файлів правил Cursor.

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

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

1. Тест-гард парності (CI). Споживач передає корінь репо й перевіряє, що дрейф порожній:

   ```js
   import { findMirrorDrift } from './mirror-parity.mjs'

   const drift = await findMirrorDrift(repoRoot)
   if (drift.length) {
     throw new Error(`Mirror drift detected for: ${drift.join(', ')}`)
   }
   ```

2. Разова регенерація дзеркал. Споживач отримує список керованих дзеркал і для кожного перезаписує файл дзеркала очікуваним вмістом:

   ```js
   import { writeFileSync } from 'node:fs'
   import { listManagedMirrors, expectedMirrorContent } from './mirror-parity.mjs'

   for (const m of listManagedMirrors(repoRoot)) {
     const expected = await expectedMirrorContent(m.canonicalPath)
     writeFileSync(m.mirrorPath, expected)
   }
   ```

   (Сам модуль `mirror-parity.mjs` записів не виконує — це задача викличного скрипта.)

Внутрішній потік виконання `findMirrorDrift`:

1. Виклик `listManagedMirrors(repoRoot)`:
   - перевірка існування `.cursor/rules`;
   - читання списку файлів;
   - фільтрація за префіксом `n-` і розширенням `.mdc`;
   - обчислення `id`, `mirrorPath`, `canonicalPath`;
   - відкидання дзеркал без канону.
2. Послідовний обхід отриманих описів:
   - читання канону → `inlineTemplateLinks` → очікуваний текст;
   - читання дзеркала → порівняння;
   - накопичення id з дрейфом.
3. Сортування результату та повернення.

Контрактна гарантія: якщо синк використовує ту ж саму функцію `inlineTemplateLinks` для генерації дзеркал, то регенерація закриває будь-який дрейф, виявлений `findMirrorDrift`. Якщо дрейф виявлено — він завжди вказує на людську помилку (зміна канону без регенерації) або на розбіжність версій трансформу.

Обмеження:

- Обхід послідовний (без `Promise.all`) — простіший контракт по помилках, але повільніший на великих наборах правил. Зважаючи на типову кількість правил у `.cursor/rules`, це не критично.
- Усі читання файлів синхронні, попри асинхронну сигнатуру `findMirrorDrift` — асинхронність зумовлена тільки інтерфейсом `inlineTemplateLinks`.
- Префікс `n-` зашитий константою — додавання інших префіксів дзеркал вимагатиме зміни модуля.
- Зовнішні дзеркала (без канону в `npm/rules/<id>/`) автоматично пропускаються; модуль не повідомляє про них окремо.
