@7n/rules 1.49.14 → 1.49.16

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.49.16] - 2026-07-27
4
+
5
+ ### Fixed
6
+
7
+ - Обмежено npm-module репозиторіями з npm publisher topology
8
+
9
+ ## [1.49.15] - 2026-07-27
10
+
11
+ ### Fixed
12
+
13
+ - refresh canonical mutation results after generated coverage tests
14
+
3
15
  ## [1.49.14] - 2026-07-27
4
16
 
5
17
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.49.14",
3
+ "version": "1.49.16",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -88,6 +88,7 @@ export async function fixWorker(violations, ctx, deps = {}) {
88
88
 
89
89
  /** @type {string[]} */
90
90
  const touchedFiles = []
91
+ const mutationRefreshFiles = []
91
92
  /** @type {Array<{provider: string, hook: string, files: string[], error: string}>} */
92
93
  const failed = []
93
94
  let feedback = null
@@ -105,6 +106,7 @@ export async function fixWorker(violations, ctx, deps = {}) {
105
106
  try {
106
107
  const res = await provider[hook]({ ...args, cwd: ctx.cwd, ctx: hookCtx(ctx, deadlineAt) })
107
108
  touchedFiles.push(...(res?.touchedFiles ?? []))
109
+ mutationRefreshFiles.push(...(res?.mutationRefreshFiles ?? []))
108
110
  if (res?.feedback?.previousError) feedback = res.feedback
109
111
  deferred.push(...(res?.deferred ?? []))
110
112
  for (const failure of res?.failed ?? []) {
@@ -137,5 +139,5 @@ export async function fixWorker(violations, ctx, deps = {}) {
137
139
  }
138
140
  }
139
141
 
140
- return { touchedFiles, failed, deferred, feedback }
142
+ return { touchedFiles, mutationRefreshFiles: [...new Set(mutationRefreshFiles)], failed, deferred, feedback }
141
143
  }
@@ -113,7 +113,7 @@ export async function lint(ctx) {
113
113
  }
114
114
 
115
115
  if (!(await provider.detect(cwd))) continue
116
- let rows = await provider.collect(cwd, {})
116
+ let rows = await provider.collect(cwd, { mutationRefreshFiles: ctx.mutationRefreshFiles ?? [] })
117
117
 
118
118
  // LLM-класифікація survived-мутантів (allowed gaps): verdict-и
119
119
  // equivalent/defensive/glue/wrapper з confidence ≥ порогу виключаються зі
@@ -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: e9224e37
6
+ crc: 9f044303
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  tier: cloud-min
9
9
  score: 100
@@ -19,7 +19,7 @@ docgen:
19
19
 
20
20
  DEFAULT_RULES_DIR задає базовий корінь правил, від якого стартує discovery, коли споживач не передав власні каталоги.
21
21
 
22
- buildDetectPlan спочатку визначає набір rules-каталогів, потім відбирає лише доступні concern-и з урахуванням capability, далі будує план виконання за режимом прогону: scoped, delta, full або repo-wide. Сам план фіксує, які concern-и запускаються whole-repo, а які лише по перетину з файлами, щоб detect і fix-pipeline працювали з однаковою картиною.
22
+ buildDetectPlan спочатку визначає набір rules-каталогів, відбирає concern-и за capability і застосовує опційний rule-level `applies` gate. Лише після цього він будує план виконання за режимом прогону: scoped, delta, full або repo-wide. Сам план фіксує, які concern-и запускаються whole-repo, а які лише по перетину з файлами, щоб detect і fix-pipeline працювали з однаковою картиною.
23
23
 
24
24
  loadEnabledLintRules використовує той самий discovery-ланцюжок, але повертає не план, а повну мапу concern-и за rule-id разом із множиною активних правил. Це потрібно зовнішнім споживачам, які мають знати, що реально доступно для прогону, не запускаючи сам detector-цикл.
25
25
 
@@ -42,6 +42,8 @@ concern тригериться на цих файлах (та сама табл
42
42
  full-scope перевірки — справа `--repo-wide`).
43
43
  - detectAll — Запускає detect-only прохід. Повертає всі violations і похідний exitCode.
44
44
 
45
+ Rule-level `applies` у `<rule>/applies/main.mjs` може повернути `false`, щоб виключити всі concern-и правила з плану для поточного репозиторію.
46
+
45
47
  ## Гарантії поведінки
46
48
 
47
49
  - Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
@@ -9,9 +9,10 @@
9
9
  * @typedef {import('../concern-meta.mjs').ConcernMeta} ConcernMeta
10
10
  * @typedef {{ ruleId: string, concern: ConcernMeta }} LintEntry
11
11
  */
12
+ import { existsSync } from 'node:fs'
12
13
  import { dirname, join } from 'node:path'
13
14
  import { env } from 'node:process'
14
- import { fileURLToPath } from 'node:url'
15
+ import { fileURLToPath, pathToFileURL } from 'node:url'
15
16
 
16
17
  import picomatch from 'picomatch'
17
18
 
@@ -126,6 +127,46 @@ async function filterByCapabilities(byRule, opts) {
126
127
  return out
127
128
  }
128
129
 
130
+ /**
131
+ * Застосовує опційний rule-level gate з `<rule>/applies/main.mjs` до всіх
132
+ * concern-ів правила. Це потрібно для доменних правил, чий канон має сенс
133
+ * лише за наявності конкретної topology в репозиторії: один gate не дає
134
+ * policy- й JS-concern-ам розійтися у власних евристиках застосовності.
135
+ *
136
+ * @param {Record<string, ConcernMeta[]>} byRule concerns за rule-id.
137
+ * @param {string} cwd корінь репозиторію, який лінтиться.
138
+ * @returns {Promise<Record<string, ConcernMeta[]>>} лише застосовні правила.
139
+ */
140
+ async function filterByRuleApplies(byRule, cwd) {
141
+ /** @type {Record<string, ConcernMeta[]>} */
142
+ const out = {}
143
+ for (const [ruleId, concerns] of Object.entries(byRule)) {
144
+ const firstConcern = concerns[0]
145
+ if (!firstConcern) continue
146
+ const appliesPath = join(dirname(firstConcern.dir), 'applies', 'main.mjs')
147
+ if (!existsSync(appliesPath)) {
148
+ out[ruleId] = concerns
149
+ continue
150
+ }
151
+ let applies
152
+ try {
153
+ const mod = await import(pathToFileURL(appliesPath).href)
154
+ applies = mod.applies
155
+ } catch (error) {
156
+ throw new Error(`rule ${ruleId}: не вдалося завантажити applies gate: ${error.message}`)
157
+ }
158
+ if (applies === undefined) {
159
+ out[ruleId] = concerns
160
+ continue
161
+ }
162
+ if (typeof applies !== 'function') {
163
+ throw new Error(`rule ${ruleId}: applies/main.mjs має експортувати applies(cwd)`)
164
+ }
165
+ if (await applies(cwd)) out[ruleId] = concerns
166
+ }
167
+ return out
168
+ }
169
+
129
170
  /**
130
171
  * Мердж concerns кількох rules-каталогів: правила зливаються за id, концерни — за іменем
131
172
  * (перший власник виграє: ядро → плагіни у порядку списку). Плагін може ДОДАВАТИ концерни
@@ -267,7 +308,8 @@ export async function buildDetectPlan(opts) {
267
308
  */
268
309
  export async function loadEnabledLintRules(opts) {
269
310
  const rulesDirs = await effectiveRulesDirs(opts)
270
- const byRule = await filterByCapabilities(await readLintConcernsByRuleMulti(rulesDirs), opts)
311
+ const capable = await filterByCapabilities(await readLintConcernsByRuleMulti(rulesDirs), opts)
312
+ const byRule = await filterByRuleApplies(capable, opts.cwd)
271
313
  const enabledSet = new Set(await enabledRuleIds(byRule, opts.cwd, rulesDirs))
272
314
  return { byRule, enabledSet }
273
315
  }
@@ -618,7 +660,8 @@ export async function detectAll(opts) {
618
660
  const baseLog = opts.log ?? (s => process.stdout.write(s))
619
661
 
620
662
  const rulesDirs = await effectiveRulesDirs(opts)
621
- const byRule = await filterByCapabilities(await readLintConcernsByRuleMulti(rulesDirs), opts)
663
+ const capable = await filterByCapabilities(await readLintConcernsByRuleMulti(rulesDirs), opts)
664
+ const byRule = await filterByRuleApplies(capable, cwd)
622
665
  const plan = await buildPlan({
623
666
  byRule,
624
667
  full,
@@ -126,10 +126,18 @@ function isStandaloneConcern(patterns) {
126
126
  * @param {string} cwd Робоча директорія для запуску детектора.
127
127
  * @param {import('./progress.mjs').ProgressReporter|null} [progress] Reporter прогресу.
128
128
  * @param {boolean} [verbose] Детальний вивід (прокидається у ctx concern-а).
129
+ * @param {string[]} [mutationRefreshFiles] Source-файли з cache-independent mutation verdict-ом.
129
130
  * @returns {Promise<LintViolation[]>} Актуальні порушення concern-а після re-detect.
130
131
  */
131
- async function reDetect(item, cwd, progress = null, verbose = false) {
132
- const ctx = { cwd, ruleId: item.entry.ruleId, concernId: item.entry.concern.name, files: item.files, verbose }
132
+ async function reDetect(item, cwd, progress = null, verbose = false, mutationRefreshFiles = []) {
133
+ const ctx = {
134
+ cwd,
135
+ ruleId: item.entry.ruleId,
136
+ concernId: item.entry.concern.name,
137
+ files: item.files,
138
+ verbose,
139
+ mutationRefreshFiles
140
+ }
133
141
  const res = await runConcernDetector(item.entry.concern, ctx)
134
142
  progress?.detectSnapshot(progressKey(item), res.violations.length)
135
143
  return res.violations
@@ -416,7 +424,7 @@ async function runRung(rung, worker, violations, feedback, rungDeps) {
416
424
  // ніколи не виконується.
417
425
  let after
418
426
  try {
419
- after = await reDetect(item, cwd, progress, verbose)
427
+ after = await reDetect(item, cwd, progress, verbose, workerResult?.mutationRefreshFiles ?? [])
420
428
  } catch (detectError) {
421
429
  snapshot.rollback()
422
430
  throw detectError