@7n/rules 1.32.0 → 1.32.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.32.2] - 2026-07-20
4
+
5
+ ### Changed
6
+
7
+ - doc-files: згенеровано файлові доки для `k8s/dremio_logging/main.mjs` і `skills/taze/js/migration-cache.mjs`, що доти ламали tree-wide doc-files/check; вручну виправлено фабрикації локальної моделі (розбиті backtick-маркери, невірні твердження про fail-safe/scope кешу міграцій)
8
+
9
+ ## [1.32.1] - 2026-07-20
10
+
11
+ ### Changed
12
+
13
+ - fix(n-rules/n-taze): не забруднювати дерево перед worktree-гейтом і не губити untracked-директорії при поверненні змін (#160)
14
+
3
15
  ## [1.32.0] - 2026-07-19
4
16
 
5
17
  ### Changed
package/bin/n-rules.js CHANGED
@@ -101,7 +101,7 @@ import { syncClaudeConfig } from '../scripts/sync-claude-config.mjs'
101
101
  import { syncGitignoreWorktree } from '../scripts/lib/sync-gitignore-worktree.mjs'
102
102
  import { upgradeNRulesToLatestAndBunInstall } from '../scripts/upgrade-n-rules-and-install.mjs'
103
103
  import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
104
- import { runSkillsCli } from '../scripts/skills-cli.mjs'
104
+ import { isTazeOrchestratorSkillArgs, runSkillsCli } from '../scripts/skills-cli.mjs'
105
105
  import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
106
106
 
107
107
  const PACKAGE_NAME = '@7n/rules'
@@ -1729,7 +1729,14 @@ try {
1729
1729
  }
1730
1730
  // `ci` (plan) — read-only гейт-команда для CI-джоб: не мутує package.json
1731
1731
  // (ensure дописав би devDependency прямо в чекауті pipeline-агента).
1732
- if (command !== 'ci') await ensureNRulesInRootDevDependencies(effectiveRoot)
1732
+ // `skill <runner> taze` JS-оркестрований worktree-only шлях
1733
+ // (skills/taze/js/orchestrate.mjs): сам створює worktree і гейтить на
1734
+ // чистоту дерева ДО checkout. Мутація тут забруднила б дерево прямо
1735
+ // перед тим гейтом і провалювала б auto-create на інакше чистому дереві —
1736
+ // оркестратор сам робить self-upgrade devDependency вже ВСЕРЕДИНІ
1737
+ // щойно створеного worktree, після власного гейту чистоти.
1738
+ const skipDevDepsEnsure = command === 'skill' && isTazeOrchestratorSkillArgs(args)
1739
+ if (command !== 'ci' && !skipDevDepsEnsure) await ensureNRulesInRootDevDependencies(effectiveRoot)
1733
1740
  // Підкоманди-оркестратори (hook/lint/skill/adr-normalize-local/taze/release тощо)
1734
1741
  // можуть спавнити внутрішню agent/LLM-сесію — ADR Stop-hooks (capture/normalize)
1735
1742
  // мають пропустити її як технічний шум, не людську думку (spec 2026-06-30).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.32.0",
3
+ "version": "1.32.2",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -7,23 +7,23 @@ docgen:
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  tier: cloud-min
9
9
  score: 95
10
- issues: anchor-miss:(k8s.mdc),judge:inaccurate:0.99
10
+ issues: anchor-miss:(k8s.mdc),judge:inaccurate:0.98
11
11
  judgeModel: openai-codex/gpt-5.4-mini
12
12
  ---
13
13
 
14
14
  ## Огляд
15
15
 
16
- `zkLogbackRootLevelViolation` перевіряє повідомлення, пов’язані з надто гучним `root` у `k8s.mdc`, а `lint` збирає ці перевірки в один pass і повертає результат без зупинки процесу. Поведінка read-only: правило не пише у ФС чи БД, працює fail-safe й не кидає винятків назовні.
16
+ Перевіряє `zookeeper.yaml` на надто гучний `root level` у вбудованому `logback.xml` і формує звіт про порушення з маркером `zk-logback-root-level`. `zkLogbackRootLevelViolation` і `lint` працюють як read-only, не змінюють ФС чи БД, і поводяться fail-safe: помилки перехоплюються, назовні винятки не виходять.
17
17
 
18
18
  ## Поведінка
19
19
 
20
- - `zkLogbackRootLevelViolation` — перевіряє, чи вбудований `logback.xml` у ZooKeeper-templated файлі не тримає `root` на надто гучному рівні; повертає текст порушення з міткою `` або `null`, якщо перевірка не потрібна або рівень уже прийнятний.
21
- - `lint` — проходить по вказаних файлах у read-only режимі, збирає порушення для `k8s.dremio_logging` і не виводить помилки назовні; пропускає файли, які не вдалося прочитати, та позначає знайдені проблеми міткою ``.
20
+ - `zkLogbackRootLevelViolation` — перевіряє, чи вбудований `logback.xml` у `zookeeper.yaml` має допустимий `root level` (`warn`, `error`, `off`); якщо блоку немає не чіпає файл, якщо рівень надто гучний або відсутній повертає текст порушення з посиланням на `k8s.mdc`.
21
+ - `lint` — проходить по вказаних файлах, безпечно читає їх як `zookeeper.yaml`-кандидати, фіксує порушення для `k8s.dremio_logging` і не перериває перевірку на помилках читання; для кожного знайденого випадку додає повідомлення з маркером `zk-logback-root-level`.
22
22
 
23
23
  ## Публічний API
24
24
 
25
- - zkLogbackRootLevelViolation — позначає ZooKeeper Helm-темплейт, якщо вбудований `logback.xml` має занадто гучний `root`-рівень (`info`, `debug`, `trace`) або взагалі не задає `root`; не чіпає файли, де `root` уже `warn`, `error` чи `off`.
26
- - lint — запускає `k8s.dremio_logging` для ZooKeeper-темплейтів у межах одного файлу, лише для читання.
25
+ - zkLogbackRootLevelViolation — знаходить у Helm-темплейті ZooKeeper вбудований `logback.xml`, де `root` заданий занадто гучно або взагалі відсутній; пропускає, якщо рівень уже `warn`, `error` чи `off`
26
+ - lint — запускає `k8s.dremio_logging` для ZooKeeper-темплейту на рівні окремого файла, без змін у вмісті
27
27
 
28
28
  ## Гарантії поведінки
29
29
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: skills-cli.mjs
4
4
  resource: npm/scripts/skills-cli.mjs
5
5
  docgen:
6
- crc: 12d3bf6d
6
+ crc: 3e5781e6
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -1,10 +1,11 @@
1
1
  /** @see ./docs/auto-worktree.md */
2
- import { existsSync } from 'node:fs'
3
- import { copyFile, mkdir, rm } from 'node:fs/promises'
4
- import { dirname, join } from 'node:path'
2
+ import { existsSync, statSync } from 'node:fs'
3
+ import { copyFile, mkdir, readdir, rm } from 'node:fs/promises'
4
+ import { dirname, join, relative } from 'node:path'
5
5
  import { createInterface } from 'node:readline/promises'
6
6
 
7
7
  const YES_RE = /^y(es)?$/i
8
+ const TRAILING_SLASH_RE = /\/$/
8
9
 
9
10
  /**
10
11
  * Питає y/N у терміналі. Поза TTY (CI, неінтерактивний виклик) — одразу `false`,
@@ -112,6 +113,31 @@ export async function ensureRunningInWorktree(
112
113
  return { cwd: newCwd, autoCreated: true, branchArg }
113
114
  }
114
115
 
116
+ /**
117
+ * Рекурсивно копіює вміст директорії (лише файли-листки; підкаталоги
118
+ * створюються по дорозі через `makeDir`). `listDir` — інжектований `readdir`
119
+ * з `{ recursive: true, withFileTypes: true }` (Node ≥20.1): кожен `Dirent`
120
+ * несе `parentPath`/`path` — реальний каталог, у якому файл лежить.
121
+ * @param {string} srcDir директорія-джерело (у worktree)
122
+ * @param {string} destDir директорія-призначення (у оригінальному дереві)
123
+ * @param {{ copy: (src: string, dest: string) => Promise<void>, makeDir: (path: string, opts?: object) => Promise<void>, listDir: (path: string, opts?: object) => Promise<Array<{ name: string, parentPath?: string, path?: string, isFile: () => boolean }>> }} io інжекти
124
+ * @returns {Promise<string[]>} шляхи скопійованих файлів відносно `srcDir` (forward-slash)
125
+ */
126
+ async function copyDirectoryRecursive(srcDir, destDir, { copy, makeDir, listDir }) {
127
+ const entries = await listDir(srcDir, { recursive: true, withFileTypes: true })
128
+ const copiedRelPaths = []
129
+ for (const entry of entries) {
130
+ if (!entry.isFile()) continue
131
+ const entryDir = entry.parentPath ?? entry.path ?? srcDir
132
+ const relFromSrc = relative(srcDir, join(entryDir, entry.name)).replaceAll('\\', '/')
133
+ const dest = join(destDir, relFromSrc)
134
+ await makeDir(dirname(dest), { recursive: true })
135
+ await copy(join(entryDir, entry.name), dest)
136
+ copiedRelPaths.push(relFromSrc)
137
+ }
138
+ return copiedRelPaths
139
+ }
140
+
115
141
  /**
116
142
  * Переносить зміни з автоствореного worktree назад у вихідне дерево як
117
143
  * **untracked/незакомічені** правки (просте копіювання файлів, без git
@@ -122,48 +148,79 @@ export async function ensureRunningInWorktree(
122
148
  * лише нову назву — стара лишається як була, прийнятний компроміс для
123
149
  * інструментів, що самі файли не перейменовують (ефект можливий лише як
124
150
  * побічний результат LLM-рефакторингу чи форматера).
151
+ *
152
+ * **Untracked-директорія цілком.** Git схлопує щойно створену untracked
153
+ * директорію в один porcelain-рядок із `/` на кінці (якщо в ній нема жодного
154
+ * файлу, вже відомого git) — `copyFile` на такий шлях впав би (`EISDIR`/`ENOENT`)
155
+ * і обривав би цикл, гублячи все, що йшло по порядку ітерації ДАЛІ. Такий
156
+ * рядок (суфікс `/` або реальний каталог на диску) копіюється рекурсивно
157
+ * (`copyDirectoryRecursive`) — весь вміст, а не один `copyFile`.
125
158
  * @param {string} worktreeCwd автостворений worktree, з якого переносимо
126
159
  * @param {string} originalCwd вихідне дерево, куди переносимо
127
160
  * @param {typeof import('node:child_process').spawnSync} spawnFn інжект для тестів
128
161
  * @param {(line: string) => void} log колбек прогресу
129
- * @param {{ copyFile?: (src: string, dest: string) => Promise<void>, rm?: (path: string, opts?: object) => Promise<void>, mkdir?: (path: string, opts?: object) => Promise<void> }} [deps] інжекти для тестів
130
- * @returns {Promise<string[]>} відносні шляхи перенесених файлів
162
+ * @param {{ copyFile?: (src: string, dest: string) => Promise<void>, rm?: (path: string, opts?: object) => Promise<void>, mkdir?: (path: string, opts?: object) => Promise<void>, readdir?: (path: string, opts?: object) => Promise<Array<object>> }} [deps] інжекти для тестів
163
+ * @returns {Promise<{ brought: string[], failed: boolean }>} відносні шляхи перенесених файлів і чи стався провал (частковий чи повний)
131
164
  */
132
165
  export async function bringChangesBackToOriginal(worktreeCwd, originalCwd, spawnFn, log, deps = {}) {
133
166
  const copy = deps.copyFile ?? copyFile
134
167
  const removeFile = deps.rm ?? rm
135
168
  const makeDir = deps.mkdir ?? mkdir
169
+ const listDir = deps.readdir ?? readdir
136
170
 
137
171
  const statusResult = spawnFn('git', ['status', '--porcelain'], { cwd: worktreeCwd, encoding: 'utf8' })
138
172
  if (statusResult.status !== 0) {
139
173
  log(
140
174
  `⚠️ Не вдалось прочитати git status у "${worktreeCwd}" — зміни НЕ перенесені назад, worktree лишиться для ручного розбору.`
141
175
  )
142
- return []
176
+ return { brought: [], failed: true }
143
177
  }
144
178
 
145
179
  const lines = statusResult.stdout.split('\n').filter(Boolean)
146
180
  if (lines.length === 0) {
147
181
  log('ℹ️ Worktree без змін — нічого переносити назад.')
148
- return []
182
+ return { brought: [], failed: false }
149
183
  }
150
184
 
151
185
  const brought = []
186
+ let failed = false
152
187
  for (const line of lines) {
153
188
  const rest = line.slice(3)
154
189
  const relPath = rest.includes(' -> ') ? rest.split(' -> ', 2)[1] : rest
155
190
  const srcPath = join(worktreeCwd, relPath)
156
191
  const destPath = join(originalCwd, relPath)
157
- if (existsSync(srcPath)) {
158
- await makeDir(dirname(destPath), { recursive: true })
159
- await copy(srcPath, destPath)
160
- } else {
161
- await removeFile(destPath, { force: true })
192
+ try {
193
+ if (!existsSync(srcPath)) {
194
+ await removeFile(destPath, { force: true, recursive: true })
195
+ brought.push(relPath)
196
+ continue
197
+ }
198
+
199
+ const isDir = relPath.endsWith('/') || statSync(srcPath).isDirectory()
200
+ if (isDir) {
201
+ const trimmedRelPath = relPath.replace(TRAILING_SLASH_RE, '')
202
+ const copiedRelPaths = await copyDirectoryRecursive(srcPath, destPath, { copy, makeDir, listDir })
203
+ for (const nestedRelPath of copiedRelPaths) {
204
+ brought.push(`${trimmedRelPath}/${nestedRelPath}`)
205
+ }
206
+ } else {
207
+ await makeDir(dirname(destPath), { recursive: true })
208
+ await copy(srcPath, destPath)
209
+ brought.push(relPath)
210
+ }
211
+ } catch (error) {
212
+ failed = true
213
+ log(
214
+ `⚠️ Не вдалось перенести "${relPath}" назад у "${originalCwd}" — ${error instanceof Error ? error.message : String(error)}`
215
+ )
162
216
  }
163
- brought.push(relPath)
164
217
  }
165
- log(`📤 Перенесено назад у "${originalCwd}" як untracked: ${brought.join(', ')}`)
166
- return brought
218
+ log(
219
+ failed
220
+ ? `⚠️ Перенесення назад у "${originalCwd}" частково провалилось — перенесено: ${brought.join(', ') || '(нічого)'}`
221
+ : `📤 Перенесено назад у "${originalCwd}" як untracked: ${brought.join(', ')}`
222
+ )
223
+ return { brought, failed }
167
224
  }
168
225
 
169
226
  /**
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: auto-worktree.mjs
4
4
  resource: npm/scripts/lib/auto-worktree.mjs
5
5
  docgen:
6
- crc: 82081402
6
+ crc: bae09068
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -244,6 +244,23 @@ export function resolveBundledPackageRoot(fromModuleUrl = import.meta.url) {
244
244
  return join(dirname(fileURLToPath(fromModuleUrl)), '..')
245
245
  }
246
246
 
247
+ /**
248
+ * Чи `argv` (аргументи після `skill`) резолвиться в JS-оркестрований
249
+ * worktree-only `taze`-шлях (`runTazeOrchestratorCli`) — той самий критерій,
250
+ * що й нижче в `runSkillsCli`. Використовується `n-rules.js`, щоб не мутувати
251
+ * root `package.json` (self-upgrade `@7n/rules`) ДО власного worktree-гейту
252
+ * оркестратора: той сам створює worktree і перевіряє чистоту дерева
253
+ * (`ensureRunningInWorktree`, `requireCleanTree: true`) — мутація package.json
254
+ * прямо перед цим викликом примусово провалила б auto-create там, де дерево
255
+ * інакше було б чисте.
256
+ * @param {string[]} argv аргументи після `skill`
257
+ * @returns {boolean} `true`, якщо запуск піде через `runTazeOrchestratorCli`
258
+ */
259
+ export function isTazeOrchestratorSkillArgs(argv) {
260
+ const [first, second] = argv
261
+ return RUNNERS.has(first) && first !== 'claude' && normalizeSkillId(second) === 'taze'
262
+ }
263
+
247
264
  /**
248
265
  * @param {string[]} argv аргументи після `skill` у `n-rules`
249
266
  * @param {{ packageRoot?: string, projectDir?: string, log?: (line: string) => void, logError?: (line: string) => void, deps?: { runPiAgentSkill?: (prompt: string, opts?: object) => Promise<{ ok: boolean, error: string|null }> } }} [options] перевизначення кореня пакета, каталогу проєкту, функцій виводу та інжектів (для тестів)
@@ -13,26 +13,25 @@ docgen:
13
13
 
14
14
  ## Огляд
15
15
 
16
- Модуль надає `DEFAULT_CACHE_DIR`, `migrationCacheKey`, `readMigrationCache`, `writeMigrationCache` і `withKnownMigrationNotes` для роботи з локальним кешем відомих результатів аналізу міграцій у межах прогону. Він зберігає та повторно читає вже відомі записи, а `withKnownMigrationNotes` підставляє їх у промпт, щоб не повторювати дослідження там, де результат уже є. Модуль fail-safe: не кидає винятків назовні й за окремих помилок повертає порожнє значення, зокрема `null`.
16
+ Модуль керує кешем уже відомих міграцій на диску (`~/.cache/n-rules/taze-migrations` за замовчуванням) — кеш персистентний і спільний для всіх repo/worktree на машині, не обмежений одним прогоном. `migrationCacheKey` формує ключ для запису, `readMigrationCache` і `writeMigrationCache` працюють із кешованими даними, а `withKnownMigrationNotes` підставляє вже відомі відомості там, де це доречно. Читання fail-safe (`readMigrationCache` перехоплює помилки й повертає `null` замість винятку); запис (`writeMigrationCache`) винятків не перехоплює.
17
17
 
18
18
  ## Поведінка
19
19
 
20
- - `DEFAULT_CACHE_DIR` — стандартний спільний каталог для кешу міграцій на цій машині, щоб різні репозиторії могли повторно використовувати вже відомі результати.
21
- - `migrationCacheKey` — перетворює назву пакета і діапазон версій на стабільний безпечний ключ для кешу.
22
- - `readMigrationCache` — читає кешований запис міграції для потрібної пари версій; якщо запису немає або він пошкоджений, мовчки повертає `null`.
23
- - `writeMigrationCache` — зберігає результат аналізу міграції в кеш, щоб наступні запуски на цій машині могли його повторити без нового дослідження.
24
- - `withKnownMigrationNotes` — додає до промпта підсумок уже відомої міграції з кешу й підказує пропустити повторне дослідження та перейти до перевірки використання в поточному проєкті.
20
+ - `DEFAULT_CACHE_DIR` — задає спільний каталог кешу міграцій для всіх репозиторіїв на цій машині.
21
+ - `migrationCacheKey` — перетворює пару версій пакета на безпечний крос-репо ключ кешу.
22
+ - `readMigrationCache` — читає кешований запис міграції для конкретної пари версій; якщо запису немає або він пошкоджений, повертає `null` без помилки.
23
+ - `writeMigrationCache` — зберігає запис про вже проаналізовану міграцію в кеш, щоб не повторювати той самий аналіз у наступних прогонових середовищах.
24
+ - `withKnownMigrationNotes` — додає до промпта підсумок відомої міграції з кешу й підказує пропустити повторне дослідження та перейти до перевірки в поточному проєкті.
25
25
 
26
26
  ## Публічний API
27
27
 
28
- - DEFAULT_CACHE_DIR — Спільний каталог кешу міграцій для всіх репозиторіїв на цій машині; не прив’язаний до конкретного worktree.
29
- - migrationCacheKey — Перетворює `` на безпечне ім’я файлу для спільного кешу між репо.
30
- - readMigrationCache — Повертає збережений запис міграції для ``, якщо його вже обробляли в іншому repo/worktree; відсутній або пошкоджений запис дає `null`.
31
- - writeMigrationCache — Записує результат LLM-обробки для ``, щоб не повторювати дослідження changelog для такого ж bump-а.
32
- - withKnownMigrationNotes — Додає до prompt короткий підсумок знайденої міграції і, якщо є кеш, пропускає дослідження changelog/diff на користь пошуку використання в поточному проєкті.
28
+ - DEFAULT_CACHE_DIR — Спільний каталог для кешу міграцій на цій машині, незалежний від конкретного repo чи worktree.
29
+ - migrationCacheKey — Перетворює `pkg`, `from` і `to` на безпечну назву файла для спільного кешу.
30
+ - readMigrationCache — Повертає збережені дані про вже відому міграцію для тієї самої пари версій, або `null`, якщо запису немає чи він пошкоджений.
31
+ - writeMigrationCache — Записує результат вже розібраної міграції в кеш, щоб наступний repo з тим самим оновленням не починав дослідження заново.
32
+ - withKnownMigrationNotes — Додає до prompt короткий підсумок знайденої міграції і пропускає початковий етап з пошуком у CHANGELOG та diff.
33
33
 
34
34
  ## Гарантії поведінки
35
35
 
36
- - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
37
- - За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
38
- - Кешує результати в межах одного прогону.
36
+ - `readMigrationCache` перехоплює помилки читання/парсингу й повертає `null` замість винятку (fail-safe); `writeMigrationCache` винятків не перехоплює.
37
+ - Кеш персистентний на диску і спільний для всіх repo/worktree на машині — не обмежений одним прогоном.
@@ -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: 7bf5d0c0
6
+ crc: 63cee070
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -2,6 +2,7 @@
2
2
  import { spawnSync } from 'node:child_process'
3
3
  import { pathToFileURL } from 'node:url'
4
4
 
5
+ import { ensureNRulesInRootDevDependencies } from '../../../scripts/ensure-n-rules-dev-dependencies.mjs'
5
6
  import {
6
7
  bringChangesBackToOriginal,
7
8
  ensureRunningInWorktree,
@@ -138,7 +139,13 @@ async function runEcosystem(provider, { cwd, runner, log, deps, spawnFn, call })
138
139
  eco.results.push({ ...entry, ...outcome })
139
140
  log(outcome.ok ? ` ✅ ${entry.pkg}` : ` ❌ ${entry.pkg}: ${outcome.error}`)
140
141
  if (outcome.ok && outcome.text) {
141
- await writeCache(entry.pkg, entry.from, entry.to, { notes: outcome.text, sourceRepo: cwd, updatedAt: new Date().toISOString() }, deps)
142
+ await writeCache(
143
+ entry.pkg,
144
+ entry.from,
145
+ entry.to,
146
+ { notes: outcome.text, sourceRepo: cwd, updatedAt: new Date().toISOString() },
147
+ deps
148
+ )
142
149
  }
143
150
  }
144
151
 
@@ -241,22 +248,50 @@ export async function runTazeOrchestrator(options = {}) {
241
248
  })
242
249
  const cwd = worktree.cwd
243
250
 
251
+ // Self-upgrade root package.json devDependency (`@7n/rules`) — навмисно
252
+ // ВСЕРЕДИНІ worktree, ПІСЛЯ гейту `ensureRunningInWorktree` на чистоту
253
+ // дерева: та ж мутація у `originalCwd` ДО цього гейту забруднила б дерево
254
+ // прямо перед перевіркою, яку сам гейт щойно пройшов (n-rules.js навмисно
255
+ // пропускає її для цього шляху). Провал — не критичний для taze-прогону,
256
+ // лише лог.
257
+ try {
258
+ const ensureDevDeps = deps.ensureNRulesInRootDevDependencies ?? ensureNRulesInRootDevDependencies
259
+ await ensureDevDeps(cwd, { silent: true })
260
+ } catch (error) {
261
+ log(
262
+ `⚠️ Self-upgrade @7n/rules у "${cwd}" провалився — пропускаю (${error instanceof Error ? error.message : String(error)})`
263
+ )
264
+ }
265
+
244
266
  let cleanedUp = false
245
267
  /**
246
268
  * Переносить зміни назад і прибирає автостворений worktree — не більше
247
269
  * одного разу (idempotent), щоб і сигнальний обробник, і `finally` могли
248
270
  * безпечно кликати те саме без подвійного `bringChangesBackToOriginal`/
249
271
  * `removeAutoCreatedWorktree`.
272
+ *
273
+ * **Захист від втрати даних.** Якщо перенесення провалилось (частково чи
274
+ * повністю — `bringChangesBackToOriginal` кидає або повертає
275
+ * `failed: true`), worktree НЕ прибирається: джерело ще не перенесених
276
+ * файлів лишається на диску для ручного відновлення замість того, щоб
277
+ * зникнути разом з `npx \@7n/mt worktree remove`.
250
278
  * @returns {Promise<void>}
251
279
  */
252
280
  const cleanupAutoCreatedWorktree = async () => {
253
281
  if (cleanedUp) return
254
282
  cleanedUp = true
283
+ let failed = true
255
284
  try {
256
- await bringChangesBackToOriginal(cwd, originalCwd, spawnFn, log, deps)
285
+ ;({ failed } = await bringChangesBackToOriginal(cwd, originalCwd, spawnFn, log, deps))
257
286
  } catch (error) {
258
287
  log(`⚠️ Перенесення змін назад провалилось: ${error instanceof Error ? error.message : String(error)}`)
259
288
  }
289
+ if (failed) {
290
+ log(
291
+ `⚠️ Перенесення назад не вдалось повністю — worktree "${cwd}" (гілка "${worktree.branchArg}") НЕ прибирається, розберіться вручну.`
292
+ )
293
+ return
294
+ }
260
295
  removeAutoCreatedWorktree(worktree.branchArg, originalCwd, spawnFn, log)
261
296
  }
262
297