# tooling.mjs — перевірка вимог правила `php.mdc`

## Огляд

Модуль `tooling.mjs` реалізує локальну перевірку відповідності PHP-проєкту вимогам внутрішнього правила `php.mdc`. Це частина системи правил Cursor: під кожне правило існує парний `check`-модуль, що повертає exit-code, придатний для запуску в CI або з CLI.

Цей конкретний модуль покриває **лише ту частину вимог, яку неможливо (або недоречно) перевірити через Rego/conftest** — а саме факт існування файлів у файловій системі:

- наявність `composer.json` у корені репозиторію;
- наявність `package.json` у корені (його структуру, зокрема скрипт `lint-php`, перевіряє вже Rego-політика `npm/policy/php/package_json/`);
- наявність файлу GitHub Actions workflow `.github/workflows/lint-php.yml` (його вміст, зокрема крок `run: bun run lint-php`, перевіряє Rego-політика `npm/policy/php/lint_php_yml/`).

Тобто `tooling.mjs` — це **FS-existence чек**. Структурний аналіз JSON/YAML делегований на `npx @nitra/cursor check` (Rego). Така декомпозиція явно прописана в JSDoc-заголовку файлу.

Модуль є **ESM** (`.mjs`), не має побічних ефектів на рівні імпорту: уся робота виконується всередині експортованої функції `check()`. Робоча директорія — поточна (`process.cwd()`), оскільки всі шляхи передаються в `existsSync` як відносні.

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

| Експорт   | Тип        | Призначення                                                        |
| --------- | ---------- | ------------------------------------------------------------------ |
| `check()` | `function` | Запускає перевірку правила `php.mdc` у поточній робочій директорії |

Інших експортів немає (default-експорту немає). Імпорт здійснюється як іменований:

```js
import { check } from './tooling.mjs'
```

## Функції

### `check()`

**Сигнатура:**

```js
export function check(): number
```

**Параметри:** немає.

**Повертає:** `number` — exit-code:

- `0` — усі перевірки пройшли (`composer.json`, `package.json` та `.github/workflows/lint-php.yml` існують у поточній директорії);
- `1` — хоча б одна перевірка провалилася (`fail` був викликаний принаймні один раз).

Конкретне значення exit-code формує об’єкт-репортер через `reporter.getExitCode()` — модуль `tooling.mjs` не визначає логіку підрахунку самостійно, а делегує її в `createCheckReporter()`.

**Алгоритм (по кроках):**

1. Створює локальний репортер: `const reporter = createCheckReporter()`.
2. Розпаковує методи `pass` і `fail` з репортера: `const { pass, fail } = reporter`.
3. Перевіряє наявність `composer.json` у CWD:
   - якщо файл існує → `pass('composer.json існує')`;
   - інакше → `fail('composer.json не знайдено в корені — додай (php.mdc)')`.
4. Перевіряє наявність `package.json` у CWD:
   - якщо файл існує → `pass('package.json є (наявність lint-php перевіряє npx @nitra/cursor fix → php.package_json)')`;
   - інакше → `fail('package.json не знайдено в корені — додай (php.mdc)')`.
5. Перевіряє наявність workflow-файлу `.github/workflows/lint-php.yml`:
   - якщо файл існує → `pass(`${wfPath} є (структуру перевіряє npx @nitra/cursor fix → php.lint_php_yml)`)`;
   - інакше → `fail(`${wfPath} не існує — створи згідно php.mdc`)`.
6. Повертає `reporter.getExitCode()`.

**Side effects:**

- **Файлова система:** виключно **читання** (`existsSync`). Жодних записів/створень файлів.
- **Stdout/stderr:** опосередковано через `pass`/`fail` репортера. Сам модуль не звертається напряму до `console.log` / `process.stdout`. Конкретний канал, формат і обсяг виводу визначає реалізація `createCheckReporter()`.
- **Process exit:** функція **не викликає** `process.exit()` самостійно — вона лише повертає число. Виклик `process.exit(check())` (або еквівалент) — відповідальність викликаючої сторони.
- **Глобальний стан:** не змінюється.
- **CWD-залежність:** усі шляхи відносні, тому результат залежить від поточної робочої директорії на момент виклику.

**Помилки/винятки:** функція не використовує `try/catch`. `existsSync` синхронна та не кидає виняток для відсутніх файлів — повертає `false`. Винятки можуть прилетіти лише з імпортованих залежностей (`createCheckReporter`), якщо ті змінять контракт; цей модуль їх не обробляє.

