@7n/rules 1.32.1 → 1.33.0

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.33.0] - 2026-07-20
4
+
5
+ ### Added
6
+
7
+ - Per-категорійний автодетект плагінів: явний непорожній plugins (лише з @7n/rules-`category`-*) домішує автодетектом категорії, яких у ньому немає
8
+
9
+ ## [1.32.2] - 2026-07-20
10
+
11
+ ### Changed
12
+
13
+ - doc-files: згенеровано файлові доки для `k8s/dremio_logging/main.mjs` і `skills/taze/js/migration-cache.mjs`, що доти ламали tree-wide doc-files/check; вручну виправлено фабрикації локальної моделі (розбиті backtick-маркери, невірні твердження про fail-safe/scope кешу міграцій)
14
+
3
15
  ## [1.32.1] - 2026-07-20
4
16
 
5
17
  ### Changed
package/bin/n-rules.js CHANGED
@@ -365,9 +365,11 @@ async function readConfig(paths = {}) {
365
365
  }
366
366
  }
367
367
 
368
- // Плагіни: явне поле plugins конфігу або автодетект; sync-контекстз установкою devDep.
369
- // Результат детекту записуємо у конфіг (нижче), щоб hook/lint не залежали від детекту й мережі.
370
- const detectedPlugins = Array.isArray(rawConfig?.plugins) ? null : resolvePluginList(cwd(), rawConfig)
368
+ // Плагіни: явне поле plugins конфігу per-категорійним backfill автодетекту ADR
369
+ // 260719-2154) або повний автодетект, коли поля нема; sync-контекст з установкою devDep.
370
+ // Результат записуємо у конфіг (нижче), щоб hook/lint не залежали від детекту й мережі.
371
+ const declaredPlugins = Array.isArray(rawConfig?.plugins) ? rawConfig.plugins : null
372
+ const resolvedPlugins = resolvePluginList(cwd(), rawConfig)
371
373
  const rulesDirs = resolveRulesDirs(cwd(), rawConfig, bundledRulesDir).map(d => d.rulesDir)
372
374
  const { names: availableRules } = await aggregateRuleSources(rulesDirs)
373
375
  const availableSkills = await discoverBundledSkillNames(bundledSkillsDir)
@@ -442,9 +444,17 @@ async function readConfig(paths = {}) {
442
444
  if (merged['disable-skills']?.length) {
443
445
  normalized['disable-skills'] = merged['disable-skills']
444
446
  }
445
- if (!('plugins' in parsedConfig) && detectedPlugins && detectedPlugins.length > 0) {
446
- normalized.plugins = detectedPlugins
447
- console.log(`🔌 Автодетект плагінів: ${detectedPlugins.join(', ')} — записано у ${CONFIG_FILE}\n`)
447
+ if (!('plugins' in parsedConfig)) {
448
+ if (resolvedPlugins.length > 0) {
449
+ normalized.plugins = resolvedPlugins
450
+ console.log(`🔌 Автодетект плагінів: ${resolvedPlugins.join(', ')} — записано у ${CONFIG_FILE}\n`)
451
+ }
452
+ } else if (declaredPlugins && JSON.stringify(resolvedPlugins) !== JSON.stringify(declaredPlugins)) {
453
+ // per-категорійний backfill (resolvePluginList, ADR 260719-2154) додав плагін
454
+ // категорії, відсутньої в явному plugins — фіксуємо результат у конфізі.
455
+ normalized.plugins = resolvedPlugins
456
+ const added = resolvedPlugins.filter(p => !declaredPlugins.includes(p))
457
+ console.log(`🔌 Доповнено plugins у ${CONFIG_FILE} (${added.join(', ')}) — категорія не була задекларована\n`)
448
458
  }
449
459
  return sortConfigIdArrays(normalized)
450
460
  }
@@ -465,7 +475,7 @@ async function readConfig(paths = {}) {
465
475
  $schema: CONFIG_SCHEMA_URL,
466
476
  rules: autoDetectedRules.rules,
467
477
  skills: autoDetectedSkills.skills,
468
- ...(detectedPlugins && detectedPlugins.length > 0 && { plugins: detectedPlugins })
478
+ ...(resolvedPlugins.length > 0 && { plugins: resolvedPlugins })
469
479
  })
470
480
  await writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8')
471
481
  console.log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.32.1",
3
+ "version": "1.33.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -7,23 +7,23 @@ docgen:
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  tier: cloud-min
9
9
  score: 95
