# lint.mjs

## Огляд

Файл `lint.mjs` — це CI-крок правила `js-lint-ci`, який реалізує крос-файлову статичну перевірку JavaScript-/TypeScript-кодової бази. Він послідовно запускає два інструменти, що працюють лише на рівні всього репозиторію (а не на рівні окремих файлів):

1. **jscpd** — детектор дубльованого коду (copy-paste detector). Знаходить ідентичні або дуже схожі блоки коду між файлами.
2. **knip** — детектор «мертвого» коду: невикористаних експортів, файлів, залежностей.

Модуль експортує одну функцію `lint`, яка ігнорує переданий список файлів (через природу інструментів — вони сканують усе репо) і повертає ненульовий код виходу при першому ж порушенні. Це класичний «fail-fast» патерн: якщо `jscpd` знайшов клони — `knip` навіть не запускається, репорт першого інструмента залишається на stdout/stderr CI-логу.

Файл є частиною контракту правил `n-cursor` для лінтингу: правило з суфіксом `-ci` означає, що воно виконується лише в `lint-ci`-фазі (на CI або при явному виклику), а не в інкрементальному per-file lint-сценарії.

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

| Експорт | Тип                       | Призначення                                                               |
| ------- | ------------------------- | ------------------------------------------------------------------------- |
| `lint`  | `function` (named export) | Єдина точка входу — виконує jscpd → knip і повертає підсумковий exit code |

Default-експорту немає. Контракт named-експорту `lint` — обов'язковий для всіх файлів `rules/<rule>/js/lint.mjs` у системі правил `n-cursor`.

## Функції

### `lint(_files, cwd)`

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

```js
export function lint(_files, cwd = process.cwd()): Promise<number>
```

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

| Параметр | Тип                     | За замовчуванням | Опис                                                                                                                                                                                                                                                           |
| -------- | ----------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `_files` | `string[] \| undefined` | —                | Список файлів для перевірки. **Ігнорується** — і jscpd, і knip працюють лише по всьому репозиторію. Префікс `_` у назві сигналізує, що аргумент не використовується. Згідно з коментарем-шапкою, функція викликається лише з `lint-ci` і завжди з `undefined`. |
| `cwd`    | `string`                | `process.cwd()`  | Корінь репозиторію — робоча директорія, з якої запускаються `bunx jscpd .` та `bunx knip`. Усі шляхи всередині інструментів резолвляться відносно `cwd`.                                                                                                       |

#### Що повертає

`Promise<number>` — резолвиться з exit code:

- `0` — обидва інструменти завершилися успішно, порушень не знайдено.
- Ненульове число (`jscpd.status` або `knip.status`) — перший інструмент, що завершився з помилкою, диктує підсумковий код виходу.
- `1` — fallback, коли `spawnSync` не повернув числовий `status` (наприклад, процес було вбито сигналом і `.status` дорівнює `null`).

Хоча функція синхронно виконує дочірні процеси через `spawnSync`, результат загорнуто у `Promise.resolve(...)` — це уніфікує контракт із асинхронними `lint`-функціями інших правил, де можуть використовуватись `spawn` чи інший асинхронний код.

#### Логіка крок-за-кроком

1. Запустити `bunx jscpd .` у директорії `cwd` з `stdio: 'inherit'` — увесь вивід інструмента йде безпосередньо в термінал/CI-лог.
2. Нормалізувати `jscpd.status`: якщо це число — взяти як є; інакше — `1`.
3. Якщо `jc !== 0` — **рано вийти** з цим кодом, не запускаючи knip.
4. Інакше — запустити `bunx knip --no-config-hints` (прапор пригнічує підказки про конфіг, щоб лог був чистішим).
5. Повернути `Promise.resolve(knip.status ?? 1)` за тим же правилом нормалізації.

#### Side effects

- **Запуск дочірніх процесів**: `spawnSync('bunx', ...)` блокує поточний потік до завершення процесу. Це навмисно — `lint` повинен дочекатися результату й повернути exit code.
- **Запис у stdout/stderr**: завдяки `stdio: 'inherit'` весь вивід jscpd і knip потрапляє в термінал викликаючого процесу. Жодних буферизованих рядків `lint` не повертає.
- **Файлова система — лише читання**: ні jscpd, ні knip із наведеними прапорами не модифікують код; вони генерують репорти в stdout (jscpd за замовчуванням також може писати HTML-репорт у `report/` директорію — це визначається його конфігом, не цим файлом).
- **Залежності від оточення**: `bunx` повинен бути доступний у `PATH` процесу. Якщо `bunx` відсутній, `spawnSync` поверне `status: null` і `error` — функція віддасть `1`.

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