**Ідемпотентність:** так, виклик не змінює стан системи; повторні виклики дадуть той самий результат за тих самих файлів у CWD.

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

### Зовнішні (Node.js builtin)

- **`node:fs`** → іменований імпорт `existsSync`. Використовується для синхронної перевірки наявності файлу/директорії за вказаним шляхом. Імпортується саме через префікс `node:` (явна вказівка на builtin-модуль).

### Внутрішні (відносні)

- **`../../../scripts/lib/check-reporter.mjs`** → іменований імпорт `createCheckReporter`. Фабрика репортера для check-модулів. Очікуваний контракт (виведений із використання в цьому файлі):
  - повертає об’єкт із методами `pass(message: string): void` та `fail(message: string): void`;
  - має метод `getExitCode(): number`, який повертає `0` за відсутності провалів і `1` (або інше ненульове) за наявності хоча б одного `fail`.

Шлях `../../../scripts/lib/check-reporter.mjs` відраховується від розташування файлу `npm/rules/php/js/tooling.mjs` й веде до `npm/scripts/lib/check-reporter.mjs`.

### Зв’язки з правилами та політиками (контекст екосистеми)

Модуль явно посилається на сусідні артефакти, які виконують споріднену перевірку (вони **не** імпортуються кодом, але є частиною повного контракту правила `php.mdc`):

- `npm/policy/php/package_json/` — Rego-політика, що перевіряє наявність скрипта `lint-php` у `package.json`;
- `npm/policy/php/lint_php_yml/` — Rego-політика, що перевіряє крок `run: bun run lint-php` у workflow `lint-php.yml`;
- запускається разом із цим модулем через CLI `npx @nitra/cursor check`.

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

### Очікуваний контекст запуску

Файл є **check-модулем правила `php.mdc`** і інтегрується в систему `@nitra/cursor`. Виклик відбувається з кореня PHP-проєкту, де очікуються:

- `composer.json` у корені;
- `package.json` у корені (зі скриптом `lint-php`);
- `.github/workflows/lint-php.yml` із кроком, який виконує `bun run lint-php`.

### Типовий програмний виклик

```js
import { check } from './npm/rules/php/js/tooling.mjs'

const code = check()
process.exit(code)
```

### Сценарій 1 — усе на місці

CWD містить `composer.json`, `package.json` і `.github/workflows/lint-php.yml`. Репортер отримує три `pass`-повідомлення, жодного `fail`. `check()` повертає `0`.

### Сценарій 2 — немає одного з файлів

Наприклад, відсутній `.github/workflows/lint-php.yml`. Репортер отримує два `pass` і один `fail` із вказівкою «`.github/workflows/lint-php.yml` не існує — створи згідно php.mdc». `check()` повертає `1`.

### Сценарій 3 — порожня директорія

Жоден із трьох файлів не існує. Репортер отримує три `fail` (із підказками щодо `php.mdc`), `check()` повертає `1`. Усі три перевірки виконуються незалежно — функція не виходить на першому ж провалі, тому користувач бачить **повний** список проблем за один прогін.

### Обмеження

- Перевіряється **лише існування** файлів — не їхній уміст. Аналіз структури `package.json` / `lint-php.yml` належить Rego-політикам, які запускаються окремо (`npx @nitra/cursor check`).
- Не перевіряється, що `composer.json` — валідний JSON або містить обов’язкові поля.
- Не перевіряється наявність директорії `vendor/`, версії PHP чи що-небудь, пов’язане з виконанням Composer.
- Усі шляхи відносні до `process.cwd()`. Виклик з іншої директорії дасть інший результат.

### Місце в архітектурі правил

```
npm/
├── rules/
│   └── php/
│       └── js/
│           └── tooling.mjs        ← цей файл (FS-existence checks)
├── policy/
│   └── php/
│       ├── package_json/          ← Rego: lint-php у package.json
│       └── lint_php_yml/          ← Rego: крок bun run lint-php у workflow
└── scripts/
    └── lib/
        └── check-reporter.mjs     ← фабрика репортера (createCheckReporter)
```

Така структура відображає принцип розділення: JS-модулі правил роблять рівно те, що **не покривається** Rego/conftest (зазвичай — операції з файловою системою, виклики зовнішніх CLI, динамічні перевірки), а декларативні перевірки виносяться в Rego-політики.