10
- issues: anchor-miss:(k8s.mdc),judge:inaccurate:0.99
10
+ issues: anchor-miss:(k8s.mdc),judge:inaccurate:0.98
11
11
  judgeModel: openai-codex/gpt-5.4-mini
12
12
  ---
13
13
 
14
14
  ## Огляд
15
15
 
16
- `zkLogbackRootLevelViolation` перевіряє повідомлення, пов’язані з надто гучним `root` у `k8s.mdc`, а `lint` збирає ці перевірки в один pass і повертає результат без зупинки процесу. Поведінка read-only: правило не пише у ФС чи БД, працює fail-safe й не кидає винятків назовні.
16
+ Перевіряє `zookeeper.yaml` на надто гучний `root level` у вбудованому `logback.xml` і формує звіт про порушення з маркером `zk-logback-root-level`. `zkLogbackRootLevelViolation` і `lint` працюють як read-only, не змінюють ФС чи БД, і поводяться fail-safe: помилки перехоплюються, назовні винятки не виходять.
17
17
 
18
18
  ## Поведінка
19
19
 
20
- - `zkLogbackRootLevelViolation` — перевіряє, чи вбудований `logback.xml` у ZooKeeper-templated файлі не тримає `root` на надто гучному рівні; повертає текст порушення з міткою `` або `null`, якщо перевірка не потрібна або рівень уже прийнятний.
21
- - `lint` — проходить по вказаних файлах у read-only режимі, збирає порушення для `k8s.dremio_logging` і не виводить помилки назовні; пропускає файли, які не вдалося прочитати, та позначає знайдені проблеми міткою ``.
20
+ - `zkLogbackRootLevelViolation` — перевіряє, чи вбудований `logback.xml` у `zookeeper.yaml` має допустимий `root level` (`warn`, `error`, `off`); якщо блоку немає не чіпає файл, якщо рівень надто гучний або відсутній повертає текст порушення з посиланням на `k8s.mdc`.
21
+ - `lint` — проходить по вказаних файлах, безпечно читає їх як `zookeeper.yaml`-кандидати, фіксує порушення для `k8s.dremio_logging` і не перериває перевірку на помилках читання; для кожного знайденого випадку додає повідомлення з маркером `zk-logback-root-level`.
22
22
 
23
23
  ## Публічний API
24
24
 
25
- - zkLogbackRootLevelViolation — позначає ZooKeeper Helm-темплейт, якщо вбудований `logback.xml` має занадто гучний `root`-рівень (`info`, `debug`, `trace`) або взагалі не задає `root`; не чіпає файли, де `root` уже `warn`, `error` чи `off`.
26
- - lint — запускає `k8s.dremio_logging` для ZooKeeper-темплейтів у межах одного файлу, лише для читання.
25
+ - zkLogbackRootLevelViolation — знаходить у Helm-темплейті ZooKeeper вбудований `logback.xml`, де `root` заданий занадто гучно або взагалі відсутній; пропускає, якщо рівень уже `warn`, `error` чи `off`
26
+ - lint — запускає `k8s.dremio_logging` для ZooKeeper-темплейту на рівні окремого файла, без змін у вмісті
27
27
 
28
28
  ## Гарантії поведінки
29
29
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: resolve-plugins.mjs
4
4
  resource: npm/scripts/lib/resolve-plugins.mjs
5
5
  docgen:
6
- crc: fb58860d
6
+ crc: 2bac9b49
7
7
  ---
8
8
 
9
9
  Резолв плагінів @7n/rules: визначає, які пакети-плагіни активні у проєкті, де їхні `rules/`-каталоги, які capabilities вони надають і які handlers надають.
@@ -2,10 +2,16 @@
2
2
  * Резолв плагінів `@7n/rules`: які пакети-плагіни активні у проєкті, де їхні `rules/`,
3
3
  * які capabilities вони дають і які handlers надають.
4
4
  *