### Stdlib (Node.js / Bun)

- `node:child_process` — використовується іменований імпорт `spawnSync`. Це синхронний варіант `spawn`, який повертає об'єкт із полями `status`, `signal`, `stdout`, `stderr`, `error` після завершення дочірнього процесу.

### Зовнішні CLI-інструменти (запускаються через `bunx`)

- **`jscpd`** — Copy/Paste Detector. Запускається з аргументом `.` (поточна директорія = `cwd`). Конфіг (правила обходу, пороги схожості, ігнор-патерни) інструмент шукає сам у `.jscpd.json`, `package.json#jscpd` тощо.
- **`knip`** — Detect unused files, dependencies and exports. Запускається з прапором `--no-config-hints`, який вимикає підказки про відсутній/неоптимальний конфіг.

### Runtime-залежності

- **`bunx`** — раннер, що йде в комплекті з Bun і виконує бінарники з node_modules або тимчасово завантажує пакет, якщо його немає локально.

### Не імпортується

- Жодних інших модулів проекту цей файл не використовує — він свідомо мінімалістичний і не залежить від інфраструктури `n-cursor` (логерів, конфіг-парсерів, fs-хелперів).

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

### Контекст виклику

Функція `lint` із цього файлу викликається механізмом правил `n-cursor` (зокрема, командою `lint-ci`). Конвенція така: для кожного правила `rules/<rule-id>/js/lint.mjs` рушій імпортує named export `lint(files, cwd)` і викликає його. Для правила `js-lint-ci` другий аргумент — корінь репо, а перший — завжди `undefined`, бо це крос-файловий аналіз.

### Псевдокод виклику ззовні

```js
import { lint } from './npm/rules/js-lint-ci/js/lint.mjs'

const code = await lint(undefined, '/path/to/repo')
if (code !== 0) process.exit(code)
```

### Сценарій 1: усе чисто

1. `bunx jscpd .` → exit 0, у логи виводиться репорт без знайдених клонів.
2. `bunx knip --no-config-hints` → exit 0, у логи — порожній/чистий звіт.
3. `lint` повертає `Promise.resolve(0)`.
4. CI-крок проходить.

### Сценарій 2: jscpd знайшов клони

1. `bunx jscpd .` → exit ≠ 0, у логи — список клонованих блоків.
2. `lint` рано виходить із цим кодом, **knip не запускається**.
3. CI-крок падає. Розробник бачить лише репорт jscpd.

### Сценарій 3: jscpd чистий, knip знайшов мертвий код

1. `bunx jscpd .` → exit 0.
2. `bunx knip --no-config-hints` → exit ≠ 0, у логи — список невикористаних експортів/файлів.
3. `lint` повертає цей код.
4. CI-крок падає з репортом knip.

### Сценарій 4: інструмент крашнувся (сигнал, відсутній bunx)

1. `spawnSync(...).status` повертає `null` (`signal` буде заповнений).
2. Перевірка `typeof ... === 'number'` дає `false` → fallback `1`.
3. `lint` повертає `1`. CI бачить generic-fail без чіткого репорту — це слабке місце, але навмисно зведене до мінімуму.

### Чому ігнорується `_files`

Файлово-орієнтовані аналізатори (eslint, stylelint, ...) працюють і per-file, і crosswise. Натомість `jscpd` і `knip` принципово міжфайлові: щоб знайти дубль або невикористаний експорт, треба бачити **всі** файли одночасно. Тому виконувати їх на підмножині не має сенсу — і per-file шлях для цього правила відсутній. Це підкреслено й у JSDoc, і в підкресленні `_` у назві аргумента.

### Спостережувані виходи

- **Логи**: jscpd і knip пишуть напряму в stdio викликаючого процесу через `stdio: 'inherit'`. `lint` нічого не агрегує.
- **Exit code**: єдиний канал зворотного зв'язку від `lint` до раннера — числовий код.

## Rebuild Test

Якщо реалізовувати файл наново за цією документацією, він має:

1. Імпортувати `spawnSync` із `node:child_process`.
2. Експортувати named function `lint(_files, cwd = process.cwd())`.
3. Запустити `bunx jscpd .` синхронно з `cwd` і `stdio: 'inherit'`.
4. Нормалізувати exit (`typeof status === 'number' ? status : 1`) і рано вийти при ≠ 0.
5. Інакше — запустити `bunx knip --no-config-hints` за тим же протоколом і повернути нормалізований exit.
6. Загорнути результат у `Promise.resolve(...)`.
7. Ігнорувати перший аргумент (префікс `_`).
8. Не імпортувати жодних інших модулів і не виконувати ніяких записів у файлову систему.
