@7n/rules 1.14.0 → 1.15.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 +13 -0
- package/package.json +2 -1
- package/rules/graphql/tooling/docs/main.md +1 -1
- package/rules/graphql/tooling/main.mjs +4 -4
- package/rules/js/eslint/docs/main.md +1 -1
- package/rules/js/eslint/main.mjs +8 -7
- package/rules/js-run/runtime/docs/main.md +1 -1
- package/rules/js-run/runtime/main.mjs +3 -3
- package/rules/k8s/manifests/main.mjs +6 -6
- package/rules/nginx-default-tpl/template/docs/main.md +1 -1
- package/rules/nginx-default-tpl/template/main.mjs +5 -5
- package/rules/tauri/tooling/docs/main.md +1 -1
- package/rules/tauri/tooling/main.mjs +1 -1
- package/scripts/lib/docs/ensure-tool.md +24 -22
- package/scripts/lib/docs/index.md +4 -1
- package/scripts/lib/docs/plugin-api.md +31 -0
- package/scripts/lib/docs/resolve-plugins.md +4 -4
- package/scripts/lib/docs/run-conftest-batch.md +5 -3
- package/scripts/lib/ensure-tool.mjs +125 -31
- package/scripts/lib/lint-surface/blocking-inventory.mjs +59 -0
- package/scripts/lib/lint-surface/docs/blocking-inventory.md +30 -0
- package/scripts/lib/lint-surface/docs/index.md +2 -0
- package/scripts/lib/lint-surface/docs/policy-lint-adapter.md +2 -3
- package/scripts/lib/lint-surface/docs/run-detectors.md +3 -1
- package/scripts/lib/lint-surface/docs/scheduler.md +39 -0
- package/scripts/lib/lint-surface/docs/types.md +2 -2
- package/scripts/lib/lint-surface/policy-lint-adapter.mjs +4 -3
- package/scripts/lib/lint-surface/run-detectors.mjs +142 -31
- package/scripts/lib/lint-surface/scheduler.mjs +96 -0
- package/scripts/lib/lint-surface/types.mjs +3 -0
- package/scripts/lib/plugin-api.mjs +71 -0
- package/scripts/lib/resolve-plugins.mjs +31 -6
- package/scripts/lib/run-conftest-batch.mjs +16 -11
- package/scripts/utils/docs/index.md +1 -0
- package/scripts/utils/docs/spawn-async.md +33 -0
- package/scripts/utils/spawn-async.mjs +112 -0
- package/skills/taze/SKILL.md +8 -1
- package/skills/taze/js/docs/index.md +6 -6
- package/skills/taze/js/docs/orchestrate.md +12 -17
- package/skills/taze/js/docs/rust-provider.md +42 -0
- package/skills/taze/js/orchestrate.mjs +113 -334
- package/skills/taze/js/rust-provider.mjs +162 -0
- package/skills/taze/js/docs/uv-diff.md +0 -41
- package/skills/taze/js/uv-diff.mjs +0 -176
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/** @see ./docs/orchestrate.md */
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
|
-
import { copyFile,
|
|
4
|
+
import { copyFile, rm } from 'node:fs/promises'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
8
|
+
import { assertEcosystemProvider } from '../../../scripts/lib/plugin-api.mjs'
|
|
9
|
+
import { readNRulesConfigLite } from '../../../scripts/lib/read-n-rules-config-lite.mjs'
|
|
10
|
+
import { getHandlers, resolvePlugins } from '../../../scripts/lib/resolve-plugins.mjs'
|
|
9
11
|
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
10
|
-
import { collectCargoDiff } from './cargo-diff.mjs'
|
|
11
12
|
import { collectTazeDiff } from './diff.mjs'
|
|
12
|
-
import
|
|
13
|
+
import rustProvider from './rust-provider.mjs'
|
|
13
14
|
|
|
14
15
|
/** Суфікс бекапу package.json — той самий, що й у `diff.mjs`/кроці 1 SKILL.md. */
|
|
15
16
|
const BACKUP_SUFFIX = '.taze-bak'
|
|
@@ -38,54 +39,6 @@ export function buildDependencyPrompt({ workspace, pkg, from, to }) {
|
|
|
38
39
|
].join('\n')
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
/**
|
|
42
|
-
* Промпт ОДНОГО ітеративного виклику для Rust-крейта — Rust-варіант
|
|
43
|
-
* [`buildDependencyPrompt`] (кроки 4-6 SKILL.md, Rust-гілка), для ОДНОГО
|
|
44
|
-
* major-крейта. Кроки 1-3/7/8 виконує оркестратор детерміновано, без LLM.
|
|
45
|
-
* @param {{manifest: string, pkg: string, from: string, to: string}} entry запис major-diff (з `collectCargoDiff`)
|
|
46
|
-
* @returns {string} готовий промпт
|
|
47
|
-
*/
|
|
48
|
-
export function buildCargoDependencyPrompt({ manifest, pkg, from, to }) {
|
|
49
|
-
return [
|
|
50
|
-
'# Major-оновлення одного Rust-крейта: перевірка сумісності й рефакторинг',
|
|
51
|
-
'',
|
|
52
|
-
`Крейт \`${pkg}\` у \`${manifest}\`: **${from} → ${to}** — вже застосовано в Cargo.toml/Cargo.lock (кроки 1-3 виконано детерміновано, без тебе). Твоя задача — лише breaking-changes-перевірка й, за потреби, рефакторинг.`,
|
|
53
|
-
'',
|
|
54
|
-
'## Кроки',
|
|
55
|
-
`1. Зібрати breaking changes цього оновлення: адреса репозиторію з поля \`repository\`/\`documentation\` крейта на crates.io (https://crates.io/crates/${pkg}) — CHANGELOG.md репозиторію чи GitHub Releases. Якщо немає — різниця по публічному API (\`pub fn\`/\`pub struct\`/\`pub trait\`) між закешованою старою версією (\`~/.cargo/registry/src/*/${pkg}-<стара-версія>/\`) і новою.`,
|
|
56
|
-
`2. Знайти використання зачепленого API в коді проєкту (\`rg -n --type rust\` по use-шляхах/викликах \`${pkg}\`).`,
|
|
57
|
-
'3. Сумісно — нічого не робити. Несумісно — застосувати міграцію (перейменувати use-шлях, оновити сигнатуру виклику, замінити видалений макрос еквівалентом).',
|
|
58
|
-
'4. Якщо були правки — запусти `cargo fmt --all -- --check`, `cargo clippy --all-targets --all-features -- -D warnings`, `cargo test`.',
|
|
59
|
-
'5. Нетривіальна/неоднозначна міграція — не вгадуй, залиш TODO-коментар із посиланням на CHANGELOG.',
|
|
60
|
-
'',
|
|
61
|
-
'У відповіді одним абзацом підсумуй: сумісно / зрефакторено (які файли) / TODO (чому).'
|
|
62
|
-
].join('\n')
|
|
63
|
-
}
|
|
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
|
-
|
|
89
42
|
/**
|
|
90
43
|
* Диспетчер одного ітеративного виклику на обраний раннер. `pi` — вбудований
|
|
91
44
|
* pi-агент (`@7n/llm-lib/agent-skill`; текст перехоплюється через `deps.out`,
|
|
@@ -154,7 +107,7 @@ function assertRunningInWorktree(cwd, spawnFn) {
|
|
|
154
107
|
}
|
|
155
108
|
|
|
156
109
|
/**
|
|
157
|
-
* Синхронно виконує детерміновану команду (bunx/bun
|
|
110
|
+
* Синхронно виконує детерміновану команду (bunx/bun), кидає з
|
|
158
111
|
* exit-кодом+stderr при провалі.
|
|
159
112
|
* @param {string} cmd бінарник
|
|
160
113
|
* @param {string[]} args аргументи
|
|
@@ -206,174 +159,94 @@ export async function cleanupBackups(cwd, workspaces, deps = {}) {
|
|
|
206
159
|
}
|
|
207
160
|
|
|
208
161
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
|
|
215
|
-
function hasCargoEdit(spawnFn) {
|
|
216
|
-
return spawnFn('cargo', ['upgrade', '--version'], { encoding: 'utf8' }).status === 0
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Бекапить кожен Cargo.toml + спільний кореневий Cargo.lock (крок 1 SKILL.md,
|
|
221
|
-
* Rust-гілка). v1: один спільний workspace/Cargo.lock у корені `cwd` —
|
|
222
|
-
* поточна реальна топологія репо (кореневий `Cargo.toml` з `[workspace]` +
|
|
223
|
-
* члени); кілька незалежних Cargo-workspace під час не підтримується.
|
|
162
|
+
* Завантажує EcosystemProvider-и з активних плагінів проєкту: `.n-rules.json`
|
|
163
|
+
* (або автодетект за файловими сигналами — `pyproject.toml` → lang-python) →
|
|
164
|
+
* `resolvePlugins` (плагін доставляється автоматично при першому запуску) → handler-модулі
|
|
165
|
+
* extension-point `taze` → default-експорт кожного валідується
|
|
166
|
+
* `assertEcosystemProvider`. Битий плагін (нема модуля/невалідна форма) —
|
|
167
|
+
* warning і пропуск, не провал прогону.
|
|
224
168
|
* @param {string} cwd корінь репо
|
|
225
|
-
* @param {string
|
|
226
|
-
* @param {{
|
|
227
|
-
* @returns {Promise<
|
|
169
|
+
* @param {(line: string) => void} log колбек прогресу
|
|
170
|
+
* @param {{ readNRulesConfigLite?: (cwd: string) => Promise<object>, resolvePlugins?: (root: string, config: object, opts?: object) => object[], getHandlers?: (root: string, config: object, point: string) => Array<{pluginName: string, modulePath: string}>, importModule?: (url: string) => Promise<object> }} [deps] інжекти для тестів
|
|
171
|
+
* @returns {Promise<object[]>} валідні провайдери плагінів
|
|
228
172
|
*/
|
|
229
|
-
export async function
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
173
|
+
export async function loadPluginTazeProviders(cwd, log, deps = {}) {
|
|
174
|
+
const readConfig = deps.readNRulesConfigLite ?? readNRulesConfigLite
|
|
175
|
+
const config = await readConfig(cwd)
|
|
176
|
+
const resolve = deps.resolvePlugins ?? resolvePlugins
|
|
177
|
+
resolve(cwd, config)
|
|
178
|
+
const handlers = (deps.getHandlers ?? getHandlers)(cwd, config, 'taze')
|
|
179
|
+
// eslint-disable-next-line no-unsanitized/method
|
|
180
|
+
const importModule = deps.importModule ?? (url => import(url))
|
|
181
|
+
|
|
182
|
+
const providers = []
|
|
183
|
+
for (const handler of handlers) {
|
|
184
|
+
try {
|
|
185
|
+
const mod = await importModule(pathToFileURL(handler.modulePath).href)
|
|
186
|
+
providers.push(assertEcosystemProvider(mod.default, handler.pluginName))
|
|
187
|
+
} catch (error) {
|
|
188
|
+
log(`⚠️ Плагін ${handler.pluginName}: taze-провайдер не завантажився — ${error.message}`)
|
|
189
|
+
}
|
|
234
190
|
}
|
|
235
|
-
|
|
236
|
-
if (existsSync(lockPath)) await copy(lockPath, `${lockPath}${BACKUP_SUFFIX}`)
|
|
191
|
+
return providers
|
|
237
192
|
}
|
|
238
193
|
|
|
239
194
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* @param {
|
|
245
|
-
* @
|
|
195
|
+
* Проганяє одну екосистему (провайдера) наскрізь: detect → available →
|
|
196
|
+
* backup → bump → diff → ізольований виклик раннера по кожному major-запису →
|
|
197
|
+
* cleanup. Виняток усередині (bump/diff/команда) не зупиняє інших провайдерів —
|
|
198
|
+
* фіксується в `error` запису екосистеми.
|
|
199
|
+
* @param {import('../../../scripts/lib/plugin-api.mjs').EcosystemProvider} provider провайдер екосистеми
|
|
200
|
+
* @param {{ cwd: string, runner: string, log: (line: string) => void, deps: object, spawnFn: typeof spawnSync, call: typeof callRunner }} ctx контекст прогону
|
|
201
|
+
* @returns {Promise<object>} запис екосистеми для звіту
|
|
246
202
|
*/
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
203
|
+
async function runEcosystem(provider, { cwd, runner, log, deps, spawnFn, call }) {
|
|
204
|
+
const base = {
|
|
205
|
+
id: provider.id,
|
|
206
|
+
title: provider.title,
|
|
207
|
+
manifestNoun: provider.manifestNoun,
|
|
208
|
+
skillSection: provider.skillSection
|
|
251
209
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const result = spawnFn(
|
|
264
|
-
'find',
|
|
265
|
-
[
|
|
266
|
-
'.',
|
|
267
|
-
'-name',
|
|
268
|
-
'Cargo.toml',
|
|
269
|
-
'-not',
|
|
270
|
-
'-path',
|
|
271
|
-
'*/node_modules/*',
|
|
272
|
-
'-not',
|
|
273
|
-
'-path',
|
|
274
|
-
'*/.worktrees/*',
|
|
275
|
-
'-not',
|
|
276
|
-
'-path',
|
|
277
|
-
'*/target/*'
|
|
278
|
-
],
|
|
279
|
-
{ cwd, encoding: 'utf8' }
|
|
280
|
-
)
|
|
281
|
-
return (result.stdout ?? '')
|
|
282
|
-
.split('\n')
|
|
283
|
-
.map(line => line.trim())
|
|
284
|
-
.filter(Boolean)
|
|
285
|
-
}
|
|
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
|
-
}
|
|
210
|
+
const eco = { ...base, manifests: [], processed: false, skippedReason: null, error: null, minorPatch: 0, results: [] }
|
|
211
|
+
try {
|
|
212
|
+
eco.manifests = provider.detect(cwd, { spawnFn })
|
|
213
|
+
if (eco.manifests.length === 0) return eco
|
|
214
|
+
|
|
215
|
+
const availability = provider.available(spawnFn)
|
|
216
|
+
if (!availability.ok) {
|
|
217
|
+
eco.skippedReason = availability.reason
|
|
218
|
+
log(`⏭ ${provider.title}: ${availability.reason}`)
|
|
219
|
+
return eco
|
|
220
|
+
}
|
|
296
221
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
}
|
|
222
|
+
log(`📦 Бекап (${provider.manifestNoun})...`)
|
|
223
|
+
await provider.backup(cwd, eco.manifests, deps)
|
|
224
|
+
await provider.bump(cwd, eco.manifests, { spawnFn, log, deps })
|
|
307
225
|
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
}
|
|
226
|
+
const diff = await provider.diff(cwd, eco.manifests, deps)
|
|
227
|
+
log(`🔍 ${provider.title} diff: ${diff.major.length} major, ${diff.minorPatch} minor/patch`)
|
|
335
228
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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' })
|
|
229
|
+
for (const entry of diff.major) {
|
|
230
|
+
log(`🔧 [${provider.id}] ${entry.pkg} (${entry.manifest}): ${entry.from} → ${entry.to}...`)
|
|
231
|
+
const outcome = await call(runner, provider.promptFor(entry), cwd, deps)
|
|
232
|
+
eco.results.push({ ...entry, ...outcome })
|
|
233
|
+
log(outcome.ok ? ` ✅ ${entry.pkg}` : ` ❌ ${entry.pkg}: ${outcome.error}`)
|
|
369
234
|
}
|
|
235
|
+
|
|
236
|
+
await provider.cleanup(cwd, eco.manifests, deps)
|
|
237
|
+
eco.processed = true
|
|
238
|
+
eco.minorPatch = diff.minorPatch
|
|
239
|
+
} catch (error) {
|
|
240
|
+
eco.error = error instanceof Error ? error.message : String(error)
|
|
241
|
+
log(`❌ ${provider.title}: ${eco.error}`)
|
|
370
242
|
}
|
|
243
|
+
return eco
|
|
371
244
|
}
|
|
372
245
|
|
|
373
246
|
/**
|
|
374
|
-
* Форматує один рядок результату ітерації (спільний для npm
|
|
247
|
+
* Форматує один рядок результату ітерації (спільний для npm-гілки й екосистем).
|
|
375
248
|
* @param {{pkg: string, ok: boolean, error: string|null, from: string, to: string}} r результат ітерації
|
|
376
|
-
* @param {string} scopeLabel мітка джерела (`workspace` для npm, `manifest` для
|
|
249
|
+
* @param {string} scopeLabel мітка джерела (`workspace` для npm, `manifest` для екосистем)
|
|
377
250
|
* @returns {string} один рядок звіту
|
|
378
251
|
*/
|
|
379
252
|
function formatResultLine(r, scopeLabel) {
|
|
@@ -382,24 +255,23 @@ function formatResultLine(r, scopeLabel) {
|
|
|
382
255
|
return ` ${status} \`${r.pkg}\` (${scopeLabel}): ${r.from} → ${r.to}${errorSuffix}`
|
|
383
256
|
}
|
|
384
257
|
|
|
385
|
-
/** Пуста гілка-екосистема (для дефолту, коли `rust`/`python` не знайдені взагалі). */
|
|
386
|
-
const EMPTY_ECOSYSTEM = { manifests: [], processed: false, skippedReason: null, minorPatch: 0, results: [] }
|
|
387
|
-
|
|
388
258
|
/**
|
|
389
|
-
* Додає секцію звіту однієї
|
|
390
|
-
*
|
|
391
|
-
* ідентичну форму результату ({manifest, pkg, from, to, ok, error}).
|
|
259
|
+
* Додає секцію звіту однієї екосистеми (Rust/Python/…) — мітки бере з самого
|
|
260
|
+
* запису (`title`/`manifestNoun`/`skillSection` — з провайдера).
|
|
392
261
|
* @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
|
-
* @
|
|
395
|
-
* @returns {number} totalChanged цієї гілки
|
|
262
|
+
* @param {{title: string, manifestNoun: string, skillSection: string, manifests: string[], processed: boolean, skippedReason: string|null, error: string|null, minorPatch: number, results: Array<{pkg:string, manifest:string, from:string, to:string, ok:boolean, error:string|null}>}} eco запис екосистеми
|
|
263
|
+
* @returns {number} totalChanged цієї екосистеми
|
|
396
264
|
*/
|
|
397
|
-
function appendEcosystemSection(lines, eco
|
|
398
|
-
if (eco.manifests.length === 0) return 0
|
|
399
|
-
lines.push('', `### ${title}`)
|
|
265
|
+
function appendEcosystemSection(lines, eco) {
|
|
266
|
+
if (eco.manifests.length === 0 && !eco.error) return 0
|
|
267
|
+
lines.push('', `### ${eco.title}`)
|
|
268
|
+
if (eco.error) {
|
|
269
|
+
lines.push(`- ❌ Провал (${eco.error}) — переглянь дерево вручну, бекапи могли лишитись`)
|
|
270
|
+
return 0
|
|
271
|
+
}
|
|
400
272
|
if (!eco.processed) {
|
|
401
273
|
lines.push(
|
|
402
|
-
`- ⏭ Пропущено (${eco.skippedReason}) — ${eco.manifests.length} ${manifestNoun}, онови вручну за ${skillSection}: ${eco.manifests.join(', ')}`
|
|
274
|
+
`- ⏭ Пропущено (${eco.skippedReason}) — ${eco.manifests.length} ${eco.manifestNoun}, онови вручну за ${eco.skillSection}: ${eco.manifests.join(', ')}`
|
|
403
275
|
)
|
|
404
276
|
return 0
|
|
405
277
|
}
|
|
@@ -417,24 +289,11 @@ function appendEcosystemSection(lines, eco, { title, manifestNoun, skillSection
|
|
|
417
289
|
* minorPatch: number,
|
|
418
290
|
* totalChanged: number,
|
|
419
291
|
* results: Array<{pkg:string, workspace:string, from:string, to:string, ok:boolean, error:string|null}>,
|
|
420
|
-
*
|
|
421
|
-
*
|
|
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?: {
|
|
428
|
-
* manifests: string[],
|
|
429
|
-
* processed: boolean,
|
|
430
|
-
* skippedReason: string|null,
|
|
431
|
-
* minorPatch: number,
|
|
432
|
-
* results: Array<{pkg:string, manifest:string, from:string, to:string, ok:boolean, error:string|null}>
|
|
433
|
-
* }
|
|
434
|
-
* }} args дані звіту
|
|
292
|
+
* ecosystems?: Array<object>
|
|
293
|
+
* }} args дані звіту (`ecosystems` — записи з `runEcosystem`, по одному на провайдера)
|
|
435
294
|
* @returns {string} markdown-звіт
|
|
436
295
|
*/
|
|
437
|
-
export function formatReport({ minorPatch, totalChanged, results,
|
|
296
|
+
export function formatReport({ minorPatch, totalChanged, results, ecosystems = [] }) {
|
|
438
297
|
const lines = [
|
|
439
298
|
'## taze: підсумок',
|
|
440
299
|
'',
|
|
@@ -445,18 +304,12 @@ export function formatReport({ minorPatch, totalChanged, results, rust = EMPTY_E
|
|
|
445
304
|
lines.push(formatResultLine(r, r.workspace))
|
|
446
305
|
}
|
|
447
306
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
title: 'Python-пакети (uv)',
|
|
455
|
-
manifestNoun: 'pyproject.toml',
|
|
456
|
-
skillSection: 'Python-гілкою SKILL.md'
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
lines.push('', `- **Всього змінено:** ${totalChanged + rustTotalChanged + pythonTotalChanged}`)
|
|
307
|
+
let ecosystemsTotal = 0
|
|
308
|
+
for (const eco of ecosystems) {
|
|
309
|
+
ecosystemsTotal += appendEcosystemSection(lines, eco)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
lines.push('', `- **Всього змінено:** ${totalChanged + ecosystemsTotal}`)
|
|
460
313
|
return lines.join('\n')
|
|
461
314
|
}
|
|
462
315
|
|
|
@@ -464,16 +317,17 @@ export function formatReport({ minorPatch, totalChanged, results, rust = EMPTY_E
|
|
|
464
317
|
* Оркеструє taze: детерміновані кроки (бекап → масовий bump → diff →
|
|
465
318
|
* прибирання → звіт) без LLM, і по одному ізольованому, обмеженому по
|
|
466
319
|
* обсягу виклику `callRunner` на кожен major-пакет (кроки 4-6 SKILL.md) —
|
|
467
|
-
* замість одного величезного непрозорого ходу на весь монорепо.
|
|
468
|
-
*
|
|
469
|
-
*
|
|
320
|
+
* замість одного величезного непрозорого ходу на весь монорепо. npm/bun-гілка
|
|
321
|
+
* вбудована; решта екосистем — EcosystemProvider-и: first-party (Rust/Cargo)
|
|
322
|
+
* плюс завантажені з плагінів (`@7n/rules-lang-*`, extension-point `taze`).
|
|
323
|
+
* Падіння одного пакета/однієї екосистеми не втрачає прогрес по інших.
|
|
470
324
|
* @param {{
|
|
471
325
|
* cwd?: string,
|
|
472
326
|
* runner?: 'pi' | 'cursor' | 'codex',
|
|
473
327
|
* log?: (line: string) => void,
|
|
474
|
-
* deps?: { spawnFn?: typeof spawnSync, collectTazeDiff?: (cwd: string) => Promise<object>,
|
|
475
|
-
* }} [options] опції + інжекти для тестів
|
|
476
|
-
* @returns {Promise<{ ok: boolean, report: string, results: Array<object>,
|
|
328
|
+
* deps?: { spawnFn?: typeof spawnSync, collectTazeDiff?: (cwd: string) => Promise<object>, callRunner?: (runner: string, prompt: string, cwd: string, deps: object) => Promise<{ok: boolean, text: string, error: string|null}>, ecosystemProviders?: object[] } & Record<string, unknown>
|
|
329
|
+
* }} [options] опції + інжекти для тестів (`deps.ecosystemProviders` повністю замінює список провайдерів)
|
|
330
|
+
* @returns {Promise<{ ok: boolean, report: string, results: Array<object>, ecosystems: Array<object> }>} результат
|
|
477
331
|
*/
|
|
478
332
|
export async function runTazeOrchestrator(options = {}) {
|
|
479
333
|
const cwd = options.cwd ?? process.cwd()
|
|
@@ -507,90 +361,15 @@ export async function runTazeOrchestrator(options = {}) {
|
|
|
507
361
|
|
|
508
362
|
await cleanupBackups(cwd, backedUpWorkspaces, deps)
|
|
509
363
|
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
rust.skippedReason =
|
|
515
|
-
'cargo-edit не встановлено (`cargo install cargo-edit`) — cargo update без нього недетермінований для major'
|
|
516
|
-
log(`⏭ Rust: ${rust.skippedReason}`)
|
|
517
|
-
} else if (rustManifests.length > 0) {
|
|
518
|
-
log('📦 Бекап Cargo.toml/Cargo.lock...')
|
|
519
|
-
await backupCargoManifests(cwd, rustManifests, deps)
|
|
520
|
-
|
|
521
|
-
log('⬆️ cargo upgrade --incompatible allow...')
|
|
522
|
-
runCommand('cargo', ['upgrade', '--incompatible', 'allow'], cwd, spawnFn)
|
|
523
|
-
log('🔄 cargo update...')
|
|
524
|
-
runCommand('cargo', ['update'], cwd, spawnFn)
|
|
525
|
-
|
|
526
|
-
const collectCargo = deps.collectCargoDiff ?? collectCargoDiff
|
|
527
|
-
const cargoDiff = await collectCargo(cwd, rustManifests)
|
|
528
|
-
log(`🔍 Rust diff: ${cargoDiff.major.length} major, ${cargoDiff.minorPatch} minor/patch`)
|
|
529
|
-
|
|
530
|
-
const rustResults = []
|
|
531
|
-
for (const entry of cargoDiff.major) {
|
|
532
|
-
log(`🔧 [rust] ${entry.pkg} (${entry.manifest}): ${entry.from} → ${entry.to}...`)
|
|
533
|
-
const outcome = await call(runner, buildCargoDependencyPrompt(entry), cwd, deps)
|
|
534
|
-
rustResults.push({ ...entry, ...outcome })
|
|
535
|
-
log(outcome.ok ? ` ✅ ${entry.pkg}` : ` ❌ ${entry.pkg}: ${outcome.error}`)
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
await cleanupCargoBackups(cwd, rustManifests, deps)
|
|
539
|
-
|
|
540
|
-
rust = {
|
|
541
|
-
manifests: rustManifests,
|
|
542
|
-
processed: true,
|
|
543
|
-
skippedReason: null,
|
|
544
|
-
minorPatch: cargoDiff.minorPatch,
|
|
545
|
-
results: rustResults
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
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
|
-
}
|
|
364
|
+
const providers = deps.ecosystemProviders ?? [rustProvider, ...(await loadPluginTazeProviders(cwd, log, deps))]
|
|
365
|
+
const ecosystems = []
|
|
366
|
+
for (const provider of providers) {
|
|
367
|
+
ecosystems.push(await runEcosystem(provider, { cwd, runner, log, deps, spawnFn, call }))
|
|
584
368
|
}
|
|
585
369
|
|
|
586
|
-
const report = formatReport({ minorPatch: diff.minorPatch, totalChanged: diff.totalChanged, results,
|
|
370
|
+
const report = formatReport({ minorPatch: diff.minorPatch, totalChanged: diff.totalChanged, results, ecosystems })
|
|
587
371
|
log(report)
|
|
588
372
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
report,
|
|
592
|
-
results,
|
|
593
|
-
rustResults: rust.results,
|
|
594
|
-
pythonResults: python.results
|
|
595
|
-
}
|
|
373
|
+
const ecosystemsOk = ecosystems.every(eco => eco.error === null && eco.results.every(r => r.ok))
|
|
374
|
+
return { ok: results.every(r => r.ok) && ecosystemsOk, report, results, ecosystems }
|
|
596
375
|
}
|