@7n/rules 1.47.0 → 1.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.47.1] - 2026-07-23
4
+
5
+ ### Fixed
6
+
7
+ - doc-files: явна діагностика замість тихого 0 кандидатів, коли задекларований плагін (.n-rules.json) не встановлений у node_modules
8
+
3
9
  ## [1.47.0] - 2026-07-23
4
10
 
5
11
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.47.0",
3
+ "version": "1.47.1",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -3,30 +3,27 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/check/main.mjs
5
5
  docgen:
6
- crc: 2af6c6b9
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- score: 100
9
- issues: judge:inaccurate:0.99
10
- judgeModel: openai-codex/gpt-5.4-mini
6
+ crc: dd3f8c2a
11
7
  ---
12
8
 
13
9
  ## Огляд
14
10
 
15
- Lint-детектор concern-а `doc-files/check`: виявляє застарілі файлові доки (CRC-mismatch, відсутність, деградація) та «сирітські» доки, чиє джерело видалено. Виключно read-only детект генерація й очистка живуть у `fix-worker.mjs` (docgen), не тут.
11
+ Lint-детектор doc-files: знаходить застарілі файлові доки (CRC-mismatch/missing/degraded) і сирітські доки, чиє джерело видалено. Read-only — саму генерацію/очистку виконує `fix-worker.mjs`.
16
12
 
17
13
  ## Поведінка
18
14
 
19
- 1. `lint(ctx)`: у delta-режимі (`ctx.files` задано) змінені шляхи зводяться до множини вихідних кодових файлів для зміненої `docs/*.md`-доки reverse-map-ом знаходиться її джерело у батьківській теці; без `ctx.files` — повний скан репо.
20
- 2. Для кожного джерела перевіряється актуальність його доки (`describeFile`/`scanForDocFiles`); застаріла/відсутня дока → порушення з причиною (`reason`) і шляхом джерела.
21
- 3. Окремо скануються «сирітські» доки (`scanOrphanedDocs`) `docs/*.md`, чиє джерело видалено; кожнапорушення `orphaned-doc`.
22
- 4. Повертається `LintResult` зі списком порушень — без жодних мутацій.
15
+ `lint(ctx)` збирає застарілі доки через `collectStale` (для конкретного набору змінених файлів або, якщо файлів не передано, повним скануванням дерева) і додає для кожної окреме порушення з причиною (`missing`/`crc-mismatch`/`degraded`) та шляхом джерела. Окремо сканує `docs/`-теки на сирітські доки (`scanOrphanedDocs`)джерело яких більше не існує.
16
+
17
+ Реверс-мапінг: якщо серед змінених файлів трапляється сама `.md`-дока (а не її джерело), `sourceForDoc` шукає відповідний вихідний файл поруч (той самий basename, легальне розширення) і саме його передає далі в перевірку застарілості так зміна доки теж тригерить звірку CRC її джерела.
18
+
19
+ Якщо мапа doc-files-розширень від активних плагінів порожня, а хоча б один плагін явно задекларований у `.n-rules.json`, але не встановлений у `node_modules` — детектор додає `diagnostics`-запис (`level: 'warn'`) з підказкою запустити `bun install`. Це відрізняє "плагін не встановлено" від "усі доки актуальні": без нього обидва випадки виглядають як 0 порушень.
23
20
 
24
21
  ## Публічний API
25
22
 
26
- collectStaleперелік застарілих доків: для `files` (delta) або всього репо (undefined);
27
- lint — детектор застарілих і сирітських файлових доків, повертає `{ violations }`.
23
+ - `lint(ctx)` детектор concern-а `doc-files/check`: повертає `{ violations, diagnostics? }`.
28
24
 
29
25
  ## Гарантії поведінки
30
26
 
31
- - Read-only: не виконує операцій запису (ФС/БД).
32
- - Fail-safe: помилка читання теки джерела дає `null`-reverse-map (джерело пропускається), не виняток.
27
+ - Read-only: не пише і не видаляє жодних файлів.
28
+ - Перехоплює помилки читання файлової системи (напр. нечитабельна тека) не пропускає винятків назовні.
29
+ - Diagnostics-попередження про невстановлений плагін рендериться лише при `--verbose` на explicit CLI-виклику (`lint doc-files --no-fix --verbose`) — у PostToolUse-хуку (без `verbose`) обчислюється, але ніколи не друкується.
@@ -5,6 +5,7 @@ import { join, dirname, basename, extname } from 'node:path'
5
5
  import { existsSync, readdirSync } from 'node:fs'
