@7n/rules 1.11.0 → 1.13.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 +12 -0
- package/bin/n-rules.js +37 -4
- package/package.json +1 -1
- package/scripts/lib/lint-surface/docs/index.md +1 -0
- package/scripts/lib/lint-surface/docs/path-scope.md +31 -0
- package/scripts/lib/lint-surface/path-scope.mjs +58 -0
- package/skills/taze/SKILL.md +70 -14
- package/skills/taze/js/docs/index.md +1 -0
- package/skills/taze/js/docs/orchestrate.md +23 -21
- package/skills/taze/js/docs/uv-diff.md +41 -0
- package/skills/taze/js/orchestrate.mjs +210 -24
- package/skills/taze/js/uv-diff.mjs +176 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.13.0] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `skill pi/cursor/codex taze` — Python/uv-гілку (`pyproject.toml`) тепер теж покриває оркестратор: детермінований `collectUvDiff` (`npm/skills/taze/js/uv-diff.mjs`, парсинг PEP 508/PEP 440 через `smol-toml`, той самий caret-семантики принцип, що й для npm/Cargo) замість ручної класифікації. Bump — по кожній прямій залежності `uv remove` + `uv add --bounds lower` (`uv` не має єдиної команди для major-стрибка), і по одному ізольованому виклику раннера на кожен major-пакет. Якщо `uv` не встановлено — Python-гілка пропускається без блокування npm/Rust-гілок (pyproject.toml лишається в звіті)
|
|
8
|
+
|
|
9
|
+
## [1.12.0] - 2026-07-17
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Додано прапор `--path <dir>` для `n-rules lint`: звужує файловий набір per-file правил до заданої піддиректорії, лишаючи корінь прогону (root-guard, `.n-rules.json`) незмінним — на відміну від `--cwd`. Несумісний з позиційним rule/concern-фільтром; у парі з `--full` full-вісь ігнорується (без machine-wide локу).
|
|
14
|
+
|
|
3
15
|
## [1.11.0] - 2026-07-17
|
|
4
16
|
|
|
5
17
|
### Added
|
package/bin/n-rules.js
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* `npx \@7n/rules lint` — data-driven оркестратор lint+конформності по `rules/<id>/meta.json` (`lint: per-file|full`):
|
|
13
13
|
* за замовчуванням fix-by-default по дельті vs origin (лише `per-file` правила); `--full` =
|
|
14
14
|
* весь репо (`per-file` ∪ `full`); `--no-fix` = без мутацій/LLM (CI); позиційні
|
|
15
|
-
* (не-флаг) аргументи — фільтр правил конформності (мапить колишній `fix <rule>`)
|
|
15
|
+
* (не-флаг) аргументи — фільтр правил конформності (мапить колишній `fix <rule>`);
|
|
16
|
+
* `--path <dir>` = звузити файловий набір до піддиректорії, корінь прогону (root-guard,
|
|
17
|
+
* `.n-rules.json`) лишається поточним каталогом/`--cwd`, не сумісний з rule-фільтром.
|
|
16
18
|
* CI = `lint --no-fix --full` (весь репо, нуль мутацій/LLM).
|
|
17
19
|
* `npx \@7n/rules skill list` — скіли пакета без синку в проєкт
|
|
18
20
|
* `npx \@7n/rules skill taze` — промпт на stdout
|
|
@@ -1632,7 +1634,16 @@ function printLintHelp() {
|
|
|
1632
1634
|
Один --full на машину: паралельні запуски стають у чергу
|
|
1633
1635
|
й бачать живий прогрес активного прогону.
|
|
1634
1636
|
--no-fix Лише детекція, без мутацій.
|
|
1635
|
-
--cwd <path> Робочий каталог замість
|
|
1637
|
+
--cwd <path> Робочий каталог (корінь прогону) замість поточного —
|
|
1638
|
+
root-guard, .n-rules.json і devDependencies читаються
|
|
1639
|
+
з нього.
|
|
1640
|
+
--path <dir> Звузити файловий набір до піддиректорії, лишивши корінь
|
|
1641
|
+
прогону незмінним (config/root-guard — з поточного каталогу
|
|
1642
|
+
чи --cwd). per-file правила фільтруються по файлах <dir>;
|
|
1643
|
+
full-scope правила (jscpd, cspell тощо) при спрацюванні
|
|
1644
|
+
все одно йдуть по всьому репо. Несумісний з позиційним
|
|
1645
|
+
rule/concern-фільтром; з --full full-вісь ігнорується
|
|
1646
|
+
(немає machine-wide локу).
|
|
1636
1647
|
--verbose Розширений вивід детекції та виправлення.
|
|
1637
1648
|
--help, -h Ця довідка.
|
|
1638
1649
|
|
|
@@ -1644,6 +1655,7 @@ function printLintHelp() {
|
|
|
1644
1655
|
npx @7n/rules lint --full
|
|
1645
1656
|
npx @7n/rules lint --no-fix eslint
|
|
1646
1657
|
npx @7n/rules lint --cwd ./packages/foo --verbose
|
|
1658
|
+
npx @7n/rules lint --path npm/scripts/lib/lint-surface --no-fix
|
|
1647
1659
|
`)
|
|
1648
1660
|
}
|
|
1649
1661
|
|
|
@@ -1717,8 +1729,29 @@ try {
|
|
|
1717
1729
|
// Fix-by-default: detect → T0 → LLM-ladder (run-fix). --no-fix: лише detect.
|
|
1718
1730
|
const cwdIdx = args.indexOf('--cwd')
|
|
1719
1731
|
const cwdArg = cwdIdx === -1 ? cwd() : resolve(args[cwdIdx + 1])
|
|
1720
|
-
|
|
1721
|
-
|
|
1732
|
+
// --path: на відміну від --cwd (підміняє корінь), лишає корінь/config
|
|
1733
|
+
// незмінними й лише звужує файловий набір до заданої піддиректорії
|
|
1734
|
+
// (той самий explicitFiles-шлях, що вже годує hook --post-tool-use/--stop).
|
|
1735
|
+
const pathIdx = args.indexOf('--path')
|
|
1736
|
+
const pathArg = pathIdx === -1 ? null : args[pathIdx + 1]
|
|
1737
|
+
const rules = args.filter(
|
|
1738
|
+
(a, i) => !a.startsWith('-') && !(cwdIdx !== -1 && i === cwdIdx + 1) && !(pathIdx !== -1 && i === pathIdx + 1)
|
|
1739
|
+
)
|
|
1740
|
+
if (pathArg !== null && rules.length > 0) {
|
|
1741
|
+
throw new Error(
|
|
1742
|
+
'--path не можна поєднувати зі scoped rule/concern фільтром (позиційні аргументи) — оберіть щось одне'
|
|
1743
|
+
)
|
|
1744
|
+
}
|
|
1745
|
+
let pathFiles = null
|
|
1746
|
+
if (pathArg !== null) {
|
|
1747
|
+
const { collectPathScopedFiles } = await import('../scripts/lib/lint-surface/path-scope.mjs')
|
|
1748
|
+
pathFiles = await collectPathScopedFiles(cwdArg, pathArg)
|
|
1749
|
+
}
|
|
1750
|
+
// --full + --path: buildPlan ігнорує full, коли передано explicitFiles (той самий
|
|
1751
|
+
// шлях), тож і тут full-вісь вважаємо неактивною — інакше глобальний machine-wide
|
|
1752
|
+
// лок --full брався б для швидкого scoped-прогону без причини.
|
|
1753
|
+
const full = args.includes('--full') && pathArg === null
|
|
1754
|
+
const lintOpts = { cwd: cwdArg, full, rules, verbose: args.includes('--verbose'), files: pathFiles }
|
|
1722
1755
|
const noFix = args.includes('--no-fix')
|
|
1723
1756
|
// Глобальна черга full-прогонів (spec 2026-07-03): одночасно виконується один
|
|
1724
1757
|
// `lint --full` на машину, паралельні --full чекають лока і бачать чергу та
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ resource: npm/scripts/lib/lint-surface/
|
|
|
13
13
|
| [ladder.mjs](ladder.md) | JS Module |
|
|
14
14
|
| [lint-lock.mjs](lint-lock.md) | JS Module |
|
|
15
15
|
| [mt-tail.mjs](mt-tail.md) | JS Module |
|
|
16
|
+
| [path-scope.mjs](path-scope.md) | JS Module |
|
|
16
17
|
| [policy-lint-adapter.mjs](policy-lint-adapter.md) | JS Module |
|
|
17
18
|
| [policy-test-step.mjs](policy-test-step.md) | JS Module |
|
|
18
19
|
| [progress.mjs](progress.md) | JS Module |
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: path-scope.mjs
|
|
4
|
+
resource: npm/scripts/lib/lint-surface/path-scope.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 0971e78e
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.94
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл звужує `n-rules lint --path <dir>` до каталогу всередині вже вибраного кореня прогону, не змінюючи сам root і не підміняючи правила з `.n-rules.json`. Він збирає всі файли під заданою піддиректорією та передає їх у `buildPlan` як `explicitFiles`, тим самим шляхом, що вже живить `hook --post-tool-use`/`--stop`; per-file concerns фільтруються за цими файлами, а `full`-scope concerns запускаються при збігу glob і все одно проходять whole-repo.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
1. `collectPathScopedFiles` приймає корінь прогону та значення `--path`, звужує область lint до каталогу всередині цього кореня й не змінює сам корінь прогону.
|
|
21
|
+
2. `collectPathScopedFiles` відхиляє шлях, якщо він веде поза межі кореня або не є каталогом.
|
|
22
|
+
3. `collectPathScopedFiles` збирає всі файли в межах вказаного каталогу, відсікаючи виключення з кореневих ignore-налаштувань і `.gitignore`-поведінки, а порожній каталог вважає валідним порожнім результатом.
|
|
23
|
+
4. `collectPathScopedFiles` повертає відсортований список шляхів відносно кореня прогону у форматі, придатному для передачі в `buildPlan` як `explicitFiles`.
|
|
24
|
+
|
|
25
|
+
## Публічний API
|
|
26
|
+
|
|
27
|
+
- collectPathScopedFiles — збирає posix-відносні від `cwd` шляхи всіх файлів у каталозі з `--path`, з урахуванням `.gitignore` і `.n-rules.json:ignore` у корені; порожній каталог повертає порожній набір без помилки
|
|
28
|
+
|
|
29
|
+
## Гарантії поведінки
|
|
30
|
+
|
|
31
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Резолвер explicit-files списку для `n-rules lint --path <dir>`.
|
|
3
|
+
*
|
|
4
|
+
* На відміну від `--cwd` (підміняє корінь прогону — root-guard, `.n-rules.json`,
|
|
5
|
+
* devDependencies), `--path` лишає корінь незмінним і лише звужує файловий
|
|
6
|
+
* набір: збирає всі файли під заданою піддиректорією й передає їх у
|
|
7
|
+
* `buildPlan` як `explicitFiles`, тим самим шляхом, що вже годує
|
|
8
|
+
* `hook --post-tool-use`/`--stop` (per-file concerns фільтруються по цих
|
|
9
|
+
* файлах; `full`-scope concerns запускаються при збігу glob, але самі
|
|
10
|
+
* все одно проходять whole-repo — так уже поводиться delta-режим).
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, statSync } from 'node:fs'
|
|
13
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path'
|
|
14
|
+
|
|
15
|
+
import { loadCursorIgnorePaths } from '../load-cursor-config.mjs'
|
|
16
|
+
import { walkDir } from '../../utils/walkDir.mjs'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Перевіряє, що резолвлений `--path` лежить усередині `cwd` (не traversal
|
|
20
|
+
* через `..` і не абсолютний шлях поза коренем прогону).
|
|
21
|
+
* @param {string} cwd абсолютний корінь прогону.
|
|
22
|
+
* @param {string} target абсолютний резолв значення `--path`.
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
function assertWithinCwd(cwd, target) {
|
|
26
|
+
const rel = relative(cwd, target)
|
|
27
|
+
if (rel === '') return
|
|
28
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
29
|
+
throw new Error(`--path має вказувати каталог усередині ${cwd} (отримано поза межами: ${target})`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Збирає posix-відносні (від `cwd`) шляхи всіх файлів під `--path`-каталогом,
|
|
35
|
+
* поважаючи `.gitignore` і `.n-rules.json:ignore` кореня. Порожній каталог —
|
|
36
|
+
* валідний порожній результат (план виявиться порожнім), не помилка.
|
|
37
|
+
* @param {string} cwd абсолютний корінь прогону (root-guard уже пройдено).
|
|
38
|
+
* @param {string} pathArg значення `--path` (відносний або абсолютний шлях).
|
|
39
|
+
* @returns {Promise<string[]>} відсортовані posix-відносні шляхи від `cwd`.
|
|
40
|
+
*/
|
|
41
|
+
export async function collectPathScopedFiles(cwd, pathArg) {
|
|
42
|
+
const target = resolve(cwd, pathArg)
|
|
43
|
+
assertWithinCwd(cwd, target)
|
|
44
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
45
|
+
throw new Error(`--path не є каталогом: ${target}`)
|
|
46
|
+
}
|
|
47
|
+
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
48
|
+
/** @type {string[]} */
|
|
49
|
+
const out = []
|
|
50
|
+
await walkDir(
|
|
51
|
+
target,
|
|
52
|
+
abs => {
|
|
53
|
+
out.push(relative(cwd, abs).split(sep).join('/'))
|
|
54
|
+
},
|
|
55
|
+
ignorePaths
|
|
56
|
+
)
|
|
57
|
+
return out.toSorted((a, b) => a.localeCompare(b))
|
|
58
|
+
}
|
package/skills/taze/SKILL.md
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: n-taze
|
|
3
3
|
description: >-
|
|
4
|
-
Оновлення версій модулів проекту (bun/npm
|
|
5
|
-
через cargo-edit
|
|
6
|
-
несумісного коду
|
|
7
|
-
version: '1.
|
|
4
|
+
Оновлення версій модулів проекту (bun/npm; якщо є Cargo.toml — Rust-крейти
|
|
5
|
+
через cargo-edit; якщо є pyproject.toml — Python-пакети через uv) з
|
|
6
|
+
аналізом major-змін і автоматичним рефакторингом несумісного коду
|
|
7
|
+
version: '1.2'
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# n-taze — Оновлення версій проекту
|
|
11
11
|
|
|
12
12
|
## Мета
|
|
13
13
|
|
|
14
|
-
Оновити всі модулі проекту (npm/bun-залежності, а за наявності `Cargo.toml` — і Rust
|
|
14
|
+
Оновити всі модулі проекту (npm/bun-залежності, а за наявності `Cargo.toml`/`pyproject.toml` — і Rust-крейти/Python-пакети) до останніх версій, виявити major-оновлення, перевірити сумісність змін з кодом проекту і за потреби зрефакторити несумісні місця.
|
|
15
15
|
|
|
16
|
-
## Оркестрація (npm/bun- і
|
|
16
|
+
## Оркестрація (npm/bun-, Rust- і Python-гілка) — не одним промптом
|
|
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 нижче для
|
|
24
|
-
2. Для кожного **окремого** major-пакета/крейта з diff-у — один ізольований, обмежений виклик обраного раннера (кроки 4-6, лише для цього запису; промпт генерує `buildDependencyPrompt`/`buildCargoDependencyPrompt`, не цей SKILL.md).
|
|
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` з `uv-diff.mjs`, парсить PEP 508/PEP 440 через `smol-toml`, той самий caret-принцип класифікації).
|
|
24
|
+
2. Для кожного **окремого** major-пакета/крейта з diff-у — один ізольований, обмежений виклик обраного раннера (кроки 4-6, лише для цього запису; промпт генерує `buildDependencyPrompt`/`buildCargoDependencyPrompt`/`buildUvDependencyPrompt`, не цей SKILL.md).
|
|
25
25
|
3. Детерміновано прибирає бекапи (крок 7) і компонує звіт (крок 8) з результатів усіх ітерацій.
|
|
26
26
|
|
|
27
27
|
Якщо `Cargo.toml` знайдені, але `cargo-edit` не встановлено — Rust-гілка **не**
|
|
28
|
-
блокує
|
|
29
|
-
знайдені `Cargo.toml` як такі, що потребують ручного прогону (нижче).
|
|
28
|
+
блокує інші гілки: оркестратор пропускає Rust-обробку і в звіті перелічує
|
|
29
|
+
знайдені `Cargo.toml` як такі, що потребують ручного прогону (нижче). Так само,
|
|
30
|
+
якщо `pyproject.toml` знайдений, але `uv` не встановлено — Python-гілка **не**
|
|
31
|
+
блокує інші гілки.
|
|
30
32
|
|
|
31
33
|
Переваги: падіння/timeout на одному пакеті/крейті не втрачає прогрес по інших;
|
|
32
34
|
кожен виклик успадковує власний timeout раннера (короткий, бо скоуп малий —
|
|
@@ -34,14 +36,15 @@ version: '1.1'
|
|
|
34
36
|
|
|
35
37
|
Кроки 1-8 нижче лишаються джерелом правди щодо ЗМІСТУ роботи (що саме робить
|
|
36
38
|
кожен крок) — оркестратор їх виконує програмно (1-3/7/8) або як промпт на
|
|
37
|
-
один запис (4-6) для
|
|
39
|
+
один запис (4-6) для всіх гілок.
|
|
38
40
|
|
|
39
41
|
## Передумови
|
|
40
42
|
|
|
41
43
|
- Чисте робоче дерево (`git status` без незакомічених змін у `package.json` / `bun.lock` / `node_modules`) — інакше різницю не відрізнити від оновлення.
|
|
42
44
|
- Встановлений `bun` і доступний `bunx`.
|
|
43
45
|
- Запуск з кореня проекту (де лежить `package.json` / `bun.lock`).
|
|
44
|
-
- Якщо в проекті є хоч один `Cargo.toml` (не в `node_modules`/`.worktrees`) — бажано встановлений `cargo-edit` (`cargo install cargo-edit`, дає команду `cargo upgrade`). Без нього major-бампи Rust-залежностей неможливо застосувати детерміновано (голий `cargo update` піднімає лише semver-сумісні версії) — оркестратор пропускає Rust-гілку (без блокування
|
|
46
|
+
- Якщо в проекті є хоч один `Cargo.toml` (не в `node_modules`/`.worktrees`) — бажано встановлений `cargo-edit` (`cargo install cargo-edit`, дає команду `cargo upgrade`). Без нього major-бампи Rust-залежностей неможливо застосувати детерміновано (голий `cargo update` піднімає лише semver-сумісні версії) — оркестратор пропускає Rust-гілку (без блокування інших гілок) і перелічує знайдені `Cargo.toml` у звіті як такі, що потребують ручного прогону кроків 2-8 нижче.
|
|
47
|
+
- Якщо в проекті є кореневий `pyproject.toml` — бажано встановлений `uv`. Без нього major-бампи Python-залежностей неможливо застосувати детерміновано — оркестратор пропускає Python-гілку (без блокування інших гілок) і перелічує `pyproject.toml` у звіті як такий, що потребує ручного прогону кроків 2-8 нижче.
|
|
45
48
|
|
|
46
49
|
### 0.2. Детекція Rust-крейтів
|
|
47
50
|
|
|
@@ -51,6 +54,14 @@ find . -name Cargo.toml -not -path "*/node_modules/*" -not -path "*/.worktrees/*
|
|
|
51
54
|
|
|
52
55
|
Якщо список непорожній — паралельно з npm-гілкою виконуються кроки 1–8 у Rust-варіанті (позначені нижче як «Rust-гілка»). Якщо порожній — Rust-кроки повністю пропускаються.
|
|
53
56
|
|
|
57
|
+
### 0.3. Детекція Python/uv-проекту
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
test -f pyproject.toml && echo pyproject.toml
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
v1: лише кореневий `pyproject.toml` (uv-конвенція single-project, без обходу workspace-членів, на відміну від Cargo). Якщо файл існує — паралельно виконуються кроки 1–8 у Python-варіанті (позначені нижче як «Python-гілка»). Якщо відсутній — Python-кроки повністю пропускаються.
|
|
64
|
+
|
|
54
65
|
## Workflow
|
|
55
66
|
|
|
56
67
|
### 1. Зафіксувати стартовий стан
|
|
@@ -71,6 +82,13 @@ cp Cargo.toml Cargo.toml.taze-bak
|
|
|
71
82
|
cp Cargo.lock Cargo.lock.taze-bak # якщо lock-файл спільний на workspace — достатньо одного бекапу в корені
|
|
72
83
|
```
|
|
73
84
|
|
|
85
|
+
**Python-гілка** — якщо на кроці 0.3 знайдений `pyproject.toml`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cp pyproject.toml pyproject.toml.taze-bak
|
|
89
|
+
cp uv.lock uv.lock.taze-bak # якщо є
|
|
90
|
+
```
|
|
91
|
+
|
|
74
92
|
### 2. Запустити оновлення
|
|
75
93
|
|
|
76
94
|
```bash
|
|
@@ -92,6 +110,17 @@ cargo update
|
|
|
92
110
|
- `cargo upgrade` (з `cargo-edit`) переписує вимоги версій у кожному `Cargo.toml` workspace-у на останні; `--incompatible allow` явно дозволяє перетинати major-межу (аналог `-r latest` у taze) — без цього флага incompatible-оновлення за замовчуванням ігноруються.
|
|
93
111
|
- `cargo update` після цього синхронізує `Cargo.lock` з новими вимогами.
|
|
94
112
|
|
|
113
|
+
**Python-гілка:**
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
for pkg in $(<список прямих залежностей із [project].dependencies>); do
|
|
117
|
+
uv remove "$pkg"
|
|
118
|
+
uv add "$pkg" --bounds lower
|
|
119
|
+
done
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`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`.
|
|
123
|
+
|
|
95
124
|
### 3. Виявити major-оновлення
|
|
96
125
|
|
|
97
126
|
> **Не порівнюй `package.json` вручну.** Класифікацію semver несе CLI — детерміновано, по всіх воркспейсах.
|
|
@@ -106,6 +135,8 @@ n-rules taze diff
|
|
|
106
135
|
|
|
107
136
|
**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>"`.
|
|
108
137
|
|
|
138
|
+
**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`.
|
|
139
|
+
|
|
109
140
|
### 4. Зібрати breaking changes по кожному major-оновленню
|
|
110
141
|
|
|
111
142
|
Для кожного модуля зі списку зібрати фактичні відмінності одним з джерел (у порядку пріоритету):
|
|
@@ -118,6 +149,8 @@ n-rules taze diff
|
|
|
118
149
|
|
|
119
150
|
**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/`.
|
|
120
151
|
|
|
152
|
+
**Python-гілка** — адресу репозиторію взяти зі сторінки `https://pypi.org/project/<name>/` (поле "Homepage"/"Source"); CHANGELOG зазвичай у `CHANGELOG.md`/`HISTORY.md` репозиторію або в GitHub Releases. Якщо немає — різниця по публічному API між закешованою старою версією (`~/.cache/uv/`) і новою (`.venv/lib/python*/site-packages/<name>/`).
|
|
153
|
+
|
|
121
154
|
### 5. Перевірити сумісність з кодом проекту
|
|
122
155
|
|
|
123
156
|
Для кожного breaking change знайти його використання в коді проекту:
|
|
@@ -139,6 +172,14 @@ rg -n "<use-шлях|функція|макрос>" --type rust
|
|
|
139
172
|
|
|
140
173
|
Та сама класифікація сумісно/несумісно.
|
|
141
174
|
|
|
175
|
+
**Python-гілка:**
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
rg -n "<імпорт|функція|клас>" --type py
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Та сама класифікація сумісно/несумісно.
|
|
182
|
+
|
|
142
183
|
### 6. Рефакторинг несумісних місць
|
|
143
184
|
|
|
144
185
|
Для кожного несумісного місця — застосувати міграцію згідно з changelog модуля (перейменувати імпорт, оновити сигнатуру виклику, замінити видалену опцію еквівалентом тощо). Після правок:
|
|
@@ -157,6 +198,14 @@ cargo clippy --all-targets --all-features -- -D warnings
|
|
|
157
198
|
cargo test
|
|
158
199
|
```
|
|
159
200
|
|
|
201
|
+
**Python-гілка** — після правок (залежно від того, що реально налаштовано в проєкті):
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
ruff check .
|
|
205
|
+
mypy .
|
|
206
|
+
pytest
|
|
207
|
+
```
|
|
208
|
+
|
|
160
209
|
Якщо міграція нетривіальна або неоднозначна — **не вгадувати**, залишити TODO у коді з посиланням на CHANGELOG і винести в підсумковий звіт як ручну дію.
|
|
161
210
|
|
|
162
211
|
### 7. Прибрати тимчасові файли
|
|
@@ -175,6 +224,12 @@ rm Cargo.toml.taze-bak Cargo.lock.taze-bak
|
|
|
175
224
|
|
|
176
225
|
(І бекапи по кожному workspace-члену, якщо створювались окремо.)
|
|
177
226
|
|
|
227
|
+
**Python-гілка:**
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
rm pyproject.toml.taze-bak uv.lock.taze-bak
|
|
231
|
+
```
|
|
232
|
+
|
|
178
233
|
### 8. Звіт користувачу
|
|
179
234
|
|
|
180
235
|
Коротко в одному повідомленні:
|
|
@@ -185,11 +240,12 @@ rm Cargo.toml.taze-bak Cargo.lock.taze-bak
|
|
|
185
240
|
- **Потребує ручного втручання:** список TODO з причиною (нетривіальна міграція / неоднозначність / падіння тестів).
|
|
186
241
|
- **Стан перевірок:** `lint` / `typecheck` / `test` — pass/fail з номером рядка, де впало.
|
|
187
242
|
|
|
188
|
-
Якщо на кроці 0.2 знайдені Rust-крейти — додати окрему секцію **Rust-крейти** з тим самим переліком (оновлено / major / зрефакторено / потребує ручного втручання), і в **Стан перевірок** — окремо `cargo fmt` / `cargo clippy` / `cargo test`.
|
|
243
|
+
Якщо на кроці 0.2 знайдені Rust-крейти — додати окрему секцію **Rust-крейти** з тим самим переліком (оновлено / major / зрефакторено / потребує ручного втручання), і в **Стан перевірок** — окремо `cargo fmt` / `cargo clippy` / `cargo test`. Якщо на кроці 0.3 знайдений `pyproject.toml` — так само окрема секція **Python-пакети (uv)**, і в **Стан перевірок** — окремо `ruff` / `mypy` / `pytest`.
|
|
189
244
|
|
|
190
245
|
## Примітка
|
|
191
246
|
|
|
192
247
|
- Не запускати `npx @7n/rules lint` паралельно з іншими ESLint-задачами — діє правило з кореневого `CLAUDE.md`.
|
|
193
248
|
- Якщо проект — `npm/` пакет цього репо, після змін у `package.json` / коді треба підняти `version` і додати запис у `CHANGELOG.md` згідно з `npm/CLAUDE.md`.
|
|
194
|
-
- При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним. Це стосується і Rust
|
|
249
|
+
- При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним. Це стосується і Rust-крейтів, і Python-пакетів окремо від npm-пакетів.
|
|
195
250
|
- `cargo upgrade --incompatible allow` редагує `Cargo.toml` навіть для залежностей без доступних breaking changes в змінах API — завжди звіряй крок 4 (CHANGELOG) перед тим, як вважати оновлення безпечним, а не лише факт успішної компіляції.
|
|
251
|
+
- `uv add <pkg>` на вже присутній залежності — no-op (specifier НЕ переписується); детерміновану гілку це не блокує (вона робить `uv remove` спершу), але при ручному прогоні кроків 2-8 легко про це забути.
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: orchestrate.mjs
|
|
4
4
|
resource: npm/skills/taze/js/orchestrate.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 56fd9864
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
8
|
score: 100
|
|
9
9
|
issues: judge:inaccurate:0.98
|
|
@@ -12,32 +12,34 @@ docgen:
|
|
|
12
12
|
|
|
13
13
|
## Огляд
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Оркеструє `taze` для трьох екосистем — npm/bun, Rust (Cargo) і Python (uv) — детерміновано, без LLM: бекап маніфестів → масовий bump (`bunx taze`/`cargo upgrade`/по-пакетний `uv remove`+`uv add --bounds lower`) → diff класифікація major vs minor/patch → прибирання бекапів → Markdown-звіт (`formatReport`). Для КОЖНОГО окремого major-пакета/крейта з diff-у виконує один ізольований, обмежений виклик обраного раннера (`buildDependencyPrompt`/`buildCargoDependencyPrompt`/`buildUvDependencyPrompt` + `callRunner`) — лише перевірка сумісності й рефакторинг, не сам bump. Файл виконує реальні файлові операції (копіює/видаляє бекапи `package.json`/`Cargo.toml`/`Cargo.lock`/`pyproject.toml`/`uv.lock`) і запускає зовнішні команди (`bunx`, `bun`, `cargo`, `uv`, `git`, `find`) — не read-only.
|
|
16
16
|
|
|
17
17
|
## Поведінка
|
|
18
18
|
|
|
19
|
-
- **buildDependencyPrompt** —
|
|
20
|
-
- **callRunner** —
|
|
21
|
-
- **backupWorkspacePackageFiles** —
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
19
|
+
- **buildDependencyPrompt** / **buildCargoDependencyPrompt** / **buildUvDependencyPrompt** — формують промпт ОДНОГО ітеративного виклику (лише перевірка сумісності й рефакторинг) для одного major-пакета npm/Cargo/uv відповідно.
|
|
20
|
+
- **callRunner** — диспетчер одного ітеративного виклику: `pi` — вбудований pi-агент (текст через `deps.out`), `cursor`/`codex` — napi-міст ACP (`@7n/llm-lib/acp`).
|
|
21
|
+
- **backupWorkspacePackageFiles** / **cleanupBackups** — бекап і прибирання `package.json` кожного воркспейсу.
|
|
22
|
+
- **backupCargoManifests** / **cleanupCargoBackups** — бекап і прибирання кожного `Cargo.toml` + спільного кореневого `Cargo.lock`.
|
|
23
|
+
- **backupUvManifest** / **cleanupUvBackups** — бекап і прибирання кореневого `pyproject.toml` + `uv.lock`.
|
|
24
|
+
- **findCargoManifests** — знаходить усі `Cargo.toml` поза `node_modules`/`.worktrees`/`target`.
|
|
25
|
+
- **findPyprojectManifest** — перевіряє наявність кореневого `pyproject.toml` (uv-конвенція — один файл, не per-package обхід).
|
|
26
|
+
- **formatReport** — компонує підсумковий Markdown-звіт (minor/patch, major-оновлення по кожній з трьох гілок, загальний обсяг змін) без окремого LLM-виклику.
|
|
27
|
+
- **runTazeOrchestrator** — повний прогін: перевіряє, що `cwd` — ізольований worktree, послідовно обробляє npm-, Rust- (за наявності `Cargo.toml` і `cargo-edit`) і Python-гілку (за наявності `pyproject.toml` і `uv`) — бекап → bump → diff → ізольовані виклики раннера по кожному major-запису → прибирання — і повертає звіт.
|
|
26
28
|
|
|
27
29
|
## Публічний API
|
|
28
30
|
|
|
29
|
-
- buildDependencyPrompt
|
|
30
|
-
- callRunner —
|
|
31
|
-
- backupWorkspacePackageFiles —
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
- buildDependencyPrompt / buildCargoDependencyPrompt / buildUvDependencyPrompt — промпт одного major-запису відповідної екосистеми.
|
|
32
|
+
- callRunner — виклик обраного раннера (`pi`/`cursor`/`codex`) з одним промптом.
|
|
33
|
+
- backupWorkspacePackageFiles / cleanupBackups — бекап/прибирання `package.json` воркспейсів.
|
|
34
|
+
- backupCargoManifests / cleanupCargoBackups — бекап/прибирання `Cargo.toml`/`Cargo.lock`.
|
|
35
|
+
- backupUvManifest / cleanupUvBackups — бекап/прибирання `pyproject.toml`/`uv.lock`.
|
|
36
|
+
- findCargoManifests — список знайдених `Cargo.toml` у репозиторії.
|
|
37
|
+
- findPyprojectManifest — список (0 або 1 запис) кореневого `pyproject.toml`.
|
|
38
|
+
- formatReport — фінальний Markdown-звіт із результатів усіх трьох гілок.
|
|
39
|
+
- runTazeOrchestrator — повна оркестрація taze для npm/bun + Rust + Python.
|
|
38
40
|
|
|
39
41
|
## Гарантії поведінки
|
|
40
42
|
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
43
|
+
- Виконує файлові операції (копіювання/видалення бекапів) і запускає зовнішні команди — НЕ read-only.
|
|
44
|
+
- Перед будь-якою мутацією перевіряє, що `cwd` — ізольований worktree (`assertRunningInWorktree`), інакше кидає виняток.
|
|
45
|
+
- Падіння одного пакета/крейта в ізольованому виклику раннера не втрачає прогрес по інших записах.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: uv-diff.mjs
|
|
4
|
+
resource: npm/skills/taze/js/uv-diff.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: a23e68cc
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
score: 100
|
|
9
|
+
issues: judge:inaccurate:0.99
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
Працює з залежностями `pyproject.toml` у форматах PEP 508 і PEP 440: `parsePep508` і `parsePep440Version` розбирають записи, `extractLowerBoundVersion` дістає нижню межу версії, `diffPyprojectDeps` і `collectUvDiff` порівнюють прямі залежності та формують diff відносно backup-версії, а `listDirectDependencies` повертає список прямих залежностей. Код read-only (лише читає файли з диска), fail-safe перехоплює помилки без винятків назовні та для частини збоїв повертає порожнє значення замість помилки.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
- `parsePep508` — розбирає рядок залежності у форматі PEP 508 на назву пакета, extras і version specifier; для невалідного рядка повертає порожній результат.
|
|
20
|
+
- `parsePep440Version` — витягує ядро версії PEP 440 і зводить відсутні компоненти до нулів; для не-версійного рядка повертає порожній результат.
|
|
21
|
+
- `extractLowerBoundVersion` — дістає нижню межу версійного specifier-а, тобто версію, яку відповідає нижньому bound у залежності; якщо межі немає, повертає порожній результат.
|
|
22
|
+
- `diffPyprojectDeps` — порівнює прямі залежності двох `pyproject.toml` за іменем пакета і класифікує зміну як major або minor/patch.
|
|
23
|
+
- `collectUvDiff` — збирає diff для `pyproject.toml` між поточним файлом і його backup-версією, а за відсутності потрібних файлів повертає порожній diff.
|
|
24
|
+
- `listDirectDependencies` — повертає список прямих залежностей із `pyproject.toml` у вигляді назв пакетів та extras.
|
|
25
|
+
|
|
26
|
+
## Публічний API
|
|
27
|
+
|
|
28
|
+
- parsePep508 — Розбирає один PEP 508-запис залежності на назву пакета, extras і version specifier.
|
|
29
|
+
- parsePep440Version — Виділяє основні компоненти PEP 440-версії; відсутні частини вважає нулями.
|
|
30
|
+
- extractLowerBoundVersion — Витягає найнижчу допустиму версію з набору `>=` / `==` / `~=`-обмежень.
|
|
31
|
+
- diffPyprojectDeps — Порівнює списки `[project].dependencies` у двох `pyproject.toml` за назвою пакета.
|
|
32
|
+
- collectUvDiff — Збирає різницю між поточним `pyproject.toml` і його backup-копією в `cwd`.
|
|
33
|
+
- listDirectDependencies — Повертає прямі залежності з `[project].dependencies` поточного `pyproject.toml` разом з extras.
|
|
34
|
+
|
|
35
|
+
Конфіги, на які спирається код: package.json
|
|
36
|
+
|
|
37
|
+
## Гарантії поведінки
|
|
38
|
+
|
|
39
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
40
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
41
|
+
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/** @see ./docs/orchestrate.md */
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
|
-
import { copyFile, rm } from 'node:fs/promises'
|
|
4
|
+
import { copyFile, readFile, rm } from 'node:fs/promises'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
|
|
7
|
+
import { parse as parseToml } from 'smol-toml'
|
|
8
|
+
|
|
7
9
|
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
8
10
|
import { collectCargoDiff } from './cargo-diff.mjs'
|
|
9
11
|
import { collectTazeDiff } from './diff.mjs'
|
|
12
|
+
import { collectUvDiff, listDirectDependencies } from './uv-diff.mjs'
|
|
10
13
|
|
|
11
14
|
/** Суфікс бекапу package.json — той самий, що й у `diff.mjs`/кроці 1 SKILL.md. */
|
|
12
15
|
const BACKUP_SUFFIX = '.taze-bak'
|
|
@@ -59,6 +62,30 @@ export function buildCargoDependencyPrompt({ manifest, pkg, from, to }) {
|
|
|
59
62
|
].join('\n')
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Промпт ОДНОГО ітеративного виклику для Python-пакета — Python/uv-варіант
|
|
67
|
+
* [`buildDependencyPrompt`] (кроки 4-6 SKILL.md, Python-гілка), для ОДНОГО
|
|
68
|
+
* major-пакета. Кроки 1-3/7/8 виконує оркестратор детерміновано, без LLM.
|
|
69
|
+
* @param {{manifest: string, pkg: string, from: string, to: string}} entry запис major-diff (з `collectUvDiff`)
|
|
70
|
+
* @returns {string} готовий промпт
|
|
71
|
+
*/
|
|
72
|
+
export function buildUvDependencyPrompt({ manifest, pkg, from, to }) {
|
|
73
|
+
return [
|
|
74
|
+
'# Major-оновлення одного Python-пакета: перевірка сумісності й рефакторинг',
|
|
75
|
+
'',
|
|
76
|
+
`Пакет \`${pkg}\` у \`${manifest}\`: **${from} → ${to}** — вже застосовано (\`uv remove\` + \`uv add --bounds lower\` виконано детерміновано, без тебе). Твоя задача — лише breaking-changes-перевірка й, за потреби, рефакторинг.`,
|
|
77
|
+
'',
|
|
78
|
+
'## Кроки',
|
|
79
|
+
`1. Зібрати breaking changes цього оновлення: CHANGELOG/Releases репозиторію пакета (адреса — зі сторінки https://pypi.org/project/${pkg}/) між ${from} і ${to}.`,
|
|
80
|
+
`2. Знайти використання зачепленого API в коді проєкту (\`rg -n --type py\` по імпортах/викликах \`${pkg}\`).`,
|
|
81
|
+
'3. Сумісно — нічого не робити. Несумісно — застосувати міграцію (перейменувати імпорт, оновити сигнатуру виклику, замінити видалений параметр еквівалентом).',
|
|
82
|
+
'4. Якщо були правки — запусти наявні в проєкті лінт/typecheck/test (`ruff`/`mypy`/`pytest` тощо — залежно від того, що реально налаштовано).',
|
|
83
|
+
'5. Нетривіальна/неоднозначна міграція — не вгадуй, залиш TODO-коментар із посиланням на CHANGELOG.',
|
|
84
|
+
'',
|
|
85
|
+
'У відповіді одним абзацом підсумуй: сумісно / зрефакторено (які файли) / TODO (чому).'
|
|
86
|
+
].join('\n')
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
/**
|
|
63
90
|
* Диспетчер одного ітеративного виклику на обраний раннер. `pi` — вбудований
|
|
64
91
|
* pi-агент (`@7n/llm-lib/agent-skill`; текст перехоплюється через `deps.out`,
|
|
@@ -257,6 +284,92 @@ export function findCargoManifests(cwd, deps = {}) {
|
|
|
257
284
|
.filter(Boolean)
|
|
258
285
|
}
|
|
259
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Чи встановлений `uv` — без нього неможливо детерміновано перетнути
|
|
289
|
+
* major-межу Python-залежностей (SKILL.md §Передумови, Python-гілка).
|
|
290
|
+
* @param {typeof spawnSync} spawnFn інжект для тестів
|
|
291
|
+
* @returns {boolean} true — `uv` доступний
|
|
292
|
+
*/
|
|
293
|
+
function hasUv(spawnFn) {
|
|
294
|
+
return spawnFn('uv', ['--version'], { encoding: 'utf8' }).status === 0
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Знаходить кореневий `pyproject.toml` (крок 0.2 SKILL.md, Python-гілка).
|
|
299
|
+
* v1: один кореневий файл, не per-package, як `find`-обхід для Cargo.toml —
|
|
300
|
+
* поточна uv-конвенція (single-project, без workspace-обходу).
|
|
301
|
+
* @param {string} cwd корінь репо
|
|
302
|
+
* @returns {string[]} `['pyproject.toml']`, якщо файл існує, інакше `[]`
|
|
303
|
+
*/
|
|
304
|
+
export function findPyprojectManifest(cwd) {
|
|
305
|
+
return existsSync(join(cwd, 'pyproject.toml')) ? ['pyproject.toml'] : []
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Бекапить pyproject.toml + uv.lock (крок 1 SKILL.md, Python-гілка) —
|
|
310
|
+
* потрібно для класифікації major/minor через `collectUvDiff` після bump-у.
|
|
311
|
+
* @param {string} cwd корінь репо
|
|
312
|
+
* @param {{ copyFile?: (src: string, dest: string) => Promise<void> }} [deps] інжект
|
|
313
|
+
* @returns {Promise<void>}
|
|
314
|
+
*/
|
|
315
|
+
export async function backupUvManifest(cwd, deps = {}) {
|
|
316
|
+
const copy = deps.copyFile ?? copyFile
|
|
317
|
+
const pyprojectPath = join(cwd, 'pyproject.toml')
|
|
318
|
+
if (existsSync(pyprojectPath)) await copy(pyprojectPath, `${pyprojectPath}${BACKUP_SUFFIX}`)
|
|
319
|
+
const lockPath = join(cwd, 'uv.lock')
|
|
320
|
+
if (existsSync(lockPath)) await copy(lockPath, `${lockPath}${BACKUP_SUFFIX}`)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Прибирає бекапи pyproject.toml/uv.lock після завершення (крок 7 SKILL.md,
|
|
325
|
+
* Python-гілка).
|
|
326
|
+
* @param {string} cwd корінь репо
|
|
327
|
+
* @param {{ rm?: (path: string, opts?: object) => Promise<void> }} [deps] інжект
|
|
328
|
+
* @returns {Promise<void>}
|
|
329
|
+
*/
|
|
330
|
+
export async function cleanupUvBackups(cwd, deps = {}) {
|
|
331
|
+
const remove = deps.rm ?? rm
|
|
332
|
+
await remove(join(cwd, `pyproject.toml${BACKUP_SUFFIX}`), { force: true })
|
|
333
|
+
await remove(join(cwd, `uv.lock${BACKUP_SUFFIX}`), { force: true })
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Піднімає кожну пряму залежність pyproject.toml через `uv remove` + `uv add
|
|
338
|
+
* <pkg>[extras] --bounds lower` (крок 2 SKILL.md, Python-гілка) — `uv` не
|
|
339
|
+
* має єдиної команди "підняти все до latest, навіть через major", на
|
|
340
|
+
* відміну від `bunx taze -w -r latest`/`cargo upgrade --incompatible allow`
|
|
341
|
+
* (підтверджено емпірично: `uv add <pkg>` на вже присутній залежності —
|
|
342
|
+
* no-op, specifier НЕ переписується без попереднього `uv remove`). Провал
|
|
343
|
+
* одного пакета (мережа/резолюція) не втрачає прогрес по інших —
|
|
344
|
+
* best-effort відновлення оригінального рядка, якщо `uv add` не вдався
|
|
345
|
+
* після `uv remove`.
|
|
346
|
+
* @param {string} cwd корінь репо
|
|
347
|
+
* @param {typeof spawnSync} spawnFn інжект для тестів
|
|
348
|
+
* @param {(line: string) => void} log колбек прогресу
|
|
349
|
+
* @param {{ readFile?: (path: string, encoding: string) => Promise<string> }} [deps] інжект
|
|
350
|
+
* @returns {Promise<void>}
|
|
351
|
+
*/
|
|
352
|
+
async function bumpUvDependencies(cwd, spawnFn, log, deps = {}) {
|
|
353
|
+
const read = deps.readFile ?? readFile
|
|
354
|
+
const text = await read(join(cwd, 'pyproject.toml'), 'utf8')
|
|
355
|
+
const manifest = parseToml(text)
|
|
356
|
+
const directDeps = listDirectDependencies(manifest)
|
|
357
|
+
|
|
358
|
+
for (const dep of directDeps) {
|
|
359
|
+
const pkgSpec = dep.extras.length > 0 ? `${dep.name}[${dep.extras.join(',')}]` : dep.name
|
|
360
|
+
const removeResult = spawnFn('uv', ['remove', dep.name], { cwd, encoding: 'utf8' })
|
|
361
|
+
if (removeResult.status !== 0) {
|
|
362
|
+
log(` ⚠️ uv remove ${dep.name}: ${removeResult.stderr || removeResult.stdout}`)
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
const addResult = spawnFn('uv', ['add', pkgSpec, '--bounds', 'lower'], { cwd, encoding: 'utf8' })
|
|
366
|
+
if (addResult.status !== 0) {
|
|
367
|
+
log(` ⚠️ uv add ${pkgSpec}: ${addResult.stderr || addResult.stdout} — відновлюю ${dep.raw}`)
|
|
368
|
+
spawnFn('uv', ['add', dep.raw], { cwd, encoding: 'utf8' })
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
260
373
|
/**
|
|
261
374
|
* Форматує один рядок результату ітерації (спільний для npm- і Rust-гілки).
|
|
262
375
|
* @param {{pkg: string, ok: boolean, error: string|null, from: string, to: string}} r результат ітерації
|
|
@@ -269,6 +382,34 @@ function formatResultLine(r, scopeLabel) {
|
|
|
269
382
|
return ` ${status} \`${r.pkg}\` (${scopeLabel}): ${r.from} → ${r.to}${errorSuffix}`
|
|
270
383
|
}
|
|
271
384
|
|
|
385
|
+
/** Пуста гілка-екосистема (для дефолту, коли `rust`/`python` не знайдені взагалі). */
|
|
386
|
+
const EMPTY_ECOSYSTEM = { manifests: [], processed: false, skippedReason: null, minorPatch: 0, results: [] }
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Додає секцію звіту однієї не-npm екосистемної гілки (Rust/Python) —
|
|
390
|
+
* спільна логіка для «Rust-крейти»/«Python-пакети (uv)», обидві мають
|
|
391
|
+
* ідентичну форму результату ({manifest, pkg, from, to, ok, error}).
|
|
392
|
+
* @param {string[]} lines масив рядків звіту (мутується)
|
|
393
|
+
* @param {{manifests:string[], processed:boolean, skippedReason:string|null, minorPatch:number, results:Array<{pkg:string, manifest:string, from:string, to:string, ok:boolean, error:string|null}>}} eco дані гілки
|
|
394
|
+
* @param {{title: string, manifestNoun: string, skillSection: string}} labels мітки секції
|
|
395
|
+
* @returns {number} totalChanged цієї гілки
|
|
396
|
+
*/
|
|
397
|
+
function appendEcosystemSection(lines, eco, { title, manifestNoun, skillSection }) {
|
|
398
|
+
if (eco.manifests.length === 0) return 0
|
|
399
|
+
lines.push('', `### ${title}`)
|
|
400
|
+
if (!eco.processed) {
|
|
401
|
+
lines.push(
|
|
402
|
+
`- ⏭ Пропущено (${eco.skippedReason}) — ${eco.manifests.length} ${manifestNoun}, онови вручну за ${skillSection}: ${eco.manifests.join(', ')}`
|
|
403
|
+
)
|
|
404
|
+
return 0
|
|
405
|
+
}
|
|
406
|
+
lines.push(`- **Оновлено (minor/patch):** ${eco.minorPatch}`, `- **Major-оновлення:** ${eco.results.length}`)
|
|
407
|
+
for (const r of eco.results) {
|
|
408
|
+
lines.push(formatResultLine(r, r.manifest))
|
|
409
|
+
}
|
|
410
|
+
return eco.minorPatch + eco.results.length
|
|
411
|
+
}
|
|
412
|
+
|
|
272
413
|
/**
|
|
273
414
|
* Компонує підсумковий звіт (крок 8 SKILL.md) детерміновано з результатів
|
|
274
415
|
* ітерацій — без окремого LLM-виклику для самого звіту.
|
|
@@ -276,7 +417,14 @@ function formatResultLine(r, scopeLabel) {
|
|
|
276
417
|
* minorPatch: number,
|
|
277
418
|
* totalChanged: number,
|
|
278
419
|
* results: Array<{pkg:string, workspace:string, from:string, to:string, ok:boolean, error:string|null}>,
|
|
279
|
-
* rust
|
|
420
|
+
* rust?: {
|
|
421
|
+
* manifests: string[],
|
|
422
|
+
* processed: boolean,
|
|
423
|
+
* skippedReason: string|null,
|
|
424
|
+
* minorPatch: number,
|
|
425
|
+
* results: Array<{pkg:string, manifest:string, from:string, to:string, ok:boolean, error:string|null}>
|
|
426
|
+
* },
|
|
427
|
+
* python?: {
|
|
280
428
|
* manifests: string[],
|
|
281
429
|
* processed: boolean,
|
|
282
430
|
* skippedReason: string|null,
|
|
@@ -286,7 +434,7 @@ function formatResultLine(r, scopeLabel) {
|
|
|
286
434
|
* }} args дані звіту
|
|
287
435
|
* @returns {string} markdown-звіт
|
|
288
436
|
*/
|
|
289
|
-
export function formatReport({ minorPatch, totalChanged, results, rust }) {
|
|
437
|
+
export function formatReport({ minorPatch, totalChanged, results, rust = EMPTY_ECOSYSTEM, python = EMPTY_ECOSYSTEM }) {
|
|
290
438
|
const lines = [
|
|
291
439
|
'## taze: підсумок',
|
|
292
440
|
'',
|
|
@@ -297,23 +445,18 @@ export function formatReport({ minorPatch, totalChanged, results, rust }) {
|
|
|
297
445
|
lines.push(formatResultLine(r, r.workspace))
|
|
298
446
|
}
|
|
299
447
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
lines.push('', `- **Всього змінено:** ${totalChanged + rustTotalChanged}`)
|
|
448
|
+
const rustTotalChanged = appendEcosystemSection(lines, rust, {
|
|
449
|
+
title: 'Rust-крейти',
|
|
450
|
+
manifestNoun: 'Cargo.toml',
|
|
451
|
+
skillSection: 'Rust-гілкою SKILL.md'
|
|
452
|
+
})
|
|
453
|
+
const pythonTotalChanged = appendEcosystemSection(lines, python, {
|
|
454
|
+
title: 'Python-пакети (uv)',
|
|
455
|
+
manifestNoun: 'pyproject.toml',
|
|
456
|
+
skillSection: 'Python-гілкою SKILL.md'
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
lines.push('', `- **Всього змінено:** ${totalChanged + rustTotalChanged + pythonTotalChanged}`)
|
|
317
460
|
return lines.join('\n')
|
|
318
461
|
}
|
|
319
462
|
|
|
@@ -328,9 +471,9 @@ export function formatReport({ minorPatch, totalChanged, results, rust }) {
|
|
|
328
471
|
* cwd?: string,
|
|
329
472
|
* runner?: 'pi' | 'cursor' | 'codex',
|
|
330
473
|
* log?: (line: string) => void,
|
|
331
|
-
* deps?: { spawnFn?: typeof spawnSync, collectTazeDiff?: (cwd: string) => Promise<object>, collectCargoDiff?: (cwd: string, manifests: string[]) => Promise<object>, callRunner?: (runner: string, prompt: string, cwd: string, deps: object) => Promise<{ok: boolean, text: string, error: string|null}> } & Record<string, unknown>
|
|
474
|
+
* deps?: { spawnFn?: typeof spawnSync, collectTazeDiff?: (cwd: string) => Promise<object>, collectCargoDiff?: (cwd: string, manifests: string[]) => Promise<object>, collectUvDiff?: (cwd: string) => Promise<object>, bumpUvDependencies?: (cwd: string, spawnFn: typeof spawnSync, log: (line: string) => void, deps: object) => Promise<void>, callRunner?: (runner: string, prompt: string, cwd: string, deps: object) => Promise<{ok: boolean, text: string, error: string|null}> } & Record<string, unknown>
|
|
332
475
|
* }} [options] опції + інжекти для тестів
|
|
333
|
-
* @returns {Promise<{ ok: boolean, report: string, results: Array<object>, rustResults: Array<object> }>} результат
|
|
476
|
+
* @returns {Promise<{ ok: boolean, report: string, results: Array<object>, rustResults: Array<object>, pythonResults: Array<object> }>} результат
|
|
334
477
|
*/
|
|
335
478
|
export async function runTazeOrchestrator(options = {}) {
|
|
336
479
|
const cwd = options.cwd ?? process.cwd()
|
|
@@ -403,8 +546,51 @@ export async function runTazeOrchestrator(options = {}) {
|
|
|
403
546
|
}
|
|
404
547
|
}
|
|
405
548
|
|
|
406
|
-
const
|
|
549
|
+
const pyprojectManifests = findPyprojectManifest(cwd)
|
|
550
|
+
let python = { manifests: pyprojectManifests, processed: false, skippedReason: null, minorPatch: 0, results: [] }
|
|
551
|
+
|
|
552
|
+
if (pyprojectManifests.length > 0 && !hasUv(spawnFn)) {
|
|
553
|
+
python.skippedReason = '`uv` не встановлено (https://docs.astral.sh/uv/getting-started/installation/)'
|
|
554
|
+
log(`⏭ Python: ${python.skippedReason}`)
|
|
555
|
+
} else if (pyprojectManifests.length > 0) {
|
|
556
|
+
log('📦 Бекап pyproject.toml/uv.lock...')
|
|
557
|
+
await backupUvManifest(cwd, deps)
|
|
558
|
+
|
|
559
|
+
log('⬆️ uv remove + uv add --bounds lower (по кожній прямій залежності)...')
|
|
560
|
+
const bump = deps.bumpUvDependencies ?? bumpUvDependencies
|
|
561
|
+
await bump(cwd, spawnFn, log, deps)
|
|
562
|
+
|
|
563
|
+
const collectUv = deps.collectUvDiff ?? collectUvDiff
|
|
564
|
+
const uvDiff = await collectUv(cwd)
|
|
565
|
+
log(`🔍 Python diff: ${uvDiff.major.length} major, ${uvDiff.minorPatch} minor/patch`)
|
|
566
|
+
|
|
567
|
+
const pythonResults = []
|
|
568
|
+
for (const entry of uvDiff.major) {
|
|
569
|
+
log(`🔧 [python] ${entry.pkg} (${entry.manifest}): ${entry.from} → ${entry.to}...`)
|
|
570
|
+
const outcome = await call(runner, buildUvDependencyPrompt(entry), cwd, deps)
|
|
571
|
+
pythonResults.push({ ...entry, ...outcome })
|
|
572
|
+
log(outcome.ok ? ` ✅ ${entry.pkg}` : ` ❌ ${entry.pkg}: ${outcome.error}`)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
await cleanupUvBackups(cwd, deps)
|
|
576
|
+
|
|
577
|
+
python = {
|
|
578
|
+
manifests: pyprojectManifests,
|
|
579
|
+
processed: true,
|
|
580
|
+
skippedReason: null,
|
|
581
|
+
minorPatch: uvDiff.minorPatch,
|
|
582
|
+
results: pythonResults
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const report = formatReport({ minorPatch: diff.minorPatch, totalChanged: diff.totalChanged, results, rust, python })
|
|
407
587
|
log(report)
|
|
408
588
|
|
|
409
|
-
return {
|
|
589
|
+
return {
|
|
590
|
+
ok: results.every(r => r.ok) && rust.results.every(r => r.ok) && python.results.every(r => r.ok),
|
|
591
|
+
report,
|
|
592
|
+
results,
|
|
593
|
+
rustResults: rust.results,
|
|
594
|
+
pythonResults: python.results
|
|
595
|
+
}
|
|
410
596
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** @see ./docs/uv-diff.md */
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { parse as parseToml } from 'smol-toml'
|
|
6
|
+
|
|
7
|
+
import { isBreaking } from './diff.mjs'
|
|
8
|
+
|
|
9
|
+
/** Дефолтний суфікс бекапу — той самий, що й для package.json/Cargo.toml. */
|
|
10
|
+
const DEFAULT_BACKUP_SUFFIX = '.taze-bak'
|
|
11
|
+
|
|
12
|
+
// Ім'я пакета (PEP 508: літери/цифри/`._-`) + опційні `[extras]` + решта
|
|
13
|
+
// (версійний specifier, можливо з `; marker` — ігнорується, не наш скоуп).
|
|
14
|
+
const PEP508_RE = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*(\[[^\]]*])?\s*([^;]*)/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Парсить один PEP 508-рядок залежності (`"typer>=0.19.1,<0.20.0"`,
|
|
18
|
+
* `"strawberry-graphql[asgi]>=0.282.0"`) на ім'я/extras/specifier.
|
|
19
|
+
* @param {string} requirement рядок із `[project].dependencies`
|
|
20
|
+
* @returns {{name: string, extras: string[], specifier: string}|null} розбір, або null для невалідного рядка
|
|
21
|
+
*/
|
|
22
|
+
export function parsePep508(requirement) {
|
|
23
|
+
if (typeof requirement !== 'string') return null
|
|
24
|
+
const m = PEP508_RE.exec(requirement)
|
|
25
|
+
if (!m) return null
|
|
26
|
+
const extras = m[2]
|
|
27
|
+
? m[2]
|
|
28
|
+
.slice(1, -1)
|
|
29
|
+
.split(',')
|
|
30
|
+
.map(s => s.trim())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
: []
|
|
33
|
+
return { name: m[1], extras, specifier: (m[3] ?? '').trim() }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 1-3 числові компоненти PEP 440 (`"1"`, `"0.4"`, `"1.2.3"`), без урахування
|
|
37
|
+
// pre/post/dev-суфіксів (поза скоупом major/minor/patch-класифікації).
|
|
38
|
+
const PEP440_VERSION_RE = /^[\s=><~!]*(\d+)(?:\.(\d+))?(?:\.(\d+))?/
|
|
39
|
+
// Операторний префікс версійного сегмента (`>=`, `==`, `~=`, `!=` тощо) — для відсікання при витягуванні голої версії.
|
|
40
|
+
const VERSION_OPERATOR_PREFIX_RE = /^[>=~!]+/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Парсить ядро PEP 440-версії (major.minor.patch, відсутні компоненти → 0).
|
|
44
|
+
* @param {string} spec версійний рядок (сегмент specifier-а, без операторного префікса)
|
|
45
|
+
* @returns {{major:number, minor:number, patch:number}|null} ядро або null для не-версії
|
|
46
|
+
*/
|
|
47
|
+
export function parsePep440Version(spec) {
|
|
48
|
+
if (typeof spec !== 'string') return null
|
|
49
|
+
const m = PEP440_VERSION_RE.exec(spec)
|
|
50
|
+
if (!m) return null
|
|
51
|
+
return { major: Number(m[1]), minor: Number(m[2] ?? 0), patch: Number(m[3] ?? 0) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Дістає нижню межу версійного specifier-а (перший сегмент `>=`/`==`/`~=`
|
|
56
|
+
* серед comma-separated списку, напр. `">=0.19.1,<0.20.0"` → `"0.19.1"`).
|
|
57
|
+
* Саме нижня межа відповідає версії, яку реально ставить `uv add --bounds lower`.
|
|
58
|
+
* @param {string} specifier версійний specifier із `parsePep508`
|
|
59
|
+
* @returns {string|null} версія нижньої межі, або null якщо немає жодного `>=`/`==`/`~=`-сегмента
|
|
60
|
+
*/
|
|
61
|
+
export function extractLowerBoundVersion(specifier) {
|
|
62
|
+
if (!specifier) return null
|
|
63
|
+
const clause = specifier
|
|
64
|
+
.split(',')
|
|
65
|
+
.map(s => s.trim())
|
|
66
|
+
.find(c => c.startsWith('>=') || c.startsWith('==') || c.startsWith('~='))
|
|
67
|
+
return clause ? clause.replace(VERSION_OPERATOR_PREFIX_RE, '').trim() : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Порівнює `[project].dependencies` двох pyproject.toml — той самий контракт,
|
|
72
|
+
* що й `diffPackageJson`/`diffCargoToml`. Матчинг по ІМЕНІ пакета (не по
|
|
73
|
+
* позиції в масиві — PEP 621 `dependencies` це список PEP 508-рядків, не
|
|
74
|
+
* мапа ім'я→версія).
|
|
75
|
+
* @param {object} oldManifest розпарсений старий pyproject.toml (бекап)
|
|
76
|
+
* @param {object} newManifest розпарсений новий pyproject.toml
|
|
77
|
+
* @param {string} manifest відносний шлях pyproject.toml (мітка джерела запису)
|
|
78
|
+
* @returns {{major: Array<{manifest:string, pkg:string, from:string, to:string}>, minorPatch:number}} зміни
|
|
79
|
+
*/
|
|
80
|
+
export function diffPyprojectDeps(oldManifest, newManifest, manifest) {
|
|
81
|
+
const oldDeps = oldManifest?.project?.dependencies ?? []
|
|
82
|
+
const newByName = new Map()
|
|
83
|
+
for (const requirement of newDeps(newManifest)) {
|
|
84
|
+
const parsed = parsePep508(requirement)
|
|
85
|
+
if (parsed) newByName.set(parsed.name, parsed)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const major = []
|
|
89
|
+
let minorPatch = 0
|
|
90
|
+
for (const oldRequirement of oldDeps) {
|
|
91
|
+
const oldParsed = parsePep508(oldRequirement)
|
|
92
|
+
if (!oldParsed) continue
|
|
93
|
+
const newParsed = newByName.get(oldParsed.name)
|
|
94
|
+
if (!newParsed || newParsed.specifier === oldParsed.specifier) continue
|
|
95
|
+
|
|
96
|
+
const from = extractLowerBoundVersion(oldParsed.specifier)
|
|
97
|
+
const to = extractLowerBoundVersion(newParsed.specifier)
|
|
98
|
+
if (from === null || to === null || from === to) continue
|
|
99
|
+
|
|
100
|
+
const fromV = parsePep440Version(from)
|
|
101
|
+
const toV = parsePep440Version(to)
|
|
102
|
+
if (fromV && toV && isBreaking(fromV, toV)) {
|
|
103
|
+
major.push({ manifest, pkg: oldParsed.name, from, to })
|
|
104
|
+
} else {
|
|
105
|
+
minorPatch += 1
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { major, minorPatch }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {object} manifest розпарсений pyproject.toml
|
|
113
|
+
* @returns {string[]} `[project].dependencies` (порожній масив, якщо відсутнє)
|
|
114
|
+
*/
|
|
115
|
+
function newDeps(manifest) {
|
|
116
|
+
return manifest?.project?.dependencies ?? []
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Читає й парсить TOML-файл, або повертає null, якщо файл відсутній/невалідний.
|
|
121
|
+
* @param {string} path абсолютний шлях
|
|
122
|
+
* @returns {Promise<object|null>} розпарсений обʼєкт або null
|
|
123
|
+
*/
|
|
124
|
+
async function readTomlOrNull(path) {
|
|
125
|
+
if (!existsSync(path)) return null
|
|
126
|
+
try {
|
|
127
|
+
return parseToml(await readFile(path, 'utf8'))
|
|
128
|
+
} catch {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Збирає diff по pyproject.toml: порівнює `<cwd>/pyproject.toml` з
|
|
135
|
+
* `<cwd>/pyproject.toml<backupSuffix>` — той самий контракт, що й
|
|
136
|
+
* `collectTazeDiff`/`collectCargoDiff`.
|
|
137
|
+
* @param {string} cwd корінь репозиторію (де лежить pyproject.toml — uv-конвенція: один кореневий файл, не per-crate, як Cargo.toml)
|
|
138
|
+
* @param {string} [backupSuffix] суфікс бекап-файлу
|
|
139
|
+
* @returns {Promise<{major: Array<{manifest:string, pkg:string, from:string, to:string}>, minorPatch:number, totalChanged:number, comparedManifests:number}>} агрегований diff
|
|
140
|
+
*/
|
|
141
|
+
export async function collectUvDiff(cwd, backupSuffix = DEFAULT_BACKUP_SUFFIX) {
|
|
142
|
+
const oldManifest = await readTomlOrNull(join(cwd, `pyproject.toml${backupSuffix}`))
|
|
143
|
+
const newManifest = await readTomlOrNull(join(cwd, 'pyproject.toml'))
|
|
144
|
+
if (!oldManifest || !newManifest) {
|
|
145
|
+
return { major: [], minorPatch: 0, totalChanged: 0, comparedManifests: 0 }
|
|
146
|
+
}
|
|
147
|
+
const res = diffPyprojectDeps(oldManifest, newManifest, 'pyproject.toml')
|
|
148
|
+
return {
|
|
149
|
+
major: res.major,
|
|
150
|
+
minorPatch: res.minorPatch,
|
|
151
|
+
totalChanged: res.major.length + res.minorPatch,
|
|
152
|
+
comparedManifests: 1
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Дістає `{name, extras, raw}` кожної прямої залежності з
|
|
158
|
+
* `[project].dependencies` поточного pyproject.toml — вхід для
|
|
159
|
+
* per-пакетного bump-циклу (`uv` немає єдиної команди "підняти все до
|
|
160
|
+
* latest, навіть через major", на відміну від `bunx taze -w -r latest`/
|
|
161
|
+
* `cargo upgrade --incompatible allow` — підтверджено емпірично: `uv add
|
|
162
|
+
* <pkg>` на вже присутній залежності НЕ переписує specifier, поки не буде
|
|
163
|
+
* `uv remove` спершу). `raw` — оригінальний PEP 508-рядок, потрібен для
|
|
164
|
+
* best-effort відновлення, якщо `uv add` після `uv remove` не вдався.
|
|
165
|
+
* @param {object} manifest розпарсений pyproject.toml
|
|
166
|
+
* @returns {Array<{name: string, extras: string[], raw: string}>} прямі залежності
|
|
167
|
+
*/
|
|
168
|
+
export function listDirectDependencies(manifest) {
|
|
169
|
+
return newDeps(manifest)
|
|
170
|
+
.map(requirement => {
|
|
171
|
+
const parsed = parsePep508(requirement)
|
|
172
|
+
return parsed ? { ...parsed, raw: requirement } : null
|
|
173
|
+
})
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.map(({ name, extras, raw }) => ({ name, extras, raw }))
|
|
176
|
+
}
|