@7n/rules 1.2.0 → 1.2.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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.1] - 2026-07-14
4
+
5
+ ### Fixed
6
+
7
+ - Bun-сумісність тестового прогону: namespace-імпорт zod (фантомний __esModule на ESM-неймспейсах ламає vitest-interop), явний env: process.env у spawnSync skills-cli (Bun дає дітям snapshot оточення), чистка bun-node-* shim-тек з PATH для дочірнього v8r (node-shebang під --bun резолвився в bun і падав на node:sea); тест run-v8r приведено до контракту verbose-виводу (#44); root scripts.test → bun run --bun vitest run
8
+ - package-manifest: VALID_MAX_BUMPS → Set (oxlint prefer-set-has), дока maxBump освіжена
9
+
3
10
  ## [1.2.0] - 2026-07-14
4
11
 
5
12
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: package-manifest.mjs
4
4
  resource: npm/rules/changelog/lib/package-manifest.mjs
5
5
  docgen:
6
- crc: ced8ad49
6
+ crc: 10bad2ce
7
7
  ---
8
8
 
9
9
  Модуль `package-manifest.mjs` реалізує **уніфіковану абстракцію маніфесту пакета** для перевірок changelog у багатомовному монорепо. Він приховує відмінності між двома типами маніфестів:
@@ -17,7 +17,7 @@ docgen:
17
17
  2. **Читання маніфесту воркспейсу** в єдиній структурі `PackageManifest` (з пріоритетом `package.json` над `pyproject.toml`).
18
18
  3. **Виявлення коренів усіх пакетів монорепо**: npm-воркспейси через скрипт `workspaces.mjs` + автоматичний пошук Python-проєктів за `pyproject.toml`.
19
19
 
20
- Базова мета — дати правилам категорії `changelog` єдину модель «пакет = (kind, ws, name, version, registryPublishable)», незалежно від мови.
20
+ Базова мета — дати правилам категорії `changelog` єдину модель «пакет = (kind, ws, name, version, registryPublishable, maxBump)», незалежно від мови.
21
21
 
22
22
  ## Експорти / API
23
23
 
@@ -44,12 +44,14 @@ docgen:
44
44
  * @property {string | null} version semver-рядок
45
45
  * @property {boolean} registryPublishable чи застосовується режим порівняння з реєстром
46
46
  * @property {string[] | null} [npmFiles] лише npm: 'files' з package.json
47
+ * @property {'major' | 'minor' | 'patch' | null} maxBump стеля для 'n-rules release' (з 'package.json#release.maxBump'); null — без обмеження
47
48
  */
48
49
  ```
49
50
 
50
51
  ### Константи модуля
51
52
 
52
53
  - `PYPROJECT_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.venv/**', '**/venv/**']` — патерни, що **виключаються** під час сканування `pyproject.toml` глобом, щоб не зачіпати чужі залежності та віртуальні середовища.
54
+ - `VALID_MAX_BUMPS` — `Set` дозволених значень `release.maxBump` (`'major' | 'minor' | 'patch'`); будь-яке інше значення тихо ігнорується (`maxBump: null`).
53
55
 
54
56
  ## Функції
55
57
 
@@ -93,14 +95,14 @@ docgen:
93
95
  - Читає текст через `readFile(pkgPath, 'utf8')` і парсить як JSON.
94
96
  - Якщо JSON — не об'єкт (null, масив, примітив), повертає `null`.
95
97
  - Обчислює `registryPublishable = name є непорожнім рядком && private !== true && files є масивом`. Тобто пакет вважається «публікованим у реєстр», тільки якщо в `package.json` явно вказано непорожній `name`, не зведено `private: true`, і визначено поле `files` (whitelist того, що публікується).
96
- - Повертає об'єкт із `kind: 'npm'`, `manifestRel: 'package.json'`, з `name`/`version` (тільки якщо вони — рядки, інакше `null`), `registryPublishable`, `npmFiles = pkg.files` або `null`.
98
+ - Повертає об'єкт із `kind: 'npm'`, `manifestRel: 'package.json'`, з `name`/`version` (тільки якщо вони — рядки, інакше `null`), `registryPublishable`, `npmFiles = pkg.files` або `null`, а також `maxBump` — валідне значення `package.json#release.maxBump` (`major`/`minor`/`patch`) або `null`, якщо поле відсутнє, не рядок чи поза дозволеним списком.
97
99
  - Будь-яка помилка читання/парсингу JSON у блоці `try` → повертає `null` (catch без логування).
98
100
  2. **Гілка Python:**
99
101
  - Складає шлях `cwd/ws/pyproject.toml`.
100
102
  - `existsSync` → якщо файлу немає, повертає `null`.
101
103
  - Читає файл і викликає `parsePyprojectFields`.
102
104
  - `registryPublishable = Boolean(name && version)` — для Python публікація в PyPI потребує лише валідних `name` і `version` (PyPI не має аналога `files` whitelist).
103
- - Повертає об'єкт із `kind: 'python'`, `manifestRel: 'pyproject.toml'`, `npmFiles: null`.
105
+ - Повертає об'єкт із `kind: 'python'`, `manifestRel: 'pyproject.toml'`, `npmFiles: null`, `maxBump: null` (стеля бампа підтримується лише для npm-маніфестів).
104
106
  - **Side effects:**
105
107
  - Синхронний `existsSync` (двічі: на `package.json` і `pyproject.toml`).
106
108
  - Асинхронне читання файлу через `fs/promises.readFile`.
@@ -27,7 +27,7 @@ import { getMonorepoPackageRootDirs, isIgnoredWorkspaceRoot } from '../../../scr
27
27
  */
28
28
 
29
29
  const PYPROJECT_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.venv/**', '**/venv/**']
30
- const VALID_MAX_BUMPS = ['major', 'minor', 'patch']
30
+ const VALID_MAX_BUMPS = new Set(['major', 'minor', 'patch'])
31
31
 
32
32
  /**
33
33
  * @param {Record<string, unknown>} pkg розпарсений package.json
@@ -37,7 +37,7 @@ function maxBumpFromPackageJson(pkg) {
37
37
  const release = pkg.release
38
38
  if (!release || typeof release !== 'object' || Array.isArray(release)) return null
39
39
  const value = /** @type {Record<string, unknown>} */ (release).maxBump
40
- return typeof value === 'string' && VALID_MAX_BUMPS.includes(value)
40
+ return typeof value === 'string' && VALID_MAX_BUMPS.has(value)
41
41
  ? /** @type {'major' | 'minor' | 'patch'} */ (value)
42
42
  : null
43
43
  }