6
6
 
7
7
  import { describeFile, isDocCandidate, isSourceFile, scanForDocFiles, scanOrphanedDocs } from '../docgen-scan/main.mjs'
8
+ import { unavailableDocFilesPlugins } from '../docgen-scan/lang-extensions.mjs'
8
9
 
9
10
  const DOC_MD_RE = /(?:^|\/)docs\/[^/]+\.md$/u
10
11
 
@@ -95,5 +96,11 @@ export function lint(ctx) {
95
96
  )
96
97
  }
97
98
 
98
- return { violations }
99
+ const unavailable = unavailableDocFilesPlugins(cwd)
100
+ if (unavailable.length === 0) return { violations }
101
+
102
+ const word = unavailable.length > 1 ? 'плагіни' : 'плагін'
103
+ const verb = unavailable.length > 1 ? 'не встановлені' : 'не встановлений'
104
+ const message = `doc-files: 0 кандидатів через недоступні плагіни — ${word} ${unavailable.join(', ')} ${verb} у node_modules — запусти bun install`
105
+ return { violations, diagnostics: [{ level: 'warn', message }] }
99
106
  }
@@ -3,32 +3,40 @@ type: JS Module
3
3
  title: lang-extensions.mjs
4
4
  resource: npm/rules/doc-files/docgen-scan/lang-extensions.mjs
5
5
  docgen:
6
- crc: 3c5bc28b
7
- model: openai-codex/gpt-5.4-mini
8
- tier: cloud-min
9
- score: 100
10
- issues: judge:inaccurate:0.98
11
- judgeModel: openai-codex/gpt-5.4-mini
6
+ crc: 14b28432
7
+ model: omlx/gemma-4-e2b-it-4bit
8
+ tier: local-min
9
+ score: 95
12
10
  ---
13
11
 
14
12
  ## Огляд
15
13
 
16
- Збирає з активних плагінів у репозиторії мапу розширень doc-files і мовні екстрактори для них, щоб інші частини системи могли підготувати обробку файла за доступними плагінними можливостями. `pluginDocFilesExtensions` формує перелік підтримуваних doc-files розширень, `loadDocFilesExtractors` підтягує екстрактори мов із плагінів, а `clearDocFilesLangCache` скидає кеш у межах прогону. Спирається на `.n-rules.json` і `.n-cursor.json` як джерело конфігурації активних плагінів та їхніх правил. Працює fail-safe: биті handler-модулі мовчки пропускає, не кидає винятків назовні, кешує стан у межах прогону.
14
+ Огляд: Модуль керує завантаженням та очищенням документів та логіки для перевірок. Забезпечує функціональний потік, який включає отримання мапи розширень, завантаження екстракторів та очищення кешу для тестування.
17
15
 
18
16
  ## Поведінка
19
17
 
20
- - `pluginDocFilesExtensions` — повертає мапу розширень doc-files, які декларують активні плагіни в репозиторії, з урахуванням кешу для поточного прогону.
21
- - `loadDocFilesExtractors` — завантажує мовні екстрактори з handler-модулів плагінів для doc-files і повертає їх за розширеннями; биті модулі мовчки пропускає, тож для таких файлів далі можливий whole-file шлях.
22
- - `clearDocFilesLangCache` скидає внутрішній кеш мовних розширень і екстракторів, щоб наступний прогін прочитав актуальний стан заново.
18
+ Поведінка крос-функціональний потік
19
+ pluginDocFilesExtensions повертає мапу розширень задекларованих у конфігах
20
+ loadDocFilesExtractors вантажує мовні екстрактори з handler-модулів
21
+ unavailableDocFilesPlugins повертає список плагінів, які не встановлені
22
+ clearDocFilesLangCache скидає кеші для тестування
23
23
 
24
24
  ## Публічний API
25
25
 
