# coverage-fix.mjs

## Огляд

Файл `npm/scripts/coverage-fix.mjs` — це ESM-модуль, який реалізує fix-режим команди `n-cursor coverage --fix`. Його призначення — автоматично запустити агента Claude Code (через офіційний SDK `@anthropic-ai/claude-agent-sdk`) і доручити йому дописати unit-тести, які «вб'ють» вцілілих мутантів, знайдених попереднім прогоном Stryker.

Модуль не виконує власних мутацій коду й не аналізує покриття самостійно — він є тонким адаптером між інвентарем вцілілих мутантів (структури `SurvivedFileGroup[]`, отримані ззовні) і LLM-агентом: формує rich-промпт з контекстом ±3 рядки навколо кожного мутанта, опційно додає приклад уже існуючого тесту, після чого ітерує по стрімінгу повідомлень агента та виводить його текстові реплики у `stdout`.

Файл задумано як єдину публічну точку входу для CLI-флоу `coverage --fix`: викликається з оркестратора `n-cursor coverage`, який групує мутанти Stryker по source-файлах і передає сюди готову структуру разом з абсолютним шляхом до кореня проєкту.

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

| Експорт                                     | Тип              | Призначення                                                                                                                          |
| ------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `fixSurvivedMutants(survived, projectRoot)` | `async function` | Публічна функція. Запускає агента, проганяє промпт через `@anthropic-ai/claude-agent-sdk` і друкує текстові повідомлення в `stdout`. |

Внутрішні символи (не експортуються):

| Символ                                  | Тип              | Призначення                                                                                                      |
| --------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------- |
| `buildFixPrompt(survived, projectRoot)` | `async function` | Формує текстовий rich-промпт для агента: список мутантів по файлах, контекст з source, приклади тестів, правила. |

JSDoc-типи, оголошені у файлі:

- `MutantDetail` — `{ line: number, col: number, mutantType: string, original: string, replacement: string }` — опис одного вцілілого мутанта.
- `SurvivedFileGroup` — `{ file: string, mutants: MutantDetail[], exampleTest: { testFile: string, code: string | null } | null, recommendationText: string | null }` — група мутантів, об'єднаних спільним source-файлом, разом з опційним прикладом існуючого тесту й текстовою рекомендацією.

## Функції

### `fixSurvivedMutants(survived, projectRoot)`

**Сигнатура:** `async function fixSurvivedMutants(survived: SurvivedFileGroup[], projectRoot: string): Promise<void>`

**Параметри:**

- `survived` — масив груп вцілілих мутантів, згрупованих по source-файлу. Кожна група містить шлях до файлу, перелік мутантів, опційний приклад тесту й опційну текстову рекомендацію.
- `projectRoot` — абсолютний шлях до кореня проєкту. Використовується одночасно як `cwd` для агента і як база для читання source-файлів при підготовці контексту в промпті.

**Що повертає:** `Promise<void>`. Корисний результат — побічний ефект: створені/оновлені тести у файловій системі (їх пише агент через інструменти `Edit`/`Bash`), а також лог у `stdout`.

**Алгоритм:**

1. Обчислює загальну кількість мутантів: `survived.reduce((s, g) => s + g.mutants.length, 0)`.
2. Якщо загалом 0 мутантів — друкує `✓ Всі мутанти вбиті — доповнення тестів не потрібне` і повертається (без запуску агента, без імпорту SDK).
3. Викликає `buildFixPrompt(survived, projectRoot)` і отримує текст промпту.
4. Друкує заголовок `🤖 coverage --fix: запускаю агента для N вцілілих мутантів...`.
5. Робить **динамічний** `await import('@anthropic-ai/claude-agent-sdk')` (щоб не вимагати пакет у звичайному coverage-прогоні без `--fix`) і дістає функцію `query`.
6. Запускає агента викликом `query({ prompt, options: { cwd: projectRoot, maxTurns: 20, allowedTools: ['Read', 'Edit', 'Bash'] } })`.
7. Ітерує отриманий `AsyncIterable` через `for await (const msg of …)`. Для повідомлень з `msg.type === 'text'` друкує `msg.text` у `process.stdout` (без додаткового переносу рядка). Інші типи повідомлень (наприклад, tool_use, tool_result) — мовчазно ігноруються.
8. Після завершення ітерації друкує фінальний `\n`.

**Опції агента:**

