@7n/rules 1.41.0 → 1.42.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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.42.0] - 2026-07-22
4
+
5
+ ### Added
6
+
7
+ - warnAboutRulesWithoutConcerns: попередження, якщо rule-id з .n-rules.json#rules не знайдено в жодному rulesDir (ядро+плагіни) — ловить дрейф конфігу після переїзду concern-ів у плагін
8
+
3
9
  ## [1.41.0] - 2026-07-22
4
10
 
5
11
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.41.0",
3
+ "version": "1.42.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: run-detectors.mjs
4
4
  resource: npm/scripts/lib/lint-surface/run-detectors.mjs
5
5
  docgen:
6
- crc: 128fe3b7
6
+ crc: a0f45fc3
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -40,3 +40,5 @@ detectAll виконує прохід лінтера у режим детекц
40
40
  **Multi-dir (плагіни):** `effectiveRulesDirs` додає rules-каталоги плагінів з `.n-rules.json` (hot-path: без install, quiet); `readLintConcernsByRuleMulti` зливає концерни за іменем (перший власник виграє) — плагін може додавати концерни до правила ядра (mixin).
41
41
 
42
42
  **Capability-гейт:** `filterByCapabilities` відкидає концерни з незадоволеним `requires.capability` (capabilities надають встановлені плагіни через маніфест `n-rules.capabilities`; явний `opts.capabilities` у тестах перекриває резолв).
43
+
44
+ **Warning про rule-id без concern-ів:** якщо rule-id з `.n-rules.json#rules` не має жодного concern-а серед усіх `rulesDirs` (ядро + плагіни) — `console.error` попереджає про можливий дрейф конфігу (типово: правило переїхало в плагін, якого консюмер не підключив у `plugins[]`). Rule-id з існуючим каталогом, але без `concern.json` (документаційні правила на кшталт `feedback`), warning не тригерить.
@@ -152,15 +152,70 @@ async function readLintConcernsByRuleMulti(rulesDirs) {
152
152
  return merged
153
153
  }
154
154
 
155
+ /**
156
+ * Імена ВСІХ каталогів верхнього рівня під `rulesDirs` — незалежно від того, чи знайшлись
157
+ * у них concern-и. Потрібно, щоб відрізнити «каталог є, просто без lint-поверхні» (легітимно
158
+ * для суто-документаційних правил на кшталт `feedback`/`local-ai`) від «каталогу немає
159
+ * взагалі ні в ядрі, ні в жодному підключеному плагіні» (ознака дрейфу конфігу — типово
160
+ * правило переїхало в плагін, якого консюмер не підключив).
161
+ * @param {string[]} rulesDirs rules-каталоги (ядро + плагіни).
162
+ * @returns {Promise<Set<string>>} унікальні імена каталогів-правил.
163
+ */
164
+ async function discoverAllRuleDirNames(rulesDirs) {
165
+ const { readdir } = await import('node:fs/promises')
166
+ /** @type {Set<string>} */
167
+ const names = new Set()
168
+ for (const dir of rulesDirs) {
169
+ let entries
170
+ try {
171
+ entries = await readdir(dir, { withFileTypes: true })
172
+ } catch {
173
+ continue
174
+ }
175
+ for (const e of entries) {
176
+ if (e.isDirectory() && !e.name.startsWith('.')) names.add(e.name)
177
+ }
178
+ }
179
+ return names
180
+ }
181
+
182
+ /**
183
+ * Попереджає про rule-id з `.n-rules.json#rules`, яких немає ЖОДНИМ каталогом ні в ядрі, ні
184
+ * в підключених плагінах (не плутати з «каталог є, але без concern-ів» — легітимний випадок
185
+ * для суто-документаційних правил). Типова причина: правило переїхало в окремий плагін
186
+ * (напр. `js` → `@7n/rules-lang-js` з фази 5c), а `plugins[]` консюмера про це не знає —
187
+ * тоді перевірки для цього правила мовчки НЕ виконуються (0 знайдених concern-ів виглядає
188
+ * як «усе чисто», хоча насправді нічого не перевірялось).
189
+ * @param {Record<string, ConcernMeta[]>} byRule concerns згруповані за rule-id (з усіх rulesDirs).
190
+ * @param {import('../read-n-rules-config-lite.mjs').LiteConfig} config розпарсений .n-rules.json.
191
+ * @param {string[]} rulesDirs rules-каталоги (ядро + плагіни), для перевірки «каталог є, але порожній».
192
+ * @returns {Promise<void>}
193
+ */
194
+ async function warnAboutRulesWithoutConcerns(byRule, config, rulesDirs) {
195
+ const missing = config.rules.filter(id => !(id in byRule))
196
+ if (missing.length === 0) return
197
+ const allDirNames = await discoverAllRuleDirNames(rulesDirs)
198
+ for (const ruleId of missing) {
199
+ if (allDirNames.has(ruleId)) continue // каталог є, просто без lint-поверхні — легітимно
200
+ console.error(
201
+ `⚠️ .n-rules.json: правило "${ruleId}" не знайдено НІ В ОДНОМУ з rulesDirs (ні в ядрі, ні в ` +
202
+ `підключених плагінах "plugins") — перевірки для нього НЕ виконуються. Якщо правило нещодавно ` +
203
+ `переїхало в окремий плагін, додай відповідний пакет у "plugins" (і в devDependencies).`
204
+ )
205
+ }
206
+ }
207
+
155
208
  /**
156
209
  * Активні rule-id з `.n-rules.json` (для delta/full режимів).
157
210
  * @param {Record<string, ConcernMeta[]>} byRule concerns згруповані за rule-id.
158
211
  * @param {string} cwd робоча директорія прогону.
212
+ * @param {string[]} rulesDirs rules-каталоги (ядро + плагіни) — для warning про відсутні правила.
159
213
  * @returns {Promise<string[]>} перелік активних rule-id.
160
214
  */