26
- - pluginDocFilesExtensions — збирає з активних плагінів карту розширень для doc-files і тримає її в процесному кеші; порожній результат означає, що жоден плагін не оголосив підтримку.
27
- - loadDocFilesExtractors підвантажує мовні extractors із plugin handler-модулів для extension-point `doc-files`; якщо модуль зламаний, його тихо пропускає і далі обробляє файл цілком.
28
- - clearDocFilesLangCacheочищає кеші, щоб тести починали з чистого стану.
26
+ - pluginDocFilesExtensions — Мапа doc-files-розширень від плагінів для репо (`.rs` 'Rust Module', …),
27
+ з кешем на процес. Порожня мапа жодний активний плагін їх не декларує.
28
+ - loadDocFilesExtractorsАсинхронно вантажить мовні екстрактори з handler-модулів плагінів
29
+ (extension-point `doc-files`): default-експорт
30
+ `{ id, extensions: string[], extractFacts?, extractUnits? }`.
31
+ Битий модуль — мовчазний пропуск (генерація тоді йде whole-file шляхом).
32
+ - unavailableDocFilesPlugins — Задекларовані у `.n-rules.json` плагіни, недоступні в `node_modules` — рахується лише
33
+ коли мапа doc-files-розширень порожня (інакше принаймні один плагін реально доступний,
34
+ шукати "недоступні" немає сенсу — не hot-path concern, рахується лише в рідкісному
35
+ порожньому випадку).
36
+ - clearDocFilesLangCache — Скидає кеші (для тестів).
29
37
 
30
38
  ## Гарантії поведінки
31
39
 
32
- - Read-only: не виконує операцій запису (ФС/БД).
40
+ - Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
33
41
  - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
34
42
  - Кешує результати в межах одного прогону.
@@ -3,7 +3,11 @@ import { existsSync, readFileSync } from 'node:fs'
3
3
  import { join } from 'node:path'
4
4
  import { pathToFileURL } from 'node:url'
5
5
 
6
- import { getDocFilesExtensions, getHandlers } from '../../../scripts/lib/resolve-plugins.mjs'
6
+ import {
7
+ getDocFilesExtensions,
8
+ getHandlers,
9
+ getUnavailableDeclaredPlugins
10
+ } from '../../../scripts/lib/resolve-plugins.mjs'
7
11
 
8
12
  /**
9
13
  * Мовні розширення doc-files від плагінів (фаза 4 spec lang-plugins-extraction).
@@ -82,6 +86,19 @@ export async function loadDocFilesExtractors(cwd) {
82
86
  return map
83
87
  }
84
88
 
89
+ /**
90
+ * Задекларовані у `.n-rules.json` плагіни, недоступні в `node_modules` — рахується лише
91
+ * коли мапа doc-files-розширень порожня (інакше принаймні один плагін реально доступний,
92
+ * шукати "недоступні" немає сенсу — не hot-path concern, рахується лише в рідкісному
93
+ * порожньому випадку).
94
+ * @param {string} cwd корінь репозиторію
95
+ * @returns {string[]} npm-імена задекларованих, але не встановлених плагінів (порожньо — усе гаразд)
96
+ */
97
+ export function unavailableDocFilesPlugins(cwd) {
98
+ if (Object.keys(pluginDocFilesExtensions(cwd)).length > 0) return []
99
+ return getUnavailableDeclaredPlugins(cwd, readPluginsConfigSync(cwd))
100
+ }
101
+
85
102
  /** Скидає кеші (для тестів). */