5
- * Джерело правди — поле `plugins: string[]` у `.n-rules.json` (завжди перекриває автодетект;
6
- * явний `[]` = «плагіни вимкнено»). Якщо поля немає — `detectPluginsFromRepo`: файлові сигнали
7
- * (`.github/workflows/*.yml` `@7n/rules-ci-github`; `azure-pipelines.yml` `@7n/rules-ci-azure`),
8
- * а без них fallback за `repository.url` кореневого package.json (`github.com` / `dev.azure.com`).
5
+ * Джерело правди — поле `plugins: string[]` у `.n-rules.json`. Явний `[]` = «плагіни
6
+ * вимкнено» (автодетект не застосовується). Якщо поля немає взагалі повний автодетект
7
+ * (`detectPluginsFromRepo`). Якщо `plugins` непорожній і складається **виключно** з пакетів
8
+ * за конвенцією `@7n/rules-<category>-<name>` (напр. `ci`, `lang`) автодетект домішує
9
+ * лише ті категорії, яких у списку немає (ADR `260719-2154-per-category-автодетект-плагінів`);
10
+ * будь-який сторонній (не `@7n/rules-*`) пакет у списку вимикає backfill повністю — змішаний
11
+ * чи повністю кастомний список означає ручне керування, без сюрпризів. Файлові сигнали
12
+ * автодетекту: `.github/workflows/*.yml` → `@7n/rules-ci-github`; `azure-pipelines.yml` →
13
+ * `@7n/rules-ci-azure`, а без них — fallback за `repository.url` кореневого package.json
14
+ * (`github.com` / `dev.azure.com`).
9
15
  *
