@7n/rules 1.9.0 → 1.11.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 +16 -0
- package/package.json +1 -1
- package/rules/js/eslint/docs/fix-worker.md +30 -0
- package/rules/js/eslint/docs/index.md +11 -0
- package/rules/js/eslint/fix-worker.mjs +99 -0
- package/rules/js/knip/concern.json +1 -0
- package/rules/text/markdownlint/docs/main.md +11 -22
- package/rules/text/markdownlint/main.mjs +12 -4
- package/skills/taze/SKILL.md +13 -11
- package/skills/taze/js/cargo-diff.mjs +117 -0
- package/skills/taze/js/docs/cargo-diff.md +41 -0
- package/skills/taze/js/docs/index.md +1 -0
- package/skills/taze/js/docs/orchestrate.md +1 -2
- package/skills/taze/js/orchestrate.mjs +160 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.11.0] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- js/eslint: fix-worker.mjs з per-file циклом замість одного великого промпту на весь concern — усунення 100% timeout на драбині, виміряно на реальних lint-прогонах
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- text/markdownlint: включати причину провалу markdownlint-cli2 у violation-повідомлення; js/knip: fixability=structural (LLM-ладдер завжди приречений на timeout)
|
|
12
|
+
|
|
13
|
+
## [1.10.0] - 2026-07-17
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `skill pi/cursor/codex taze` — Rust-гілку (`Cargo.toml`) тепер теж покриває оркестратор: детермінований `collectCargoDiff` (`npm/skills/taze/js/cargo-diff.mjs`, парсинг через `smol-toml`, той самий caret-семантики принцип, що й для npm) замість ручної класифікації, і по одному ізольованому виклику раннера на кожен major-крейт. Якщо `cargo-edit` не встановлено — Rust-гілка пропускається без блокування npm-гілки (перелік Cargo.toml лишається в звіті)
|
|
18
|
+
|
|
3
19
|
## [1.9.0] - 2026-07-17
|
|
4
20
|
|
|
5
21
|
### Added
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: fix-worker.mjs
|
|
4
|
+
resource: npm/rules/js/eslint/fix-worker.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 1eb9c7fc
|
|
7
|
+
model: manual
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Огляд
|
|
11
|
+
|
|
12
|
+
Custom fix-worker `js/eslint` (перекриває дефолтний `default-worker.mjs`): замість одного `runAgentFix`-виклику з усіма порушеннями з усіх файлів concern-а — окрема агентна сесія на кожен файл, у межах спільного дедлайну rung-а. Мотивація — виміряно на реальних lint-прогонах: одна сесія, що жонглює кількома файлами одразу, стабільно впирається у timeout на всіх 4 rung-ах драбини (local-min→cloud-avg), навіть при малому обсязі порушень; та сама модель, scoped на один файл, укладається в бюджет і закриває більшість порушень.
|
|
13
|
+
|
|
14
|
+
## Поведінка
|
|
15
|
+
|
|
16
|
+
1. Групує вхідні `violations` за `file`; порушення без `file` ігноруються.
|
|
17
|
+
2. Дедлайн — `DEADLINE_FRACTION` (0.8) від `ctx.timeoutMs`; цикл перевіряє дедлайн ПЕРЕД стартом кожного файлу і не починає новий, якщо час вичерпано.
|
|
18
|
+
3. Кожен файл отримує РЕШТУ бюджету до дедлайну (не фіксований поділ `timeoutMs / files.length`) — перший (часто найважчий) файл отримує найбільше часу.
|
|
19
|
+
4. На кожен файл — окремий `runAgentFix` із `targetFiles: [file]` і власним `verify` (`verifyFile`): item-scoped canonical re-detect лише цього файлу, не всього concern-а — інакше evidence-гейт хибно вважав би файл незакритим через порушення в ІНШИХ файлах.
|
|
20
|
+
5. Один файл, що завершився з `error`, не обриває цикл — пропускається, наступні файли все одно обробляються в межах дедлайну.
|
|
21
|
+
6. Повертає лише `touchedFiles` з успішних (`!error`) викликів; success rung-а все одно визначає whole-concern canonical re-detect runner-а (`runRung`), не цей worker.
|
|
22
|
+
|
|
23
|
+
## Публічний API
|
|
24
|
+
|
|
25
|
+
- `fixWorker(violations, ctx)` — контракт `FixWorkerFn`; резолвиться автоматично замість `default-worker.mjs`, бо лежить як `fix-worker.mjs` поруч із `main.mjs` concern-а.
|
|
26
|
+
|
|
27
|
+
## Гарантії поведінки
|
|
28
|
+
|
|
29
|
+
- Пише лише у файли з переданих `violations` (`targetFiles: [file]` на кожен виклик) — той самий semantic-collateral guard, що й у дефолтного worker-а.
|
|
30
|
+
- `recordWrite` (не durable) на кожен файл: rollback-контракт незмінний — якщо після worker-а в concern-і лишилось хоч одне порушення будь-де, `runRung` відкочує ВСІ правки цього rung-а, включно з уже полагодженими файлами.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: npm/rules/js/eslint
|
|
4
|
+
resource: npm/rules/js/eslint/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ------------------------------- | --------- |
|
|
9
|
+
| [fix-eslint.mjs](fix-eslint.md) | JS Module |
|
|
10
|
+
| [fix-worker.mjs](fix-worker.md) | JS Module |
|
|
11
|
+
| [main.mjs](main.md) | JS Module |
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fix-worker для `js/eslint`: окрема агентна сесія НА ФАЙЛ замість одного великого промпту
|
|
3
|
+
* на всі файли concern-а одразу. Дефолтний worker (`default-worker.mjs`) шле ВСІ порушення
|
|
4
|
+
* з УСІХ файлів в один `runAgentFix`-виклик — на реальних lint-прогонах (tauri-components)
|
|
5
|
+
* це давало 100% timeout на всіх 4 rung-ах драбини (local-min→cloud-avg), навіть коли
|
|
6
|
+
* порушень було лише 20-90 у 1-3 файлах: одна сесія жонглює кількома файлами одразу і не
|
|
7
|
+
* встигає в rung-таймаут. Виміряно (2026-07-17, ручний timing-експеримент поза pipeline):
|
|
8
|
+
* одна сесія, scoped на ОДИН найважчий файл (21 порушення), закрила 76% (16/21) за 91с із
|
|
9
|
+
* бюджету 120с (cloud-min) — per-file scoping вкладається, mega-prompt — ні.
|
|
10
|
+
*
|
|
11
|
+
* Rollback-контракт незмінний: `recordWrite` (не durable) на кожен файл — правки наявного
|
|
12
|
+
* стороннього коду. Якщо після worker-а `runRung`-ів canonical re-detect (whole-concern)
|
|
13
|
+
* знайде хоч одне порушення будь-де, увесь rung однаково відкотиться (S1) — той самий
|
|
14
|
+
* контракт, що й у дефолтного worker-а; батчинг підвищує ймовірність, що ЦЕЙ rung дійде
|
|
15
|
+
* до 0 порушень, а не замінює rollback-семантику.
|
|
16
|
+
*
|
|
17
|
+
* Дедлайн (`DEADLINE_FRACTION` від `ctx.timeoutMs`, той самий підхід, що й
|
|
18
|
+
* `doc-files/check/fix-worker.mjs`): цикл не стартує наступний файл, якщо дедлайн
|
|
19
|
+
* настав — worker повертає часткову роботу штатно, замість фонової сесії, що триває
|
|
20
|
+
* поверх backstop ×1.25 runner-а. Кожен файл отримує РЕШТУ бюджету до дедлайну (не
|
|
21
|
+
* фіксований `timeoutMs / files.length`) — перший (часто найважчий) файл отримує
|
|
22
|
+
* найбільше часу, а не штучно урізаний рівний шматок.
|
|
23
|
+
* @typedef {import('../../../scripts/lib/lint-surface/types.mjs').FixWorkerFn} FixWorkerFn
|
|
24
|
+
*/
|
|
25
|
+
import { resolve } from 'node:path'
|
|
26
|
+
|
|
27
|
+
import { anchoredEnabled } from '../../../scripts/lib/lint-surface/default-worker.mjs'
|
|
28
|
+
import { renderViolations } from '../../../scripts/lib/lint-surface/render.mjs'
|
|
29
|
+
import { lint } from './main.mjs'
|
|
30
|
+
|
|
31
|
+
/** Частка ctx.timeoutMs, після якої цикл не стартує наступний файл (запас до backstop ×1.25). */
|
|
32
|
+
const DEADLINE_FRACTION = 0.8
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Item-scoped (один файл) canonical re-detect для evidence-гейта `runAgentFix` —
|
|
36
|
+
* НЕ те саме, що whole-concern `ctx.verify` з `runRung` (той перевірив би ВСІ файли
|
|
37
|
+
* concern-а й дав би хибний "не готово" навіть коли саме ЦЕЙ файл уже чистий).
|
|
38
|
+
* @param {string} cwd абсолютний корінь проєкту
|
|
39
|
+
* @param {string} ruleId id правила
|
|
40
|
+
* @param {string} concernId id concern-а
|
|
41
|
+
* @param {string} file posix-relative шлях файлу, що перевіряється
|
|
42
|
+
* @returns {Promise<{ ok: boolean, output: string }>} вердикт verify-петлі
|
|
43
|
+
*/
|
|
44
|
+
async function verifyFile(cwd, ruleId, concernId, file) {
|
|
45
|
+
const { violations: after } = await lint({ cwd, ruleId, concernId, files: [file] })
|
|
46
|
+
const stamped = after.map(v => ({ ...v, ruleId, concernId }))
|
|
47
|
+
return { ok: stamped.length === 0, output: renderViolations(stamped) }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @type {FixWorkerFn} */
|
|
51
|
+
export async function fixWorker(violations, ctx) {
|
|
52
|
+
// lazy import — тримає detect-шлях вільним від pi/oxc (read-only --no-fix не вантажить їх).
|
|
53
|
+
const [{ runAgentFix }, { isLocalModel }, { extractContext }] = await Promise.all([
|
|
54
|
+
import('@7n/llm-lib/agent-fix'),
|
|
55
|
+
import('@7n/llm-lib/model-tiers'),
|
|
56
|
+
import('../../../scripts/utils/ast-extract.mjs')
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
/** @type {Map<string, import('../../../scripts/lib/lint-surface/types.mjs').LintViolation[]>} */
|
|
60
|
+
const byFile = new Map()
|
|
61
|
+
for (const v of violations) {
|
|
62
|
+
if (!v.file) continue
|
|
63
|
+
const arr = byFile.get(v.file)
|
|
64
|
+
if (arr) arr.push(v)
|
|
65
|
+
else byFile.set(v.file, [v])
|
|
66
|
+
}
|
|
67
|
+
const files = byFile.keys().toArray()
|
|
68
|
+
if (files.length === 0) return { touchedFiles: [] }
|
|
69
|
+
|
|
70
|
+
const deadlineAt = ctx.timeoutMs ? Date.now() + Math.round(ctx.timeoutMs * DEADLINE_FRACTION) : null
|
|
71
|
+
const anchoredEdits = anchoredEnabled(ctx.model, isLocalModel)
|
|
72
|
+
|
|
73
|
+
const touchedFiles = []
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
if (deadlineAt && Date.now() >= deadlineAt) break
|
|
76
|
+
const callTimeoutMs = deadlineAt ? Math.max(1000, deadlineAt - Date.now()) : ctx.timeoutMs
|
|
77
|
+
|
|
78
|
+
const res = await runAgentFix(ctx.ruleId, renderViolations(byFile.get(file)), ctx.cwd, {
|
|
79
|
+
model: ctx.model,
|
|
80
|
+
tier: ctx.tier,
|
|
81
|
+
timeoutMs: callTimeoutMs,
|
|
82
|
+
feedback: ctx.feedback ?? null,
|
|
83
|
+
caller: `fix:${ctx.ruleId}/${ctx.concernId}:${ctx.tier}:${file}`,
|
|
84
|
+
recordWrite: ctx.recordWrite,
|
|
85
|
+
chain: ctx.chain ?? null,
|
|
86
|
+
targetFiles: [file],
|
|
87
|
+
verify: () => verifyFile(ctx.cwd, ctx.ruleId, ctx.concernId, file),
|
|
88
|
+
verifyMax: ctx.verifyMax,
|
|
89
|
+
anchoredEdits,
|
|
90
|
+
deps: { astContext: p => extractContext(resolve(ctx.cwd, p)) }
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Один файл не впорався — не кидаємо, пробуємо решту в межах дедлайну; whole-concern
|
|
94
|
+
// canonical re-detect runner-а (не цей worker) визначить, чи rung закрито.
|
|
95
|
+
if (!res.error) touchedFiles.push(...(res.touchedFiles ?? []))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { touchedFiles }
|
|
99
|
+
}
|
|
@@ -3,38 +3,27 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/text/markdownlint/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model:
|
|
8
|
-
tier: cloud-avg
|
|
9
|
-
score: 95
|
|
10
|
-
issues: anchor-miss:(text.mdc),judge:inaccurate:0.98
|
|
11
|
-
judgeModel: openai-codex/gpt-5.4-mini
|
|
6
|
+
crc: 6aec4713
|
|
7
|
+
model: manual
|
|
12
8
|
---
|
|
13
9
|
|
|
14
10
|
## Огляд
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
Multi-surface detector `text/markdownlint`: `policy` перевіряє наявність `.markdownlint-cli2.jsonc`, `lint` запускає сам `markdownlint-cli2` (delta — по `ctx.files`, full — за glob `**/*.md`/`**/*.mdc`) і повертає обидва результати в одному `LintResult`.
|
|
17
13
|
|
|
18
14
|
## Поведінка
|
|
19
15
|
|
|
20
|
-
1. `
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
4. `lint` не запускає markdownlint, якщо серед цільових файлів немає Markdown-документів, і повертає лише вже зібрані порушення політики.
|
|
27
|
-
|
|
28
|
-
5. `lint` приглушує прямий вивід markdownlint, щоб detector повертав уніфікований результат lint-перевірки без шуму в консолі.
|
|
29
|
-
|
|
30
|
-
6. `lint` додає порушення з маркером ``, якщо markdownlint знаходить проблеми у Markdown або MDC-файлах.
|
|
31
|
-
|
|
32
|
-
7. `lint` повертає спільний список порушень для policy- та lint-поверхонь, щоб markdown-концерн поводився як один цілісний detector.
|
|
16
|
+
1. Policy-перевірка (`evaluatePolicyConcern`) додає порушення, якщо `.markdownlint-cli2.jsonc` відсутній.
|
|
17
|
+
2. Якщо серед цільових файлів немає Markdown/MDC — `markdownlint-cli2` не запускається, повертаються лише policy-порушення.
|
|
18
|
+
3. `logMessage` (banner, `Finding:`/`Found:`/`Linting:`/`Summary:` прогрес-текст) — no-op, не деталь порушення.
|
|
19
|
+
4. `logError` (один готовий рядок на порушення від дефолтного форматера markdownlint-cli2: `<file>:<line>:<col> <rule> <опис> [<деталь>]`) — накопичується в масив і вбудовується у violation-повідомлення. Без цього LLM fix-worker (і non-verbose підсумок) бачив лише голе "markdownlint знайшов порушення", без файлу/рядка/правила.
|
|
20
|
+
5. Ненульовий exit-код `markdownlint-cli2` → одне порушення `reason: 'markdownlint'` з накопиченою деталлю.
|
|
33
21
|
|
|
34
22
|
## Публічний API
|
|
35
23
|
|
|
36
|
-
- lint —
|
|
24
|
+
- `lint(ctx)` — detector-контракт unified lint surface; повертає `{ violations }` (policy + lint-порушення разом).
|
|
37
25
|
|
|
38
26
|
## Гарантії поведінки
|
|
39
27
|
|
|
40
|
-
- Read-only: не
|
|
28
|
+
- Read-only: не пише `.markdownlint-cli2.jsonc` сам — генерація конфігу окремим T0 (не в цьому detector-і).
|
|
29
|
+
- `logError`-деталь — єдине джерело причини провалу; текстові евристики за кодом виходу не використовуються.
|
|
@@ -32,22 +32,30 @@ export async function lint(ctx) {
|
|
|
32
32
|
const targets = ctx.files === undefined ? DEFAULT_MD_GLOBS : ctx.files.filter(f => MD_EXT_RE.test(f))
|
|
33
33
|
if (targets.length === 0) return { violations }
|
|
34
34
|
|
|
35
|
+
// logError отримує один готовий рядок на порушення через default output formatter
|
|
36
|
+
// markdownlint-cli2 ("<file>:<line>:<col> <rule> <опис> [<деталь>]") — раніше глушився,
|
|
37
|
+
// detector повертав лише голе "щось не пройшло" без файлу/правила/причини, тож LLM
|
|
38
|
+
// fix-worker (як і non-verbose підсумок) не мав інформації, що саме виправляти
|
|
39
|
+
// (той самий патерн, що й text/run-v8r до фіксу). logMessage лишається no-op —
|
|
40
|
+
// banner/Finding/Found/Linting/Summary прогрес-текст, не деталь порушення.
|
|
41
|
+
const errorLines = []
|
|
35
42
|
const code = await markdownlintCli2({
|
|
36
43
|
directory: ctx.cwd,
|
|
37
44
|
argv: targets,
|
|
38
45
|
logMessage: () => {
|
|
39
|
-
//
|
|
46
|
+
// прогрес-статус markdownlint-cli2 (banner, Finding/Found/Linting/Summary) — не деталь
|
|
40
47
|
},
|
|
41
|
-
logError:
|
|
42
|
-
|
|
48
|
+
logError: message => {
|
|
49
|
+
errorLines.push(message)
|
|
43
50
|
}
|
|
44
51
|
})
|
|
45
52
|
if (code !== 0) {
|
|
53
|
+
const detail = errorLines.length > 0 ? `:\n${errorLines.join('\n')}` : ''
|
|
46
54
|
violations.push({
|
|
47
55
|
ruleId: ctx.ruleId,
|
|
48
56
|
concernId: ctx.concernId,
|
|
49
57
|
reason: 'markdownlint',
|
|
50
|
-
message:
|
|
58
|
+
message: `markdownlint знайшов порушення у *.md/*.mdc (text.mdc)${detail}`
|
|
51
59
|
})
|
|
52
60
|
}
|
|
53
61
|
|
package/skills/taze/SKILL.md
CHANGED
|
@@ -13,33 +13,35 @@ version: '1.1'
|
|
|
13
13
|
|
|
14
14
|
Оновити всі модулі проекту (npm/bun-залежності, а за наявності `Cargo.toml` — і Rust-крейти) до останніх версій, виявити major-оновлення, перевірити сумісність змін з кодом проекту і за потреби зрефакторити несумісні місця.
|
|
15
15
|
|
|
16
|
-
## Оркестрація (npm/bun-гілка) — не одним промптом
|
|
16
|
+
## Оркестрація (npm/bun- і Rust-гілка) — не одним промптом
|
|
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 нижче (бекап → `bunx taze -w -r latest` → `bun install` → `n-rules taze diff`).
|
|
24
|
-
2. Для кожного **окремого** major
|
|
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"`).
|
|
24
|
+
2. Для кожного **окремого** major-пакета/крейта з diff-у — один ізольований, обмежений виклик обраного раннера (кроки 4-6, лише для цього запису; промпт генерує `buildDependencyPrompt`/`buildCargoDependencyPrompt`, не цей SKILL.md).
|
|
25
25
|
3. Детерміновано прибирає бекапи (крок 7) і компонує звіт (крок 8) з результатів усіх ітерацій.
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
Якщо `Cargo.toml` знайдені, але `cargo-edit` не встановлено — Rust-гілка **не**
|
|
28
|
+
блокує npm-гілку: оркестратор пропускає Rust-обробку і в звіті перелічує
|
|
29
|
+
знайдені `Cargo.toml` як такі, що потребують ручного прогону (нижче).
|
|
30
|
+
|
|
31
|
+
Переваги: падіння/timeout на одному пакеті/крейті не втрачає прогрес по інших;
|
|
32
|
+
кожен виклик успадковує власний timeout раннера (короткий, бо скоуп малий —
|
|
33
|
+
один запис, не весь монорепо); видно прогрес по записах, а не чорну скриньку.
|
|
30
34
|
|
|
31
35
|
Кроки 1-8 нижче лишаються джерелом правди щодо ЗМІСТУ роботи (що саме робить
|
|
32
36
|
кожен крок) — оркестратор їх виконує програмно (1-3/7/8) або як промпт на
|
|
33
|
-
один
|
|
34
|
-
інших скілів) досі стосується лише **Rust-гілки** (нижче) — вона оркестратором
|
|
35
|
-
не покрита.
|
|
37
|
+
один запис (4-6) для обох гілок.
|
|
36
38
|
|
|
37
39
|
## Передумови
|
|
38
40
|
|
|
39
41
|
- Чисте робоче дерево (`git status` без незакомічених змін у `package.json` / `bun.lock` / `node_modules`) — інакше різницю не відрізнити від оновлення.
|
|
40
42
|
- Встановлений `bun` і доступний `bunx`.
|
|
41
43
|
- Запуск з кореня проекту (де лежить `package.json` / `bun.lock`).
|
|
42
|
-
- Якщо в проекті є хоч один `Cargo.toml` (не в `node_modules`/`.worktrees`) —
|
|
44
|
+
- Якщо в проекті є хоч один `Cargo.toml` (не в `node_modules`/`.worktrees`) — бажано встановлений `cargo-edit` (`cargo install cargo-edit`, дає команду `cargo upgrade`). Без нього major-бампи Rust-залежностей неможливо застосувати детерміновано (голий `cargo update` піднімає лише semver-сумісні версії) — оркестратор пропускає Rust-гілку (без блокування npm-гілки) і перелічує знайдені `Cargo.toml` у звіті як такі, що потребують ручного прогону кроків 2-8 нижче.
|
|
43
45
|
|
|
44
46
|
### 0.2. Детекція Rust-крейтів
|
|
45
47
|
|
|
@@ -102,7 +104,7 @@ n-rules taze diff
|
|
|
102
104
|
|
|
103
105
|
Покриває **прямі** залежності з `package.json` (root + воркспейси). Транзитивні major-стрибки (`bun.lock`) — за потреби переглянь окремо; основний ризик breaking-змін — у прямих.
|
|
104
106
|
|
|
105
|
-
**Rust
|
|
107
|
+
**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>"`.
|
|
106
108
|
|
|
107
109
|
### 4. Зібрати breaking changes по кожному major-оновленню
|
|
108
110
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** @see ./docs/cargo-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
|
+
/** Поля Cargo.toml із залежностями, які порівнюємо (аналог DEP_FIELDS у diff.mjs). */
|
|
10
|
+
const CARGO_DEP_FIELDS = ['dependencies', 'dev-dependencies', 'build-dependencies']
|
|
11
|
+
|
|
12
|
+
/** Дефолтний суфікс бекапу — той самий, що й для package.json (крок 1 SKILL.md, Rust-гілка). */
|
|
13
|
+
const DEFAULT_BACKUP_SUFFIX = '.taze-bak'
|
|
14
|
+
|
|
15
|
+
// 1-3 числові компоненти (`"1"`, `"0.4"`, `"1.2.3"`), опційний `=`/`~`/`^`/`>=`-префікс.
|
|
16
|
+
//Cargo трактує відсутні компоненти як 0 і для матчингу, і для caret-сумісності —
|
|
17
|
+
// той самий "найлівіша ненульова компонента" принцип, що й у isBreaking з diff.mjs.
|
|
18
|
+
const CARGO_VERSION_RE = /^[\s=~^><]*(\d+)(?:\.(\d+))?(?:\.(\d+))?/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Парсить Cargo-версійний specifier (1-3 компоненти, відсутні → 0).
|
|
22
|
+
* @param {string} spec версійний specifier із Cargo.toml (`"1"`, `"0.4.2"`, `"=1.2.3"`)
|
|
23
|
+
* @returns {{major:number, minor:number, patch:number}|null} ядро або null для не-semver
|
|
24
|
+
*/
|
|
25
|
+
export function parseCargoVersion(spec) {
|
|
26
|
+
if (typeof spec !== 'string') return null
|
|
27
|
+
const m = CARGO_VERSION_RE.exec(spec)
|
|
28
|
+
if (!m) return null
|
|
29
|
+
return { major: Number(m[1]), minor: Number(m[2] ?? 0), patch: Number(m[3] ?? 0) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Дістає версійний specifier одного запису залежності Cargo.toml — рядок
|
|
34
|
+
* напряму (`tokio = "1"`), або поле `version` inline-таблиці
|
|
35
|
+
* (`serde = { version = "1", features = [...] }`).
|
|
36
|
+
* @param {unknown} value значення запису залежності
|
|
37
|
+
* @returns {string|null} specifier, або null для path/git-залежності (без номера версії)
|
|
38
|
+
*/
|
|
39
|
+
export function extractCargoVersionSpec(value) {
|
|
40
|
+
if (typeof value === 'string') return value
|
|
41
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && typeof value.version === 'string') {
|
|
42
|
+
return value.version
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Порівнює два розпарсені Cargo.toml і повертає зміни залежностей —
|
|
49
|
+
* той самий контракт, що й `diffPackageJson` у diff.mjs, лише для Cargo.
|
|
50
|
+
* @param {object} oldManifest розпарсений старий Cargo.toml (бекап)
|
|
51
|
+
* @param {object} newManifest розпарсений новий Cargo.toml
|
|
52
|
+
* @param {string} manifest відносний шлях Cargo.toml (мітка джерела запису)
|
|
53
|
+
* @returns {{major: Array<{manifest:string, pkg:string, from:string, to:string}>, minorPatch:number}} зміни
|
|
54
|
+
*/
|
|
55
|
+
export function diffCargoToml(oldManifest, newManifest, manifest) {
|
|
56
|
+
const major = []
|
|
57
|
+
let minorPatch = 0
|
|
58
|
+
for (const field of CARGO_DEP_FIELDS) {
|
|
59
|
+
const oldDeps = oldManifest?.[field]
|
|
60
|
+
const newDeps = newManifest?.[field]
|
|
61
|
+
if (!oldDeps || !newDeps) continue
|
|
62
|
+
for (const [crate, oldValue] of Object.entries(oldDeps)) {
|
|
63
|
+
const newValue = newDeps[crate]
|
|
64
|
+
if (newValue === undefined) continue
|
|
65
|
+
const from = extractCargoVersionSpec(oldValue)
|
|
66
|
+
const to = extractCargoVersionSpec(newValue)
|
|
67
|
+
if (from === null || to === null || from === to) continue
|
|
68
|
+
const fromV = parseCargoVersion(from)
|
|
69
|
+
const toV = parseCargoVersion(to)
|
|
70
|
+
if (fromV && toV && isBreaking(fromV, toV)) {
|
|
71
|
+
major.push({ manifest, pkg: crate, from, to })
|
|
72
|
+
} else {
|
|
73
|
+
minorPatch += 1
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { major, minorPatch }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Читає й парсить TOML-файл, або повертає null, якщо файл відсутній/невалідний.
|
|
82
|
+
* @param {string} path абсолютний шлях
|
|
83
|
+
* @returns {Promise<object|null>} розпарсений обʼєкт або null
|
|
84
|
+
*/
|
|
85
|
+
async function readTomlOrNull(path) {
|
|
86
|
+
if (!existsSync(path)) return null
|
|
87
|
+
try {
|
|
88
|
+
return parseToml(await readFile(path, 'utf8'))
|
|
89
|
+
} catch {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Збирає diff по всіх Cargo.toml монорепо: для кожного шляху зі списку
|
|
96
|
+
* порівнює `<manifest>` з `<manifest><backupSuffix>` — той самий контракт,
|
|
97
|
+
* що й `collectTazeDiff` для package.json.
|
|
98
|
+
* @param {string} cwd корінь репозиторію
|
|
99
|
+
* @param {string[]} manifestPaths відносні шляхи Cargo.toml (з `findCargoManifests`)
|
|
100
|
+
* @param {string} [backupSuffix] суфікс бекап-файлу
|
|
101
|
+
* @returns {Promise<{major: Array<{manifest:string, pkg:string, from:string, to:string}>, minorPatch:number, totalChanged:number, comparedManifests:number}>} агрегований diff
|
|
102
|
+
*/
|
|
103
|
+
export async function collectCargoDiff(cwd, manifestPaths, backupSuffix = DEFAULT_BACKUP_SUFFIX) {
|
|
104
|
+
const major = []
|
|
105
|
+
let minorPatch = 0
|
|
106
|
+
let comparedManifests = 0
|
|
107
|
+
for (const manifest of manifestPaths) {
|
|
108
|
+
const oldManifest = await readTomlOrNull(join(cwd, `${manifest}${backupSuffix}`))
|
|
109
|
+
const newManifest = await readTomlOrNull(join(cwd, manifest))
|
|
110
|
+
if (!oldManifest || !newManifest) continue
|
|
111
|
+
comparedManifests += 1
|
|
112
|
+
const res = diffCargoToml(oldManifest, newManifest, manifest)
|
|
113
|
+
major.push(...res.major)
|
|
114
|
+
minorPatch += res.minorPatch
|
|
115
|
+
}
|
|
116
|
+
return { major, minorPatch, totalChanged: major.length + minorPatch, comparedManifests }
|
|
117
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: cargo-diff.mjs
|
|
4
|
+
resource: npm/skills/taze/js/cargo-diff.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: afe20497
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.99
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
`package.json`
|
|
17
|
+
|
|
18
|
+
Файл читає версії з `Cargo.toml`, виділяє специфікації залежностей, порівнює дві версії маніфесту та збирає diff по змінах версій. Для монорепо проходить по списку `Cargo.toml` і агрегує результати для всіх знайдених маніфестів. Усі помилки перехоплює fail-safe: не кидає винятків назовні й за окремих збоїв повертає порожнє значення замість помилки.
|
|
19
|
+
|
|
20
|
+
## Поведінка
|
|
21
|
+
|
|
22
|
+
- parseCargoVersion — виділяє версійну частину Cargo-специфікатора й повертає її як нормалізоване числове ядро; для не-версійних значень повертає null.
|
|
23
|
+
- extractCargoVersionSpec — дістає версійний specifier із запису залежності Cargo.toml; для path/git-залежностей без версії повертає null.
|
|
24
|
+
- diffCargoToml — порівнює два розпарсені Cargo.toml і визначає зміни залежностей, розділяючи їх на major та minor/patch.
|
|
25
|
+
- collectCargoDiff — проходить по списку Cargo.toml у монорепо, зіставляє кожен файл із його backup-файлом і збирає агрегований diff; якщо один із файлів відсутній або невалідний, пропускає його без падіння.
|
|
26
|
+
|
|
27
|
+
## Публічний API
|
|
28
|
+
|
|
29
|
+
- Конфіги, на які спирається код: `package.json` — джерело списку залежностей і меж для порівняння змін.
|
|
30
|
+
- `parseCargoVersion` — розбирає Cargo-версійний specifier і заповнює відсутні частини нулями.
|
|
31
|
+
- `extractCargoVersionSpec` — дістає версію залежності з запису `Cargo.toml`, як з короткого рядка, так і з inline-таблиці.
|
|
32
|
+
- `diffCargoToml` — порівнює два розібрані `Cargo.toml` і повертає зміни по залежностях.
|
|
33
|
+
- `collectCargoDiff` — збирає diff для всіх `Cargo.toml` у монорепо, зіставляючи кожен маніфест із його backup-версією.
|
|
34
|
+
|
|
35
|
+
Changelog: не перевіряв — змін у файлах не вносив.
|
|
36
|
+
|
|
37
|
+
## Гарантії поведінки
|
|
38
|
+
|
|
39
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
40
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
41
|
+
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: orchestrate.mjs
|
|
4
4
|
resource: npm/skills/taze/js/orchestrate.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 1ff52a80
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
|
-
tier: cloud-min
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.98
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -5,6 +5,7 @@ import { copyFile, rm } from 'node:fs/promises'
|
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
|
|
7
7
|
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
8
|
+
import { collectCargoDiff } from './cargo-diff.mjs'
|
|
8
9
|
import { collectTazeDiff } from './diff.mjs'
|
|
9
10
|
|
|
10
11
|
/** Суфікс бекапу package.json — той самий, що й у `diff.mjs`/кроці 1 SKILL.md. */
|
|
@@ -34,6 +35,30 @@ export function buildDependencyPrompt({ workspace, pkg, from, to }) {
|
|
|
34
35
|
].join('\n')
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Промпт ОДНОГО ітеративного виклику для Rust-крейта — Rust-варіант
|
|
40
|
+
* [`buildDependencyPrompt`] (кроки 4-6 SKILL.md, Rust-гілка), для ОДНОГО
|
|
41
|
+
* major-крейта. Кроки 1-3/7/8 виконує оркестратор детерміновано, без LLM.
|
|
42
|
+
* @param {{manifest: string, pkg: string, from: string, to: string}} entry запис major-diff (з `collectCargoDiff`)
|
|
43
|
+
* @returns {string} готовий промпт
|
|
44
|
+
*/
|
|
45
|
+
export function buildCargoDependencyPrompt({ manifest, pkg, from, to }) {
|
|
46
|
+
return [
|
|
47
|
+
'# Major-оновлення одного Rust-крейта: перевірка сумісності й рефакторинг',
|
|
48
|
+
'',
|
|
49
|
+
`Крейт \`${pkg}\` у \`${manifest}\`: **${from} → ${to}** — вже застосовано в Cargo.toml/Cargo.lock (кроки 1-3 виконано детерміновано, без тебе). Твоя задача — лише breaking-changes-перевірка й, за потреби, рефакторинг.`,
|
|
50
|
+
'',
|
|
51
|
+
'## Кроки',
|
|
52
|
+
`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}-<стара-версія>/\`) і новою.`,
|
|
53
|
+
`2. Знайти використання зачепленого API в коді проєкту (\`rg -n --type rust\` по use-шляхах/викликах \`${pkg}\`).`,
|
|
54
|
+
'3. Сумісно — нічого не робити. Несумісно — застосувати міграцію (перейменувати use-шлях, оновити сигнатуру виклику, замінити видалений макрос еквівалентом).',
|
|
55
|
+
'4. Якщо були правки — запусти `cargo fmt --all -- --check`, `cargo clippy --all-targets --all-features -- -D warnings`, `cargo test`.',
|
|
56
|
+
'5. Нетривіальна/неоднозначна міграція — не вгадуй, залиш TODO-коментар із посиланням на CHANGELOG.',
|
|
57
|
+
'',
|
|
58
|
+
'У відповіді одним абзацом підсумуй: сумісно / зрефакторено (які файли) / TODO (чому).'
|
|
59
|
+
].join('\n')
|
|
60
|
+
}
|
|
61
|
+
|
|
37
62
|
/**
|
|
38
63
|
* Диспетчер одного ітеративного виклику на обраний раннер. `pi` — вбудований
|
|
39
64
|
* pi-агент (`@7n/llm-lib/agent-skill`; текст перехоплюється через `deps.out`,
|
|
@@ -154,10 +179,54 @@ export async function cleanupBackups(cwd, workspaces, deps = {}) {
|
|
|
154
179
|
}
|
|
155
180
|
|
|
156
181
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
182
|
+
* Чи встановлений cargo-edit (дає `cargo upgrade`) — без нього неможливо
|
|
183
|
+
* детерміновано перетнути major-межу Rust-залежностей (голий `cargo update`
|
|
184
|
+
* піднімає лише semver-сумісні версії; SKILL.md §Передумови).
|
|
185
|
+
* @param {typeof spawnSync} spawnFn інжект для тестів
|
|
186
|
+
* @returns {boolean} true — `cargo upgrade` доступна
|
|
187
|
+
*/
|
|
188
|
+
function hasCargoEdit(spawnFn) {
|
|
189
|
+
return spawnFn('cargo', ['upgrade', '--version'], { encoding: 'utf8' }).status === 0
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Бекапить кожен Cargo.toml + спільний кореневий Cargo.lock (крок 1 SKILL.md,
|
|
194
|
+
* Rust-гілка). v1: один спільний workspace/Cargo.lock у корені `cwd` —
|
|
195
|
+
* поточна реальна топологія репо (кореневий `Cargo.toml` з `[workspace]` +
|
|
196
|
+
* члени); кілька незалежних Cargo-workspace під час не підтримується.
|
|
197
|
+
* @param {string} cwd корінь репо
|
|
198
|
+
* @param {string[]} manifestPaths відносні шляхи Cargo.toml (з `findCargoManifests`)
|
|
199
|
+
* @param {{ copyFile?: (src: string, dest: string) => Promise<void> }} [deps] інжект
|
|
200
|
+
* @returns {Promise<void>}
|
|
201
|
+
*/
|
|
202
|
+
export async function backupCargoManifests(cwd, manifestPaths, deps = {}) {
|
|
203
|
+
const copy = deps.copyFile ?? copyFile
|
|
204
|
+
for (const manifest of manifestPaths) {
|
|
205
|
+
const manifestPath = join(cwd, manifest)
|
|
206
|
+
if (existsSync(manifestPath)) await copy(manifestPath, `${manifestPath}${BACKUP_SUFFIX}`)
|
|
207
|
+
}
|
|
208
|
+
const lockPath = join(cwd, 'Cargo.lock')
|
|
209
|
+
if (existsSync(lockPath)) await copy(lockPath, `${lockPath}${BACKUP_SUFFIX}`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Прибирає бекапи Cargo.toml/Cargo.lock після завершення (крок 7 SKILL.md,
|
|
214
|
+
* Rust-гілка).
|
|
215
|
+
* @param {string} cwd корінь репо
|
|
216
|
+
* @param {string[]} manifestPaths відносні шляхи Cargo.toml (з `findCargoManifests`)
|
|
217
|
+
* @param {{ rm?: (path: string, opts?: object) => Promise<void> }} [deps] інжект
|
|
218
|
+
* @returns {Promise<void>}
|
|
219
|
+
*/
|
|
220
|
+
export async function cleanupCargoBackups(cwd, manifestPaths, deps = {}) {
|
|
221
|
+
const remove = deps.rm ?? rm
|
|
222
|
+
for (const manifest of manifestPaths) {
|
|
223
|
+
await remove(join(cwd, `${manifest}${BACKUP_SUFFIX}`), { force: true })
|
|
224
|
+
}
|
|
225
|
+
await remove(join(cwd, `Cargo.lock${BACKUP_SUFFIX}`), { force: true })
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Знаходить Cargo.toml поза node_modules/.worktrees/target (крок 0.2 SKILL.md).
|
|
161
230
|
* @param {string} cwd корінь репо
|
|
162
231
|
* @param {{ spawnFn?: typeof spawnSync }} [deps] інжект
|
|
163
232
|
* @returns {string[]} відносні шляхи знайдених Cargo.toml
|
|
@@ -188,13 +257,36 @@ export function findCargoManifests(cwd, deps = {}) {
|
|
|
188
257
|
.filter(Boolean)
|
|
189
258
|
}
|
|
190
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Форматує один рядок результату ітерації (спільний для npm- і Rust-гілки).
|
|
262
|
+
* @param {{pkg: string, ok: boolean, error: string|null, from: string, to: string}} r результат ітерації
|
|
263
|
+
* @param {string} scopeLabel мітка джерела (`workspace` для npm, `manifest` для Rust)
|
|
264
|
+
* @returns {string} один рядок звіту
|
|
265
|
+
*/
|
|
266
|
+
function formatResultLine(r, scopeLabel) {
|
|
267
|
+
const status = r.ok ? '✅' : '❌'
|
|
268
|
+
const errorSuffix = r.error ? ` — ${r.error}` : ''
|
|
269
|
+
return ` ${status} \`${r.pkg}\` (${scopeLabel}): ${r.from} → ${r.to}${errorSuffix}`
|
|
270
|
+
}
|
|
271
|
+
|
|
191
272
|
/**
|
|
192
273
|
* Компонує підсумковий звіт (крок 8 SKILL.md) детерміновано з результатів
|
|
193
274
|
* ітерацій — без окремого LLM-виклику для самого звіту.
|
|
194
|
-
* @param {{
|
|
275
|
+
* @param {{
|
|
276
|
+
* minorPatch: number,
|
|
277
|
+
* totalChanged: number,
|
|
278
|
+
* results: Array<{pkg:string, workspace:string, from:string, to:string, ok:boolean, error:string|null}>,
|
|
279
|
+
* rust: {
|
|
280
|
+
* manifests: string[],
|
|
281
|
+
* processed: boolean,
|
|
282
|
+
* skippedReason: string|null,
|
|
283
|
+
* minorPatch: number,
|
|
284
|
+
* results: Array<{pkg:string, manifest:string, from:string, to:string, ok:boolean, error:string|null}>
|
|
285
|
+
* }
|
|
286
|
+
* }} args дані звіту
|
|
195
287
|
* @returns {string} markdown-звіт
|
|
196
288
|
*/
|
|
197
|
-
export function formatReport({ minorPatch, totalChanged, results,
|
|
289
|
+
export function formatReport({ minorPatch, totalChanged, results, rust }) {
|
|
198
290
|
const lines = [
|
|
199
291
|
'## taze: підсумок',
|
|
200
292
|
'',
|
|
@@ -202,17 +294,26 @@ export function formatReport({ minorPatch, totalChanged, results, rustCrates })
|
|
|
202
294
|
`- **Major-оновлення:** ${results.length}`
|
|
203
295
|
]
|
|
204
296
|
for (const r of results) {
|
|
205
|
-
|
|
206
|
-
const errorSuffix = r.error ? ` — ${r.error}` : ''
|
|
207
|
-
lines.push(` ${status} \`${r.pkg}\` (${r.workspace}): ${r.from} → ${r.to}${errorSuffix}`)
|
|
297
|
+
lines.push(formatResultLine(r, r.workspace))
|
|
208
298
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
)
|
|
299
|
+
|
|
300
|
+
let rustTotalChanged = 0
|
|
301
|
+
if (rust.manifests.length > 0) {
|
|
302
|
+
lines.push('', '### Rust-крейти')
|
|
303
|
+
if (rust.processed) {
|
|
304
|
+
lines.push(`- **Оновлено (minor/patch):** ${rust.minorPatch}`, `- **Major-оновлення:** ${rust.results.length}`)
|
|
305
|
+
for (const r of rust.results) {
|
|
306
|
+
lines.push(formatResultLine(r, r.manifest))
|
|
307
|
+
}
|
|
308
|
+
rustTotalChanged = rust.minorPatch + rust.results.length
|
|
309
|
+
} else {
|
|
310
|
+
lines.push(
|
|
311
|
+
`- ⏭ Пропущено (${rust.skippedReason}) — ${rust.manifests.length} Cargo.toml, онови вручну за Rust-гілкою SKILL.md: ${rust.manifests.join(', ')}`
|
|
312
|
+
)
|
|
313
|
+
}
|
|
214
314
|
}
|
|
215
|
-
|
|
315
|
+
|
|
316
|
+
lines.push('', `- **Всього змінено:** ${totalChanged + rustTotalChanged}`)
|
|
216
317
|
return lines.join('\n')
|
|
217
318
|
}
|
|
218
319
|
|
|
@@ -227,9 +328,9 @@ export function formatReport({ minorPatch, totalChanged, results, rustCrates })
|
|
|
227
328
|
* cwd?: string,
|
|
228
329
|
* runner?: 'pi' | 'cursor' | 'codex',
|
|
229
330
|
* log?: (line: string) => void,
|
|
230
|
-
* 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}> } & Record<string, unknown>
|
|
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>
|
|
231
332
|
* }} [options] опції + інжекти для тестів
|
|
232
|
-
* @returns {Promise<{ ok: boolean, report: string, results: Array<object> }>} результат
|
|
333
|
+
* @returns {Promise<{ ok: boolean, report: string, results: Array<object>, rustResults: Array<object> }>} результат
|
|
233
334
|
*/
|
|
234
335
|
export async function runTazeOrchestrator(options = {}) {
|
|
235
336
|
const cwd = options.cwd ?? process.cwd()
|
|
@@ -237,11 +338,10 @@ export async function runTazeOrchestrator(options = {}) {
|
|
|
237
338
|
const log = options.log ?? (line => console.log(line))
|
|
238
339
|
const deps = options.deps ?? {}
|
|
239
340
|
const spawnFn = deps.spawnFn ?? spawnSync
|
|
341
|
+
const call = deps.callRunner ?? callRunner
|
|
240
342
|
|
|
241
343
|
assertRunningInWorktree(cwd, spawnFn)
|
|
242
344
|
|
|
243
|
-
const rustCrates = findCargoManifests(cwd, { spawnFn })
|
|
244
|
-
|
|
245
345
|
log('📦 Бекап package.json...')
|
|
246
346
|
const backedUpWorkspaces = await backupWorkspacePackageFiles(cwd, deps)
|
|
247
347
|
|
|
@@ -255,7 +355,6 @@ export async function runTazeOrchestrator(options = {}) {
|
|
|
255
355
|
log(`🔍 diff: ${diff.major.length} major, ${diff.minorPatch} minor/patch`)
|
|
256
356
|
|
|
257
357
|
const results = []
|
|
258
|
-
const call = deps.callRunner ?? callRunner
|
|
259
358
|
for (const entry of diff.major) {
|
|
260
359
|
log(`🔧 ${entry.pkg} (${entry.workspace}): ${entry.from} → ${entry.to}...`)
|
|
261
360
|
const outcome = await call(runner, buildDependencyPrompt(entry), cwd, deps)
|
|
@@ -265,8 +364,47 @@ export async function runTazeOrchestrator(options = {}) {
|
|
|
265
364
|
|
|
266
365
|
await cleanupBackups(cwd, backedUpWorkspaces, deps)
|
|
267
366
|
|
|
268
|
-
const
|
|
367
|
+
const rustManifests = findCargoManifests(cwd, { spawnFn })
|
|
368
|
+
let rust = { manifests: rustManifests, processed: false, skippedReason: null, minorPatch: 0, results: [] }
|
|
369
|
+
|
|
370
|
+
if (rustManifests.length > 0 && !hasCargoEdit(spawnFn)) {
|
|
371
|
+
rust.skippedReason =
|
|
372
|
+
'cargo-edit не встановлено (`cargo install cargo-edit`) — cargo update без нього недетермінований для major'
|
|
373
|
+
log(`⏭ Rust: ${rust.skippedReason}`)
|
|
374
|
+
} else if (rustManifests.length > 0) {
|
|
375
|
+
log('📦 Бекап Cargo.toml/Cargo.lock...')
|
|
376
|
+
await backupCargoManifests(cwd, rustManifests, deps)
|
|
377
|
+
|
|
378
|
+
log('⬆️ cargo upgrade --incompatible allow...')
|
|
379
|
+
runCommand('cargo', ['upgrade', '--incompatible', 'allow'], cwd, spawnFn)
|
|
380
|
+
log('🔄 cargo update...')
|
|
381
|
+
runCommand('cargo', ['update'], cwd, spawnFn)
|
|
382
|
+
|
|
383
|
+
const collectCargo = deps.collectCargoDiff ?? collectCargoDiff
|
|
384
|
+
const cargoDiff = await collectCargo(cwd, rustManifests)
|
|
385
|
+
log(`🔍 Rust diff: ${cargoDiff.major.length} major, ${cargoDiff.minorPatch} minor/patch`)
|
|
386
|
+
|
|
387
|
+
const rustResults = []
|
|
388
|
+
for (const entry of cargoDiff.major) {
|
|
389
|
+
log(`🔧 [rust] ${entry.pkg} (${entry.manifest}): ${entry.from} → ${entry.to}...`)
|
|
390
|
+
const outcome = await call(runner, buildCargoDependencyPrompt(entry), cwd, deps)
|
|
391
|
+
rustResults.push({ ...entry, ...outcome })
|
|
392
|
+
log(outcome.ok ? ` ✅ ${entry.pkg}` : ` ❌ ${entry.pkg}: ${outcome.error}`)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
await cleanupCargoBackups(cwd, rustManifests, deps)
|
|
396
|
+
|
|
397
|
+
rust = {
|
|
398
|
+
manifests: rustManifests,
|
|
399
|
+
processed: true,
|
|
400
|
+
skippedReason: null,
|
|
401
|
+
minorPatch: cargoDiff.minorPatch,
|
|
402
|
+
results: rustResults
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const report = formatReport({ minorPatch: diff.minorPatch, totalChanged: diff.totalChanged, results, rust })
|
|
269
407
|
log(report)
|
|
270
408
|
|
|
271
|
-
return { ok: results.every(r => r.ok), report, results }
|
|
409
|
+
return { ok: results.every(r => r.ok) && rust.results.every(r => r.ok), report, results, rustResults: rust.results }
|
|
272
410
|
}
|