@7n/rules 1.19.0 → 1.20.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.20.0] - 2026-07-18
4
+
5
+ ### Changed
6
+
7
+ - Автодетект `@7n/rules-lang-rust` покриває монорепо: сигнал Cargo.toml шукається не лише в корені, а й у підтеках до 3 рівнів (обмежений BFS повз приховані/node_modules/target — дешево для hot-path hook) — Tauri-кейс `app/src-tauri/Cargo.toml` вмикає плагін без ручного `.n-rules.json`. Python лишається кореневим (uv-провайдер v1 обробляє тільки кореневий pyproject.toml)
8
+
9
+ ## [1.19.1] - 2026-07-18
10
+
11
+ ### Changed
12
+
13
+ - експортовано `parseNRulesCmd` і `relevantDomains` з `scripts/lib/lint-surface/ci-plan.mjs` — спільні хелпери для fix-хендлерів автоміграції service-канону (ci-azure, ci-github), усунуто jscpd-дублікат
14
+
3
15
  ## [1.19.0] - 2026-07-18
4
16
 
5
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -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: d1bfa8f2
6
+ crc: ab45e0ca
7
7
  ---
8
8
 
9
9
  Резолв плагінів @7n/rules: визначає, які пакети-плагіни активні у проєкті, де їхні `rules/`-каталоги, які capabilities вони надають і які handlers надають.
@@ -14,7 +14,8 @@
14
14
  * `--base`-ref) — warning і ВСІ домени true (запускаємо більше, ніколи не
15
15
  * скіпаємо мовчки).
16
16
  */
17
- import { appendFileSync } from 'node:fs'
17
+ import { appendFileSync, existsSync, statSync } from 'node:fs'
18
+ import { resolve } from 'node:path'
18
19
  import { env } from 'node:process'
19
20
 
20
21
  import picomatch from 'picomatch'