10
16
  * Установка: `ensurePluginInstalled` — плагін стає devDependency через `bun add -d` (bun сам
11
17
  * резолвить актуальну версію; зміна видима у diff package.json). Фейл установки (offline,
@@ -153,18 +159,91 @@ export function detectPluginsFromRepo(projectRoot) {
153
159
  return out
154
160
  }
155
161
 
162
+ /** Naming convention плагінів, які розпізнає автодетект: `@7n/rules-<category>-<name>`. */
163
+ const PLUGIN_CATEGORY_RE = /^@7n\/rules-([a-z0-9]+)-/u
164
+
165
+ /**
166
+ * Категорія плагіна за naming convention `@7n/rules-<category>-<name>` (напр. `ci`, `lang`).
167
+ * `null` — пакет поза цією конвенцією (сторонній/кастомний плагін); такий пакет ніколи не
168
+ * зʼявляється сам через автодетект і, якщо присутній у явному `config.plugins`, вимикає
169
+ * per-категорійний backfill для всього списку (див. `resolvePluginList`).
170
+ * @param {string} pkg npm-ім'я плагіна
171
+ * @returns {string | null} категорія або null
172
+ */
173
+ export function pluginCategory(pkg) {
174
+ const m = PLUGIN_CATEGORY_RE.exec(pkg)
175
+ return m ? m[1] : null
176
+ }
177
+
178
+ /** Усі категорії, які реально може повернути `detectPluginsFromRepo` — для short-circuit коли declared вже покриває все. */
179
+ const ALL_KNOWN_CATEGORIES = new Set(
180
+ [...Object.values(KNOWN_CI_PLUGINS), ...Object.values(KNOWN_LANG_PLUGINS).map(l => l.pkg)]
181
+ .map(pkg => pluginCategory(pkg))
182
+ .filter(c => c !== null)
183
+ )
184
+
185
+ /** Кеш `resolvePluginList` на процес: `root::declared-json` → результат (щоб не дублювати warning). */
186
+ const PLUGIN_LIST_CACHE = new Map()
187
+
156
188
  /**
157
- * Список плагінів проєкту: явний `config.plugins` (включно з порожнім = вимкнено) або автодетект.
189
+ * Список плагінів проєкту: явний `config.plugins` або автодетект.
190
+ *
191
+ * Явний `plugins` непорожній і складається **виключно** з пакетів `@7n/rules-<category>-*` —
192
+ * автодетект домішує лише категорії, відсутні в списку (ADR
193
+ * `260719-2154-per-category-автодетект-плагінів`); категорія, присутня хоч одним пакетом,
194
+ * лишається зафіксованою користувачем. Якщо `plugins` містить хоча б один сторонній
195
+ * (не `@7n/rules-*`) пакет — це сигнал ручного керування, backfill вимикається повністю
196
+ * (як і раніше: список повертається як є). Явний `[]` — «плагіни вимкнено», без backfill.
197
+ * Поле відсутнє взагалі — повний автодетект.
198
+ *
199
+ * Результат кешується на процес за `(projectRoot, declared)` — виклик з `resolvePlugins`
200
+ * (через `resolveRulesDirs` тощо) і прямий виклик у sync-CLI інакше дублювали б і файловий
201
+ * скан, і warning про backfill.
158
202
  * @param {string} projectRoot корінь репозиторію
159
203
  * @param {{ plugins?: unknown } | null | undefined} config розпарсений `.n-rules.json` (може бути відсутній)
204
+ * @param {{ quiet?: boolean }} [options] `quiet:true` — без warning-у про плагін, доданий автодетектом (hot-path);
205
+ * ігнорується при cache hit — warning друкується щонайбільше раз на `(root, declared)` за процес
160
206
  * @returns {string[]} npm-імена плагінів
161
207
  */
162
- export function resolvePluginList(projectRoot, config) {
208
+ export function resolvePluginList(projectRoot, config, options = {}) {
209
+ const root = resolve(projectRoot)
163
210
  const declared = config?.plugins
164
- if (Array.isArray(declared)) {
165
- return declared.filter(p => typeof p === 'string' && p.trim() !== '')
211
+ const cacheKey = `${root}::${Array.isArray(declared) ? JSON.stringify(declared) : '∅'}`
212
+ const cached = PLUGIN_LIST_CACHE.get(cacheKey)
213
+ if (cached) return cached
214
+
215
+ const result = computePluginList(root, declared, options)
216
+ PLUGIN_LIST_CACHE.set(cacheKey, result)
217
+ return result
218
+ }
219
+
220
+ /**
221
+ * Обчислення `resolvePluginList` без кешу (винесено окремо, щоб кеш-обгортка лишалась тонкою).
222
+ * @param {string} root абсолютний корінь репозиторію (вже пройшов через `resolve()`)
223
+ * @param {unknown} declared сире значення `config.plugins`
224
+ * @param {{ quiet?: boolean }} options прокинуті опції виклику
225
+ * @returns {string[]} npm-імена плагінів
226
+ */
227
+ function computePluginList(root, declared, options) {
228
+ if (!Array.isArray(declared)) return detectPluginsFromRepo(root)
229
+
230
+ const names = declared.filter(p => typeof p === 'string' && p.trim() !== '')
231
+ if (names.length === 0) return names
232
+
233
+ const declaredCategories = new Set(names.map(pkg => pluginCategory(pkg)))
234
+ // Хоч один сторонній пакет у списку — не вгадуємо намір, повертаємо як є.
235
+ if (declaredCategories.has(null)) return names
236
+ // Усі відомі категорії вже покриті явним списком — не марнуємо файлові сигнали.
237
+ if ([...ALL_KNOWN_CATEGORIES].every(c => declaredCategories.has(c))) return names
238
+
239
+ const missing = detectPluginsFromRepo(root).filter(pkg => !declaredCategories.has(pluginCategory(pkg)))
240
+ if (missing.length > 0 && options.quiet !== true) {
241
+ const word = missing.length > 1 ? 'плагіни' : 'плагін'
242
+ console.warn(
243
+ `⚠️ Додано автодетектом ${word} ${missing.join(', ')} — категорія не задекларована явно в .n-rules.json\n`
244
+ )
166
245
  }
167
- return detectPluginsFromRepo(projectRoot)
246
+ return [...names, ...missing]
168
247
  }
169
248
 
170
249
  /**
@@ -240,7 +319,7 @@ function readPluginManifest(packageRoot) {
240
319
  */
241
320
  export function resolvePlugins(projectRoot, config, options = {}) {
242
321
  const root = resolve(projectRoot)
243
- const names = resolvePluginList(root, config)
322
+ const names = resolvePluginList(root, config, { quiet: options.quiet })
244
323
  const cacheKey = `${root}${names.join(',')}${options.allowInstall !== false}`
245
324
  const cached = RESOLVE_CACHE.get(cacheKey)
246
325
  if (cached) return cached
@@ -341,4 +420,5 @@ export function getHandlers(projectRoot, config, point) {
341
420
  /** Скидає кеш резолву (для тестів). */
342
421
  export function clearPluginResolveCache() {
343
422
  RESOLVE_CACHE.clear()
423
+ PLUGIN_LIST_CACHE.clear()
344
424
  }
@@ -13,26 +13,25 @@ docgen:
13
13
 
14
14
  ## Огляд
15
15
 
16
- Модуль надає `DEFAULT_CACHE_DIR`, `migrationCacheKey`, `readMigrationCache`, `writeMigrationCache` і `withKnownMigrationNotes` для роботи з локальним кешем відомих результатів аналізу міграцій у межах прогону. Він зберігає та повторно читає вже відомі записи, а `withKnownMigrationNotes` підставляє їх у промпт, щоб не повторювати дослідження там, де результат уже є. Модуль fail-safe: не кидає винятків назовні й за окремих помилок повертає порожнє значення, зокрема `null`.
16
+ Модуль керує кешем уже відомих міграцій на диску (`~/.cache/n-rules/taze-migrations` за замовчуванням) — кеш персистентний і спільний для всіх repo/worktree на машині, не обмежений одним прогоном. `migrationCacheKey` формує ключ для запису, `readMigrationCache` і `writeMigrationCache` працюють із кешованими даними, а `withKnownMigrationNotes` підставляє вже відомі відомості там, де це доречно. Читання fail-safe (`readMigrationCache` перехоплює помилки й повертає `null` замість винятку); запис (`writeMigrationCache`) винятків не перехоплює.
17
17
 
18
18
  ## Поведінка
19
19
 
20
- - `DEFAULT_CACHE_DIR` — стандартний спільний каталог для кешу міграцій на цій машині, щоб різні репозиторії могли повторно використовувати вже відомі результати.
21
- - `migrationCacheKey` — перетворює назву пакета і діапазон версій на стабільний безпечний ключ для кешу.
22
- - `readMigrationCache` — читає кешований запис міграції для потрібної пари версій; якщо запису немає або він пошкоджений, мовчки повертає `null`.
23
- - `writeMigrationCache` — зберігає результат аналізу міграції в кеш, щоб наступні запуски на цій машині могли його повторити без нового дослідження.
24
- - `withKnownMigrationNotes` — додає до промпта підсумок уже відомої міграції з кешу й підказує пропустити повторне дослідження та перейти до перевірки використання в поточному проєкті.
20
+ - `DEFAULT_CACHE_DIR` — задає спільний каталог кешу міграцій для всіх репозиторіїв на цій машині.
21
+ - `migrationCacheKey` — перетворює пару версій пакета на безпечний крос-репо ключ кешу.
22
+ - `readMigrationCache` — читає кешований запис міграції для конкретної пари версій; якщо запису немає або він пошкоджений, повертає `null` без помилки.
23
+ - `writeMigrationCache` — зберігає запис про вже проаналізовану міграцію в кеш, щоб не повторювати той самий аналіз у наступних прогонових середовищах.
24
+ - `withKnownMigrationNotes` — додає до промпта підсумок відомої міграції з кешу й підказує пропустити повторне дослідження та перейти до перевірки в поточному проєкті.
25
25
 
26
26
  ## Публічний API
27
27
 
28
- - DEFAULT_CACHE_DIR — Спільний каталог кешу міграцій для всіх репозиторіїв на цій машині; не прив’язаний до конкретного worktree.
29
- - migrationCacheKey — Перетворює `` на безпечне ім’я файлу для спільного кешу між репо.
30
- - readMigrationCache — Повертає збережений запис міграції для ``, якщо його вже обробляли в іншому repo/worktree; відсутній або пошкоджений запис дає `null`.
31
- - writeMigrationCache — Записує результат LLM-обробки для ``, щоб не повторювати дослідження changelog для такого ж bump-а.
32
- - withKnownMigrationNotes — Додає до prompt короткий підсумок знайденої міграції і, якщо є кеш, пропускає дослідження changelog/diff на користь пошуку використання в поточному проєкті.
28
+ - DEFAULT_CACHE_DIR — Спільний каталог для кешу міграцій на цій машині, незалежний від конкретного repo чи worktree.
29
+ - migrationCacheKey — Перетворює `pkg`, `from` і `to` на безпечну назву файла для спільного кешу.
30
+ - readMigrationCache — Повертає збережені дані про вже відому міграцію для тієї самої пари версій, або `null`, якщо запису немає чи він пошкоджений.
31
+ - writeMigrationCache — Записує результат вже розібраної міграції в кеш, щоб наступний repo з тим самим оновленням не починав дослідження заново.
32
+ - withKnownMigrationNotes — Додає до prompt короткий підсумок знайденої міграції і пропускає початковий етап з пошуком у CHANGELOG та diff.
33
33
 
34
34
  ## Гарантії поведінки
35
35
 
36
- - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
37
- - За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
38
- - Кешує результати в межах одного прогону.
36
+ - `readMigrationCache` перехоплює помилки читання/парсингу й повертає `null` замість винятку (fail-safe); `writeMigrationCache` винятків не перехоплює.
37
+ - Кеш персистентний на диску і спільний для всіх repo/worktree на машині — не обмежений одним прогоном.