161
- async function enabledRuleIds(byRule, cwd) {
215
+ async function enabledRuleIds(byRule, cwd, rulesDirs) {
162
216
  const config = await readNRulesConfigLite(cwd)
163
217
  if (!config.exists) return []
218
+ await warnAboutRulesWithoutConcerns(byRule, config, rulesDirs)
164
219
  return Object.keys(byRule).filter(id => isRuleEnabled(config, id))
165
220
  }
166
221
 
@@ -189,7 +244,8 @@ function sortEntries(entries) {
189
244
  * @returns {Promise<PlanItem[]>} впорядкований план прогону.
190
245
  */
191
246
  export async function buildDetectPlan(opts) {
192
- const byRule = await filterByCapabilities(await readLintConcernsByRuleMulti(await effectiveRulesDirs(opts)), opts)
247
+ const rulesDirs = await effectiveRulesDirs(opts)
248
+ const byRule = await filterByCapabilities(await readLintConcernsByRuleMulti(rulesDirs), opts)
193
249
  return buildPlan({
194
250
  byRule,
195
251
  full: opts.full === true,
@@ -198,7 +254,8 @@ export async function buildDetectPlan(opts) {
198
254
  pathMode: opts.pathMode === true,
199
255
  repoWide: opts.repoWide === true,
200
256
  baseRef: typeof opts.baseRef === 'string' ? opts.baseRef : null,
201
- cwd: opts.cwd
257
+ cwd: opts.cwd,
258
+ rulesDirs
202
259
  })
203
260
  }
204
261
 
@@ -209,8 +266,9 @@ export async function buildDetectPlan(opts) {
209
266
  * @returns {Promise<{ byRule: Record<string, ConcernMeta[]>, enabledSet: Set<string> }>} concerns і активні правила.
210
267
  */
211
268
  export async function loadEnabledLintRules(opts) {
212
- const byRule = await filterByCapabilities(await readLintConcernsByRuleMulti(await effectiveRulesDirs(opts)), opts)
213
- const enabledSet = new Set(await enabledRuleIds(byRule, opts.cwd))
269
+ const rulesDirs = await effectiveRulesDirs(opts)
270
+ const byRule = await filterByCapabilities(await readLintConcernsByRuleMulti(rulesDirs), opts)
271
+ const enabledSet = new Set(await enabledRuleIds(byRule, opts.cwd, rulesDirs))
214
272
  return { byRule, enabledSet }
215
273
  }
216
274
 
@@ -378,6 +436,7 @@ export function computeActiveDomains(byRule, enabledSet, changed) {
378
436
  * @param {boolean} [args.repoWide] `--repo-wide`: лише full-scope concerns, whole-repo.
379
437
  * @param {string|null} [args.baseRef] явна база дельти (`--base <ref>`) замість каскаду main→origin/main.
380
438
  * @param {string} args.cwd робоча директорія прогону.
439
+ * @param {string[]} [args.rulesDirs] rules-каталоги (ядро + плагіни) — для warning про відсутні правила.
381
440
  * @returns {Promise<PlanItem[]>} впорядкований план прогону.
382
441
  */
383
442
  async function buildPlan({
@@ -388,14 +447,15 @@ async function buildPlan({
388
447
  pathMode = false,
389
448
  repoWide = false,
390
449
  baseRef = null,
391
- cwd
450
+ cwd,
451
+ rulesDirs = []
392
452
  }) {
393
453
  // scoped + --path: per-file concerns названих правил × перетин path ∩ дельта
394
454
  if (rules.length > 0 && explicitFiles !== null) return buildScopedDeltaPlan(byRule, rules, explicitFiles)
395
455
  // scoped: усі lint-concerns названих правил, whole-repo
396
456
  if (rules.length > 0) return buildScopedPlan(byRule, rules)
397
457
 
398
- const enabled = await enabledRuleIds(byRule, cwd)
458
+ const enabled = await enabledRuleIds(byRule, cwd, rulesDirs)
399
459
  const enabledSet = new Set(enabled)
400
460
 
401
461
  // repo-wide: лише full-scope concerns (окремий CI-workflow, не гейтить деплой)
@@ -557,7 +617,8 @@ export async function detectAll(opts) {
557
617
  const verbose = opts.verbose === true
558
618
  const baseLog = opts.log ?? (s => process.stdout.write(s))
559
619
 
560
- const byRule = await filterByCapabilities(await readLintConcernsByRuleMulti(await effectiveRulesDirs(opts)), opts)
620
+ const rulesDirs = await effectiveRulesDirs(opts)
621
+ const byRule = await filterByCapabilities(await readLintConcernsByRuleMulti(rulesDirs), opts)
561
622
  const plan = await buildPlan({
562
623
  byRule,
563
624
  full,
@@ -566,7 +627,8 @@ export async function detectAll(opts) {
566
627
  pathMode: opts.pathMode === true,
567
628
  repoWide: opts.repoWide === true,
568
629
  baseRef: typeof opts.baseRef === 'string' ? opts.baseRef : null,
569
- cwd
630
+ cwd,
631
+ rulesDirs
570
632
  })
571
633
 
572
634
  // Detect-only бар — ЛИШЕ в TTY (без тикера «виправлено»). У не-TTY (hooks, CI-gate,