@@ -29,6 +30,59 @@ const TEST_FILE_GLOBS = ['**/*.test.*', '**/*.spec.*', '**/test_*.py', '**/*_tes
29
30
  /** Зарезервовані ключі outputs — домен із таким ключем був би колізією. */
30
31
  const RESERVED_OUTPUT_KEYS = new Set(['any', 'has_tests', 'domains'])
31
32
 
33
+ const WS_RE = /\s+/u
34
+
35
+ /**
36
+ * Розбирає команду `n-rules lint …`/`n-rules ci plan …` токенами (без regex —
37
+ * стійко до багаторядкових script-ів і без бектрекінгу). Спільне для
38
+ * fix-хендлерів автоміграції service-канону (ci-azure, ci-github).
39
+ * @param {string} cmd повний текст команди кроку.
40
+ * @param {string} marker підрядок-початок (`n-rules lint` | `n-rules ci plan`).
41
+ * @returns {{ domain: string|null, path: string|null }|null} розбір або null, якщо маркера немає.
42
+ */
43
+ export function parseNRulesCmd(cmd, marker) {
44
+ const at = cmd.indexOf(marker)
45
+ if (at === -1) return null
46
+ const tokens = cmd
47
+ .slice(at + marker.length)
48
+ .split(WS_RE)
49
+ .filter(Boolean)
50
+ let domain = null
51
+ let path = null
52
+ for (let i = 0; i < tokens.length; i++) {
53
+ const t = tokens[i]
54
+ if (t === '--path') {
55
+ path = tokens[i + 1] ?? null
56
+ i++
57
+ continue
58
+ }
59
+ if (!t.startsWith('-') && domain === null && path === null) domain = t
60
+ }
61
+ return { domain, path }
62
+ }
63
+
64
+ /**
65
+ * Релевантні домени сервісу: enabled-правила, чиї per-file glob-и матчать
66
+ * бодай один файл піддерева (та сама таблиця, що `ci plan` по не-дельті).
67
+ * Спільне для fix-хендлерів автоміграції service-канону (ci-azure, ci-github).
68
+ * @param {string} cwd корінь consumer-репо.
69
+ * @param {string} servicePath каталог сервісу.
70
+ * @returns {Promise<string[]>} відсортовані rule-id.
71
+ */
72
+ export async function relevantDomains(cwd, servicePath) {
73
+ const abs = resolve(cwd, servicePath)
74
+ if (!existsSync(abs) || !statSync(abs).isDirectory()) return []
75
+ const files = await collectPathScopedFiles(cwd, servicePath)
76
+ const { byRule, enabledSet } = await loadEnabledLintRules({ cwd })
77
+ const active = computeActiveDomains(byRule, enabledSet, files)
78
+ return active
79
+ .entries()
80
+ .filter(([, st]) => st.triggered)
81
+ .map(([id]) => id)
82
+ .toArray()
83
+ .toSorted((a, b) => a.localeCompare(b))
84
+ }
85
+
32
86
  /**
33
87
  * Rule-id → ключ output-змінної: `-` → `_` (GA/Azure не люблять дефіси в
34
88
  * іменах змінних; `npm-module` → `npm_module`). Експортовано для fix-хендлерів
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: ci-plan.mjs
4
4
  resource: npm/scripts/lib/lint-surface/ci-plan.mjs
5
5
  docgen:
6
- crc: ce7f2a96
6
+ crc: 7df6d5b8
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 90
9
9
  issues: internal-name:computeActiveDomains,judge:inaccurate:0.99
@@ -28,12 +28,55 @@ export const KNOWN_CI_PLUGINS = Object.freeze({
28
28
  azure: '@7n/rules-ci-azure'
29
29
  })
30
30
 
31
- /** Відомі мовні плагіни: файловий сигнал екосистеми в корені репо → npm-пакет. */
31
+ /**
32
+ * Відомі мовні плагіни: файловий сигнал екосистеми → npm-пакет. `maxDepth` —
33
+ * до якої глибини шукати сигнал: python — лише корінь (uv-провайдер v1
34
+ * обробляє тільки кореневий pyproject.toml); rust — до 3 рівнів, бо в
35
+ * монорепо Cargo.toml часто вкладений (Tauri `app/src-tauri/Cargo.toml`),
36
+ * а провайдер обробляє всі знайдені маніфести.
37
+ */
32
38
  export const KNOWN_LANG_PLUGINS = Object.freeze({
33
- python: { signal: 'pyproject.toml', pkg: '@7n/rules-lang-python' },
34
- rust: { signal: 'Cargo.toml', pkg: '@7n/rules-lang-rust' }
39
+ python: { signal: 'pyproject.toml', pkg: '@7n/rules-lang-python', maxDepth: 0 },
40
+ rust: { signal: 'Cargo.toml', pkg: '@7n/rules-lang-rust', maxDepth: 3 }
35
41
  })
36
42
 
43
+ /** Теки, у які неглибокий скан мовних сигналів не заходить (плюс усі приховані). */
44
+ const LANG_SCAN_SKIP_DIRS = new Set(['node_modules', 'target', 'dist', 'coverage', 'build', 'vendor'])
45
+
46
+ /**
47
+ * Чи є файл-сигнал у корені або підтеках до `maxDepth` рівнів — обмежений
48
+ * BFS-обхід (пропускає приховані теки, node_modules, target тощо), щоб
49
+ * лишатись дешевим на hot-path (hook викликається на кожен файл).
50
+ * @param {string} projectRoot корінь репозиторію
51
+ * @param {string} signal ім'я файлу-сигналу (напр. `Cargo.toml`)
52
+ * @param {number} maxDepth 0 — лише корінь; N — до N рівнів підтек
53
+ * @returns {boolean} true — сигнал знайдено
54
+ */
55
+ function hasLangSignal(projectRoot, signal, maxDepth) {
56
+ let level = [projectRoot]
57
+ for (let depth = 0; depth <= maxDepth && level.length > 0; depth++) {
58
+ if (level.some(dir => existsSync(join(dir, signal)))) return true
59
+ if (depth < maxDepth) level = level.flatMap(listScannableSubdirs)
60
+ }
61
+ return false
62
+ }
63
+
64
+ /**
65
+ * Видимі підтеки для неглибокого скану (без прихованих і службових).
66
+ * @param {string} dir тека
67
+ * @returns {string[]} абсолютні шляхи підтек (порожньо, якщо нечитабельна)
68
+ */
69
+ function listScannableSubdirs(dir) {
70
+ try {
71
+ return readdirSync(dir, { withFileTypes: true })
72
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !LANG_SCAN_SKIP_DIRS.has(entry.name))
73
+ .map(entry => join(dir, entry.name))
74
+ } catch {
75
+ /* нечитабельна тека — пропускаємо гілку */
76
+ return []
77
+ }
78
+ }
79
+
37
80
  const WORKFLOW_YML_RE = /\.ya?ml$/u
38
81
  const GITHUB_URL_RE = /github\.com/iu
39
82
  const AZURE_URL_RE = /dev\.azure\.com|visualstudio\.com/iu
@@ -97,14 +140,14 @@ function detectCiPlugins(projectRoot) {
97
140
  /**
98
141
  * Автодетект плагінів за станом репозиторію: CI-плагіни (файлові сигнали з
99
142
  * fallback на `repository.url`) + мовні плагіни (лише файлові сигнали —
100
- * маніфест екосистеми в корені pyproject.toml, кореневий Cargo.toml; URL-fallback для мов безглуздий).
143
+ * маніфест екосистеми в корені або, для rust, у підтеках до 3 рівнів; URL-fallback для мов безглуздий).
101
144
  * @param {string} projectRoot корінь репозиторію
102
145
  * @returns {string[]} npm-імена плагінів
103
146
  */
104
147
  export function detectPluginsFromRepo(projectRoot) {
105
148
  const out = detectCiPlugins(projectRoot)
106
- for (const { signal, pkg } of Object.values(KNOWN_LANG_PLUGINS)) {
107
- if (existsSync(join(projectRoot, signal))) out.push(pkg)
149
+ for (const { signal, pkg, maxDepth } of Object.values(KNOWN_LANG_PLUGINS)) {
150
+ if (hasLangSignal(projectRoot, signal, maxDepth)) out.push(pkg)
108
151
  }
109
152
  return out
110
153
  }