@7n/rules 1.49.21 → 1.49.23
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.23] - 2026-07-27
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Treat reconcile diffs containing only release entries as patch-equivalent
|
|
8
|
+
|
|
9
|
+
## [1.49.22] - 2026-07-27
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Retry initial PR check discovery and prune rebuildable forensic dependencies
|
|
14
|
+
|
|
3
15
|
## [1.49.21] - 2026-07-27
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/package.json
CHANGED
|
@@ -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:
|
|
6
|
+
crc: 69749e90
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 100
|
|
9
9
|
---
|
|
@@ -20,11 +20,13 @@ docgen:
|
|
|
20
20
|
|
|
21
21
|
Для корисної групи JS створює ізольований worktree від policy base, застосовує commits або stash і відсікає порожній tree diff. `captureCachedBehaviorBaseline` кешує Promise baseline за base OID, щоб concurrent PR-групи не дублювали test run. `validateBehaviorState` перевіряє Git state, scoped docs/lint, tests відносно baseline і changelog. `remediateBehaviorState` запускає canonical fixers для code та non-code directories; після remediation final gates повторюються у read-only режимі.
|
|
22
22
|
|
|
23
|
-
`runAsync` запускає install, tests, lint і PR checks без блокування event loop. Після push та `gh pr create` функція `verifyPullRequestReadiness` очікує terminal checks і передає набори PR/base checks у `classifyPullRequestChecks`. Успішний outcome `pr-created` повертається лише для `ready`; regression, baseline-red, pending, timeout або unreadable checks зберігають branch, URL і worktree та блокують cleanup.
|
|
23
|
+
`runAsync` запускає install, tests, lint і PR checks без блокування event loop. Після push та `gh pr create` функція `verifyPullRequestReadiness` очікує terminal checks і передає набори PR/base checks у `classifyPullRequestChecks`. Якщо initial rollup порожній, orchestration дає GitHub коротке вікно на реєстрацію checks і повторює watch. Успішний outcome `pr-created` повертається лише для `ready`; regression, baseline-red, pending, timeout або unreadable checks зберігають branch, URL і worktree та блокують cleanup.
|
|
24
24
|
|
|
25
25
|
`commitPendingChanges` створює додатковий commit лише для staged remediation. Чистий index після перенесення branch-source є валідним, коли корисні commits уже присутні в `HEAD`. Порожній PR check rollup вважається непідтвердженим, а не успішним.
|
|
26
26
|
|
|
27
|
-
`
|
|
27
|
+
`hasOnlyChangeEntries` відсікає tree diff, у якому лишилися тільки `*/.changes/*.md`: такий результат вважається `patch-equivalent`, не створює PR і дозволяє cleanup source branch.
|
|
28
|
+
|
|
29
|
+
`pruneForensicDependencies` прибирає лише rebuildable `node_modules` зі збереженого forensic worktree. `cleanupSource` видаляє лише точний доказово безпечний branch/stash. Cleanup review-source дозволений після `drop` або коли всі його групи завершились `pr-created` чи `patch-equivalent`; `failed` і всі `pr-checks-*` outcomes лишають source та forensic worktree.
|
|
28
30
|
|
|
29
31
|
## Публічний API
|
|
30
32
|
|
|
@@ -36,6 +38,8 @@ docgen:
|
|
|
36
38
|
- `captureBehaviorBaseline`, `captureCachedBehaviorBaseline` — фіксують і кешують test baseline.
|
|
37
39
|
- `validateBehaviorState`, `validateFinalProjectGates`, `remediateBehaviorState` — виконують behavioral та canonical gates.
|
|
38
40
|
- `commitPendingChanges` — комітить лише staged remediation, не вимагаючи порожнього commit для вже перенесених commits.
|
|
41
|
+
- `hasOnlyChangeEntries` — розпізнає release-metadata-only diff, який не потребує PR.
|
|
42
|
+
- `pruneForensicDependencies` — звільняє rebuildable dependencies без втрати Git evidence.
|
|
39
43
|
- `classifyPullRequestChecks`, `verifyPullRequestReadiness` — класифікують CI відносно base commit.
|
|
40
44
|
- `formatOutcomeCounts`, `formatReport` — формують точний deterministic summary.
|
|
41
45
|
- `cleanupSource`, `runGitReconcileOrchestrator` — виконують безпечний cleanup і координують повний flow.
|
|
@@ -47,6 +51,8 @@ docgen:
|
|
|
47
51
|
- Canonical fixers охоплюють code/non-code scope, після чого gates повторюються без fix.
|
|
48
52
|
- PR вважається створеним успішно лише після terminal green checks.
|
|
49
53
|
- Порожній PR check rollup fail-closed зберігає forensic worktree.
|
|
54
|
+
- Forensic worktree зберігає Git evidence без накопичення `node_modules`.
|
|
55
|
+
- Diff лише з `.changes` не створює PR і не блокує cleanup source branch.
|
|
50
56
|
- CI regression, baseline-red або непідтверджений стан завершують orchestration non-zero і зберігають forensic refs/worktree.
|
|
51
57
|
- Cleanup не видаляє protected, open-PR, kept, failed або `pr-checks-*` sources.
|
|
52
58
|
- `spawnSync.error`, зокрема `ENOENT`, зберігається в command diagnostics.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** @see ./docs/orchestrate.md */
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process'
|
|
3
3
|
import { once } from 'node:events'
|
|
4
|
-
import { appendFileSync, existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs'
|
|
5
5
|
import { dirname, isAbsolute, join } from 'node:path'
|
|
6
6
|
import { performance } from 'node:perf_hooks'
|
|
7
7
|
import { env } from 'node:process'
|
|
8
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
8
9
|
|
|
9
10
|
import { renderProgressLine } from '../../../scripts/lib/lint-surface/progress.mjs'
|
|
10
11
|
import { readGitPolicy } from '../../../scripts/lib/git-policy.mjs'
|
|
@@ -24,6 +25,7 @@ const REF_HEADS_RE = /^refs\/heads\//
|
|
|
24
25
|
const REF_ORIGIN_RE = /^refs\/remotes\/origin\//
|
|
25
26
|
const RENAME_DELETE_CONFLICT_RE = /^CONFLICT \(rename\/delete\): .+? renamed to (.+?) in .+?, but deleted/
|
|
26
27
|
const SOURCE_CODE_RE = /\.(?:js|mjs|ts|vue|rs|py)$/
|
|
28
|
+
const CHANGE_ENTRY_RE = /(^|\/)\.changes\/[^/]+\.md$/
|
|
27
29
|
const WHITESPACE_RE = /\s+/
|
|
28
30
|
const ACP_PROGRESS_ENV = 'N_LLM_ACP_PROGRESS'
|
|
29
31
|
const REF_INVENTORY_FORMAT = ['%(refname)', '%00', '%(object', 'name)', '%00', '%(committer', 'date:iso-strict)'].join(
|
|
@@ -1007,6 +1009,16 @@ export function sourceDirectories(paths) {
|
|
|
1007
1009
|
return [...new Set(paths.filter(path => SOURCE_CODE_RE.test(path)).map(path => dirname(path)))].toSorted()
|
|
1008
1010
|
}
|
|
1009
1011
|
|
|
1012
|
+
/**
|
|
1013
|
+
* Визначає технічний залишок, який містить лише release entries. Такі файли
|
|
1014
|
+
* не доводять окремої корисної поведінки й не мають породжувати PR.
|
|
1015
|
+
* @param {string[]} paths змінені шляхи відносно policy base
|
|
1016
|
+
* @returns {boolean} чи всі зміни лежать безпосередньо у `.changes/`
|
|
1017
|
+
*/
|
|
1018
|
+
export function hasOnlyChangeEntries(paths) {
|
|
1019
|
+
return paths.length > 0 && paths.every(path => CHANGE_ENTRY_RE.test(path))
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1010
1022
|
/**
|
|
1011
1023
|
* Збирає tracked і untracked code directories відносно policy base ref.
|
|
1012
1024
|
* @param {string} cwd worktree
|
|
@@ -1021,6 +1033,40 @@ function changedPaths(cwd, spawnFn) {
|
|
|
1021
1033
|
return [...new Set([...tracked, ...untracked])]
|
|
1022
1034
|
}
|
|
1023
1035
|
|
|
1036
|
+
/**
|
|
1037
|
+
* @param {string} cwd worktree
|
|
1038
|
+
* @param {typeof spawnSync} spawnFn інжект
|
|
1039
|
+
* @returns {boolean} чи tree diff складається лише з release entries
|
|
1040
|
+
*/
|
|
1041
|
+
function hasOnlyChangeEntriesFromBase(cwd, spawnFn) {
|
|
1042
|
+
return hasOnlyChangeEntries(changedPaths(cwd, spawnFn))
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Прибирає no-op або change-only worktree до дорогих behavioral/CI gates.
|
|
1047
|
+
* @param {object} args контекст materialized worktree
|
|
1048
|
+
* @returns {{status:string,branch:string,rationale?:string}|null} terminal outcome
|
|
1049
|
+
*/
|
|
1050
|
+
function discardPatchEquivalentWorktree(args) {
|
|
1051
|
+
const { worktree, rootCwd, spawnFn, onProgress, validated = false } = args
|
|
1052
|
+
if (!hasChangesFromBase(worktree.cwd, spawnFn)) {
|
|
1053
|
+
onProgress('remove no-op worktree')
|
|
1054
|
+
removeReconcileWorktree(worktree, rootCwd, spawnFn)
|
|
1055
|
+
return { status: 'patch-equivalent', branch: worktree.branch }
|
|
1056
|
+
}
|
|
1057
|
+
if (!hasOnlyChangeEntriesFromBase(worktree.cwd, spawnFn)) return null
|
|
1058
|
+
|
|
1059
|
+
onProgress('remove change-only worktree')
|
|
1060
|
+
removeReconcileWorktree(worktree, rootCwd, spawnFn)
|
|
1061
|
+
return {
|
|
1062
|
+
status: 'patch-equivalent',
|
|
1063
|
+
branch: worktree.branch,
|
|
1064
|
+
rationale: validated
|
|
1065
|
+
? 'Після Git validation лишилися тільки release entries у .changes/'
|
|
1066
|
+
: 'Після перенесення лишилися тільки release entries у .changes/'
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1024
1070
|
/**
|
|
1025
1071
|
* Повертає директорії зміненого коду.
|
|
1026
1072
|
* @param {string} cwd worktree
|
|
@@ -1456,19 +1502,36 @@ export function classifyPullRequestChecks(prChecks, baseChecks) {
|
|
|
1456
1502
|
}
|
|
1457
1503
|
}
|
|
1458
1504
|
|
|
1505
|
+
/**
|
|
1506
|
+
* Видаляє лише rebuildable dependencies зі збереженого forensic worktree.
|
|
1507
|
+
* Git metadata, commits і tracked/untracked зміни не зачіпаються.
|
|
1508
|
+
* @param {string} worktreeCwd шлях worktree
|
|
1509
|
+
* @returns {boolean} чи було що прибрати
|
|
1510
|
+
*/
|
|
1511
|
+
export function pruneForensicDependencies(worktreeCwd) {
|
|
1512
|
+
const dependencies = join(worktreeCwd, 'node_modules')
|
|
1513
|
+
if (!existsSync(dependencies)) return false
|
|
1514
|
+
try {
|
|
1515
|
+
rmSync(dependencies, { recursive: true, force: true })
|
|
1516
|
+
return true
|
|
1517
|
+
} catch {
|
|
1518
|
+
return false
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1459
1522
|
/**
|
|
1460
1523
|
* Чекає terminal CI state й порівнює failed checks з base commit.
|
|
1461
1524
|
* @param {object} args PR context
|
|
1462
1525
|
* @returns {Promise<{status:string,error?:string}>} readiness
|
|
1463
1526
|
*/
|
|
1464
1527
|
export async function verifyPullRequestReadiness(args) {
|
|
1465
|
-
const { url, cwd, spawnFn = spawnSync, asyncSpawnFn } = args
|
|
1528
|
+
const { url, cwd, spawnFn = spawnSync, asyncSpawnFn, delayFn = delay } = args
|
|
1466
1529
|
const longRunner = resolveAsyncSpawn(spawnFn, asyncSpawnFn)
|
|
1467
1530
|
await runAsync('gh', ['pr', 'checks', url, '--watch', '--interval', '10'], cwd, longRunner, {
|
|
1468
1531
|
allowFailure: true,
|
|
1469
1532
|
timeoutMs: PR_CHECK_TIMEOUT_MS
|
|
1470
1533
|
})
|
|
1471
|
-
|
|
1534
|
+
let view = await runAsync(
|
|
1472
1535
|
'gh',
|
|
1473
1536
|
['pr', 'view', url, '--json', 'statusCheckRollup,baseRefOid'],
|
|
1474
1537
|
cwd,
|
|
@@ -1478,7 +1541,28 @@ export async function verifyPullRequestReadiness(args) {
|
|
|
1478
1541
|
if (view.status !== 0) {
|
|
1479
1542
|
return { status: 'pr-checks-unverified', error: `Не вдалося прочитати PR checks: ${view.stderr || view.error}` }
|
|
1480
1543
|
}
|
|
1481
|
-
|
|
1544
|
+
let pr = parseJson(view.stdout, null)
|
|
1545
|
+
if (pr && Array.isArray(pr.statusCheckRollup) && pr.statusCheckRollup.length === 0) {
|
|
1546
|
+
await delayFn(10_000)
|
|
1547
|
+
await runAsync('gh', ['pr', 'checks', url, '--watch', '--interval', '10'], cwd, longRunner, {
|
|
1548
|
+
allowFailure: true,
|
|
1549
|
+
timeoutMs: PR_CHECK_TIMEOUT_MS
|
|
1550
|
+
})
|
|
1551
|
+
view = await runAsync(
|
|
1552
|
+
'gh',
|
|
1553
|
+
['pr', 'view', url, '--json', 'statusCheckRollup,baseRefOid'],
|
|
1554
|
+
cwd,
|
|
1555
|
+
longRunner,
|
|
1556
|
+
{ allowFailure: true }
|
|
1557
|
+
)
|
|
1558
|
+
if (view.status !== 0) {
|
|
1559
|
+
return {
|
|
1560
|
+
status: 'pr-checks-unverified',
|
|
1561
|
+
error: `Не вдалося повторно прочитати PR checks: ${view.stderr || view.error}`
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
pr = parseJson(view.stdout, null)
|
|
1565
|
+
}
|
|
1482
1566
|
if (!pr || !Array.isArray(pr.statusCheckRollup) || !pr.baseRefOid) {
|
|
1483
1567
|
return { status: 'pr-checks-unverified', error: 'GitHub повернув неповний PR check rollup' }
|
|
1484
1568
|
}
|
|
@@ -1596,11 +1680,8 @@ async function createPullRequest(args) {
|
|
|
1596
1680
|
log,
|
|
1597
1681
|
onProgress
|
|
1598
1682
|
})
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
removeReconcileWorktree(worktree, rootCwd, spawnFn)
|
|
1602
|
-
return { status: 'patch-equivalent', branch: worktree.branch }
|
|
1603
|
-
}
|
|
1683
|
+
const appliedOutcome = discardPatchEquivalentWorktree({ worktree, rootCwd, spawnFn, onProgress })
|
|
1684
|
+
if (appliedOutcome) return appliedOutcome
|
|
1604
1685
|
const verification =
|
|
1605
1686
|
changedSourceDirectories(worktree.cwd, spawnFn).length > 0
|
|
1606
1687
|
? await finalizeBehavior({
|
|
@@ -1620,11 +1701,14 @@ async function createPullRequest(args) {
|
|
|
1620
1701
|
if (unresolved.length > 0) throw new Error(`Нерозв'язані конфлікти: ${unresolved.join(', ')}`)
|
|
1621
1702
|
git(['diff', '--check'], worktree.cwd, spawnFn)
|
|
1622
1703
|
git(['add', '-A'], worktree.cwd, spawnFn)
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1704
|
+
const validatedOutcome = discardPatchEquivalentWorktree({
|
|
1705
|
+
worktree,
|
|
1706
|
+
rootCwd,
|
|
1707
|
+
spawnFn,
|
|
1708
|
+
onProgress,
|
|
1709
|
+
validated: true
|
|
1710
|
+
})
|
|
1711
|
+
if (validatedOutcome) return validatedOutcome
|
|
1628
1712
|
onProgress('delta lint')
|
|
1629
1713
|
const finalGates = await passFinalProjectGates({
|
|
1630
1714
|
cwd: worktree.cwd,
|
|
@@ -1669,6 +1753,8 @@ async function createPullRequest(args) {
|
|
|
1669
1753
|
asyncSpawnFn: deps.asyncSpawnFn
|
|
1670
1754
|
})
|
|
1671
1755
|
if (readiness.status !== 'ready') {
|
|
1756
|
+
onProgress('prune forensic dependencies')
|
|
1757
|
+
pruneForensicDependencies(worktree.cwd)
|
|
1672
1758
|
return {
|
|
1673
1759
|
status: readiness.status,
|
|
1674
1760
|
error: readiness.error,
|
|
@@ -1682,6 +1768,10 @@ async function createPullRequest(args) {
|
|
|
1682
1768
|
return { status: 'pr-created', url: createdPr, branch: worktree.branch }
|
|
1683
1769
|
} catch (error) {
|
|
1684
1770
|
const setup = /** @type {{branch?:string,worktree?:string}} */ (error)
|
|
1771
|
+
if (worktree?.cwd) {
|
|
1772
|
+
onProgress('prune forensic dependencies')
|
|
1773
|
+
pruneForensicDependencies(worktree.cwd)
|
|
1774
|
+
}
|
|
1685
1775
|
return {
|
|
1686
1776
|
status: 'failed',
|
|
1687
1777
|
error: error instanceof Error ? error.message : String(error),
|