@7n/rules 1.49.12 → 1.49.14

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.49.14] - 2026-07-27
4
+
5
+ ### Changed
6
+
7
+ - release: @7n/rules@1.49.9
8
+
9
+ ## [1.49.13] - 2026-07-26
10
+
11
+ ### Fixed
12
+
13
+ - Виправлено фактичний шлях worktree, setup failure і spawnSync-діагностику в git-reconcile
14
+ - pass coverage mutation verdict feedback to the next ladder rung
15
+
3
16
  ## [1.49.12] - 2026-07-26
4
17
 
5
18
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.49.12",
3
+ "version": "1.49.14",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -65,7 +65,7 @@
65
65
  "rename-yaml-extensions": "bun ./bin/n-rules.js rename-yaml-extensions"
66
66
  },
67
67
  "dependencies": {
68
- "@7n/llm-lib": "2.9.7",
68
+ "@7n/llm-lib": "2.9.8",
69
69
  "@7n/mt": "^0.5.1",
70
70
  "@zed-industries/agent-client-protocol": "^0.4.5",
71
71
  "ajv": "^8.20.0",
@@ -4,10 +4,7 @@ title: npm/rules/release
4
4
  resource: npm/rules/release/
5
5
  ---
6
6
 
7
- # npm/rules/release
8
-
9
7
  | Файл | Тип |
10
8
  | ------------------------- | --------- |
11
9
  | [change.mjs](change.md) | JS Module |
12
- | [main.mjs](main.md) | JS Module |
13
10
  | [release.mjs](release.md) | JS Module |
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: release.mjs
4
4
  resource: npm/rules/release/release.mjs
5
5
  docgen:
6
- crc: 8042a5fb
6
+ crc: 496b51f4
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  tier: cloud-min
9
9
  score: 100
@@ -12,18 +12,22 @@ docgen:
12
12
 
13
13
  ## Огляд
14
14
 
15
- Файл агрегує per-workspace change-файли у version-bump і `CHANGELOG`, комітить, ставить тег `<name>@<version>` та видаляє використані change-файли. Запускається у CI на `main` за дизайном `n-rules-release-design`, варіант A, і сам нічого не публікує. Публічні функції: `release`, `runReleaseCli`.
15
+ `release` агрегує per-workspace change-файли у version-bump і CHANGELOG, комітить результат, ставить тег `<name>@<version>` та видаляє використані change-файли. Запускається у CI на `main` за `n-rules-release-design`, варіант A, і сам нічого не публікує.
16
+
17
+ `runReleaseCli` — CLI wrapper для запуску релізного процесу. Він потрібен, щоб ініціювати `release` із командного рядка.
16
18
 
17
19
  ## Поведінка
18
20
 
19
- `release` бере корінь репозиторію та дату, читає `package.json` як джерело workspace-метаданих і далі проходить по кожному workspace у порядку, заданому маніфестом. Для кожного workspace він збирає зміни, оновлює версію, дописує відповідний блок до CHANGELOG і прибирає використані change-файли; якщо для workspace немає релевантних змін, він не входить до списку релізу. Після цього `release` синхронізує точний runtime-пін `@7n/rules` із одночасно випущеним `@7n/llm-lib`, щоб transport не лишався на старій транзитивній версії, а потім формує коміт, анотовані теги `<name>@<version>` і, за увімкненого push, доводить release-коміт до апстріму з ретраями на випадок паралельних push у ту саму гілку. Поверненням є лише перелік фактично зрелізованих пакетів; самі артефакти публікації тут не виконуються.
21
+ `release` послідовно обходить workspaces, збирає change-файли з їхніх директорій, визначає новий bump, оновлює версію в маніфесті, дописує секцію в CHANGELOG і прибирає використані change-файли. Якщо для пакета немає явних change-файлів, але є коміти, він все одно формує релізний запис на основі цього джерела змін.
22
+
23
+ Результат `release` — список фактично релізнутих workspace-ів із новими версіями; цей список далі йде в commit-back і створення анотованих тегів `\<name>@\<version>`. Якщо релізу для workspace нема, він не потрапляє до вихідного списку й не отримує тег.
20
24
 
21
- `runReleaseCli` є тонкою CLI-обгорткою над `release`: запускає той самий потік з дефолтними параметрами, а в тестах дозволяє підмінити корінь, дату, git-раннер і режим push. Якщо release завершується без помилки, CLI повертає успішний exit-код; якщо ні — не приховує збій.
25
+ `runReleaseCli` лише запускає `release` і передає в GitHub Actions перелік workspace-ів, для яких справді з’явилася нова версія.
22
26
 
23
27
  ## Публічний API
24
28
 
25
- - release — агрегує per-workspace change-файли у version-bump і CHANGELOG, створює commit, ставить тег `<name>@<version>` та видаляє use-up change-файли; спирається на налаштування з package.json.
26
- - runReleaseCli — CLI-entry для запуску `release`, щоб виконати той самий release-сценарій з командного рядка без окремого ручного складання кроків.
29
+ - release — агрегує per-workspace change-файли у version-bump і CHANGELOG, комітить зміни, ставить тег `<name>@<version>` та видаляє use-up change-файли; спирається на package.json.
30
+ - runReleaseCli — CLI-entry point для ручного запуску `release` без окремої релізної логіки.
27
31
 
28
32
  ## Гарантії поведінки
29
33
 
@@ -90,6 +90,7 @@ export async function fixWorker(violations, ctx, deps = {}) {
90
90
  const touchedFiles = []
91
91
  /** @type {Array<{provider: string, hook: string, files: string[], error: string}>} */
92
92
  const failed = []
93
+ let feedback = null
93
94
  const deferred = []
94
95
  /**
95
96
  * Викликає опційний fix-hook провайдера, збирає touchedFiles; виняток хука не
@@ -104,6 +105,7 @@ export async function fixWorker(violations, ctx, deps = {}) {
104
105
  try {
105
106
  const res = await provider[hook]({ ...args, cwd: ctx.cwd, ctx: hookCtx(ctx, deadlineAt) })
106
107
  touchedFiles.push(...(res?.touchedFiles ?? []))
108
+ if (res?.feedback?.previousError) feedback = res.feedback
107
109
  deferred.push(...(res?.deferred ?? []))
108
110
  for (const failure of res?.failed ?? []) {
109
111
  const error = failure?.error ?? 'невідома причина'
@@ -135,5 +137,5 @@ export async function fixWorker(violations, ctx, deps = {}) {
135
137
  }
136
138
  }
137
139
 
138
- return { touchedFiles, failed, deferred }
140
+ return { touchedFiles, failed, deferred, feedback }
139
141
  }
@@ -454,6 +454,10 @@ async function runRung(rung, worker, violations, feedback, rungDeps) {
454
454
 
455
455
  const vetoed = after.length === 0 && !error && (collateralAll.length > 0 || brokenTest !== null)
456
456
  const touchedFiles = workerResult?.touchedFiles ?? []
457
+ // Provider може повернути quality-verdict без worker exception (наприклад,
458
+ // generated coverage test зелений у Vitest, але не вбив target mutant). Це
459
+ // не змінює retry policy; лише дає наступному ladder rung-у конкретний feedback.
460
+ const workerFeedback = workerResult?.feedback?.previousError ?? null
457
461
 
458
462
  if (after.length === 0 && !error && !vetoed) {
459
463
  log(` ✅ ${rung.tier} (${rung.model}): ${ruleId}/${concernName}\n`)
@@ -501,7 +505,7 @@ async function runRung(rung, worker, violations, feedback, rungDeps) {
501
505
  outcome: {
502
506
  action: decideAfterFailure(rung, error),
503
507
  violations: after.length > 0 ? after : violations,
504
- feedback: { previousModel: rung.model, previousError: error ?? silentFailureNote }
508
+ feedback: { previousModel: rung.model, previousError: error ?? workerFeedback ?? silentFailureNote }
505
509
  }
506
510
  }
507
511
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: orchestrate.mjs
4
4
  resource: npm/skills/git-reconcile/js/orchestrate.mjs
5
5
  docgen:
6
- crc: c3233b69
6
+ crc: 6e9cb156
7
7
  model: openai-codex/gpt-5.5
8
8
  tier: cloud-avg
9
9
  score: 100
@@ -21,7 +21,7 @@ docgen:
21
21
 
22
22
  Для review-кандидатів `buildTriagePrompt` формує обмежене завдання для LLM: модель отримує вже пораховані Git-факти й має повернути лише JSON-рішення. `callRunner` викликає обраний runner, `parseDecisionEnvelope` дістає структуровану відповідь, а `validateTriageOutcome` приймає тільки повний і узгоджений набір рішень для поточного batch. `callWithValidatedFallback` спершу пробує дешевший рівень, а дорожчий використовує лише після конкретного validation failure.
23
23
 
24
- Під час матеріалізації рішення source переноситься у керований worktree. `branchSlug` забезпечує передбачувані rescue-гілки, а `ensureLocalWorktreeExclude` не дає службовим worktree забруднювати root `git status`. Якщо cherry-pick стає порожнім, `skipEmptyCherryPick` дозволяє skip лише для доведеного semantic no-op, а `finishCherryPick` завершує активний перенос без прийняття неперевіреного Git-стану. `hasChangesFromBase` відсікає PR без реального tree diff.
24
+ Під час матеріалізації рішення source переноситься у керований worktree. `branchSlug` забезпечує передбачувані rescue-гілки без крайового дефіса після скорочення, а `ensureLocalWorktreeExclude` не дає службовим worktree забруднювати root `git status`. Після `@7n/mt` actual checkout визначається через `git worktree list --porcelain`, тому collision sanitized-каталогу не веде до реконструйованого cwd. Setup failure повертається як `failed` лише для цієї PR-групи з branch, фактичним worktree та `spawnSync`-діагностикою; source і forensic checkout не очищуються. Якщо cherry-pick стає порожнім, `skipEmptyCherryPick` дозволяє skip лише для доведеного semantic no-op, а `finishCherryPick` завершує активний перенос без прийняття неперевіреного Git-стану. `hasChangesFromBase` відсікає PR без реального tree diff.
25
25
 
26
26
  Behavior-gates будуються навколо baseline: `captureBehaviorBaseline` фіксує стан чистої `origin/<baseBranch>` із repository Git policy, а `captureCachedBehaviorBaseline` перевикористовує цей результат у межах одного прогону для однакової бази. `validateBehaviorState` перевіряє Git-консистентність, scoped lint/docs і test outcome; правила запуску тестів і скриптів беруться з `package.json`. `testFailureSignatures` нормалізує failures, а `acceptsTestOutcome` дозволяє red baseline тільки без нових failures. Якщо валідація падає через типовий formatting, CSpell, docs або changelog-дефект, `remediateBehaviorState` запускає canonical fixers перед ескалацією LLM.
27
27
 
@@ -31,58 +31,42 @@ Behavior-gates будуються навколо baseline: `captureBehaviorBasel
31
31
 
32
32
  ## Публічний API
33
33
 
34
- - createPhaseProgress — Створює ANSI-free snapshot progress для однієї фази. Однаковий append-only
35
- формат у TTY/CI не засмічує captured output cursor-control кодами, а
36
- heartbeat показує elapsed time довгих LLM-етапів.
34
+ - createPhaseProgress — Створює ANSI-free snapshot progress для однієї фази.
37
35
  - parseWorktrees — Парсить `git worktree list --porcelain` у branch→path.
38
- - dedupeRefs — Дедуплікує local/remote refs одного commit: remote має пріоритет, але
39
- worktree-protection локального ref переноситься у запис.
36
+ - dedupeRefs — Дедуплікує local/remote refs одного commit.
40
37
  - conflictFiles — Витягає конфліктні файли з `git merge-tree`.
41
- - inventoryRepository — Збирає детермінований Git inventory. Нічого не видаляє і не змінює у
42
- checkout, крім оновлення remote refs через fetch --prune.
43
- - buildTriagePrompt — Формує bounded semantic-triage prompt. Git-факти вже пораховані JS; модель
44
- не виконує shell-команди й повертає лише JSON-рішення.
38
+ - inventoryRepository — Збирає детермінований Git inventory.
39
+ - buildTriagePrompt Формує bounded semantic-triage prompt.
45
40
  - parseDecisionEnvelope — Витягає JSON object із чистої або fenced відповіді.
46
41
  - callRunner — Викликає вибраний LLM runner для одного bounded-завдання.
47
- - callWithValidatedFallback — Виконує bounded LLM-крок через min, валідовує результат JS-функцією і
48
- викликає max лише після конкретного провалу.
49
- - validateTriageOutcomeСтруктурно перевіряє triage-рішення: рівно один verdict на candidate,
50
- валідні actions/groups і лише відомі commit OID.
51
- - branchSlugПеретворює довільний title/ref на branch slug.
52
- - ensureLocalWorktreeExcludeДодає `.worktrees/` до локального Git exclude без tracked-змін у consumer.
53
- Це не замінює repository Vitest excludes, але не лишає root checkout dirty
54
- через керовані або forensic worktree.
55
- - skipEmptyCherryPickПропускає лише підтверджений empty cherry-pick: sequencer активний,
56
- конфліктів немає, staged diff порожній.
57
- - finishCherryPickЗавершує активний cherry-pick: semantic no-op пропускає, непорожній
58
- продовжує. Відсутній sequencer не потребує дії.
59
- - hasChangesFromBaseПеревіряє реальний tree diff, а не лише кількість commits ahead.
60
- - testFailureSignaturesВитягає стабільні Vitest failure identifiers без summary/timing.
61
- - acceptsTestOutcomeДозволяє red baseline лише якщо після перенесення не з'явилось нових
62
- Vitest failures. Нерозпізнаний red output завжди fail-closed.
63
- - sourceDirectoriesЗводить змінені code paths до найвужчих директорій для scoped gates.
64
- - changedNonCodeDirectories — Повертає директорії non-code змін для фінального domain lint.
65
- - remediateBehaviorState — Запускає canonical fixers у worktree до ескалації min→max. Це прибирає
66
- formatting/CSpell/doc/changelog дефекти без повторного behavioral LLM.
67
- - captureBehaviorBaseline — Фіксує test baseline на чистій policy base branch до перенесення source.
68
- - captureCachedBehaviorBaseline — Повторно використовує test baseline однієї policy base branch між PR-групами.
69
- Залежності все одно встановлюються в кожному окремому worktree.
70
- - validateBehaviorState — Додає до Git-state validation test script із репозиторію і changelog gate.
71
- Саме ці докази вирішують, чи приймати min-результат або ескалювати на max.
72
- - validateFinalProjectGates — Фінальний domain gate охоплює non-code зміни, зокрема workflows, dependency
73
- manifests і правила. Code directories уже пройшли scoped lint і tests.
74
- - cleanupSource — Видаляє точний source після Git-доказу неактуальності або успішного
75
- перенесення. Protected/open-PR refs не потрапляють у цей крок.
42
+ - callWithValidatedFallback — Виконує min, валідує результат і викликає max лише після validation failure.
43
+ - validateTriageOutcome Структурно перевіряє triage-рішення.
44
+ - branchSlugПеретворює title/ref на валідний короткий branch slug.
45
+ - ensureLocalWorktreeExclude Додає `.worktrees/` до локального Git exclude без tracked-змін.
46
+ - skipEmptyCherryPickПропускає лише підтверджений empty cherry-pick.
47
+ - finishCherryPickЗавершує активний cherry-pick.
48
+ - hasChangesFromBase Перевіряє реальний tree diff, а не commits ahead.
49
+ - testFailureSignatures Витягає стабільні Vitest failure identifiers.
50
+ - acceptsTestOutcomeДозволяє red baseline лише без нових failures.
51
+ - sourceDirectories Зводить code paths до найвужчих директорій для scoped gates.
52
+ - changedNonCodeDirectoriesПовертає директорії non-code змін для domain lint.
53
+ - remediateBehaviorState Запускає canonical fixers перед behavioral max fallback.
54
+ - captureBehaviorBaselineФіксує test baseline на чистій policy base branch.
55
+ - captureCachedBehaviorBaselineКешує test baseline між PR-групами.
56
+ - validateBehaviorStateДодає Git-state validation, tests і changelog gate.
57
+ - validateFinalProjectGates Перевіряє фінальні non-code domain gates.
58
+ - cleanupSourceВидаляє тільки доказово безпечний точний source.
76
59
  - formatReport — Формує deterministic report.
77
- - runWithConcurrency — Виконує async jobs із bounded concurrency та стабільним порядком output.
60
+ - runWithConcurrency — Виконує async jobs із bounded concurrency та стабільним output.
78
61
  - normalizePrConcurrency — Нормалізує bounded concurrency PR-фази.
79
- - runGitReconcileOrchestrator — JS-оркестратор: inventory → bounded LLM triage deterministic PR pipeline.
62
+ - runGitReconcileOrchestrator — Координує inventory, triage, PR і cleanup.
80
63
 
81
64
  ## Гарантії поведінки
82
65
 
83
66
  - Progress є append-only та не містить ANSI cursor-control sequences.
84
67
  - Не більше чотирьох PR-груп виконуються одночасно; типовий ліміт — три.
85
68
  - Canonical fixers мають пріоритет перед behavioral `max` fallback.
86
- - Cleanup починається лише після завершення всіх PR jobs і не видаляє
87
- protected/open-PR/failed sources.
69
+ - Cleanup починається лише після завершення всіх PR jobs і не видаляє protected/open-PR/failed sources.
70
+ - `spawnSync.error`, зокрема `ENOENT`, не губиться в command diagnostics.
71
+ - Setup failure зберігає forensic worktree і не зупиняє незалежні PR-групи.
88
72
  - Test baseline кешується за OID policy base branch у межах одного прогону.
@@ -138,7 +138,7 @@ export function createPhaseProgress(args) {
138
138
  * @param {string} cwd робочий каталог
139
139
  * @param {typeof spawnSync} spawnFn інжект для тестів
140
140
  * @param {{ allowFailure?: boolean, input?: string }} [options] режим помилки та stdin
141
- * @returns {{ status: number, stdout: string, stderr: string }} результат
141
+ * @returns {{ status: number, stdout: string, stderr: string, error: string }} результат
142
142
  */
143
143
  function run(command, args, cwd, spawnFn, options = {}) {
144
144
  const childEnv = { ...env, GIT_EDITOR: 'true' }
@@ -153,11 +153,12 @@ function run(command, args, cwd, spawnFn, options = {}) {
153
153
  const normalized = {
154
154
  status: result.status ?? 1,
155
155
  stdout: result.stdout ?? '',
156
- stderr: result.stderr ?? ''
156
+ stderr: result.stderr ?? '',
157
+ error: result.error ? `${result.error.name ?? 'Error'}${result.error.code ? ` ${result.error.code}` : ''}: ${result.error.message}` : ''
157
158
  }
158
159
  if (!options.allowFailure && normalized.status !== 0) {
159
160
  throw new Error(
160
- `${command} ${args.join(' ')} → exit ${normalized.status}: ${normalized.stderr || normalized.stdout}`
161
+ `${command} ${args.join(' ')} → exit ${normalized.status}: ${normalized.stderr || normalized.stdout || normalized.error}`
161
162
  )
162
163
  }
163
164
  return normalized
@@ -666,6 +667,7 @@ export function branchSlug(value) {
666
667
  .replaceAll(/[^a-z0-9]+/g, '-')
667
668
  .replaceAll(/^-|-$/g, '')
668
669
  .slice(0, 40)
670
+ .replaceAll(/^-|-$/g, '')
669
671
  return slug || 'change'
670
672
  }
671
673
 
@@ -706,12 +708,22 @@ function chooseBranch(title, cwd, spawnFn) {
706
708
  function createReconcileWorktree(title, source, cwd, spawnFn) {
707
709
  const baseRef = policyBaseRef(cwd)
708
710
  const branch = chooseBranch(title, cwd, spawnFn)
709
- run('npx', ['@7n/mt', 'worktree', 'create', branch, `git-reconcile: ${source}`], cwd, spawnFn)
710
- const worktreeCwd = join(cwd, '.worktrees', branch.replaceAll('/', '-'))
711
- git(['switch', '--detach', baseRef], worktreeCwd, spawnFn)
712
- git(['branch', '-f', branch, baseRef], worktreeCwd, spawnFn)
713
- git(['switch', branch], worktreeCwd, spawnFn)
714
- return { branch, cwd: worktreeCwd }
711
+ let worktreeCwd
712
+ try {
713
+ run('npx', ['@7n/mt', 'worktree', 'create', branch, `git-reconcile: ${source}`], cwd, spawnFn)
714
+ const worktrees = parseWorktrees(git(['worktree', 'list', '--porcelain'], cwd, spawnFn).stdout)
715
+ worktreeCwd = worktrees.get(`refs/heads/${branch}`)
716
+ if (!worktreeCwd) throw new Error(`@7n/mt не зареєстрував worktree для ${branch}`)
717
+ git(['switch', '--detach', baseRef], worktreeCwd, spawnFn)
718
+ git(['branch', '-f', branch, baseRef], worktreeCwd, spawnFn)
719
+ git(['switch', branch], worktreeCwd, spawnFn)
720
+ return { branch, cwd: worktreeCwd }
721
+ } catch (error) {
722
+ const setupError = error instanceof Error ? error : new Error(String(error))
723
+ setupError.branch = branch
724
+ if (worktreeCwd) setupError.worktree = worktreeCwd
725
+ throw setupError
726
+ }
715
727
  }
716
728
 
717
729
  /**
@@ -1225,11 +1237,12 @@ async function createPullRequest(args) {
1225
1237
  return { status: 'kept', error: 'LLM не вибрала жодного валідного commit oid' }
1226
1238
  }
1227
1239
 
1228
- onProgress('worktree')
1229
- const worktree = createReconcileWorktree(group.title, source, rootCwd, spawnFn)
1240
+ let worktree
1230
1241
  let createdPr
1231
- log(`🌿 ${source} → ${worktree.branch}`)
1232
1242
  try {
1243
+ onProgress('worktree')
1244
+ worktree = createReconcileWorktree(group.title, source, rootCwd, spawnFn)
1245
+ log(`🌿 ${source} → ${worktree.branch}`)
1233
1246
  const sourceMayChangeCode = (candidate.changedFiles ?? []).some(path => SOURCE_CODE_RE.test(path))
1234
1247
  let baseline = null
1235
1248
  if (sourceMayChangeCode) {
@@ -1318,11 +1331,12 @@ async function createPullRequest(args) {
1318
1331
  removeReconcileWorktree(worktree, rootCwd, spawnFn)
1319
1332
  return { status: 'pr-created', url: createdPr, branch: worktree.branch }
1320
1333
  } catch (error) {
1334
+ const setup = /** @type {{branch?:string,worktree?:string}} */ (error)
1321
1335
  return {
1322
1336
  status: 'failed',
1323
1337
  error: error instanceof Error ? error.message : String(error),
1324
- branch: worktree.branch,
1325
- worktree: worktree.cwd,
1338
+ branch: worktree?.branch ?? setup.branch,
1339
+ worktree: worktree?.cwd ?? setup.worktree,
1326
1340
  ...(createdPr && { url: createdPr })
1327
1341
  }
1328
1342
  }