@7n/rules 1.23.0 → 1.24.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.24.0] - 2026-07-19
4
+
5
+ ### Added
6
+
7
+ - Фаза 4b spec lang-plugins-extraction: SKILL-фрагменти плагінів — sync доклеює конвенційні `skills/<id>/SKILL.fragment.md` активних плагінів до синхронізованого SKILL.md між стабільними маркерами (ідемпотентний ре-синк, блок зникає без активних фрагментів). Мовні гілки taze SKILL.md (Rust: детекція/cargo-команди/перевірки; Python: uv-цикл/PEP-класифікація) переїхали у фрагменти lang-плагінів — канон ядра описує npm/bun-гілку і generic-скелет
8
+
9
+ ## [1.23.1] - 2026-07-18
10
+
11
+ ### Changed
12
+
13
+ - Оновлено peer-версію @earendil-works/pi-coding-agent/pi-ai до ~0.80.10 — підтримка gpt-5.6-sol через openai-codex backend
14
+
3
15
  ## [1.23.0] - 2026-07-18
4
16
 
5
17
  ### Changed
package/bin/n-rules.js CHANGED
@@ -86,10 +86,11 @@ import {
86
86
  import { detectAutoSkills } from '../scripts/auto-skills.mjs'
87
87
  import { readSkillMetaRaw } from '../scripts/lib/skill-meta.mjs'
88
88
  import { injectWorktreeNotice } from '../scripts/lib/worktree-notice.mjs'
89
+ import { collectSkillFragments, injectSkillFragments } from '../scripts/lib/skill-fragments.mjs'
89
90
  import { injectRootNotice } from '../scripts/lib/root-notice.mjs'
90
91
  import { listProjectRulesMdcFiles } from '../scripts/lib/list-project-rules-mdc.mjs'
91
92
  import { ensureNRulesInRootDevDependencies } from '../scripts/ensure-n-rules-dev-dependencies.mjs'
92
- import { resolvePluginList, resolveRulesDirs } from '../scripts/lib/resolve-plugins.mjs'
93
+ import { resolvePluginList, resolvePlugins, resolveRulesDirs } from '../scripts/lib/resolve-plugins.mjs'
93
94
  import { assertCwdIsProjectRoot } from '../scripts/lib/assert-project-root.mjs'
94
95
  import { syncClaudeConfig } from '../scripts/sync-claude-config.mjs'
95
96
  import { syncGitignoreWorktree } from '../scripts/lib/sync-gitignore-worktree.mjs'
@@ -885,15 +886,19 @@ async function syncAgentsMd(agentsTemplatePath = BUNDLED_AGENTS_TEMPLATE_PATH) {
885
886
  * Копіює лише skills зі списку configSkills (джерело: skills/<id>/ у пакеті)
886
887
  * @param {string[]} configSkills id без префікса n-
887
888
  * @param {string} [bundledSkillsDir] каталог `skills/` у корені пакету-джерела
889
+ * @param {{ plugins?: unknown } | null} [config] конфіг `.n-rules.json` — активні плагіни для SKILL-фрагментів
888
890
  * @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих копіювань
889
891
  */
890
- async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
892
+ async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR, config = null) {
891
893
  if (configSkills.length === 0 || !existsSync(bundledSkillsDir)) {
892
894
  return { success: 0, fail: 0 }
893
895
  }
894
896
 
895
897
  const skillsRoot = join(cwd(), SKILLS_DIR)
896
898
  await mkdir(skillsRoot, { recursive: true })
899
+ // Активні плагіни — джерело конвенційних фрагментів skills/<id>/SKILL.fragment.md
900
+ // (фаза 4b spec lang-plugins-extraction): мовні гілки скіла їдуть з плагіном.
901
+ const activePlugins = resolvePlugins(cwd(), config, { allowInstall: false, quiet: true })
897
902
 
898
903
  let success = 0
899
904
  let fail = 0
@@ -923,6 +928,7 @@ async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
923
928
  if (entry.name === 'SKILL.md') {
924
929
  content = injectWorktreeNotice(content, worktree)
925
930
  content = injectRootNotice(content, rootOnly)
931
+ content = injectSkillFragments(content, collectSkillFragments(id, activePlugins))
926
932
  }
927
933
  await writeFile(join(destDir, entry.name), content, 'utf8')
928
934
  }
@@ -1487,7 +1493,7 @@ async function runSync() {
1487
1493
 
1488
1494
  await runSyncStep('❌ Skills: ', async () => {
1489
1495
  const { fail: skillFail } = await captureOutput(async () => {
1490
- const { success: skillOk, fail } = await syncSkills(skills, bundledSkillsDir)
1496
+ const { success: skillOk, fail } = await syncSkills(skills, bundledSkillsDir, config)
1491
1497
  if (skills.length > 0) {
1492
1498
  console.log(`\n🧩 Skills: ${skillOk} скопійовано, ${fail} з помилками`)
1493
1499
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -86,8 +86,8 @@
86
86
  },
87
87
  "optionalDependencies": {
88
88
  "@agentclientprotocol/claude-agent-acp": "^0.59.0",
89
- "@earendil-works/pi-ai": "0.80.2",
90
- "@earendil-works/pi-coding-agent": "0.80.2"
89
+ "@earendil-works/pi-ai": "0.80.10",
90
+ "@earendil-works/pi-coding-agent": "0.80.10"
91
91
  },
92
92
  "engines": {
93
93
  "bun": ">=1.3",
@@ -0,0 +1,27 @@
1
+ ---
2
+ type: JS Module
3
+ title: skill-fragments.mjs
4
+ resource: npm/scripts/lib/skill-fragments.mjs
5
+ docgen:
6
+ crc: 8b9c6f98
7
+ ---
8
+
9
+ ## Огляд
10
+
11
+ Фрагменти SKILL.md від плагінів (фаза 4b spec lang-plugins-extraction): плагін шипить конвенційний файл `skills/<skillId>/SKILL.fragment.md` із власною секцією скіла (напр. Rust-гілку taze), а синк доклеює фрагменти активних плагінів до скопійованого `SKILL.md` між стабільними маркерами. Так мовні знання скіла їдуть разом із кодом плагіна, а не сиротіють у ядрі.
12
+
13
+ ## Поведінка
14
+
15
+ - **collectSkillFragments** — збирає фрагменти скіла з активних плагінів у порядку списку плагінів; відсутній чи порожній файл фрагмента мовчки пропускається.
16
+ - **injectSkillFragments** — вшиває блок фрагментів у текст SKILL.md між маркерами `n-rules:plugin-fragments:start/end` (кожен фрагмент — у власних маркерах з іменем плагіна). Ре-синк ідемпотентний: наявний блок замінюється повністю; порожній список фрагментів — блок прибирається зовсім, текст без блоку не змінюється.
17
+
18
+ ## Публічний API
19
+
20
+ - FRAGMENTS_START / FRAGMENTS_END — стабільні маркери блоку фрагментів.
21
+ - collectSkillFragments(skillId, plugins) — знайдені фрагменти `{ pluginName, content }`.
22
+ - injectSkillFragments(content, fragments) — текст SKILL.md з актуальним блоком.
23
+
24
+ ## Гарантії поведінки
25
+
26
+ - Read-only щодо ФС: лише читає файли фрагментів; запис робить викликач (синк).
27
+ - Ідемпотентність вшивання: повторний виклик із тим самим списком не змінює текст.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Фрагменти SKILL.md від плагінів (фаза 4b spec lang-plugins-extraction).
3
+ *
4
+ * Плагін шипить конвенційний файл `skills/<skillId>/SKILL.fragment.md` —
5
+ * власну секцію скіла (напр. Rust-гілку taze). Під час синку скіла ядро
6
+ * доклеює фрагменти АКТИВНИХ плагінів до скопійованого `SKILL.md` між
7
+ * стабільними маркерами — ре-синк ідемпотентний: наявний блок замінюється
8
+ * повністю, без активних фрагментів — видаляється. Так мовні знання їдуть
9
+ * разом із кодом плагіна, а не сиротіють у ядрі.
10
+ */
11
+ import { existsSync, readFileSync } from 'node:fs'
12
+ import { join } from 'node:path'
13
+
14
+ /** Маркер початку блоку плагінних фрагментів (стабільний). */
15
+ export const FRAGMENTS_START = '<!-- n-rules:plugin-fragments:start -->'
16
+ /** Маркер кінця блоку плагінних фрагментів. */
17
+ export const FRAGMENTS_END = '<!-- n-rules:plugin-fragments:end -->'
18
+
19
+ /**
20
+ * Збирає фрагменти скіла з активних плагінів (у порядку списку плагінів).
21
+ * @param {string} skillId id скіла без префікса (напр. `taze`)
22
+ * @param {Array<{ name: string, packageRoot: string }>} plugins активні плагіни (з `resolvePlugins`)
23
+ * @returns {Array<{ pluginName: string, content: string }>} знайдені фрагменти
24
+ */
25
+ export function collectSkillFragments(skillId, plugins) {
26
+ const out = []
27
+ for (const plugin of plugins) {
28
+ const fragmentPath = join(plugin.packageRoot, 'skills', skillId, 'SKILL.fragment.md')
29
+ if (!existsSync(fragmentPath)) continue
30
+ const content = readFileSync(fragmentPath, 'utf8').trim()
31
+ if (content !== '') out.push({ pluginName: plugin.name, content })
32
+ }
33
+ return out
34
+ }
35
+
36
+ /**
37
+ * Вшиває блок фрагментів у текст SKILL.md (перед фінальним переносом рядка).
38
+ * Наявний блок між маркерами замінюється; порожній список фрагментів — блок
39
+ * прибирається зовсім.
40
+ * @param {string} content текст SKILL.md
41
+ * @param {Array<{ pluginName: string, content: string }>} fragments зібрані фрагменти
42
+ * @returns {string} текст із актуальним блоком фрагментів
43
+ */
44
+ export function injectSkillFragments(content, fragments) {
45
+ const startIdx = content.indexOf(FRAGMENTS_START)
46
+ const endIdx = content.indexOf(FRAGMENTS_END)
47
+ let base = content
48
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
49
+ base = content.slice(0, startIdx) + content.slice(endIdx + FRAGMENTS_END.length)
50
+ base = `${base.trimEnd()}\n`
51
+ }
52
+ if (fragments.length === 0) return base
53
+
54
+ const parts = fragments.map(
55
+ f => `<!-- n-rules:plugin:${f.pluginName}:start -->\n\n${f.content}\n\n<!-- n-rules:plugin:${f.pluginName}:end -->`
56
+ )
57
+ const block = `${FRAGMENTS_START}\n\n${parts.join('\n\n')}\n\n${FRAGMENTS_END}`
58
+ return `${base.trimEnd()}\n\n${block}\n`
59
+ }
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: n-taze
3
3
  description: >-
4
- Оновлення версій модулів проекту (bun/npm; якщо є Cargo.toml — Rust-крейти
5
- через cargo-edit; якщо є pyproject.toml Python-пакети через uv) з
6
- аналізом major-змін і автоматичним рефакторингом несумісного коду
4
+ Оновлення версій модулів проекту (bun/npm вбудовано; інші екосистеми — Rust,
5
+ Python тощо через активні lang-плагіни) з аналізом major-змін і
6
+ автоматичним рефакторингом несумісного коду
7
7
  version: '1.2'
8
8
  ---
9
9
 
@@ -11,63 +11,42 @@ version: '1.2'
11
11
 
12
12
  ## Мета
13
13
 
14
- Оновити всі модулі проекту (npm/bun-залежності, а за наявності `Cargo.toml`/`pyproject.toml` і Rust-крейти/Python-пакети) до останніх версій, виявити major-оновлення, перевірити сумісність змін з кодом проекту і за потреби зрефакторити несумісні місця.
14
+ Оновити всі модулі проекту (npm/bun-залежності вбудовано; інші екосистеми — за активними lang-плагінами, їхні гілки описані у фрагментах наприкінці цього файлу) до останніх версій, виявити major-оновлення, перевірити сумісність змін з кодом проекту і за потреби зрефакторити несумісні місця.
15
15
 
16
- ## Оркестрація (npm/bun-, Rust- і Python-гілка) — не одним промптом
16
+ ## Оркестрація (npm/bun + екосистемні гілки плагінів) — не одним промптом
17
17
 
18
18
  `npx @7n/rules skill pi|cursor|codex taze` **не** передає цей файл одним суцільним
19
19
  промптом в один агентський хід — так робив старий дизайн, і саме тому реальний
20
20
  прогін міг зависати без жодної діагностики (один величезний непрозорий хід на
21
21
  весь монорепо, без проміжних зупинок для перевірки прогресу). Замість цього `npm/skills/taze/js/orchestrate.mjs`:
22
22
 
23
- 1. Детерміновано, без LLM, виконує кроки 1-3 нижче для всіх гілок: npm/bun (бекап → `bunx taze -w -r latest` → `bun install` → `n-rules taze diff`); якщо знайдені `Cargo.toml`, Rust (бекап → `cargo upgrade --incompatible allow` → `cargo update` → `collectCargoDiff` з `cargo-diff.mjs`, детермінований cargo-еквівалент `n-rules taze diff` парсить кожен `Cargo.toml` через `smol-toml`, класифікує major за тим самим правилом caret-семантики, включно з Cargo-скороченими версіями `"1"`/`"0.4"`); якщо знайдений кореневий `pyproject.toml`, Python (бекаппо кожній прямій залежності `uv remove <pkg>` + `uv add <pkg>[extras] --bounds lower` `collectUvDiff`, парсить PEP 508/PEP 440 через `smol-toml`, той самий caret-принцип класифікації).
24
- 2. Для кожного **окремого** major-пакета/крейта з diff-у — один ізольований, обмежений виклик обраного раннера (кроки 4-6, лише для цього запису; промпт генерує `buildDependencyPrompt`/`buildCargoDependencyPrompt`/`buildUvDependencyPrompt`, не цей SKILL.md).
23
+ 1. Детерміновано, без LLM, виконує кроки 1-3 нижче: npm/bun вбудовано (бекап → `bunx taze -w -r latest` → `bun install` → `n-rules taze diff`), а кожну екосистемну гілку — через її EcosystemProvider (detectbackup bumpdiff; деталі команд у фрагменті відповідного плагіна наприкінці файлу).
24
+ 2. Для кожного **окремого** major-запису з diff-у — один ізольований, обмежений виклик обраного раннера (кроки 4-6, лише для цього запису; промпт генерує `buildDependencyPrompt` ядра або `promptFor` провайдера, не цей SKILL.md).
25
25
  3. Детерміновано прибирає бекапи (крок 7) і компонує звіт (крок 8) з результатів усіх ітерацій.
26
26
 
27
27
  Не-npm екосистеми оркестратор веде через **EcosystemProvider-порт**
28
28
  (`@7n/rules/plugin-api`, spec `docs/specs/2026-07-18-lang-plugins-extraction-spec.md`)
29
- плагіни, що доставляються автоматично при першому запуску за файловим сигналом
30
- (extension-point `taze`): Rust **`@7n/rules-lang-rust`** (кореневий `Cargo.toml`),
31
- Python **`@7n/rules-lang-python`** (кореневий `pyproject.toml`). На чисто-JS
32
- проєкті без цих сигналів у звіті немає навіть згадки про інші екосистеми.
33
-
34
- Якщо `Cargo.toml` знайдені, але `cargo-edit` не встановлено — Rust-гілка **не**
35
- блокує інші гілки: оркестратор пропускає Rust-обробку і в звіті перелічує
36
- знайдені `Cargo.toml` як такі, що потребують ручного прогону (нижче). Так само,
37
- якщо `pyproject.toml` знайдений, але `uv` не встановлено — Python-гілка **не**
38
- блокує інші гілки.
29
+ lang-плагіни доставляються автоматично при першому запуску за файловим
30
+ сигналом (extension-point `taze`) і додають свої гілки як фрагменти цього
31
+ SKILL.md (блок наприкінці файлу). На чисто-JS проєкті без таких сигналів у
32
+ звіті немає навіть згадки про інші екосистеми. Якщо тулчейн гілки не
33
+ встановлено — вона пропускається з причиною у звіті, не блокуючи інші.
39
34
 
40
35
  Переваги: падіння/timeout на одному пакеті/крейті не втрачає прогрес по інших;
41
36
  кожен виклик успадковує власний timeout раннера (короткий, бо скоуп малий —
42
37
  один запис, не весь монорепо); видно прогрес по записах, а не чорну скриньку.
43
38
 
44
- Кроки 1-8 нижче лишаються джерелом правди щодо ЗМІСТУ роботи (що саме робить
45
- кожен крок) — оркестратор їх виконує програмно (1-3/7/8) або як промпт на
46
- один запис (4-6) для всіх гілок.
39
+ Кроки 1-8 нижче лишаються джерелом правди щодо ЗМІСТУ роботи npm/bun-гілки
40
+ скелетом для екосистемних) — оркестратор їх виконує програмно (1-3/7/8)
41
+ або як промпт на один запис (4-6). Мовні відповідники кроків — у фрагментах
42
+ плагінів наприкінці файлу.
47
43
 
48
44
  ## Передумови
49
45
 
50
46
  - Чисте робоче дерево (`git status` без незакомічених змін у `package.json` / `bun.lock` / `node_modules`) — інакше різницю не відрізнити від оновлення.
51
47
  - Встановлений `bun` і доступний `bunx`.
52
48
  - Запуск з кореня проекту (де лежить `package.json` / `bun.lock`).
53
- - Якщо в проекті є хоч один `Cargo.toml` (не в `node_modules`/`.worktrees`) — бажано встановлений `cargo-edit` (`cargo install cargo-edit`, дає команду `cargo upgrade`). Без нього major-бампи Rust-залежностей неможливо застосувати детерміновано (голий `cargo update` піднімає лише semver-сумісні версії) оркестратор пропускає Rust-гілку (без блокування інших гілок) і перелічує знайдені `Cargo.toml` у звіті як такі, що потребують ручного прогону кроків 2-8 нижче.
54
- - Якщо в проекті є кореневий `pyproject.toml` — бажано встановлений `uv`. Без нього major-бампи Python-залежностей неможливо застосувати детерміновано — оркестратор пропускає Python-гілку (без блокування інших гілок) і перелічує `pyproject.toml` у звіті як такий, що потребує ручного прогону кроків 2-8 нижче.
55
-
56
- ### 0.2. Детекція Rust-крейтів
57
-
58
- ```bash
59
- find . -name Cargo.toml -not -path "*/node_modules/*" -not -path "*/.worktrees/*" -not -path "*/target/*"
60
- ```
61
-
62
- Якщо список непорожній — паралельно з npm-гілкою виконуються кроки 1–8 у Rust-варіанті (позначені нижче як «Rust-гілка»). Якщо порожній — Rust-кроки повністю пропускаються.
63
-
64
- ### 0.3. Детекція Python/uv-проекту
65
-
66
- ```bash
67
- test -f pyproject.toml && echo pyproject.toml
68
- ```
69
-
70
- v1: лише кореневий `pyproject.toml` (uv-конвенція single-project, без обходу workspace-членів, на відміну від Cargo). Якщо файл існує — паралельно виконуються кроки 1–8 у Python-варіанті (позначені нижче як «Python-гілка»). Якщо відсутній — Python-кроки повністю пропускаються.
49
+ - Передумови екосистемних гілок (інструменти на кшталт cargo-edit чи uv) — у фрагментах плагінів наприкінці файлу; відсутній тулчейн не блокує інші гілки.
71
50
 
72
51
  ## Workflow
73
52
 
@@ -82,20 +61,6 @@ cp bun.lock bun.lock.taze-bak
82
61
 
83
62
  (У monorepo — також усі `*/package.json` воркспейсів. Файли тимчасові, видалити в кінці.)
84
63
 
85
- **Rust-гілка** — для кожного знайденого на кроці 0.2 `Cargo.toml` (включно з кореневим, якщо є):
86
-
87
- ```bash
88
- cp Cargo.toml Cargo.toml.taze-bak
89
- cp Cargo.lock Cargo.lock.taze-bak # якщо lock-файл спільний на workspace — достатньо одного бекапу в корені
90
- ```
91
-
92
- **Python-гілка** — якщо на кроці 0.3 знайдений `pyproject.toml`:
93
-
94
- ```bash
95
- cp pyproject.toml pyproject.toml.taze-bak
96
- cp uv.lock uv.lock.taze-bak # якщо є
97
- ```
98
-
99
64
  ### 2. Запустити оновлення
100
65
 
101
66
  ```bash
@@ -107,27 +72,6 @@ bun install
107
72
  - `-r` — рекурсивно по всіх воркспейсах.
108
73
  - `latest` — піднімати навіть major.
109
74
 
110
- **Rust-гілка:**
111
-
112
- ```bash
113
- cargo upgrade --incompatible allow
114
- cargo update
115
- ```
116
-
117
- - `cargo upgrade` (з `cargo-edit`) переписує вимоги версій у кожному `Cargo.toml` workspace-у на останні; `--incompatible allow` явно дозволяє перетинати major-межу (аналог `-r latest` у taze) — без цього флага incompatible-оновлення за замовчуванням ігноруються.
118
- - `cargo update` після цього синхронізує `Cargo.lock` з новими вимогами.
119
-
120
- **Python-гілка:**
121
-
122
- ```bash
123
- for pkg in $(<список прямих залежностей із [project].dependencies>); do
124
- uv remove "$pkg"
125
- uv add "$pkg" --bounds lower
126
- done
127
- ```
128
-
129
- `uv` **не має** єдиної команди "підняти все до latest, навіть через major" (на відміну від `bunx taze -w -r latest`/`cargo upgrade --incompatible allow`) — підтверджено емпірично: `uv add <pkg>` на вже присутній залежності НЕ переписує specifier без попереднього `uv remove`. `--bounds lower` записує нижню межу без верхньої (`>=<latest>`), коректно зберігає `[extras]`. Провал одного пакета (мережа/резолюція) не втрачає прогрес по інших — оркестратор best-effort відновлює оригінальний рядок, якщо `uv add` не вдався після `uv remove`.
130
-
131
75
  ### 3. Виявити major-оновлення
132
76
 
133
77
  > **Не порівнюй `package.json` вручну.** Класифікацію semver несе CLI — детерміновано, по всіх воркспейсах.
@@ -140,10 +84,6 @@ n-rules taze diff
140
84
 
141
85
  Покриває **прямі** залежності з `package.json` (root + воркспейси). Транзитивні major-стрибки (`bun.lock`) — за потреби переглянь окремо; основний ризик breaking-змін — у прямих.
142
86
 
143
- **Rust-гілка (оркестровано)** — `collectCargoDiff` (`npm/skills/taze/js/cargo-diff.mjs`) робить те саме детерміновано: парсить кожен `Cargo.toml.taze-bak`/`Cargo.toml` через `smol-toml`, порівнює `dependencies`/`dev-dependencies`/`build-dependencies` (рядок чи `{ version = "...", features = [...] }`), класифікує за тим самим правилом caret-семантики — Cargo-скорочені версії (`"1"`, `"0.4"`) трактуються як відсутні компоненти = 0. Ручний прогін поза оркестратором — той самий принцип: `diff Cargo.toml.taze-bak Cargo.toml` по рядках `<name> = "<version>"`.
144
-
145
- **Python-гілка (оркестровано)** — `collectUvDiff` (`npm/skills/taze/js/uv-diff.mjs`) робить те саме детерміновано: парсить `pyproject.toml.taze-bak`/`pyproject.toml` через `smol-toml`, порівнює `[project].dependencies` (масив PEP 508-рядків, матчинг за іменем пакета — не за позицією), дістає нижню межу PEP 440-специфікатора (`extractLowerBoundVersion`) і класифікує за тим самим правилом caret-семантики. Ручний прогін поза оркестратором — той самий принцип: `diff pyproject.toml.taze-bak pyproject.toml` по записах `[project].dependencies`.
146
-
147
87
  ### 4. Зібрати breaking changes по кожному major-оновленню
148
88
 
149
89
  Для кожного модуля зі списку зібрати фактичні відмінності одним з джерел (у порядку пріоритету):
@@ -154,10 +94,6 @@ n-rules taze diff
154
94
 
155
95
  Цікавлять: видалені/перейменовані експорти, змінені сигнатури функцій, змінені типи, змінена поведінка за замовчуванням, видалені CLI-прапорці.
156
96
 
157
- **Rust-гілка** — адресу репозиторію взяти з поля `repository`/`documentation` крейта на `crates.io` (`https://crates.io/crates/<name>`) або з `[package.metadata]`; CHANGELOG зазвичай у `CHANGELOG.md` репозиторію або в GitHub Releases. Якщо немає — `cargo doc` різниця по публічному API (`pub fn`/`pub struct`/`pub trait`) між закешованою старою версією (`~/.cargo/registry/src/*/<name>-<old-version>/`) і новою (`~/.cargo/registry/src/*/<name>-<new-version>/`) через `diff -r src/`.
158
-
159
- **Python-гілка** — адресу репозиторію взяти зі сторінки `https://pypi.org/project/<name>/` (поле "Homepage"/"Source"); CHANGELOG зазвичай у `CHANGELOG.md`/`HISTORY.md` репозиторію або в GitHub Releases. Якщо немає — різниця по публічному API між закешованою старою версією (`~/.cache/uv/`) і новою (`.venv/lib/python*/site-packages/<name>/`).
160
-
161
97
  ### 5. Перевірити сумісність з кодом проекту
162
98
 
163
99
  Для кожного breaking change знайти його використання в коді проекту:
@@ -171,22 +107,6 @@ rg -n "<імпорт|функція|опція>" --type ts --type js --type vue
171
107
  - **сумісно** — проект не використовує зачеплене API → нічого не робити.
172
108
  - **несумісно** — використання знайдено → перейти до п. 6.
173
109
 
174
- **Rust-гілка:**
175
-
176
- ```bash
177
- rg -n "<use-шлях|функція|макрос>" --type rust
178
- ```
179
-
180
- Та сама класифікація сумісно/несумісно.
181
-
182
- **Python-гілка:**
183
-
184
- ```bash
185
- rg -n "<імпорт|функція|клас>" --type py
186
- ```
187
-
188
- Та сама класифікація сумісно/несумісно.
189
-
190
110
  ### 6. Рефакторинг несумісних місць
191
111
 
192
112
  Для кожного несумісного місця — застосувати міграцію згідно з changelog модуля (перейменувати імпорт, оновити сигнатуру виклику, замінити видалену опцію еквівалентом тощо). Після правок:
@@ -197,24 +117,6 @@ bun run typecheck # якщо є
197
117
  bun test # якщо є
198
118
  ```
199
119
 
200
- **Rust-гілка** — після правок:
201
-
202
- ```bash
203
- cargo fmt --all -- --check
204
- cargo clippy --all-targets --all-features -- -D warnings
205
- cargo test
206
- ```
207
-
208
- **Python-гілка** — після правок (залежно від того, що реально налаштовано в проєкті):
209
-
210
- ```bash
211
- ruff check .
212
- mypy .
213
- pytest
214
- ```
215
-
216
- Якщо міграція нетривіальна або неоднозначна — **не вгадувати**, залишити TODO у коді з посиланням на CHANGELOG і винести в підсумковий звіт як ручну дію.
217
-
218
120
  ### 7. Прибрати тимчасові файли
219
121
 
220
122
  ```bash
@@ -223,20 +125,6 @@ rm package.json.taze-bak bun.lock.taze-bak
223
125
 
224
126
  (І решту бекапів воркспейсів, якщо створювались.)
225
127
 
226
- **Rust-гілка:**
227
-
228
- ```bash
229
- rm Cargo.toml.taze-bak Cargo.lock.taze-bak
230
- ```
231
-
232
- (І бекапи по кожному workspace-члену, якщо створювались окремо.)
233
-
234
- **Python-гілка:**
235
-
236
- ```bash
237
- rm pyproject.toml.taze-bak uv.lock.taze-bak
238
- ```
239
-
240
128
  ### 8. Звіт користувачу
241
129
 
242
130
  Коротко в одному повідомленні:
@@ -247,12 +135,10 @@ rm pyproject.toml.taze-bak uv.lock.taze-bak
247
135
  - **Потребує ручного втручання:** список TODO з причиною (нетривіальна міграція / неоднозначність / падіння тестів).
248
136
  - **Стан перевірок:** `lint` / `typecheck` / `test` — pass/fail з номером рядка, де впало.
249
137
 
250
- Якщо на кроці 0.2 знайдені Rust-крейти додати окрему секцію **Rust-крейти** з тим самим переліком (оновлено / major / зрефакторено / потребує ручного втручання), і в **Стан перевірок** окремо `cargo fmt` / `cargo clippy` / `cargo test`. Якщо на кроці 0.3 знайдений `pyproject.toml` — так само окрема секція **Python-пакети (uv)**, і в **Стан перевірок** — окремо `ruff` / `mypy` / `pytest`.
138
+ Для кожної обробленої екосистемної гілкиокрема секція з тим самим переліком (оновлено / major / зрефакторено / потребує ручного втручання) і власними перевірками (див. фрагменти плагінів наприкінці файлу).
251
139
 
252
140
  ## Примітка
253
141
 
254
142
  - Не запускати `npx @7n/rules lint` паралельно з іншими ESLint-задачами — діє правило з кореневого `CLAUDE.md`.
255
143
  - Якщо проект — `npm/` пакет цього репо, після змін у `package.json` / коді треба підняти `version` і додати запис у `CHANGELOG.md` згідно з `npm/CLAUDE.md`.
256
- - При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним. Це стосується і Rust-крейтів, і Python-пакетів окремо від npm-пакетів.
257
- - `cargo upgrade --incompatible allow` редагує `Cargo.toml` навіть для залежностей без доступних breaking changes в змінах API — завжди звіряй крок 4 (CHANGELOG) перед тим, як вважати оновлення безпечним, а не лише факт успішної компіляції.
258
- - `uv add <pkg>` на вже присутній залежності — no-op (specifier НЕ переписується); детерміновану гілку це не блокує (вона робить `uv remove` спершу), але при ручному прогоні кроків 2-8 легко про це забути.
144
+ - При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним. Це стосується і пакетів екосистемних гілок окремо від npm-пакетів.