- `cwd: projectRoot` — поточна тека агента, від якої він читає файли й запускає команди.
- `maxTurns: 20` — обмеження на кількість turns у діалозі з моделлю, щоб уникнути нескінченних циклів.
- `allowedTools: ['Read', 'Edit', 'Bash']` — агент має право читати файли, редагувати їх (фактично — тест-файли, бо source заборонено правилом у промпті) і виконувати shell-команди (зокрема `bun test`).

**Side effects:**

- Запис у `process.stdout` (`console.log` + `process.stdout.write`).
- Динамічний імпорт зовнішнього пакету `@anthropic-ai/claude-agent-sdk` — у разі його відсутності `await import(…)` кине помилку (її обробка делегована викликачу).
- Мережеві виклики до API Anthropic (виконує сам SDK).
- Зміни у файловій системі проєкту: створення/редагування тест-файлів та потенційне виконання тестових команд агентом через інструмент `Bash`.

**Граничні випадки:**

- `totalMutants === 0` — рання терміновість без імпорту SDK.
- Помилка `import('@anthropic-ai/claude-agent-sdk')` (пакет не встановлений) — пробрасується нагору; цей файл її не ловить.
- Помилка з боку стріму `query(...)` — пробрасується нагору; final `\n` не друкується.

### `buildFixPrompt(survived, projectRoot)` (internal)

**Сигнатура:** `async function buildFixPrompt(survived: SurvivedFileGroup[], projectRoot: string): Promise<string>`

**Параметри:** ті самі семантики, що у `fixSurvivedMutants`.

**Що повертає:** текст rich-промпту для агента.

**Алгоритм:**

1. Готує пустий масив `sections`.
2. Для кожної групи `{ file, mutants, exampleTest }`:
   1. Намагається прочитати source-файл `join(projectRoot, file)` у `utf8` і розбити на рядки. У разі помилки (`catch {}`) — `srcLines` лишається `[]`, і контекст просто не додасться.
   2. Для кожного мутанта в групі формує опис:
      - `ctxStart = Math.max(0, m.line - 4)` — індекс початку контексту (0-based, бо рядки в редакторі 1-based; `m.line - 4` дає 3 рядки до мутанта).
      - `ctxEnd = Math.min(srcLines.length, m.line + 3)` — індекс кінця (виключний), 3 рядки після мутанта.
      - `context` — зріз `srcLines.slice(ctxStart, ctxEnd)`, де кожен рядок префіксується абсолютним номером `${ctxStart + i + 1}: ${l}` і з'єднується через `\n`.
      - Опис мутанта — markdown-bullet з полями: рядок, колонка, тип мутації (`m.mutantType` у backticks), оригінал (`m.original`), вцілілий варіант (`m.replacement`), а нижче — fenced code block з контекстом (якщо контекст не порожній). Порожні елементи фільтруються через `.filter(Boolean)`.
   3. Якщо є `exampleTest.code` — додає окрему секцію `Приклад тесту з \`${exampleTest.testFile}\``з fenced`js`-блоком. Інакше `exampleSection` — порожній рядок.
   4. Заштовхує у `sections` рядок виду:
      ```
      ### `<file>`<exampleSection>
      <mutantDescriptions>
      ```
3. Повертає одну склеєну рядкову відповідь, що складається з:
   - Заголовка-завдання (4 рядки): «Твоє завдання — написати unit-тести…» з конкретною інструкцією: знайти/створити тест-файл, додати кейс, що провалиться при заміні коду на «вцілілий варіант».
   - Розділу `## Вцілілі мутанти` зі склеєними `sections`.
   - Розділу `## Правила`:
     - «Не змінюй source-файли — лише test-файли.»
     - «Використовуй той самий test-фреймворк, що вже в проєкті.»
     - «Запусти `bun test` (або відповідну команду) після кожного файлу — переконайся, що 0 fail.»
     - «Якщо мутант охоплений іншим новим тестом — не дублюй.»

**Side effects:** читання source-файлів через `fs/promises.readFile`. Помилки читання (відсутній файл, нечитабельний шлях) тихо проковтуються — функція гарантує, що повертає валідний промпт навіть для повністю відсутніх файлів (просто без контексту).

**Граничні випадки:**

- `srcLines.length === 0` — `context` стає порожнім рядком, fenced-блок з контекстом не додається до bullet.
- `m.line` поза межами файлу — `slice` поверне порожній зріз; жодного винятку.
- `exampleTest === null` або `exampleTest.code === null/undefined` — `exampleSection` залишається порожнім (через `?.code` optional chaining).
- `mutants.length === 0` у групі — група все одно потрапить у `sections` з заголовком і порожнім тілом (виклична сторона зазвичай не подає пусті групи, бо `fixSurvivedMutants` ще раніше відсіче випадок `totalMutants === 0`).

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