@@ -3,9 +3,7 @@ type: JS Module
3
3
  title: release.mjs
4
4
  resource: npm/rules/release/release.mjs
5
5
  docgen:
6
- crc: 475bce65
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- score: 100
6
+ crc: 3ac9ee4c
9
7
  ---
10
8
 
11
9
  Файл автоматизує процес випуску версій. Він агрегує зміна-файли для всіх робочих просторів у `version-bump` та `CHANGELOG`, комітить зміни, ставить тег `<name>@<version>` та видаляє використані зміна-файли. Цей процес виконується у CI на гілці `main` (n-rules-release-design, варіант A). Публічні функції, що надає модуль, — `release` та `runReleaseCli`.
@@ -13,6 +11,9 @@ docgen:
13
11
  ## Поведінка
14
12
 
15
13
  release агрегує change-файли для всіх робочих просторів, оновлює версії в маніфестах, додає секції до `CHANGELOG.md`, видаляє використані change-файли, створює коміт та анотовані теги для всіх зібраних релізів, а потім намагається пушити їх у апстрім з повторними спробами.
14
+
15
+ Обчислений bump обрізається зверху стелею `package.json#release.maxBump` робочого простору: наприклад, `major`-change-файл при стелі `minor` дає лише minor-реліз. Коли стеля реально спрацювала, у консоль друкується попередження з початковим bump, стелею та фінальною версією.
16
+
16
17
  runReleaseCli виконує процес релізу, викликаючи функцію release, та виводить відповідний статус у консоль.
17
18
 
18
19
  ## Публічний API
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: aggregate.mjs
4
4
  resource: npm/rules/release/lib/aggregate.mjs
5
5
  docgen:
6
- crc: 1f78a0fe
6
+ crc: ff9ff216
7
7
  ---
8
8
 
9
9
  Модуль `aggregate.mjs` забезпечує **агрегацію change-файлів одного workspace** у дві основні сутності:
@@ -23,7 +23,8 @@ docgen:
23
23
  | `maxBump(bumps)` | функція | Найвищий пріоритет бампа у списку (`major` > `minor` > `patch`) |
24
24
  | `renderChangelogSection(version, date, entries)` | функція | Рендер однієї версійної секції у markdown |
25
25
  | `prependChangelogSection(existingText, sectionBlock)` | функція | Вставка нової секції зверху наявного `CHANGELOG.md` |
