# trufflehog.mjs

## Огляд

Модуль `trufflehog.mjs` — це FS-частина (файлово-системна перевірка) правила `security` з родини cursor-rules монорепозиторію. Його єдина відповідальність — перевірити, що в корені перевіреного репозиторію присутні два артефакти, без яких правило `security.mdc` вважається порушеним:

1. файл `package.json` у корені — самим фактом існування (структуру цього файлу валідує окрема policy `security.package_json` через Rego/OPA, тут вона навмисно не дублюється);
2. файл `.trufflehog-exclude` у корені, який містить канонічний набір patterns. Оскільки `.trufflehog-exclude` — це plain-text формат (а не JSON/YAML), порівняння виконується методом «text-subset»: усі канонічні рядки зі snippet-шаблону мають бути присутні в репозитарному файлі, але репозиторій може мати додаткові локальні рядки.

Модуль експортує єдину async-функцію `check`, яка повертає exit-код для подальшого використання CLI-обгорткою (наприклад, скриптом-агрегатором перевірок або CI-сценарієм). Усі повідомлення про результат проходять через спільний reporter, тож формат виводу й коди виходу узгоджені з іншими `check-*` модулями цього репозиторію.

Файл вмонтований у конвенцію `npm/rules/<rule>/js/`: він навмисно не виконує мережевих або системних операцій, не пише в файли, не запускає підпроцеси — це чиста read-only перевірка артефактів у переданому або поточному cwd.

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

Модуль експортує лише one named export:

| Експорт | Тип                                                | Призначення                                                                                                      |
| ------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `check` | `async function (cwd?: string) => Promise<number>` | Запустити перевірки FS-частини правила `security` для заданого кореня репозиторію і повернути числовий exit-код. |

Default export відсутній. Жодних інших символів (констант, класів, утиліт) модуль назовні не передає; внутрішні константи `HERE` та `SNIPPET_PATH` залишаються приватними для модуля.

## Функції

### `check(cwd?)`

Головна (і єдина) експортована функція модуля.

Сигнатура:

```
async function check(cwd: string = process.cwd()): Promise<number>
```

Параметри:

| Ім'я  | Тип      | Default         | Опис                                                                                                                                                                                                                                                                |
| ----- | -------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cwd` | `string` | `process.cwd()` | Абсолютний (або відносний до процесу) шлях до кореня репозиторію, який треба перевірити. Усі шляхи до `package.json` і `.trufflehog-exclude` будують через `join(cwd, ...)`. Якщо параметр не передано — використовується поточний робочий каталог процесу Node.js. |

Повертає: `Promise<number>` — exit-код, який повертає reporter (`reporter.getExitCode()`). Конвенція reporter-а: `0` — усі перевірки `pass`, ненульове значення — є хоча б одна `fail`. Конкретне ненульове значення (1, 2 тощо) визначається реалізацією `createCheckReporter` у `scripts/lib/check-reporter.mjs` і модулем `trufflehog.mjs` не нав'язується.

Покрокова поведінка:

1. Створює локальний reporter через `createCheckReporter()` і деструктурує з нього методи `pass` та `fail` для читабельності викликів.
2. Перевіряє наявність `package.json` у корені:
   - якщо файлу немає — викликає `fail('package.json не знайдено в корені — додай (security.mdc)')` і **одразу** повертає `reporter.getExitCode()`, не виконуючи решту перевірок (це early-exit, оскільки без `package.json` решта канонів безпеки втрачає сенс);
   - якщо файл є — викликає `pass('package.json є (структуру перевіряє Rego)')` і йде далі.
3. Перевіряє наявність `.trufflehog-exclude` у корені:
   - якщо файлу немає — викликає `fail('.trufflehog-exclude не знайдено в корені — додай за каноном (security.mdc)')` і повертає exit-код (теж early-exit);
   - якщо файл є — переходить до text-subset порівняння.
4. Читає вміст обох файлів — фактичного `.trufflehog-exclude` із cwd та snippet-шаблону `templates/trufflehog/.trufflehog-exclude.snippet.txt` поряд із модулем — в `utf8`.
5. Викликає `checkTextSubset(actual, template, { targetPath: '.trufflehog-exclude', source: 'security.mdc' })`. Утиліта повертає масив рядків-повідомлень про помилки: порожній масив означає, що всі канонічні patterns з snippet-у наявні у фактичному файлі.
6. Для кожного повідомлення з `errors` викликає `fail(msg)`. Якщо `errors` порожній — викликає `pass('.trufflehog-exclude містить канонічні patterns')`.
7. Повертає `reporter.getExitCode()`.

Side effects:

- Читання файлової системи: синхронний `existsSync` (двічі) та асинхронний `readFile` (двічі);
- Запис у reporter (внутрішній акумулятор стану `pass`/`fail`), який, залежно від реалізації, може писати в `stdout`/`stderr`;
- Жодних мутацій FS, жодних мережевих викликів, жодних env-залежностей крім стандартного `process.cwd()` як default-значення параметра.

Поведінкові інваріанти:

- Функція **не** кидає винятки в нормальному сценарії «файла нема» — це штатний `fail`. Винятки можливі лише з нижчих рівнів: наприклад, якщо `readFile` не може прочитати існуючий файл через права доступу, або якщо `SNIPPET_PATH` не існує в дистрибутиві модуля (це вже баг дистрибутива, а не вхідних даних).
- Порядок перевірок фіксований: `package.json` → `.trufflehog-exclude` (наявність) → `.trufflehog-exclude` (text-subset). При фейлі на ранньому кроці наступні **не** виконуються.
- Перевірка text-subset не вимагає точної рівності файлів: snippet — це мінімум, репозиторій може містити додаткові рядки понад нього.

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

Built-in модулі Node.js:

- `node:fs` — використовується іменований імпорт `existsSync` для перевірки наявності `package.json` та `.trufflehog-exclude` без читання їхнього вмісту;
- `node:fs/promises` — іменований імпорт `readFile` для асинхронного читання текстового вмісту обох файлів у кодуванні `utf8`;
- `node:path` — `dirname` для отримання директорії поточного модуля з його URL і `join` для побудови шляхів до `package.json`, `.trufflehog-exclude` та snippet-шаблону;
- `node:url` — `fileURLToPath` для перетворення `import.meta.url` у звичайний шлях файлової системи (стандартний ESM-патерн).

Внутрішні модулі репозиторію:

- `../../../scripts/lib/check-reporter.mjs` — фабрика reporter-а `createCheckReporter`. Reporter інкапсулює стан перевірок: акумулює `pass`/`fail`-повідомлення та віддає кінцевий exit-код через `getExitCode()`. Уся семантика виводу (де і як друкується, як кодуються рівні) лежить там.
- `../../../scripts/lib/template.mjs` — утиліта `checkTextSubset(actual, template, opts)`. Перевіряє, що `template` є підмножиною `actual` (у текстовому сенсі — рядки/блоки шаблону мають зустрічатися у фактичному файлі). Опції `targetPath` та `source` використовуються для форматування повідомлень про помилки (щоб користувач бачив, який саме файл і яке правило порушено).

Файлові артефакти, які модуль читає в runtime:

- `<cwd>/package.json` — фактичний package.json репозиторію, що перевіряється (лише наявність);
- `<cwd>/.trufflehog-exclude` — фактичний exclude-файл (наявність + вміст);
- `<HERE>/templates/trufflehog/.trufflehog-exclude.snippet.txt` — канонічний snippet-шаблон, що поставляється разом із модулем. `HERE` = `dirname(fileURLToPath(import.meta.url))`, тобто директорія самого `trufflehog.mjs`. Цей файл — джерело істини для text-subset порівняння.

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

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

Типовий сценарій використання — виклик з вищерівневого скрипта-агрегатора перевірок правила `security`, який послідовно прогоняє FS-частину (`trufflehog.mjs`) і policy-частину (Rego/OPA для структури `package.json`). У цій ролі модуль працює як CLI-сумісна функція:

```
import { check } from './trufflehog.mjs'