**Зовнішні (npm):**

- `@anthropic-ai/claude-agent-sdk` — офіційний SDK Claude Code, експортує функцію `query()` з async-generator-інтерфейсом для стрімінгу повідомлень агента. Завантажується через **dynamic import**, виключно в момент виклику `fixSurvivedMutants` з ненульовим списком мутантів. Декларується у `dependencies` файлу `npm/package.json` (як зазначено у JSDoc-шапці модуля).

**Node.js stdlib:**

- `node:fs/promises` — використовується `readFile` для читання source-файлів при формуванні контексту в промпті.
- `node:path` — використовується `join` для зчеплення `projectRoot` з відносним шляхом файлу мутанта.

**Глобальні API середовища:**

- `console.log` — друк інформаційних рядків (старт/пропуск).
- `process.stdout.write` — потоковий друк reply-чанків агента (без auto-newline).

**Внутрішні залежності проєкту:**

- Модуль не імпортує інших файлів з `npm/scripts/` напряму, але формат вхідної структури `SurvivedFileGroup` визначається сусідніми скриптами в каталозі `npm/scripts/` (там, де живе оркестратор `coverage`, який збирає й групує мутантів Stryker і передає сюди готовий масив).

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

Типовий ланцюжок:

1. CLI `n-cursor coverage --fix` (зовнішня обгортка) запускає Stryker і збирає список вцілілих мутантів.
2. Оркестратор групує мутантів по `file`, готує `exampleTest` (опційно) і викликає:

   ```js
   import { fixSurvivedMutants } from './npm/scripts/coverage-fix.mjs'

   await fixSurvivedMutants(survived, projectRoot)
   ```

3. `fixSurvivedMutants`:
   - Перевіряє, що мутанти взагалі є; якщо ні — друкує success-повідомлення і виходить.
   - Будує промпт через `buildFixPrompt` (читання source-файлів для контексту відбувається саме тут).
   - Динамічно імпортує `@anthropic-ai/claude-agent-sdk`.
   - Стрімить агента з обмеженнями: `cwd = projectRoot`, до 20 turns, інструменти `Read | Edit | Bash`.
   - Друкує всі текстові реплики у `stdout` у реальному часі.
4. Агент за правилами промпту читає source-файли (лише для розуміння), знаходить/створює відповідні test-файли, дописує кейси, запускає `bun test` і повторює, доки `maxTurns` або модель не вирішить, що готово.
5. Після завершення стріму `fixSurvivedMutants` дописує фінальний `\n` і повертає `void`.

**Принципи дизайну, що випливають із коду:**

- **Lazy SDK loading.** Імпорт SDK — динамічний, щоб звичайний `coverage` (без `--fix`) не вимагав встановленого `@anthropic-ai/claude-agent-sdk` і не платив за його завантаження.
- **Ізоляція знань про Stryker.** Цей файл нічого не знає про сам Stryker — він працює з уже нормалізованою структурою `SurvivedFileGroup[]`; уся інтеграція зі Stryker лежить в іншому модулі-оркестраторі.
- **No source mutation by agent.** Жорстке правило в промпті («Не змінюй source-файли — лише test-файли») задає очікувану поведінку агента; додатково обмежений набір інструментів (`Read | Edit | Bash`) дає змогу йому редагувати тести й запускати тестову команду, але не лазити в систему по непотрібному.
- **Стрімінг тільки текстових повідомлень.** Інші типи (наприклад, tool_use / tool_result) свідомо не рендеряться, щоб лог у консолі лишався читабельним для людини.

**Приклад вхідної структури:**

```js
const survived = [
  {
    file: 'src/foo.js',
    mutants: [{ line: 42, col: 10, mutantType: 'ConditionalExpression', original: 'a > b', replacement: 'a >= b' }],
    exampleTest: { testFile: 'src/foo.test.js', code: "test('foo', () => { /* ... */ })" },
    recommendationText: null
  }
]

await fixSurvivedMutants(survived, '/abs/path/to/project')
```

> Примітка: поле `recommendationText` присутнє в JSDoc-типі `SurvivedFileGroup`, але в поточній реалізації `coverage-fix.mjs` воно **не** використовується при формуванні промпту — резервоване для майбутнього розширення (наприклад, додавання текстових порад поряд із прикладом тесту).