86
103
  export function clearDocFilesLangCache() {
87
104
  EXT_CACHE.clear()
@@ -3,15 +3,122 @@ type: JS Module
3
3
  title: resolve-plugins.mjs
4
4
  resource: npm/scripts/lib/resolve-plugins.mjs
5
5
  docgen:
6
- crc: 2bac9b49
6
+ crc: 52b1765f
7
+ model: omlx/gemma-4-e2b-it-4bit
8
+ tier: local-min
9
+ score: 40
10
+ issues: internal-name:hasLangSignal,internal-name:listScannableSubdirs,internal-name:readRepositoryUrl,internal-name:hasGithubWorkflows,internal-name:detectCiPlugins,surzhik,best-of-2:retry-error
7
11
  ---
8
12
 
9
- Резолв плагінів @7n/rules: визначає, які пакети-плагіни активні у проєкті, де їхні `rules/`-каталоги, які capabilities вони надають і які handlers надають.
13
+ ## Огляд
10
14
 
11
- Джерело правди поле `plugins: string[]` у `.n-rules.json`; воно завжди перекриває автодетект, а явний порожній масив означає «плагіни вимкнено». Якщо поля немає, `detectPluginsFromRepo` шукає файлові сигнали: наявність yml у `.github/workflows/` дає `@7n/rules-ci-github`, файл `azure-pipelines.yml` у корені `@7n/rules-ci-azure` (реєстр `KNOWN_CI_PLUGINS`). Лише коли файлових CI-сигналів немає, вмикається fallback за `repository.url` кореневого package.json (`github.com` → github, `dev.azure.com`/`visualstudio.com` → azure). Обидва сигнали дають обидва плагіни, жодного — порожній список. Окремо від CI детектяться мовні плагіни (реєстр `KNOWN_LANG_PLUGINS`, лише файлові сигнали, без URL-fallback): кореневий `pyproject.toml` → `@7n/rules-lang-python`.
15
+ Огляд: Цей файл забезпечує пошук сигналів-маркерів у файловій системі для визначення активності плагінів. Він використовує BFS-обхід для пошуку цих сигналів, визначає доступні `capabilities`, розв'язує список плагінів з конфігурації, гарантує встановлення необхідних пакетів, обробляє маніфести плагінів та надає інструменти для роботи з їхніми обробниками (`handlers`).
12
16
 
13
- `ensurePluginInstalled` ставить відсутній плагін через `bun add -d` (пакет стає devDependency — зміна видима у diff). Будь-який фейл установки (offline, пакет не опублікований) — warning і graceful skip, ніколи не hard-fail: лінт і синк мають працювати без мережі.
17
+ ## Поведінка
14
18
 
15
- `resolvePlugins(projectRoot, config, options)` головна функція: повертає масив `{name, packageRoot, rulesDir, manifest}` доступних плагінів з кешем на процес. `options.allowInstall: false` hot-path режим (hook, lint): лише вже встановлені пакети, без `bun add`; `options.quiet: true` глушить warning-и (hook викликається на кожен файл). Плагін, що декларує правила (`contributes.rules !== false`), але не має каталогу `rules/`, пропускається як битий; плагін із явним `contributes.rules: false` (лише handlers, як `lang-*`) — легальний, `resolveRulesDirs` його просто не включає у джерела правил.
19
+ Перевіряє наявність файлу-сигналу у корені або підтеках до `maxDepth` рівнів. Використовує BFS-обхід, який пропускає приховані директорії, `node_modules`, та `target`, для забезпечення ефективного виклику на hot-path. Повертає `true` при знаходженні сигналу. Викликає `listScannableSubdirs`.
16
20
 
17
- Маніфест — блок `"n-rules"` у package.json плагіна: `capabilities` (масив рядків на кшталт `ci:github`, живлять гейт концернів `requires.capability`) і `contributes.handlers` (мапа extension-point → відносний шлях модуля). `getActiveCapabilities` агрегує capabilities усіх плагінів у Set; `getHandlers(point)` повертає абсолютні шляхи модулів-обробників (перший реальний споживач — taze-оркестратор, extension-point `taze`). `resolveRulesDirs` віддає впорядковані джерела правил: ядро завжди перше (його правила й концерни виграють колізії), далі плагіни у порядку списку. `clearPluginResolveCache` скидає кеш (для тестів).
21
+ ### listScannableSubdirs
22
+ Отримує видимі підтеки для неглибокого скану, ігноруючи приховані та службові директорії. Повертає масив абсолютних шляхів підтеків (порожній, якщо директорія нечитабельна).
23
+
24
+ ### readRepositoryUrl
25
+ Отримує `repository.url` з кореневого `package.json` (як рядок або об'єкт). Повертає `null`, якщо URL відсутній або файл нечитабельний.
26
+
27
+ ### hasGithubWorkflows
28
+ Перевіряє наявність файлів у директорії `.github/workflows/` будь-якого YAML/YML файлу. Повертає `true` при наявності.
29
+
30
+ ### detectCiPlugins
31
+ Автоматично визначає CI-плагіни за станом репозиторію. Пріоритет надається файловим сигналам. Якщо файлових сигналів немає, використовується `repository.url` з кореневого `package.json`. Якщо обидва сигнали присутні, обидва плагіни повертаються. Якщо немає жодного сигналу, повертається порожній масив. Викликає `hasGithubWorkflows` та `readRepositoryUrl`.
32
+
33
+ ### detectPluginsFromRepo
34
+ Автоматично визначає плагіни за станом репозиторію. Використовує файлові сигнали як пріоритет, з fallback на `repository.url` лише для CI-плагінів, коли файлових сигналів немає. Для Rust використовується перевірка до трьох рівнів підтек. Викликає `detectCiPlugins` та `hasLangSignal`.
35
+
36
+ ### pluginCategory
37
+ Визначає категорію плагіна на основі naming convention `@7n/rules-<category>-<name>`. Повертає `null` для пакетів, що не відповідають цій конвенції (сторонні/кастомні плагіни) — такий пакет ніколи не з'являється сам через автодетект, а якщо присутній у явному `config.plugins`, вимикає backfill категорій для всього списку.
38
+
39
+ ### resolvePluginList
40
+ Обчислює список плагінів проєкту. Використовує явне поле `config.plugins` з `.n-rules.json` або автоматичний автодетект. Явний пустий масив означає "плагіни вимкнено". Якщо `config.plugins` містить сторонній пакет, який не є `@7n/rules-*`, то backfill для заповнення списку вимикається повністю.
41
+
42
+ ### computePluginList
43
+ Обчислює список плагінів без кешу. Використовує сире значення `config.plugins` з `.n-rules.json`. Повертає список плагінів. Викликає `detectPluginsFromRepo` та `pluginCategory`.
44
+
45
+ ### ensurePluginInstalled
46
+ Гарантує наявність пакета: якщо він відсутній у `node_modules`, виконує `bun add -d <pkg>` для додання його як `devDependency`. Фейл повертає `false` з попередженням.
47
+
48
+ ### readPluginManifest
49
+ Отримує нормалізований маніфест плагіна з його `package.json` з блоку `"n-rules"`.
50
+
51
+ ### resolvePlugins
52
+ Виконує повне визначення доступних плагінів проєкту з використанням кешу. Дозволяє встановити вже встановлені пакети через `allowInstall: false` (для hot-path хука), ігнорує `bun add`, ігнорує `quiet` для уникнення попереджень при виклику на кожному файлі. Повертає масив доступних плагінів.
53
+
54
+ ### resolveRulesDirs
55
+ Отримує шляхи до каталогів для всіх поверхонь ядра. Ядро має пріоритет. Надає масив об'єктів, що містять ім'я плагіна, шлях до його директорії та шлях до його кореня пакета.
56
+
57
+ ### getActiveCapabilities
58
+ Отримує набір capability-рядків з усіх доступних плагінів, що використовуються для визначення `requires.capability` у `concern.json`.
59
+
60
+ ### getDocFilesExtensions
61
+ Агрегує розширення файлів, що використовуються для генерації мапи від розширення до типу-мітка. Повертає мапу `extension -> type-мітка`.
62
+
63
+ ### getUnavailableDeclaredPlugins
64
+ Повертає список плагінів, які задекларовані у `config.plugins`, але відсутні у `node_modules`. Нічого не встановлює і нічого не друкує — чистий предикат для explicit CLI-діагностики.
65
+
66
+ ### getHandlers
67
+ Отримує шляхи до модулів-обробників для певного extension-point правила ядра.
68
+
69
+ ### clearPluginResolveCache
70
+ Скидає кеш, призначений для тестів.
71
+
72
+ ## Публічний API
73
+
74
+ - KNOWN_CI_PLUGINS — Відомі CI-плагіни для автовизначення: сигнал у дереві репо → npm-пакет.
75
+ - KNOWN_LANG_PLUGINS — Відомі мовні плагіни: файловий сигнал екосистеми → npm-пакет. `maxDepth` —
76
+ до якої глибини шукати сигнал: python — лише корінь (uv-провайдер v1
77
+ обробляє тільки кореневий pyproject.toml; js — кореневий package.json); rust — до 3 рівнів, бо в
78
+ монорепо Cargo.toml часто вкладений (Tauri `app/src-tauri/Cargo.toml`),
79
+ а провайдер обробляє всі знайдені маніфести.
80
+ - detectPluginsFromRepo — Автодетект плагінів за станом репозиторію: CI-плагіни (файлові сигнали з
81
+ fallback на `repository.url`) + мовні плагіни (лише файлові сигнали —
82
+ маніфест екосистеми в корені або, для rust, у підтеках до 3 рівнів; URL-fallback для мов безглуздий).
83
+ - pluginCategory — Категорія плагіна за naming convention `@7n/rules-<category>-<name>` (напр. `ci`, `lang`).
84
+ `null` — пакет поза цією конвенцією (сторонній/кастомний плагін); такий пакет ніколи не
85
+ зʼявляється сам через автодетект і, якщо присутній у явному `config.plugins`, вимикає
86
+ per-категорійний backfill для всього списку (див. `resolvePluginList`).
87
+ - resolvePluginList — Список плагінів проєкту: явний `config.plugins` або автодетект.
88
+
89
+ Явний `plugins` непорожній і складається **виключно** з пакетів `@7n/rules-<category>-*` —
90
+ автодетект домішує лише категорії, відсутні в списку (ADR
91
+ `260719-2154-per-category-автодетект-плагінів`); категорія, присутня хоч одним пакетом,
92
+ лишається зафіксованою користувачем. Якщо `plugins` містить хоча б один сторонній
93
+ (не `@7n/rules-*`) пакет — це сигнал ручного керування, backfill вимикається повністю
94
+ (як і раніше: список повертається як є). Явний `[]` — «плагіни вимкнено», без backfill.
95
+ Поле відсутнє взагалі — повний автодетект.
96
+
97
+ Результат кешується на процес за `(projectRoot, declared)` — виклик з `resolvePlugins`
98
+ (через `resolveRulesDirs` тощо) і прямий виклик у sync-CLI інакше дублювали б і файловий
99
+ скан, і warning про backfill.
100
+ ігнорується при cache hit — warning друкується щонайбільше раз на `(root, declared)` за процес
101
+ - ensurePluginInstalled — Гарантує, що плагін встановлений: якщо `node_modules/<pkg>` нема — `bun add -d <pkg>`
102
+ (дописує devDependency і ставить). Фейл — warning + false, без винятку.
103
+ - resolvePlugins — Повний резолв плагінів проєкту (з кешем на процес).
104
+ лише вже встановлені пакети, без `bun add`; `quiet` — без warning-ів (hook на кожен файл)
105
+ - resolveRulesDirs — Rules-каталоги для всіх поверхонь ядра: ядро першим (його правила/концерни виграють
106
+ колізії), далі плагіни у порядку списку.
107
+ - getActiveCapabilities — Активні capabilities від усіх доступних плагінів (для гейта `requires.capability` у concern.json).
108
+ - getDocFilesExtensions — Агреговані doc-files-розширення активних плагінів: '.rs' → 'Rust Module' тощо.
109
+ Синхронно і без установки (hot-path hook) — лише вже встановлені плагіни.
110
+ - getUnavailableDeclaredPlugins — Задекларовані у `config.plugins` пакети, недоступні в `node_modules` (не встановлені).
111
+ Не встановлює нічого (`allowInstall: false`) і не друкує — чистий предикат для
112
+ explicit CLI-діагностики (напр. doc-files: 0 кандидатів через невстановлений плагін),
113
+ яка сама вирішує, коли й де показати попередження. Автодетектовані (не задекларовані
114
+ явно) плагіни тут не враховуються — сигнал стосується саме явного `.n-rules.json`.
115
+ - getHandlers — Handlers для extension-point правила ядра (v1 — лише API; перший споживач — v2).
116
+ - clearPluginResolveCache — Скидає кеш резолву (для тестів).
117
+
118
+ ## Гарантії поведінки
119
+
120
+ - Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
121
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
122
+ - За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
123
+ - Кешує результати в межах одного прогону.
124
+ - Свідомо пропускає шляхи: `.github`, `.git`, `node_modules`.
@@ -401,6 +401,26 @@ export function getDocFilesExtensions(projectRoot, config) {
401
401
  return out
402
402
  }
403
403
 
404
+ /**
405
+ * Задекларовані у `config.plugins` пакети, недоступні в `node_modules` (не встановлені).
406
+ * Не встановлює нічого (`allowInstall: false`) і не друкує — чистий предикат для
407
+ * explicit CLI-діагностики (напр. doc-files: 0 кандидатів через невстановлений плагін),
408
+ * яка сама вирішує, коли й де показати попередження. Автодетектовані (не задекларовані
409
+ * явно) плагіни тут не враховуються — сигнал стосується саме явного `.n-rules.json`.
410
+ * @param {string} projectRoot корінь репозиторію
411
+ * @param {{ plugins?: unknown } | null | undefined} config розпарсений `.n-rules.json`
412
+ * @returns {string[]} npm-імена задекларованих, але не встановлених плагінів (порожньо — усе гаразд)
413
+ */
414
+ export function getUnavailableDeclaredPlugins(projectRoot, config) {
415
+ const declared = Array.isArray(config?.plugins)
416
+ ? config.plugins.filter(p => typeof p === 'string' && p.trim() !== '')
417
+ : []
418
+ if (declared.length === 0) return []
419
+ const root = resolve(projectRoot)
420
+ const availableNames = new Set(resolvePlugins(root, config, { allowInstall: false, quiet: true }).map(p => p.name))
421
+ return declared.filter(name => !availableNames.has(name))
422
+ }
423
+
404
424
  /**
405
425
  * Handlers для extension-point правила ядра (v1 — лише API; перший споживач — v2).
406
426
  * @param {string} projectRoot корінь репозиторію