26
- | `aggregateWorkspace({ currentVersion, changeFiles, date })` | функція | Високорівневе об’єднання change-файлів workspace у `newVersion` + `sectionBlock` |
26
+ | `capBump(bump, cap)` | функція | Обрізає bump зверху стелею `release.maxBump` (`major` `minor` тощо) |
27
+ | `aggregateWorkspace({ currentVersion, changeFiles, date, maxBumpCap })` | функція | Високорівневе об’єднання change-файлів workspace у `newVersion` + `sectionBlock` |
27
28
 
28
29
  Усі експорти — **іменовані** (`export function …`). Default-export відсутній.
29
30
 
@@ -76,6 +77,14 @@ docgen:
76
77
  - `maxBump(['patch'])` → `'patch'`
77
78
  - `maxBump([])` → `'patch'` (через fallback `?? 'patch'`)
78
79
 
80
+ ### `capBump(bump, cap)`
81
+
82
+ **Сигнатура:** `capBump(bump: string, cap?: string | null): string`
83
+
84
+ Обмежує обчислений bump **зверху** стелею з `package.json#release.maxBump`: якщо `bump` суворіший за `cap` (наприклад, `major` при `cap: 'minor'`), повертається `cap`; інакше — `bump` без змін. `cap` у `null`/`undefined` вимикає обмеження. Так пакет може заборонити автоматичну зміну власної major-версії, навіть якщо change-файл явно поставив `major`.
85
+
86
+ **Side effects:** немає.
87
+
79
88
  ### `renderChangelogSection(version, date, entries)`
80
89
 
81
90
  **Сигнатура:** `renderChangelogSection(version: string, date: string, entries: Array<{ section: string, description: string }>): string`
@@ -152,7 +161,7 @@ docgen:
152
161
  - `existingText = 'тут нічого корисного'`:
153
162
  - вихід: `# Changelog\n\n<sectionBlock>` (попередній вміст відкидається).
154
163
 
155
- ### `aggregateWorkspace({ currentVersion, changeFiles, date })`
164
+ ### `aggregateWorkspace({ currentVersion, changeFiles, date, maxBumpCap })`
156
165
 
157
166
  **Сигнатура:**
158
167
 
@@ -161,6 +170,7 @@ aggregateWorkspace({
161
170
  currentVersion: string,
162
171
  changeFiles: Array<{ file: string, entry: { bump: string, section: string, description: string } }>,
163
172
  date: string,
173
+ maxBumpCap?: string | null,
164
174
  }): { newVersion: string, sectionBlock: string, consumedFiles: string[] } | null
165
175
  ```
166
176
 
@@ -169,12 +179,13 @@ aggregateWorkspace({
169
179
  - `currentVersion` — поточна версія маніфесту workspace (`x.y.z`).
170
180
  - `changeFiles` — масив об’єктів, що відповідає виходу `readChangeFiles` з `change-file.mjs`. Кожен елемент має `file` (ім’я файлу `.md`) та `entry` (розпарсений frontmatter + опис).
171
181
  - `date` — рядок дати `YYYY-MM-DD` для секції CHANGELOG.
182
+ - `maxBumpCap` — стеля bump з `package.json#release.maxBump` (`'major' | 'minor' | 'patch'`); `null`/відсутній — без обмеження.
172
183
 
173
184
  **Повертає:**
174
185
 
175
186
  - `null`, якщо `changeFiles.length === 0` (явна ознака «нема чого релізити»).
176
187
  - Інакше — об’єкт:
177
- - `newVersion` — результат `bumpVersion(currentVersion, maxBump(<усі bumps>))`.
188
+ - `newVersion` — результат `bumpVersion(currentVersion, capBump(maxBump(<усі bumps>), maxBumpCap))`.
178
189
  - `sectionBlock` — результат `renderChangelogSection(newVersion, date, <усі entries>)`.
179
190
  - `consumedFiles` — імена change-файлів (`c.file`), які мають бути видалені викликачем після успішного запису маніфесту й `CHANGELOG.md`.
180
191
 