const code = await check()           // використає process.cwd()
process.exit(code)
```

Або з явним коренем:

```
import { check } from './trufflehog.mjs'

const code = await check('/path/to/repo')
process.exit(code)
```

Послідовність кроків у runtime (happy path):

1. Викликач передає `cwd` (або не передає — береться `process.cwd()`).
2. Створюється reporter — порожній акумулятор стану.
3. `existsSync(join(cwd, 'package.json'))` → `true` → `pass(...)`.
4. `existsSync(join(cwd, '.trufflehog-exclude'))` → `true` → перехід до читання.
5. Паралельно (через `await` послідовно, але без перехресної залежності) читаються `actual` та `template`.
6. `checkTextSubset(actual, template, opts)` → `[]` (порожній масив помилок).
7. Цикл `for (const msg of errors)` нічого не виконує.
8. Викликається `pass('.trufflehog-exclude містить канонічні patterns')`.
9. Повертається `reporter.getExitCode()` — у happy-сценарії це `0`.

Гілки відмови (sad paths):

- Немає `package.json` у `cwd` → один `fail` про відсутність → exit-код ≠ 0, перевірка `.trufflehog-exclude` **не** виконується.
- Є `package.json`, але немає `.trufflehog-exclude` → `pass` для першого та `fail` для другого → exit-код ≠ 0.
- Обидва файли є, але `.trufflehog-exclude` не містить деяких канонічних patterns → стільки `fail`, скільки повідомлень повернула `checkTextSubset` → exit-код ≠ 0. Жодного `pass` про канонічність у цьому випадку не друкується.

Контекст у проєкті:

- Модуль належить до родини `npm/rules/<rule>/js/` і реалізує JS-half правила `security`. Поряд із ним очікується тека `templates/trufflehog/` з файлом `.trufflehog-exclude.snippet.txt` — без неї `readFile` для snippet кине помилку.
- Текстовий формат повідомлень `pass`/`fail` ідентичний іншим перевіркам репозиторію (через спільний `check-reporter`), що дозволяє агрегатору одноманітно показувати результати.
- Розділення «FS-частина перевіряє наявність, Rego — структуру» — навмисний design choice: він зафіксований у `security.mdc` і повторений у doc-string модуля. У `trufflehog.mjs` **не** можна додавати парсинг JSON `package.json` чи логіку валідації полів — це порушує контракт із Rego-policy.
- text-subset (а не точна рівність) обраний саме тому, що `.trufflehog-exclude` — plain text без структури: канон постачає мінімум обов'язкових patterns, а команди можуть розширювати свій exclude-файл локальними правилами без порушення security-канону.

Rebuild test: за цією документацією можна відновити поведінку модуля від інтерфейсу до side-effects — сигнатуру `check`, default-значення `cwd`, послідовність двох FS-перевірок з early-exit, виклик `checkTextSubset` з опціями `targetPath`/`source`, формування exit-коду через `reporter.getExitCode()`, перелік повідомлень `fail`/`pass` і відсутність зовнішніх npm-залежностей.
