@7n/rules 1.49.6 → 1.49.8
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.49.8] - 2026-07-26
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- release: @7n/llm-lib@2.9.6, @7n/rules@1.49.7
|
|
8
|
+
|
|
9
|
+
## [1.49.7] - 2026-07-26
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Усунено порожні PR, помилкові empty cherry-pick, неповний cleanup aliases і зайві LLM-виклики в `git-reconcile`.
|
|
14
|
+
|
|
3
15
|
## [1.49.6] - 2026-07-26
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules",
|
|
3
|
-
"version": "1.49.
|
|
3
|
+
"version": "1.49.8",
|
|
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.
|
|
68
|
+
"@7n/llm-lib": "2.9.6",
|
|
69
69
|
"@7n/mt": "^0.5.1",
|
|
70
70
|
"@zed-industries/agent-client-protocol": "^0.4.5",
|
|
71
71
|
"ajv": "^8.20.0",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* у CI на `main` (n-rules-release-design, варіант A). Сам нічого не публікує.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync } from 'node:fs'
|
|
7
|
-
import { readFile, rm, writeFile } from 'node:fs/promises'
|
|
7
|
+
import { appendFile, readFile, rm, writeFile } from 'node:fs/promises'
|
|
8
8
|
import { join } from 'node:path'
|
|
9
9
|
import { env } from 'node:process'
|
|
10
10
|
|
|
@@ -225,6 +225,18 @@ export async function release(opts = {}) {
|
|
|
225
225
|
return released
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Передає GitHub Actions перелік workspace-ів, для яких release справді створив
|
|
230
|
+
* нову версію. Publish-кроки використовують його замість broad path-filter-а,
|
|
231
|
+
* щоб не викликати npm publish для пакетів без нового релізу.
|
|
232
|
+
* @param {Array<{ ws: string }>} released фактично випущені workspaces
|
|
233
|
+
* @returns {Promise<void>} завершення запису або no-op поза GitHub Actions
|
|
234
|
+
*/
|
|
235
|
+
async function writeReleasedWorkspacesOutput(released) {
|
|
236
|
+
if (!env.GITHUB_OUTPUT) return
|
|
237
|
+
await appendFile(env.GITHUB_OUTPUT, `workspaces=${JSON.stringify(released.map(({ ws }) => ws))}\n`)
|
|
238
|
+
}
|
|
239
|
+
|
|
228
240
|
/**
|
|
229
241
|
* @param {string[]} _args аргументи CLI (наразі без опцій)
|
|
230
242
|
* @param {import('./release.mjs').ReleaseOpts} [opts] опції для тестів (cwd, date, runGit, push)
|
|
@@ -237,6 +249,7 @@ export async function runReleaseCli(_args, opts = {}) {
|
|
|
237
249
|
// через `env:` на кроці — поле, якого немає в канонічному сніпеті, тож subset-перевірка не ламається.
|
|
238
250
|
const push = opts.push ?? (env.N_RULES_RELEASE_PUSH ?? env.N_CURSOR_RELEASE_PUSH) !== '0'
|
|
239
251
|
const released = await release({ ...opts, push })
|
|
252
|
+
await writeReleasedWorkspacesOutput(released)
|
|
240
253
|
if (released.length === 0) {
|
|
241
254
|
console.log('release: немає змін для релізу')
|
|
242
255
|
} else {
|
|
@@ -23,7 +23,9 @@ npx @7n/rules skill pi git-reconcile
|
|
|
23
23
|
|
|
24
24
|
JS виконує `fetch`, inventory, patch-equivalence, дедуплікацію refs, збір
|
|
25
25
|
worktree/PR/stash, підготовку worktree від `origin/main`, cherry-pick або
|
|
26
|
-
застосування stash, gates, commit, push, PR і фінальний звіт.
|
|
26
|
+
застосування stash, gates, commit, push, PR і фінальний звіт. Semantic no-op
|
|
27
|
+
після conflict resolution детерміновано пропускає через `cherry-pick --skip`;
|
|
28
|
+
порожній tree diff не push-иться і не створює PR.
|
|
27
29
|
|
|
28
30
|
LLM отримує лише bounded-завдання:
|
|
29
31
|
|
|
@@ -72,6 +74,11 @@ transport. Після провалу `max` джерело fail-closed лишає
|
|
|
72
74
|
`npm exec --package`.
|
|
73
75
|
- За невизначеності джерело лишається `kept`; misleading ready PR не
|
|
74
76
|
створюється.
|
|
77
|
+
- Перед push обов'язково проходять фінальний tree-diff guard, domain lint для
|
|
78
|
+
non-code paths, changelog і `git diff --check`; code changes додатково
|
|
79
|
+
проходять scoped docs/lint та tests.
|
|
80
|
+
- Behavioral LLM не викликається для змін без code paths; test baseline
|
|
81
|
+
актуального `origin/main` кешується між PR-групами.
|
|
75
82
|
- Cleanup виконує лише JS і тільки після inventory/PR-фази: видаляє точні refs,
|
|
76
83
|
уже merged/patch-equivalent, явно класифіковані як `drop` або повністю
|
|
77
84
|
перенесені в успішний PR.
|
|
@@ -85,5 +92,5 @@ transport. Після провалу `max` джерело fail-closed лишає
|
|
|
85
92
|
Оркестратор повертає для кожного джерела один verdict: `merged`,
|
|
86
93
|
`patch-equivalent`, `open-pr`, `protected`, `pr-created`, `kept`,
|
|
87
94
|
`drop-recommended` або `failed`. Для `pr-created` додає URL, перенесені коміти,
|
|
88
|
-
конфлікти та виконані перевірки. Для cleanup ref звіт також містить точний OID
|
|
89
|
-
|
|
95
|
+
конфлікти та виконані перевірки. Для cleanup ref звіт також містить точний OID
|
|
96
|
+
і видалені aliases, а для failure — branch та збережений worktree.
|
|
@@ -3,12 +3,9 @@ type: JS Module
|
|
|
3
3
|
title: orchestrate.mjs
|
|
4
4
|
resource: npm/skills/git-reconcile/js/orchestrate.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model:
|
|
8
|
-
|
|
9
|
-
score: 100
|
|
10
|
-
issues: judge-refine:kept-original,judge:inaccurate:0.99
|
|
11
|
-
judgeModel: openai-codex/gpt-5.4-mini
|
|
6
|
+
crc: f0c9ea76
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
score: 70
|
|
12
9
|
---
|
|
13
10
|
|
|
14
11
|
## Огляд
|
|
@@ -19,59 +16,42 @@ JS виконує inventory, validation, materialization, gates, PR і cleanup,
|
|
|
19
16
|
|
|
20
17
|
## Поведінка
|
|
21
18
|
|
|
22
|
-
- Збирає refs, worktree, open PR, patch-equivalent commits і stash.
|
|
23
|
-
worktree захищає за checkout HEAD OID.
|
|
19
|
+
- Збирає refs, aliases, worktree, open PR, patch-equivalent commits і stash.
|
|
20
|
+
Detached worktree захищає за checkout HEAD OID.
|
|
24
21
|
- Ізолює вкладені `npx` від `npm_config_package` зовнішнього `npm exec`.
|
|
22
|
+
- Приглушує дубльовані ACP events, якщо користувач не ввімкнув explicit verbose.
|
|
25
23
|
- Показує inventory elapsed time та окремі progress bars для triage, PR і
|
|
26
|
-
cleanup;
|
|
27
|
-
- Перевіряє повноту triage, groups та commit OID.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
gate. LLM prompt дозволяє лише narrow tests.
|
|
42
|
-
- Створює PR і лише після успіху точково прибирає перенесені або доведено
|
|
43
|
-
неактуальні refs/stash; cleanup refs у звіті мають точний OID.
|
|
24
|
+
cleanup; довгі deterministic gates отримують окремі stage labels.
|
|
25
|
+
- Перевіряє повноту triage, groups та commit OID. `max` запускається лише після
|
|
26
|
+
конкретного провалу validation.
|
|
27
|
+
- Створює worktree від `origin/main`; baseline tests кешує за OID бази між
|
|
28
|
+
PR-групами. Для змін без code paths behavioral LLM не викликається.
|
|
29
|
+
- Red baseline допускається лише для розпізнаних Vitest failures без нових
|
|
30
|
+
failures після перенесення.
|
|
31
|
+
- Для code paths запускає scoped `doc-files`, lint і tests. Перед push запускає
|
|
32
|
+
domain lint для non-code paths, changelog та `git diff --check`.
|
|
33
|
+
- Після conflict resolution порожній cherry-pick пропускається лише за
|
|
34
|
+
активного `CHERRY_PICK_HEAD`, відсутніх conflicts і порожнього staged diff.
|
|
35
|
+
- Tree-diff guard двічі відсікає semantic no-op. Порожня група отримує
|
|
36
|
+
`patch-equivalent`, worktree прибирається, push і PR не виконуються.
|
|
37
|
+
- Cleanup видаляє всі точні local/remote aliases і звітує їх. Failed source
|
|
38
|
+
зберігає branch, worktree і, якщо PR уже створено, його URL.
|
|
44
39
|
|
|
45
40
|
## Публічний API
|
|
46
41
|
|
|
47
|
-
- parseWorktrees —
|
|
48
|
-
- dedupeRefs —
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
валідні actions/groups і лише відомі commit OID.
|
|
61
|
-
- branchSlug — Перетворює довільний title/ref на branch slug.
|
|
62
|
-
- skipEmptyCherryPick — Пропускає лише підтверджений empty cherry-pick: sequencer активний,
|
|
63
|
-
конфліктів немає, staged diff порожній.
|
|
64
|
-
- testFailureSignatures — Витягає стабільні Vitest failure identifiers без summary/timing.
|
|
65
|
-
- acceptsTestOutcome — Дозволяє red baseline лише якщо після перенесення не з'явилось нових
|
|
66
|
-
Vitest failures. Нерозпізнаний red output завжди fail-closed.
|
|
67
|
-
- sourceDirectories — Зводить змінені code paths до найвужчих директорій для scoped gates.
|
|
68
|
-
- captureBehaviorBaseline — Фіксує test baseline на чистому origin/main до перенесення source.
|
|
69
|
-
- validateBehaviorState — Додає до Git-state validation test script із репозиторію і changelog gate.
|
|
70
|
-
Саме ці докази вирішують, чи приймати min-результат або ескалювати на max.
|
|
71
|
-
- cleanupSource — Видаляє точний source після Git-доказу неактуальності або успішного
|
|
72
|
-
перенесення. Protected/open-PR refs не потрапляють у цей крок.
|
|
73
|
-
- formatReport — Формує deterministic report.
|
|
74
|
-
- runGitReconcileOrchestrator — JS-оркестратор: inventory → bounded LLM triage → deterministic PR pipeline.
|
|
42
|
+
- `parseWorktrees` — парсить `git worktree list --porcelain`.
|
|
43
|
+
- `dedupeRefs` — зводить local/remote refs одного commit зі збереженням aliases.
|
|
44
|
+
- `inventoryRepository` — збирає детермінований Git inventory.
|
|
45
|
+
- `callWithValidatedFallback` — виконує min → validation → max fallback.
|
|
46
|
+
- `finishCherryPick` — пропускає semantic no-op або продовжує непорожній pick.
|
|
47
|
+
- `hasChangesFromBase` — перевіряє реальний tree diff, а не commits ahead.
|
|
48
|
+
- `captureCachedBehaviorBaseline` — кешує test baseline за OID `origin/main`.
|
|
49
|
+
- `changedNonCodeDirectories` — групує non-code paths у domain directories.
|
|
50
|
+
- `validateBehaviorState` — перевіряє scoped code gates і test regression.
|
|
51
|
+
- `validateFinalProjectGates` — перевіряє non-code domains та changelog.
|
|
52
|
+
- `cleanupSource` — видаляє тільки доказово безпечні точні refs або stash.
|
|
53
|
+
- `formatReport` — формує deterministic звіт із cleanup та failure details.
|
|
54
|
+
- `runGitReconcileOrchestrator` — координує inventory, triage, PR і cleanup.
|
|
75
55
|
|
|
76
56
|
## Гарантії поведінки
|
|
77
57
|
|
|
@@ -82,4 +62,5 @@ Vitest failures. Нерозпізнаний red output завжди fail-closed.
|
|
|
82
62
|
- Stash видаляється точково за стабільним commit ID; `git stash clear` не
|
|
83
63
|
використовується.
|
|
84
64
|
- Worktree із проваленим перенесенням зберігається для діагностики.
|
|
65
|
+
- Порожній tree diff не може створити remote branch або PR.
|
|
85
66
|
- Progress не змішує фази з різною вартістю в удаваний global percentage.
|
|
@@ -3,6 +3,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs'
|
|
4
4
|
import { dirname, join } from 'node:path'
|
|
5
5
|
import { performance } from 'node:perf_hooks'
|
|
6
|
+
import { env } from 'node:process'
|
|
6
7
|
|
|
7
8
|
import { createProgressReporter } from '../../../scripts/lib/lint-surface/progress.mjs'
|
|
8
9
|
|
|
@@ -19,6 +20,7 @@ const REF_ORIGIN_RE = /^refs\/remotes\/origin\//
|
|
|
19
20
|
const RENAME_DELETE_CONFLICT_RE = /^CONFLICT \(rename\/delete\): .+? renamed to (.+?) in .+?, but deleted/
|
|
20
21
|
const SOURCE_CODE_RE = /\.(?:js|mjs|ts|vue|rs|py)$/
|
|
21
22
|
const WHITESPACE_RE = /\s+/
|
|
23
|
+
const ACP_PROGRESS_ENV = 'N_LLM_ACP_PROGRESS'
|
|
22
24
|
const REF_INVENTORY_FORMAT = ['%(refname)', '%00', '%(object', 'name)', '%00', '%(committer', 'date:iso-strict)'].join(
|
|
23
25
|
''
|
|
24
26
|
)
|
|
@@ -84,13 +86,13 @@ function createPhaseProgress(args) {
|
|
|
84
86
|
* @returns {{ status: number, stdout: string, stderr: string }} результат
|
|
85
87
|
*/
|
|
86
88
|
function run(command, args, cwd, spawnFn, options = {}) {
|
|
87
|
-
const
|
|
88
|
-
if (command === 'npx') delete
|
|
89
|
+
const childEnv = { ...env, GIT_EDITOR: 'true' }
|
|
90
|
+
if (command === 'npx') delete childEnv.npm_config_package
|
|
89
91
|
const result = spawnFn(command, args, {
|
|
90
92
|
cwd,
|
|
91
93
|
encoding: 'utf8',
|
|
92
94
|
input: options.input,
|
|
93
|
-
env,
|
|
95
|
+
env: childEnv,
|
|
94
96
|
maxBuffer: 16 * 1024 * 1024
|
|
95
97
|
})
|
|
96
98
|
const normalized = {
|
|
@@ -216,7 +218,7 @@ function branchState({ merged, novelCommitIds, pr, worktree }) {
|
|
|
216
218
|
* Збирає відкриті PR; недоступний gh не блокує git-inventory.
|
|
217
219
|
* @param {string} cwd корінь
|
|
218
220
|
* @param {typeof spawnSync} spawnFn інжект
|
|
219
|
-
* @returns {Map<string,{number:number,url:string}
|
|
221
|
+
* @returns {{items:Map<string,{number:number,url:string}>,warning:string|null}} inventory
|
|
220
222
|
*/
|
|
221
223
|
function openPullRequests(cwd, spawnFn) {
|
|
222
224
|
const result = run(
|
|
@@ -226,9 +228,18 @@ function openPullRequests(cwd, spawnFn) {
|
|
|
226
228
|
spawnFn,
|
|
227
229
|
{ allowFailure: true }
|
|
228
230
|
)
|
|
229
|
-
if (result.status !== 0)
|
|
231
|
+
if (result.status !== 0) {
|
|
232
|
+
const detail = result.stderr || result.stdout || `exit ${result.status}`
|
|
233
|
+
return {
|
|
234
|
+
items: new Map(),
|
|
235
|
+
warning: `GitHub PR inventory недоступний: ${detail}`
|
|
236
|
+
}
|
|
237
|
+
}
|
|
230
238
|
const rows = /** @type {Array<{headRefName:string,number:number,url:string}>} */ (parseJson(result.stdout, []))
|
|
231
|
-
return
|
|
239
|
+
return {
|
|
240
|
+
items: new Map(rows.map(row => [row.headRefName, { number: row.number, url: row.url }])),
|
|
241
|
+
warning: null
|
|
242
|
+
}
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
/**
|
|
@@ -290,9 +301,9 @@ export function inventoryRepository(cwd, deps = {}) {
|
|
|
290
301
|
worktreeState.branches,
|
|
291
302
|
worktreeState.commits
|
|
292
303
|
)
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
304
|
+
const prInventory = openPullRequests(cwd, spawnFn)
|
|
305
|
+
const prs = prInventory.items
|
|
306
|
+
const warnings = prInventory.warning ? [prInventory.warning] : []
|
|
296
307
|
|
|
297
308
|
const branches = refs.map(item => {
|
|
298
309
|
const name = branchName(item.ref)
|
|
@@ -324,6 +335,7 @@ export function inventoryRepository(cwd, deps = {}) {
|
|
|
324
335
|
return {
|
|
325
336
|
source: `${SOURCE_BRANCH_PREFIX}${item.ref}`,
|
|
326
337
|
ref: item.ref,
|
|
338
|
+
aliases: item.aliases,
|
|
327
339
|
name,
|
|
328
340
|
oid: item.oid,
|
|
329
341
|
date: item.date,
|
|
@@ -430,11 +442,17 @@ export async function callRunner(runner, prompt, cwd, deps = {}, tier = 'max') {
|
|
|
430
442
|
const module = await import('@7n/llm-lib/acp')
|
|
431
443
|
runAcpAgent = module.runAcpAgent
|
|
432
444
|
}
|
|
445
|
+
const previousProgress = env[ACP_PROGRESS_ENV]
|
|
446
|
+
const verbose = ['1', 'true'].includes((env.N_LLM_ACP_VERBOSE ?? '').toLowerCase())
|
|
447
|
+
if (previousProgress === undefined && !verbose) env[ACP_PROGRESS_ENV] = '0'
|
|
433
448
|
try {
|
|
434
449
|
const text = await runAcpAgent(runner, prompt, cwd, { tier })
|
|
435
450
|
return { ok: true, text, error: null }
|
|
436
451
|
} catch (error) {
|
|
437
452
|
return { ok: false, text: '', error: error instanceof Error ? error.message : String(error) }
|
|
453
|
+
} finally {
|
|
454
|
+
if (previousProgress === undefined) delete env[ACP_PROGRESS_ENV]
|
|
455
|
+
else env[ACP_PROGRESS_ENV] = previousProgress
|
|
438
456
|
}
|
|
439
457
|
}
|
|
440
458
|
|
|
@@ -628,6 +646,21 @@ function createReconcileWorktree(title, source, cwd, spawnFn) {
|
|
|
628
646
|
return { branch, cwd: worktreeCwd }
|
|
629
647
|
}
|
|
630
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Прибирає reconciliation worktree або fail-closed лишає source неочищеним.
|
|
651
|
+
* @param {{branch:string,cwd:string}} worktree створений worktree
|
|
652
|
+
* @param {string} rootCwd корінь репо
|
|
653
|
+
* @param {typeof spawnSync} spawnFn інжект
|
|
654
|
+
*/
|
|
655
|
+
function removeReconcileWorktree(worktree, rootCwd, spawnFn) {
|
|
656
|
+
const removed = run('npx', ['@7n/mt', 'worktree', 'remove', worktree.branch], rootCwd, spawnFn, {
|
|
657
|
+
allowFailure: true
|
|
658
|
+
})
|
|
659
|
+
if (removed.status !== 0) {
|
|
660
|
+
throw new Error(`worktree cleanup: ${removed.stderr || removed.stdout}`)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
631
664
|
/**
|
|
632
665
|
* Перевіряє, чи лишились unmerged paths.
|
|
633
666
|
* @param {string} cwd worktree
|
|
@@ -660,6 +693,42 @@ export function skipEmptyCherryPick(cwd, spawnFn = spawnSync) {
|
|
|
660
693
|
return true
|
|
661
694
|
}
|
|
662
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Завершує активний cherry-pick: semantic no-op пропускає, непорожній
|
|
698
|
+
* продовжує. Відсутній sequencer не потребує дії.
|
|
699
|
+
* @param {string} cwd worktree
|
|
700
|
+
* @param {typeof spawnSync} spawnFn інжект
|
|
701
|
+
* @returns {'none'|'skipped'|'continued'} виконана дія
|
|
702
|
+
*/
|
|
703
|
+
export function finishCherryPick(cwd, spawnFn = spawnSync) {
|
|
704
|
+
const inProgress =
|
|
705
|
+
git(['rev-parse', '-q', '--verify', 'CHERRY_PICK_HEAD'], cwd, spawnFn, {
|
|
706
|
+
allowFailure: true
|
|
707
|
+
}).status === 0
|
|
708
|
+
if (!inProgress) return 'none'
|
|
709
|
+
if (skipEmptyCherryPick(cwd, spawnFn)) return 'skipped'
|
|
710
|
+
git(['cherry-pick', '--continue'], cwd, spawnFn)
|
|
711
|
+
return 'continued'
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Перевіряє реальний tree diff, а не лише кількість commits ahead.
|
|
716
|
+
* @param {string} cwd worktree
|
|
717
|
+
* @param {typeof spawnSync} spawnFn інжект
|
|
718
|
+
* @returns {boolean} чи є що переносити в PR
|
|
719
|
+
*/
|
|
720
|
+
export function hasChangesFromBase(cwd, spawnFn = spawnSync) {
|
|
721
|
+
return (
|
|
722
|
+
git(['diff', '--quiet', `${BASE_REF}...HEAD`], cwd, spawnFn, {
|
|
723
|
+
allowFailure: true
|
|
724
|
+
}).status !== 0 ||
|
|
725
|
+
git(['diff', '--quiet', BASE_REF, '--'], cwd, spawnFn, {
|
|
726
|
+
allowFailure: true
|
|
727
|
+
}).status !== 0 ||
|
|
728
|
+
git(['ls-files', '--others', '--exclude-standard'], cwd, spawnFn).stdout.trim().length > 0
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
663
732
|
/**
|
|
664
733
|
* Перевіряє, що agentic-крок залишив Git у консистентному стані.
|
|
665
734
|
* @param {string} cwd worktree
|
|
@@ -731,10 +800,38 @@ export function sourceDirectories(paths) {
|
|
|
731
800
|
* @param {typeof spawnSync} spawnFn інжект
|
|
732
801
|
* @returns {string[]} directories
|
|
733
802
|
*/
|
|
734
|
-
function
|
|
803
|
+
function changedPaths(cwd, spawnFn) {
|
|
735
804
|
const tracked = git(['diff', '--name-only', BASE_REF, '--'], cwd, spawnFn).stdout.split('\n').filter(Boolean)
|
|
736
805
|
const untracked = git(['ls-files', '--others', '--exclude-standard'], cwd, spawnFn).stdout.split('\n').filter(Boolean)
|
|
737
|
-
return
|
|
806
|
+
return [...new Set([...tracked, ...untracked])]
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Повертає директорії зміненого коду.
|
|
811
|
+
* @param {string} cwd worktree
|
|
812
|
+
* @param {typeof spawnSync} spawnFn інжект
|
|
813
|
+
* @returns {string[]} directories
|
|
814
|
+
*/
|
|
815
|
+
function changedSourceDirectories(cwd, spawnFn) {
|
|
816
|
+
return sourceDirectories(changedPaths(cwd, spawnFn)).filter(path => existsSync(join(cwd, path === '.' ? '' : path)))
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Повертає директорії non-code змін для фінального domain lint.
|
|
821
|
+
* @param {string} cwd worktree
|
|
822
|
+
* @param {typeof spawnSync} spawnFn інжект
|
|
823
|
+
* @returns {string[]} directories
|
|
824
|
+
*/
|
|
825
|
+
export function changedNonCodeDirectories(cwd, spawnFn = spawnSync) {
|
|
826
|
+
return [
|
|
827
|
+
...new Set(
|
|
828
|
+
changedPaths(cwd, spawnFn)
|
|
829
|
+
.filter(path => !SOURCE_CODE_RE.test(path))
|
|
830
|
+
.map(path => dirname(path))
|
|
831
|
+
)
|
|
832
|
+
]
|
|
833
|
+
.filter(path => existsSync(join(cwd, path === '.' ? '' : path)))
|
|
834
|
+
.toSorted()
|
|
738
835
|
}
|
|
739
836
|
|
|
740
837
|
/**
|
|
@@ -742,16 +839,19 @@ function changedSourceDirectories(cwd, spawnFn) {
|
|
|
742
839
|
* зміненого коду, не торкаючись repository-wide baseline.
|
|
743
840
|
* @param {string} cwd worktree
|
|
744
841
|
* @param {typeof spawnSync} spawnFn інжект
|
|
842
|
+
* @param {(stage:string)=>void} [onProgress] stage callback
|
|
745
843
|
* @returns {{ok:boolean,error?:string}} gate
|
|
746
844
|
*/
|
|
747
|
-
function validateScopedProjectGates(cwd, spawnFn) {
|
|
845
|
+
function validateScopedProjectGates(cwd, spawnFn, onProgress = noop) {
|
|
748
846
|
for (const path of changedSourceDirectories(cwd, spawnFn)) {
|
|
847
|
+
onProgress(`doc-files (${path})`)
|
|
749
848
|
const docs = run('npx', ['@7n/rules', 'lint', 'doc-files', '--path', path], cwd, spawnFn, {
|
|
750
849
|
allowFailure: true
|
|
751
850
|
})
|
|
752
851
|
if (docs.status !== 0) {
|
|
753
852
|
return { ok: false, error: `doc-files (${path}): ${docs.stderr || docs.stdout}` }
|
|
754
853
|
}
|
|
854
|
+
onProgress(`scoped lint (${path})`)
|
|
755
855
|
const lint = run('npx', ['@7n/rules', 'lint', '--path', path, '--no-fix'], cwd, spawnFn, {
|
|
756
856
|
allowFailure: true
|
|
757
857
|
})
|
|
@@ -788,18 +888,36 @@ export function captureBehaviorBaseline(cwd, spawnFn = spawnSync) {
|
|
|
788
888
|
return { tests }
|
|
789
889
|
}
|
|
790
890
|
|
|
891
|
+
/**
|
|
892
|
+
* Повторно використовує test baseline одного origin/main між PR-групами.
|
|
893
|
+
* Залежності все одно встановлюються в кожному окремому worktree.
|
|
894
|
+
* @param {string} cwd worktree
|
|
895
|
+
* @param {Map<string,object>} cache кеш за OID бази
|
|
896
|
+
* @param {typeof spawnSync} spawnFn інжект
|
|
897
|
+
* @returns {{baseline:object,cached:boolean}} baseline і ознака cache hit
|
|
898
|
+
*/
|
|
899
|
+
export function captureCachedBehaviorBaseline(cwd, cache, spawnFn = spawnSync) {
|
|
900
|
+
ensureWorktreeDependencies(cwd, spawnFn)
|
|
901
|
+
const baseOid = git(['rev-parse', BASE_REF], cwd, spawnFn).stdout.trim()
|
|
902
|
+
if (cache.has(baseOid)) return { baseline: cache.get(baseOid), cached: true }
|
|
903
|
+
const baseline = captureBehaviorBaseline(cwd, spawnFn)
|
|
904
|
+
cache.set(baseOid, baseline)
|
|
905
|
+
return { baseline, cached: false }
|
|
906
|
+
}
|
|
907
|
+
|
|
791
908
|
/**
|
|
792
909
|
* Додає до Git-state validation test script із репозиторію і changelog gate.
|
|
793
910
|
* Саме ці докази вирішують, чи приймати min-результат або ескалювати на max.
|
|
794
911
|
* @param {string} cwd worktree
|
|
795
912
|
* @param {typeof spawnSync} spawnFn інжект
|
|
796
913
|
* @param {{tests:{status:number,stdout:string,stderr:string}|null}|null} [baseline] стан origin/main
|
|
914
|
+
* @param {(stage:string)=>void} [onProgress] stage callback
|
|
797
915
|
* @returns {{ok:boolean,error?:string}} validation
|
|
798
916
|
*/
|
|
799
|
-
export function validateBehaviorState(cwd, spawnFn = spawnSync, baseline = null) {
|
|
917
|
+
export function validateBehaviorState(cwd, spawnFn = spawnSync, baseline = null, onProgress = noop) {
|
|
800
918
|
const gitState = validateGitState(cwd, spawnFn)
|
|
801
919
|
if (!gitState.ok) return gitState
|
|
802
|
-
const projectGates = validateScopedProjectGates(cwd, spawnFn)
|
|
920
|
+
const projectGates = validateScopedProjectGates(cwd, spawnFn, onProgress)
|
|
803
921
|
if (!projectGates.ok) return projectGates
|
|
804
922
|
const postGateGitState = validateGitState(cwd, spawnFn)
|
|
805
923
|
if (!postGateGitState.ok) return postGateGitState
|
|
@@ -808,6 +926,7 @@ export function validateBehaviorState(cwd, spawnFn = spawnSync, baseline = null)
|
|
|
808
926
|
if (existsSync(packageJsonPath)) {
|
|
809
927
|
const packageJson = parseJson(readFileSync(packageJsonPath, 'utf8'), {})
|
|
810
928
|
if (packageJson?.scripts?.test) {
|
|
929
|
+
onProgress('project tests')
|
|
811
930
|
const tests = run('bun', ['run', 'test'], cwd, spawnFn, { allowFailure: true })
|
|
812
931
|
if (!acceptsTestOutcome(baseline?.tests ?? null, tests)) {
|
|
813
932
|
return { ok: false, error: `bun run test: ${tests.stderr || tests.stdout}` }
|
|
@@ -815,6 +934,7 @@ export function validateBehaviorState(cwd, spawnFn = spawnSync, baseline = null)
|
|
|
815
934
|
}
|
|
816
935
|
}
|
|
817
936
|
|
|
937
|
+
onProgress('changelog')
|
|
818
938
|
const changelog = run('npx', ['@7n/rules', 'lint', 'changelog', '--no-fix'], cwd, spawnFn, { allowFailure: true })
|
|
819
939
|
if (changelog.status !== 0) {
|
|
820
940
|
return { ok: false, error: `changelog gate: ${changelog.stderr || changelog.stdout}` }
|
|
@@ -822,6 +942,27 @@ export function validateBehaviorState(cwd, spawnFn = spawnSync, baseline = null)
|
|
|
822
942
|
return { ok: true }
|
|
823
943
|
}
|
|
824
944
|
|
|
945
|
+
/**
|
|
946
|
+
* Фінальний domain gate охоплює non-code зміни, зокрема workflows, dependency
|
|
947
|
+
* manifests і правила. Code directories уже пройшли scoped lint і tests.
|
|
948
|
+
* @param {string} cwd worktree
|
|
949
|
+
* @param {typeof spawnSync} spawnFn інжект
|
|
950
|
+
* @returns {{ok:boolean,error?:string}} gate
|
|
951
|
+
*/
|
|
952
|
+
export function validateFinalProjectGates(cwd, spawnFn = spawnSync) {
|
|
953
|
+
for (const path of changedNonCodeDirectories(cwd, spawnFn)) {
|
|
954
|
+
const lint = run('npx', ['@7n/rules', 'lint', '--path', path, '--no-fix'], cwd, spawnFn, {
|
|
955
|
+
allowFailure: true
|
|
956
|
+
})
|
|
957
|
+
if (lint.status !== 0) return { ok: false, error: `domain lint (${path}): ${lint.stderr || lint.stdout}` }
|
|
958
|
+
}
|
|
959
|
+
const changelog = run('npx', ['@7n/rules', 'lint', 'changelog', '--no-fix'], cwd, spawnFn, {
|
|
960
|
+
allowFailure: true
|
|
961
|
+
})
|
|
962
|
+
if (changelog.status !== 0) return { ok: false, error: `changelog gate: ${changelog.stderr || changelog.stdout}` }
|
|
963
|
+
return { ok: true }
|
|
964
|
+
}
|
|
965
|
+
|
|
825
966
|
/**
|
|
826
967
|
* Просить LLM розв'язати лише вже матеріалізований конфлікт.
|
|
827
968
|
* @param {object} args контекст
|
|
@@ -868,13 +1009,7 @@ async function applySource(args) {
|
|
|
868
1009
|
throw new Error(`cherry-pick ${oid}: ${result.stderr || result.stdout}`)
|
|
869
1010
|
}
|
|
870
1011
|
await resolveConflict({ runner, source: oid, worktreeCwd, deps, spawnFn, log, onProgress })
|
|
871
|
-
|
|
872
|
-
git(['rev-parse', '-q', '--verify', 'CHERRY_PICK_HEAD'], worktreeCwd, spawnFn, {
|
|
873
|
-
allowFailure: true
|
|
874
|
-
}).status === 0
|
|
875
|
-
) {
|
|
876
|
-
git(['cherry-pick', '--continue'], worktreeCwd, spawnFn)
|
|
877
|
-
}
|
|
1012
|
+
finishCherryPick(worktreeCwd, spawnFn)
|
|
878
1013
|
}
|
|
879
1014
|
return
|
|
880
1015
|
}
|
|
@@ -918,7 +1053,8 @@ async function finalizeBehavior(args) {
|
|
|
918
1053
|
log,
|
|
919
1054
|
label: `behavior ${source}`,
|
|
920
1055
|
onAttempt: ({ tier }) => onProgress('behavior validation', tier),
|
|
921
|
-
validate: () =>
|
|
1056
|
+
validate: () =>
|
|
1057
|
+
validateBehaviorState(worktreeCwd, spawnFn, baseline, step => onProgress(`behavior validation: ${step}`))
|
|
922
1058
|
})
|
|
923
1059
|
if (!outcome.ok) throw new Error(`LLM behavioral verification: ${outcome.error}`)
|
|
924
1060
|
return outcome.text.slice(0, PROMPT_TEXT_LIMIT)
|
|
@@ -931,7 +1067,7 @@ async function finalizeBehavior(args) {
|
|
|
931
1067
|
* @returns {Promise<{status:string,url?:string,branch?:string,error?:string,worktree?:string}>} результат
|
|
932
1068
|
*/
|
|
933
1069
|
async function createPullRequest(args) {
|
|
934
|
-
const { candidate, group, runner, rootCwd, deps, spawnFn, log, onProgress = noop } = args
|
|
1070
|
+
const { candidate, group, runner, rootCwd, baselineCache, deps, spawnFn, log, onProgress = noop } = args
|
|
935
1071
|
const source = candidate.source
|
|
936
1072
|
const validCommitIds = new Set(candidate.commits?.map(commit => commit.oid))
|
|
937
1073
|
const commits = source.startsWith(SOURCE_BRANCH_PREFIX)
|
|
@@ -943,10 +1079,17 @@ async function createPullRequest(args) {
|
|
|
943
1079
|
|
|
944
1080
|
onProgress('worktree')
|
|
945
1081
|
const worktree = createReconcileWorktree(group.title, source, rootCwd, spawnFn)
|
|
1082
|
+
let createdPr
|
|
946
1083
|
log(`🌿 ${source} → ${worktree.branch}`)
|
|
947
1084
|
try {
|
|
948
|
-
|
|
949
|
-
|
|
1085
|
+
const sourceMayChangeCode = (candidate.changedFiles ?? []).some(path => SOURCE_CODE_RE.test(path))
|
|
1086
|
+
let baseline = null
|
|
1087
|
+
if (sourceMayChangeCode) {
|
|
1088
|
+
onProgress('baseline tests')
|
|
1089
|
+
const captured = captureCachedBehaviorBaseline(worktree.cwd, baselineCache, spawnFn)
|
|
1090
|
+
baseline = captured.baseline
|
|
1091
|
+
if (captured.cached) onProgress('baseline tests (cached)')
|
|
1092
|
+
}
|
|
950
1093
|
onProgress('apply')
|
|
951
1094
|
await applySource({
|
|
952
1095
|
source,
|
|
@@ -960,17 +1103,25 @@ async function createPullRequest(args) {
|
|
|
960
1103
|
log,
|
|
961
1104
|
onProgress
|
|
962
1105
|
})
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1106
|
+
if (!hasChangesFromBase(worktree.cwd, spawnFn)) {
|
|
1107
|
+
onProgress('remove no-op worktree')
|
|
1108
|
+
removeReconcileWorktree(worktree, rootCwd, spawnFn)
|
|
1109
|
+
return { status: 'patch-equivalent', branch: worktree.branch }
|
|
1110
|
+
}
|
|
1111
|
+
const verification =
|
|
1112
|
+
changedSourceDirectories(worktree.cwd, spawnFn).length > 0
|
|
1113
|
+
? await finalizeBehavior({
|
|
1114
|
+
runner,
|
|
1115
|
+
source,
|
|
1116
|
+
rationale: group.rationale ?? candidate.rationale ?? '',
|
|
1117
|
+
worktreeCwd: worktree.cwd,
|
|
1118
|
+
baseline,
|
|
1119
|
+
deps,
|
|
1120
|
+
spawnFn,
|
|
1121
|
+
log,
|
|
1122
|
+
onProgress
|
|
1123
|
+
})
|
|
1124
|
+
: 'Додатковий behavioral LLM не потрібен: code paths не змінено.'
|
|
974
1125
|
onProgress('Git validation')
|
|
975
1126
|
const unresolved = unresolvedFiles(worktree.cwd, spawnFn)
|
|
976
1127
|
if (unresolved.length > 0) throw new Error(`Нерозв'язані конфлікти: ${unresolved.join(', ')}`)
|
|
@@ -981,15 +1132,14 @@ async function createPullRequest(args) {
|
|
|
981
1132
|
allowFailure: true
|
|
982
1133
|
}).status !== 0
|
|
983
1134
|
if (staged) git(['commit', '-m', group.title], worktree.cwd, spawnFn)
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
allowFailure: true
|
|
989
|
-
})
|
|
990
|
-
if (changelog.status !== 0) {
|
|
991
|
-
throw new Error(`changelog gate: ${changelog.stderr || changelog.stdout}`)
|
|
1135
|
+
if (!hasChangesFromBase(worktree.cwd, spawnFn)) {
|
|
1136
|
+
onProgress('remove no-op worktree')
|
|
1137
|
+
removeReconcileWorktree(worktree, rootCwd, spawnFn)
|
|
1138
|
+
return { status: 'patch-equivalent', branch: worktree.branch }
|
|
992
1139
|
}
|
|
1140
|
+
onProgress('delta lint')
|
|
1141
|
+
const finalGates = validateFinalProjectGates(worktree.cwd, spawnFn)
|
|
1142
|
+
if (!finalGates.ok) throw new Error(finalGates.error)
|
|
993
1143
|
git(['diff', '--check', `${BASE_REF}...HEAD`], worktree.cwd, spawnFn)
|
|
994
1144
|
onProgress('push')
|
|
995
1145
|
git(['push', '-u', 'origin', worktree.branch], worktree.cwd, spawnFn)
|
|
@@ -1002,28 +1152,28 @@ async function createPullRequest(args) {
|
|
|
1002
1152
|
'',
|
|
1003
1153
|
'Перевірки:',
|
|
1004
1154
|
'- `git diff --check origin/main...HEAD`',
|
|
1155
|
+
'- scoped code lint/tests та domain lint для non-code paths',
|
|
1005
1156
|
'- `npx @7n/rules lint changelog --no-fix`',
|
|
1006
1157
|
verification ? `- LLM behavioral verification: ${verification.slice(0, 1000)}` : ''
|
|
1007
1158
|
]
|
|
1008
1159
|
.filter(Boolean)
|
|
1009
1160
|
.join('\n')
|
|
1010
|
-
|
|
1161
|
+
createdPr = run(
|
|
1011
1162
|
'gh',
|
|
1012
1163
|
['pr', 'create', '--base', 'main', '--head', worktree.branch, '--title', group.title, '--body', body],
|
|
1013
1164
|
worktree.cwd,
|
|
1014
1165
|
spawnFn
|
|
1015
1166
|
).stdout.trim()
|
|
1016
1167
|
onProgress('remove worktree')
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
})
|
|
1020
|
-
return { status: 'pr-created', url: pr, branch: worktree.branch }
|
|
1168
|
+
removeReconcileWorktree(worktree, rootCwd, spawnFn)
|
|
1169
|
+
return { status: 'pr-created', url: createdPr, branch: worktree.branch }
|
|
1021
1170
|
} catch (error) {
|
|
1022
1171
|
return {
|
|
1023
1172
|
status: 'failed',
|
|
1024
1173
|
error: error instanceof Error ? error.message : String(error),
|
|
1025
1174
|
branch: worktree.branch,
|
|
1026
|
-
worktree: worktree.cwd
|
|
1175
|
+
worktree: worktree.cwd,
|
|
1176
|
+
...(createdPr && { url: createdPr })
|
|
1027
1177
|
}
|
|
1028
1178
|
}
|
|
1029
1179
|
}
|
|
@@ -1044,19 +1194,21 @@ export function cleanupSource(candidate, rootCwd, spawnFn = spawnSync) {
|
|
|
1044
1194
|
.filter(Boolean)
|
|
1045
1195
|
.map(line => line.split('\0'))
|
|
1046
1196
|
const row = rows.find(([, oid]) => oid === candidate.oid)
|
|
1047
|
-
if (!row) return { status: 'already-removed' }
|
|
1197
|
+
if (!row) return { status: 'already-removed', removedRefs: [] }
|
|
1048
1198
|
git(['stash', 'drop', row[0]], rootCwd, spawnFn)
|
|
1049
|
-
return { status: 'removed' }
|
|
1199
|
+
return { status: 'removed', removedRefs: [row[0]] }
|
|
1050
1200
|
}
|
|
1051
1201
|
|
|
1202
|
+
const removedRefs = []
|
|
1052
1203
|
for (const ref of candidate.aliases ?? [candidate.ref]) {
|
|
1053
1204
|
if (ref.startsWith('refs/remotes/origin/')) {
|
|
1054
1205
|
git(['push', 'origin', '--delete', branchName(ref)], rootCwd, spawnFn)
|
|
1055
1206
|
} else if (ref.startsWith('refs/heads/')) {
|
|
1056
1207
|
git(['branch', '-D', branchName(ref)], rootCwd, spawnFn)
|
|
1057
1208
|
}
|
|
1209
|
+
removedRefs.push(ref)
|
|
1058
1210
|
}
|
|
1059
|
-
return { status: 'removed' }
|
|
1211
|
+
return { status: 'removed', removedRefs }
|
|
1060
1212
|
} catch (error) {
|
|
1061
1213
|
return {
|
|
1062
1214
|
status: 'cleanup-failed',
|
|
@@ -1065,6 +1217,17 @@ export function cleanupSource(candidate, rootCwd, spawnFn = spawnSync) {
|
|
|
1065
1217
|
}
|
|
1066
1218
|
}
|
|
1067
1219
|
|
|
1220
|
+
/**
|
|
1221
|
+
* Форматує aliases, які cleanup фактично видалив.
|
|
1222
|
+
* @param {object|undefined} cleanup cleanup result
|
|
1223
|
+
* @returns {string} report suffix
|
|
1224
|
+
*/
|
|
1225
|
+
function formatRemovedRefs(cleanup) {
|
|
1226
|
+
if (!cleanup?.removedRefs?.length) return ''
|
|
1227
|
+
const refs = cleanup.removedRefs.map(ref => `\`${ref}\``).join(',')
|
|
1228
|
+
return `; refs=${refs}`
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1068
1231
|
/**
|
|
1069
1232
|
* Формує deterministic report.
|
|
1070
1233
|
* @param {{inventory:object,results:Array<object>}} args дані
|
|
@@ -1078,16 +1241,21 @@ export function formatReport({ inventory, results }) {
|
|
|
1078
1241
|
if (branch.pr?.url) suffix = ` — ${branch.pr.url}`
|
|
1079
1242
|
else if (branch.worktree) suffix = ` — ${branch.worktree}`
|
|
1080
1243
|
const cleanupOid = branch.oid ? `; oid=${branch.oid}` : ''
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1244
|
+
const removedRefs = formatRemovedRefs(branch.cleanup)
|
|
1245
|
+
const cleanup = branch.cleanup ? `; cleanup=${branch.cleanup.status}${cleanupOid}${removedRefs}` : ''
|
|
1246
|
+
lines.push(`- \`${branch.source ?? branch.name}\`: ${branch.state}${cleanup}${suffix}`)
|
|
1083
1247
|
}
|
|
1084
1248
|
for (const result of results) {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1249
|
+
const details = [
|
|
1250
|
+
result.url,
|
|
1251
|
+
result.error,
|
|
1252
|
+
result.rationale,
|
|
1253
|
+
result.worktree && `worktree=${result.worktree}`
|
|
1254
|
+
].filter(Boolean)
|
|
1255
|
+
const suffix = details.length > 0 ? ` — ${details.join('; ')}` : ''
|
|
1089
1256
|
const cleanupOid = result.oid ? `; oid=${result.oid}` : ''
|
|
1090
|
-
const
|
|
1257
|
+
const removedRefs = formatRemovedRefs(result.cleanup)
|
|
1258
|
+
const cleanup = result.cleanup ? `; cleanup=${result.cleanup.status}${cleanupOid}${removedRefs}` : ''
|
|
1091
1259
|
lines.push(`- \`${result.source}\`: ${result.status}${cleanup}${suffix}`)
|
|
1092
1260
|
}
|
|
1093
1261
|
for (const warning of inventory.warnings) lines.push(`- ⚠️ ${warning}`)
|
|
@@ -1150,7 +1318,7 @@ async function triageCandidates(args) {
|
|
|
1150
1318
|
* @returns {Promise<Array<object>>} results одного source
|
|
1151
1319
|
*/
|
|
1152
1320
|
async function materializeDecision(args) {
|
|
1153
|
-
const { decision, candidate, runner, rootCwd, deps, spawnFn, log, createPr, progress, prState } = args
|
|
1321
|
+
const { decision, candidate, runner, rootCwd, baselineCache, deps, spawnFn, log, createPr, progress, prState } = args
|
|
1154
1322
|
if (decision.action !== 'pr') {
|
|
1155
1323
|
return [
|
|
1156
1324
|
{
|
|
@@ -1175,6 +1343,7 @@ async function materializeDecision(args) {
|
|
|
1175
1343
|
group,
|
|
1176
1344
|
runner,
|
|
1177
1345
|
rootCwd,
|
|
1346
|
+
baselineCache,
|
|
1178
1347
|
deps,
|
|
1179
1348
|
spawnFn,
|
|
1180
1349
|
log,
|
|
@@ -1239,8 +1408,10 @@ function cleanupMaterializedSources(args) {
|
|
|
1239
1408
|
for (const [source, candidate] of bySource) {
|
|
1240
1409
|
const sourceResults = results.filter(result => result.source === source)
|
|
1241
1410
|
const dropped = sourceResults.some(result => result.status === 'drop-recommended')
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1411
|
+
const allTransferred =
|
|
1412
|
+
sourceResults.length > 0 &&
|
|
1413
|
+
sourceResults.every(result => ['pr-created', 'patch-equivalent'].includes(result.status))
|
|
1414
|
+
if (!dropped && !allTransferred) continue
|
|
1244
1415
|
cleanupState.index += 1
|
|
1245
1416
|
const key = `cleanup-${cleanupState.index}`
|
|
1246
1417
|
progress.step(key, source)
|
|
@@ -1269,8 +1440,10 @@ function countCleanupSources(inventory, bySource, results) {
|
|
|
1269
1440
|
.filter(source => {
|
|
1270
1441
|
const sourceResults = results.filter(result => result.source === source)
|
|
1271
1442
|
const dropped = sourceResults.some(result => result.status === 'drop-recommended')
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1443
|
+
const allTransferred =
|
|
1444
|
+
sourceResults.length > 0 &&
|
|
1445
|
+
sourceResults.every(result => ['pr-created', 'patch-equivalent'].includes(result.status))
|
|
1446
|
+
return dropped || allTransferred
|
|
1274
1447
|
})
|
|
1275
1448
|
.toArray().length
|
|
1276
1449
|
return inactive + reviewed
|
|
@@ -1294,6 +1467,7 @@ export async function runGitReconcileOrchestrator(options = {}) {
|
|
|
1294
1467
|
const reporterFactory = deps.createProgressReporter ?? createProgressReporter
|
|
1295
1468
|
const isTTY = options.isTTY ?? process.stdout.isTTY === true
|
|
1296
1469
|
const now = deps.now ?? (() => performance.now())
|
|
1470
|
+
const baselineCache = new Map()
|
|
1297
1471
|
|
|
1298
1472
|
const inventoryStartedAt = now()
|
|
1299
1473
|
log('⏳ 1/4 inventory')
|
|
@@ -1349,6 +1523,7 @@ export async function runGitReconcileOrchestrator(options = {}) {
|
|
|
1349
1523
|
candidates,
|
|
1350
1524
|
runner,
|
|
1351
1525
|
rootCwd,
|
|
1526
|
+
baselineCache,
|
|
1352
1527
|
deps,
|
|
1353
1528
|
spawnFn,
|
|
1354
1529
|
log,
|