@@ -211,7 +222,7 @@ aggregateWorkspace({
211
222
  1. **Збір change-файлів.** Викликач звертається до `readChangeFiles(ws, cwd)` з `change-file.mjs`, отримуючи `Array<{ file, entry }>` — усі `.md` із `<ws>/.changes/`, відсортовані за іменем (тобто де-факто за `timestamp`).
212
223
  2. **Отримання поточної версії.** Викликач читає `package.json` (або інший маніфест) і дістає `currentVersion`.
213
224
  3. **Дата.** Викликач формує `date = new Date().toISOString().slice(0, 10)` або еквівалент.
214
- 4. **Агрегація.** Виклик `aggregateWorkspace({ currentVersion, changeFiles, date })`:
225
+ 4. **Агрегація.** Виклик `aggregateWorkspace({ currentVersion, changeFiles, date, maxBumpCap })`:
215
226
  - Якщо повертає `null` — викликач пропускає workspace (нема `change-файлів`).
216
227
  - Якщо повертає об’єкт — отримуємо `newVersion`, `sectionBlock`, `consumedFiles`.
217
228
  5. **Оновлення `CHANGELOG.md`.** Викликач читає поточний `CHANGELOG.md` (або порожній рядок, якщо файлу нема), застосовує `prependChangelogSection(existingText, sectionBlock)` і записує результат на диск.
@@ -244,7 +255,7 @@ aggregateWorkspace({
244
255
  5. Експортує функцію `prependChangelogSection(existingText, sectionBlock)`, яка:
245
256
  - Якщо `existingText.trimStart()` не починається з `# Changelog`, повертає `# Changelog\n\n<sectionBlock>`.
246
257
  - Інакше відокремлює перший рядок (`head`) і решту (`rest`, із `trimStart`) і повертає `head + '\n\n' + sectionBlock + '\n' + rest`.
247
- 6. Експортує функцію `aggregateWorkspace({ currentVersion, changeFiles, date })`, яка:
258
+ 6. Експортує функцію `aggregateWorkspace({ currentVersion, changeFiles, date, maxBumpCap })`, яка:
248
259
  - Повертає `null`, якщо `changeFiles` порожній.
249
- - Інакше повертає `{ newVersion, sectionBlock, consumedFiles }`, де `newVersion = bumpVersion(currentVersion, maxBump(<усі c.entry.bump>))`, `sectionBlock = renderChangelogSection(newVersion, date, <усі c.entry>)`, `consumedFiles = <усі c.file>`.
260
+ - Інакше повертає `{ newVersion, sectionBlock, consumedFiles }`, де `newVersion = bumpVersion(currentVersion, capBump(maxBump(<усі c.entry.bump>), maxBumpCap))`, `sectionBlock = renderChangelogSection(newVersion, date, <усі c.entry>)`, `consumedFiles = <усі c.file>`.
250
261
  7. Не виконує жодного I/O й не залежить від `node:*`.
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/text/run-v8r/main.mjs
5
5
  docgen:
6
- crc: f6f78d26
6
+ crc: 804dce1c
7
7
  ---
8
8
 
9
9
  ## Огляд
@@ -23,6 +23,7 @@ docgen:
23
23
  - `runV8rWithGlobs(globs?)` — послідовно запускає `v8r` для кожного glob-у з `V8R_CONFIG_FILE`, вказаним на резольвнутий конфіг; при коді 0/98 вивід приховано, інакше друкується й повертається код першого невдалого прогону.
24
24
  - `runV8rWithFiles(files)` — delta-режим: один запуск `v8r` по конкретних існуючих шляхах (код 98 неможливий), порожній список — одразу 0.
25
25
  - `warnAboutRemoteSchemaFallback(stderrText)` — парсить stderr `v8r` і на кожен файл, чию схему знайдено мережевим fallback-ом (schemastore, не customCatalog), друкує в stdout пораду додати схему в каталог.
26
+ - `stripBunNodeShimDirs(pathValue)` — прибирає з PATH shim-теки `bun-node-*`, які `bun run --bun` додає з підміненим `node`: дочірній `v8r` (node-shebang) інакше виконувався б під bun і падав на непідтримуваному `node:sea`. Дочірній процес `v8r` завжди отримує очищений PATH.
26
27
  - `lint(ctx)` — detector `text/run-v8r`: `ctx.files` → delta по відфільтрованих розширеннях, без `ctx.files` → full за `DEFAULT_V8R_GLOBS`.
27
28
 
28
29
  ## Публічний API
@@ -32,6 +33,7 @@ docgen:
32
33
  - `writeResolvedV8rConfig()` — матеріалізація тимчасового конфігу, повертає його шлях.
33
34
  - `runV8rWithGlobs(globs?)`, `runV8rWithFiles(files)` — точки входу перевірки (full/delta).
34
35
  - `warnAboutRemoteSchemaFallback(stderrText)` — попередження про мережевий fallback схем.
36
+ - `stripBunNodeShimDirs(pathValue)` — PATH без shim-тек `bun-node-*` (для дочірнього v8r).
35
37
  - `lint(ctx)` — інтеграція в lint-пайплайн.
36
38
 
37
39
  ## Гарантії поведінки
@@ -47,7 +47,8 @@
47
47
  import { spawnSync } from 'node:child_process'
48
48
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
49
49
  import { tmpdir } from 'node:os'
50
- import { dirname, isAbsolute, join } from 'node:path'
50
+ import { basename, delimiter, dirname, isAbsolute, join } from 'node:path'
51
+ import { env } from 'node:process'
51
52
  import { fileURLToPath } from 'node:url'
52
53
 
53
54
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
@@ -150,6 +151,21 @@ export function warnAboutRemoteSchemaFallback(stderrText) {
150
151
  }
151
152
  }
152
153
 
154
+ /**
155
+ * Прибирає з PATH shim-теки `bun-node-*`: їх додає `bun run --bun`, підміняючи `node` через
156
+ * symlink на bun. `bun x v8r` поважає node-shebang і бере `node` з PATH — під shim v8r виконується bun-ом
157
+ * і падає на непідтримуваному `node:sea`, тому дочірній v8r має бачити справжній node.
158
+ * @param {string | undefined} pathValue значення PATH батьківського процесу
159
+ * @returns {string | undefined} PATH без shim-тек (undefined — якщо PATH не задано)
160
+ */
161
+ export function stripBunNodeShimDirs(pathValue) {
162
+ if (!pathValue) return pathValue
163
+ return pathValue
164
+ .split(delimiter)
165
+ .filter(entry => !basename(entry).startsWith('bun-node-'))
166
+ .join(delimiter)
167
+ }
168
+
153
169
  /**
154
170
  * Один виклик `bun x v8r <targets...>` з підготовленим `customCatalog`-конфігом.
155
171
  * @param {string[]} targets glob-и або конкретні шляхи файлів
@@ -165,7 +181,7 @@ function runOneV8rInvocation(targets, configPath, verbose = false) {
165
181
  maxBuffer: 50 * 1024 * 1024,
166
182
  shell: false,
167
183
  stdio: ['ignore', 'pipe', 'pipe'],
168
- env: { ...process.env, V8R_CONFIG_FILE: configPath }
184
+ env: { ...env, PATH: stripBunNodeShimDirs(env.PATH), V8R_CONFIG_FILE: configPath }
169
185
  })
170
186
 
171
187
  if (result.error) {
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: skills-cli.mjs
4
4
  resource: npm/scripts/skills-cli.mjs
5
5
  docgen:
6
- crc: 3a6aab39
6
+ crc: 30b6209c
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: normalize-pipeline.mjs
4
4
  resource: npm/scripts/lib/adr/normalize-pipeline.mjs
5
5
  docgen:
6
- crc: 9c2cbb81
6
+ crc: f8bd18e2
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -23,7 +23,9 @@
23
23
  *
24
24
  * Повертає той самий operations[]-контракт, що й single-shot — apply-логіка спільна.
25
25
  */
26
- import { z } from 'zod'
26
+ // Namespace-імпорт замість `import { z }`: Bun показує фантомний `__esModule` на ESM-неймспейсах,
27
+ // через що interop у vitest приймає default-експорт zod за CJS-обгортку і губить named-експорт `z`.
28
+ import * as z from 'zod'
27
29
  import { runOneShot } from '@7n/llm-lib/one-shot'
28
30
  import { startChain } from '@7n/llm-lib/chain'
29
31
  import { CLOUD_MIN, resolveModel } from '@7n/llm-lib/model-tiers'
@@ -55,7 +55,9 @@ const USAGE_LINES = [
55
55
  * @returns {boolean} чи знайдено бінарник у PATH
56
56
  */
57
57
  function isBinaryInPath(name) {
58
- const probe = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8' })
58
+ // Явний `env: process.env` Bun без нього дає дітям snapshot оточення зі старту процесу
59
+ // і не бачить runtime-змін `process.env.PATH` (та сама причина, що в resolve-cmd.mjs).
60
+ const probe = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8', env: process.env })
59
61
  return probe.status === 0
60
62
  }
61
63
 
@@ -196,7 +198,8 @@ function runLlmCli(kind, prompt, projectDir, logError) {
196
198
  input: prompt,
197
199
  cwd: projectDir,
198
200
  stdio: ['pipe', 'inherit', 'inherit'],
199
- encoding: 'utf8'
201
+ encoding: 'utf8',
202
+ env: process.env
200
203
  })
201
204
  return result.status ?? 1
202
205
  }