@7n/rules 1.49.7 → 1.49.9
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 +12 -0
- package/package.json +1 -1
- package/rules/changelog/consistency/comparison-models.mdc +5 -5
- package/rules/changelog/consistency/docs/index.md +10 -0
- package/rules/changelog/consistency/docs/main.md +16 -24
- package/rules/changelog/consistency/main.mjs +18 -19
- package/rules/release/release.mjs +14 -1
- package/schemas/n-rules.json +18 -0
- package/scripts/lib/changed-files.mjs +17 -14
- package/scripts/lib/docs/changed-files.md +35 -18
- package/scripts/lib/docs/git-policy.md +34 -0
- package/scripts/lib/docs/index.md +1 -0
- package/scripts/lib/git-policy.mjs +45 -0
- package/skills/git-reconcile/js/orchestrate.mjs +40 -28
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
|
|
16
16
|
### local-only
|
|
17
17
|
|
|
18
|
-
**npm:** `private: true` або без `files`. **Python:** без пари name+version для реєстру.
|
|
18
|
+
**npm:** `private: true` або без `files`. **Python:** без пари name+version для реєстру. Гілки визначає `.n-rules.json:git`: `baseBranch` (за замовчуванням `main`) і `releaseBranches` (за замовчуванням `["main"]`). На обох значеннях за замовчуванням блок `git` у конфіг не пишуть.
|
|
19
19
|
|
|
20
|
-
1. На **`
|
|
21
|
-
2. На **`
|
|
22
|
-
3. На
|
|
20
|
+
1. На **`baseBranch`** local-only не активний (крім незакомічених registry-published).
|
|
21
|
+
2. На гілці з **`releaseBranches`** — diff від її `origin/<branch>`; без remote — від `HEAD~1`.
|
|
22
|
+
3. На feature-гілці — merge-base з кандидатами `baseBranch` і `releaseBranches`. Для кожного кандидата локальна та `origin/`-версії порівнюються, береться новіша спільна база; порядок кандидатів детермінований конфігом.
|
|
23
23
|
4. Drift `version` від бази → **fail** (ручний bump заборонено). Зміни фіксуй change-файлом; bump зробить CI.
|
|
24
24
|
|
|
25
|
-
Якщо немає git або
|
|
25
|
+
Якщо немає git або refs із Git policy — local-only пропускається.
|
|
26
26
|
|
|
27
27
|
Merge-коміт (готовий, з другим предком, або `MERGE_HEAD` під час незавершеного `git commit`) пропускається цілком — changeset документують feature-коміти, а не інтеграційний merge.
|
|
28
28
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: npm/rules/changelog/consistency
|
|
4
|
+
resource: npm/rules/changelog/consistency/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ----------------------------------------- | --------- |
|
|
9
|
+
| [fix-consistency.mjs](fix-consistency.md) | JS Module |
|
|
10
|
+
| [main.mjs](main.md) | JS Module |
|
|
@@ -3,39 +3,31 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/changelog/consistency/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model:
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
crc: 40857795
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 95
|
|
10
|
+
issues: anchor-miss:(n-changelog.mdc),judge-refine:kept-original,judge:inaccurate:0.99
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
10
12
|
---
|
|
11
13
|
|
|
12
14
|
## Огляд
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
Цей файл гарантує готовність пакетів у монорепозиторії до публікації. Він сканує кореневі директорії, визначає локальні та зареєстровані пакети, використовуючи інформацію з конфігураційного файлу res.json. Далі він порівнює версію кожного пакета з даними, опублікованими на https://pypi.org/pypi/, та виконує локальні перевірки змін за допомогою Git.
|
|
16
|
-
|
|
17
|
-
Поведінка:
|
|
18
|
-
Ініціалізує процес перевірки, який аналізує кореневі директорії проєктів монорепозиторію. Проходиться по знайдених директоріях, визначаючись пакети з реєстру та локальні. Для кожного пакета, які можуть бути опубліковані в реєстрі, здійснюється перевірка відповідності `CHANGELOG.md` та порівняння версії з опублікованою через звернення до мережі. Якщо пакети не є опублікованими, виконується перевірка локальних змін на основі інформації з Git. Якщо виявлені зміни, і пакет не має файлу `.changes/CHANGELOG.md`, створюється цей файл з дефолтними даними. При виявленні розбіжностей версій виводиться повідомлення про заборону. Усі операції перехоплюють помилки (fail-safe), не генеруючи зовнішніх винятків, і повертають порожнє значення замість них за певних умов. Фінальний код виходу визначається на основі результатів перевірок.
|
|
16
|
+
`lint` перевіряє, чи workspace справді потребує changelog-запису або bump, щоб не реагувати на merge-сценарії, застарілу базу та шум від ігнорованих шляхів. Для published workspace він звіряє локальний стан із уже опублікованою версією. Функція працює fail-safe: перехоплює помилки, не кидає їх назовні та за окремих збоїв повертає порожнє значення замість винятку.
|
|
19
17
|
|
|
20
18
|
## Поведінка
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
7. `checkPublishedWorkspace` перевіряє наявність та відповідність `CHANGELOG.md`, а також порівнює версію пакета з опублікованою версією через мережевий запит до https://pypi.org/pypi/.
|
|
31
|
-
8. Якщо пакети не є опублікованими, викликається `runLocalOnlyChecks` для перевірки змін.
|
|
32
|
-
9. `runLocalOnlyChecks` використовує інформацію з Git для порівняння поточного стану з базою гілки.
|
|
33
|
-
10. Якщо виявлені зміни, і пакет не має файлу `.changes/CHANGELOG.md`, то в режимі автоматичного виправлення створюється цей файл з дефолтними даними.
|
|
34
|
-
11. При виявленні різниці версій, яка свідчить про ручний збій (version вищий, ніж опублікований), виводиться повідомлення про заборону.
|
|
35
|
-
12. `main` повертає код виходу на основі результатів перевірок.
|
|
20
|
+
1. Перевіряє, чи репозиторій перебуває в merge-сценарії, і якщо так — не вважає це дефектом changelog-дисципліни: merge вже несе задокументовані зміни, тому окремий change-файл тут не потрібен.
|
|
21
|
+
2. Визначає базу порівняння для changelog-навігації так, щоб брати найновішу з локальної та remote-версії базової гілки; це зменшує хибні спрацювання на застарілому локальному `main`/`dev` і коректно працює, коли remote відсутній.
|
|
22
|
+
3. Для кожного workspace оцінює, чи є релевантні зміни відносно цієї бази, і відсікає шум від ігнорованих changelog-шляхів та ignore-правил. Це потрібно, щоб перевірка реагувала лише на зміни, які справді потребують bump або change-файлу.
|
|
23
|
+
4. Для published workspace перевіряє, що changelog присутній і має базовий формат, а також що опублікована версія не відстає від локальної. Якщо версія в дереві випереджає опубліковану — це вважається ручним bump поза CI, а не штатним станом.
|
|
24
|
+
5. Якщо published workspace має релевантні Git-зміни без change-файлу, або фіксує це як помилку, або в autofix-режимі створює change-файл із автоматичним описом і ставить його в індекс, щоб коміт не падав. Для published-пакетів додатково вимагається, щоб `files` у маніфесті містив `CHANGELOG.md`, бо релізний changelog має публікуватися разом із пакетом.
|
|
25
|
+
6. Для workspace, які змінюються лише локально, звіряє версію з базою порівняння, перевіряє наявність незрелізних change-файлів і, якщо треба, також пропонує або створює change-файл. Повідомлення формуються в стилі, сумісному з правилами ``.
|
|
26
|
+
7. Резолвить опубліковану версію залежно від типу пакета: для Python орієнтується на PyPI, для інших — на npm. Пошук публікації йде через `https://pypi.org/pypi/`, а також через конфігурацію `res.json`, якщо вона визначає очікувану поведінку workspace.
|
|
27
|
+
8. Працює fail-safe: мережеві чи git-помилки не пробиваються назовні як винятки, а перетворюються на м’який результат або перевірочний фейл, щоб один збій не ламав увесь lint-прогін.
|
|
36
28
|
|
|
37
29
|
## Гарантії поведінки
|
|
38
30
|
|
|
39
|
-
-
|
|
31
|
+
- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
|
|
40
32
|
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
41
33
|
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
@@ -6,6 +6,7 @@ import { join } from 'node:path'
|
|
|
6
6
|
import { promisify } from 'node:util'
|
|
7
7
|
|
|
8
8
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
9
|
+
import { readGitPolicy } from '../../../scripts/lib/git-policy.mjs'
|
|
9
10
|
import {
|
|
10
11
|
getMonorepoProjectRootDirs,
|
|
11
12
|
manifestFilePath,
|
|
@@ -35,12 +36,6 @@ const AUTOFIX_SECTION = 'Changed'
|
|
|
35
36
|
/** Fallback-опис, коли subject останнього коміту порожній (напр. порожній репозиторій). */
|
|
36
37
|
const AUTOFIX_FALLBACK_MESSAGE = 'оновлення'
|
|
37
38
|
|
|
38
|
-
/** Кандидати інтеграційними тести гілки для feature-гілок (перша наявна; див. n-changelog.mdc). */
|
|
39
|
-
const FEATURE_BASE_BRANCH_CANDIDATES = Object.freeze(['dev', 'main'])
|
|
40
|
-
|
|
41
|
-
/** Гілка `dev`: local-only не активний (крім незакомічених registry-published). */
|
|
42
|
-
const LOCAL_ONLY_SKIP_BRANCH = 'dev'
|
|
43
|
-
|
|
44
39
|
/**
|
|
45
40
|
* Префікси шляхів (posix), які не вважаються релізними змінами — інверсія glob (n-changelog.mdc):
|
|
46
41
|
* документація (`docs/`, `doc/`) та синхронізований із `@7n/rules` інструментарій
|
|
@@ -191,28 +186,30 @@ async function resolveMergeBase(baseRef, cwd) {
|
|
|
191
186
|
* @returns {Promise<{ ref: string, label: string } | null>} результат
|
|
192
187
|
*/
|
|
193
188
|
async function resolveChangelogComparisonPoint(branch, cwd) {
|
|
194
|
-
|
|
189
|
+
const policy = readGitPolicy(cwd)
|
|
190
|
+
if (branch === policy.baseBranch) {
|
|
195
191
|
return null
|
|
196
192
|
}
|
|
197
193
|
|
|
198
|
-
if (branch
|
|
199
|
-
const
|
|
200
|
-
const
|
|
194
|
+
if (policy.releaseBranches.includes(branch ?? '')) {
|
|
195
|
+
const originRef = `origin/${branch}`
|
|
196
|
+
const originRaw = await gitOrNull(['rev-parse', '--verify', '--quiet', originRef], cwd)
|
|
197
|
+
const originSha = originRaw?.trim()
|
|
201
198
|
const headRaw = await gitOrNull(['rev-parse', 'HEAD'], cwd)
|
|
202
199
|
const headSha = headRaw?.trim()
|
|
203
|
-
if (
|
|
204
|
-
return { ref:
|
|
200
|
+
if (originSha && headSha && (originSha === headSha || (await isGitAncestor(originRef, 'HEAD', cwd)))) {
|
|
201
|
+
return { ref: originRef, label: branch }
|
|
205
202
|
}
|
|
206
203
|
const parent = await gitOrNull(['rev-parse', '--verify', '--quiet', 'HEAD~1'], cwd)
|
|
207
204
|
if (typeof parent === 'string' && parent.trim().length > 0) {
|
|
208
|
-
return { ref: parent.trim(), label:
|
|
205
|
+
return { ref: parent.trim(), label: `${branch}~1` }
|
|
209
206
|
}
|
|
210
207
|
return null
|
|
211
208
|
}
|
|
212
209
|
|
|
213
210
|
// Feature-гілка: база — новіший merge-base серед локальної та origin-версії кандидата
|
|
214
211
|
// (див. resolveNewestMergeBase; застарілий локальний main не має перекривати origin).
|
|
215
|
-
for (const name of
|
|
212
|
+
for (const name of policy.integrationBranches) {
|
|
216
213
|
const mergeBase = await resolveNewestMergeBase(name, cwd)
|
|
217
214
|
if (!mergeBase) {
|
|
218
215
|
continue
|
|
@@ -591,7 +588,8 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, _Vcurrent, sub
|
|
|
591
588
|
|
|
592
589
|
const branch = await currentBranchName(cwd)
|
|
593
590
|
|
|
594
|
-
|
|
591
|
+
const policy = readGitPolicy(cwd)
|
|
592
|
+
if (branch === policy.baseBranch) {
|
|
595
593
|
if (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces, cwd)) {
|
|
596
594
|
await fixOrFailPublishedWorkspace(manifest, label, mf, autofix, pass, fail, cwd)
|
|
597
595
|
}
|
|
@@ -604,7 +602,7 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, _Vcurrent, sub
|
|
|
604
602
|
return
|
|
605
603
|
}
|
|
606
604
|
|
|
607
|
-
if (branch
|
|
605
|
+
if (policy.releaseBranches.includes(branch ?? '') && (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces, cwd))) {
|
|
608
606
|
await fixOrFailPublishedWorkspace(manifest, label, mf, autofix, pass, fail, cwd)
|
|
609
607
|
}
|
|
610
608
|
}
|
|
@@ -732,13 +730,14 @@ async function runLocalOnlyChecks(localOnly, subWorkspaces, autofix, pass, fail,
|
|
|
732
730
|
return
|
|
733
731
|
}
|
|
734
732
|
const branch = await currentBranchName(cwd)
|
|
735
|
-
|
|
736
|
-
|
|
733
|
+
const policy = readGitPolicy(cwd)
|
|
734
|
+
if (branch === policy.baseBranch) {
|
|
735
|
+
pass(`changelog: поточна гілка = ${policy.baseBranch} — local-only перевірку пропущено`)
|
|
737
736
|
return
|
|
738
737
|
}
|
|
739
738
|
const comparison = await resolveChangelogComparisonPoint(branch, cwd)
|
|
740
739
|
if (!comparison) {
|
|
741
|
-
pass('changelog:
|
|
740
|
+
pass('changelog: Git policy refs (та origin/*) не знайдено — local-only перевірку пропущено')
|
|
742
741
|
return
|
|
743
742
|
}
|
|
744
743
|
|
|
@@ -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 {
|
package/schemas/n-rules.json
CHANGED
|
@@ -59,6 +59,24 @@
|
|
|
59
59
|
"minLength": 1
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
|
+
"git": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"additionalProperties": false,
|
|
65
|
+
"description": "Git policy проєкту. Відсутній блок еквівалентний baseBranch/releaseBranches = main.",
|
|
66
|
+
"properties": {
|
|
67
|
+
"baseBranch": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"minLength": 1,
|
|
70
|
+
"description": "Головна integration-гілка і перший кандидат бази delta; за замовчуванням main."
|
|
71
|
+
},
|
|
72
|
+
"releaseBranches": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"minItems": 1,
|
|
75
|
+
"description": "Довгоживучі release/integration гілки; разом з baseBranch утворюють delta та protected набір. За замовчуванням [main].",
|
|
76
|
+
"items": { "type": "string", "minLength": 1 }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
62
80
|
"version": {
|
|
63
81
|
"type": "string",
|
|
64
82
|
"description": "Застаріле поле, ігнорується CLI. Правила завжди копіюються з каталогу mdc/ установленого пакету (node_modules або кеш npx); змініть версію через оновлення залежності."
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { spawnSync } from 'node:child_process'
|
|
9
9
|
|
|
10
10
|
import { isWorktreeCheckoutPath } from '../utils/walkDir.mjs'
|
|
11
|
+
import { readGitPolicy } from './git-policy.mjs'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @param {string[]} args аргументи git
|
|
@@ -47,18 +48,16 @@ function dropWorktreeCheckouts(paths) {
|
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Визначає git base для scoped-перевірок без зовнішнього runtime-стану.
|
|
50
|
-
* Кандидати —
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* fetch може відставати вже `origin/main`. Якщо доступний лише один ref (офлайн,
|
|
55
|
-
* без remote) — його merge-base; якщо жодного — null, і caller порівнює лише
|
|
51
|
+
* Кандидати — effective Git policy: `baseBranch` + `releaseBranches`, кожна у
|
|
52
|
+
* `origin/` та локальній формах. Беремо **найновіший** сумісний merge-base; це
|
|
53
|
+
* захищає від stale-ref і вже інтегрованих змін між довгоживучими середовищами.
|
|
54
|
+
* Якщо жодного ref немає — null, і caller порівнює лише
|
|
56
55
|
* робоче дерево з HEAD. Повернений sha завжди досяжний (це merge-base існуючого
|
|
57
56
|
* ref), тож fail-closed перевірка в `collectChangedFilesSince` не спрацює хибно.
|
|
58
|
-
* Явний `baseRef` (CI: `--base origin/
|
|
57
|
+
* Явний `baseRef` (CI: `--base origin/dev` після fetch) вимикає вибір —
|
|
59
58
|
* merge-base рахується лише проти нього.
|
|
60
59
|
* @param {string} [cwd] корінь репо
|
|
61
|
-
* @param {string|null} [baseRef] явний ref бази замість
|
|
60
|
+
* @param {string|null} [baseRef] явний ref бази замість Git policy
|
|
62
61
|
* @returns {string|null} merge-base commit або null
|
|
63
62
|
*/
|
|
64
63
|
export function resolveChangedBase(cwd = process.cwd(), baseRef = null) {
|
|
@@ -67,12 +66,16 @@ export function resolveChangedBase(cwd = process.cwd(), baseRef = null) {
|
|
|
67
66
|
return result.status === 0 && !result.error ? result.stdout.trim() : ''
|
|
68
67
|
}
|
|
69
68
|
if (baseRef) return mergeBaseWith(baseRef) || null
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
69
|
+
const { integrationBranches } = readGitPolicy(cwd)
|
|
70
|
+
const bases = integrationBranches.flatMap(name => [`origin/${name}`, name]).map(mergeBaseWith).filter(Boolean)
|
|
71
|
+
if (bases.length === 0) return null
|
|
72
|
+
let newest = bases[0]
|
|
73
|
+
for (const candidate of bases.slice(1)) {
|
|
74
|
+
if (candidate === newest) continue
|
|
75
|
+
const ancestry = spawnSync('git', ['merge-base', '--is-ancestor', newest, candidate], { cwd })
|
|
76
|
+
if (ancestry.status === 0 && !ancestry.error) newest = candidate
|
|
77
|
+
}
|
|
78
|
+
return newest
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
/**
|
|
@@ -3,33 +3,50 @@ type: JS Module
|
|
|
3
3
|
title: changed-files.mjs
|
|
4
4
|
resource: npm/scripts/lib/changed-files.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 0482c036
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
7
9
|
score: 100
|
|
10
|
+
issues: judge-refine:kept-original,judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
8
12
|
---
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл збирає перелік файлів для quick-режиму lint-оркестратора: змінені та staged через `git diff HEAD`, а також нові untracked через `git ls-files --others --exclude-standard`. Видалені файли не потрапляють у результат.
|
|
17
|
+
|
|
18
|
+
`collectChangedFiles`, `resolveChangedBase` і `collectChangedFilesSince` потрібні, щоб визначати базу порівняння та повертати лише актуально змінені робочі файли для перевірки. Поза git-репо або при помилці git повертається порожній список.
|
|
11
19
|
|
|
12
20
|
## Поведінка
|
|
13
21
|
|
|
14
|
-
collectChangedFiles
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
`collectChangedFiles` формує scope quick-перевірки з поточного робочого дерева: бере змінені tracked/staged файли відносно `HEAD`, додає нові untracked файли та повертає унікальні relative-posix шляхи без видалених файлів.
|
|
23
|
+
|
|
24
|
+
`resolveChangedBase` потрібна перед scoped-перевірками, коли scope має рахуватися не лише від `HEAD`, а від інтеграційної бази. Вона визначає досяжний merge-base для поточної гілки: або за явно заданим base ref, або за політикою git-гілок репозиторію. Якщо придатної бази немає, результатом є відсутність бази, і подальший збір змін має перейти до quick-поведінки.
|
|
25
|
+
|
|
26
|
+
`collectChangedFilesSince` споживає базу з `resolveChangedBase` або її відсутність. За наявної бази вона збирає всі файли, змінені від цієї бази до поточного робочого дерева, включно із закоміченими після бази, staged і незакоміченими змінами, та додає untracked файли. За відсутньої бази делегує збір до `collectChangedFiles`.
|
|
27
|
+
|
|
28
|
+
Усі результати є списками унікальних шляхів для наступних lint-етапів. Для обох режимів діє спільне правило: видалені файли не потрапляють у scope, а файли всередині службових worktree-чекаутів відкидаються, щоб сесійні копії репозиторію не лінтилися як робочий код. Поза git-репозиторієм або при помилці git звичайний quick-збір повертає порожній список, але scoped-збір із недосяжною базою завершується явною помилкою, щоб перевірка не пройшла з порожнім scope помилково.
|
|
17
29
|
|
|
18
30
|
## Публічний API
|
|
19
31
|
|
|
20
|
-
- collectChangedFiles
|
|
21
|
-
- resolveChangedBase
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
- collectChangedFiles — Relative-posix список змінених + untracked файлів робочого дерева.
|
|
33
|
+
- resolveChangedBase — Визначає git base для scoped-перевірок без зовнішнього runtime-стану.
|
|
34
|
+
Кандидати — effective Git policy: `baseBranch` + `releaseBranches`, кожна у
|
|
35
|
+
`origin/` та локальній формах. Беремо **найновіший** сумісний merge-base; це
|
|
36
|
+
захищає від stale-ref і вже інтегрованих змін між довгоживучими середовищами.
|
|
37
|
+
Якщо жодного ref немає — null, і caller порівнює лише
|
|
38
|
+
робоче дерево з HEAD. Повернений sha завжди досяжний (це merge-base існуючого
|
|
39
|
+
ref), тож fail-closed перевірка в `collectChangedFilesSince` не спрацює хибно.
|
|
40
|
+
Явний `baseRef` (CI: `--base origin/dev` після fetch) вимикає вибір —
|
|
41
|
+
merge-base рахується лише проти нього.
|
|
42
|
+
- collectChangedFilesSince — Список змінених + untracked файлів **відносно базового комміту**.
|
|
43
|
+
|
|
44
|
+
`git diff <base>` (без `..`/`...`, без `HEAD`) порівнює base-комміт із поточним
|
|
45
|
+
**робочим деревом** — тобто однаково ловить і закомічене від base, і staged, і
|
|
46
|
+
незакомічені модифікації. Це гарантує однакову поведінку незалежно від того, чи
|
|
47
|
+
зміни вже закомічені у worktree. Без `base` — fallback на `collectChangedFiles`
|
|
48
|
+
(робоче дерево vs HEAD).
|
|
28
49
|
|
|
29
50
|
## Гарантії поведінки
|
|
30
51
|
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
34
|
-
- За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.
|
|
35
|
-
- Не звертається до мережі.
|
|
52
|
+
- (специфічних машинно-виведених гарантій немає)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: git-policy.mjs
|
|
4
|
+
resource: npm/scripts/lib/git-policy.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 5809ac19
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 100
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
`readGitPolicy` читає Git policy проєкту з `.n-rules.json` або `.n-cursor.json`, щоб delta-перевірки, changelog і branch tooling користувалися спільним набором базової, release, integration і protected гілок. Якщо політика відсутня, неповна або непридатна, повертає історичний канон `main`, не зупиняючи роботу інструментів. Помилки перехоплюються і не кидаються назовні.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
1. `readGitPolicy` шукає компактну політику Git у конфігах `.n-rules.json`, `.n-cursor.json`, щоб узгодити базову, release, integration і protected гілки для delta-перевірок, changelog і branch tooling.
|
|
20
|
+
2. Якщо конфіг відсутній, неповний або не може бути прочитаний як валідні дані, функція безпечно повертається до історичного канону `main` і не перериває роботу інструментів винятком.
|
|
21
|
+
3. Базова гілка береться з політики лише тоді, коли вона задана змістовним іменем; інакше використовується `main`.
|
|
22
|
+
4. Release-гілки беруться з політики як набір змістовних унікальних назв; якщо придатних значень немає, release-набором стає `main`.
|
|
23
|
+
5. Integration-гілки формуються як об’єднання базової гілки та release-гілок без дублювання, щоб downstream-інструменти мали єдине джерело правди для порівнянь.
|
|
24
|
+
6. Protected-гілки збігаються з integration-гілками, бо ці довгоживучі гілки мають однаково враховуватися як цільові й захищені.
|
|
25
|
+
|
|
26
|
+
## Публічний API
|
|
27
|
+
|
|
28
|
+
- readGitPolicy — Повертає effective policy. `releaseBranches` одночасно є integration-кандидатами
|
|
29
|
+
delta та захищеними довгоживучими гілками; `baseBranch` додається до обох наборів.
|
|
30
|
+
|
|
31
|
+
## Гарантії поведінки
|
|
32
|
+
|
|
33
|
+
- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
|
|
34
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
@@ -20,6 +20,7 @@ resource: npm/scripts/lib/
|
|
|
20
20
|
| [ensure-tool.mjs](ensure-tool.md) | JS Module |
|
|
21
21
|
| [generated-markdown.mjs](generated-markdown.md) | JS Module |
|
|
22
22
|
| [gha-workflow.mjs](gha-workflow.md) | JS Module |
|
|
23
|
+
| [git-policy.mjs](git-policy.md) | JS Module |
|
|
23
24
|
| [glob-to-regex.mjs](glob-to-regex.md) | JS Module |
|
|
24
25
|
| [inline-template-links.mjs](inline-template-links.md) | JS Module |
|
|
25
26
|
| [js-source-signals.mjs](js-source-signals.md) | JS Module |
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Читає компактну Git policy проєкту для delta, changelog і branch tooling.
|
|
3
|
+
* Відсутній або неповний конфіг зберігає історичний канон `main`.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILES = ['.n-rules.json', '.n-cursor.json']
|
|
9
|
+
const DEFAULT_BASE_BRANCH = 'main'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {unknown} value потенційне ім'я гілки
|
|
13
|
+
* @returns {string|null} нормалізоване ім'я або null
|
|
14
|
+
*/
|
|
15
|
+
function branchName(value) {
|
|
16
|
+
if (typeof value !== 'string') return null
|
|
17
|
+
const name = value.trim()
|
|
18
|
+
return name.length > 0 ? name : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Повертає effective policy. `releaseBranches` одночасно є integration-кандидатами
|
|
23
|
+
* delta та захищеними довгоживучими гілками; `baseBranch` додається до обох наборів.
|
|
24
|
+
* @param {string} [cwd] корінь репозиторію
|
|
25
|
+
* @returns {{baseBranch:string,releaseBranches:string[],integrationBranches:string[],protectedBranches:string[]}}
|
|
26
|
+
*/
|
|
27
|
+
export function readGitPolicy(cwd = process.cwd()) {
|
|
28
|
+
let raw = null
|
|
29
|
+
for (const file of CONFIG_FILES) {
|
|
30
|
+
const path = join(cwd, file)
|
|
31
|
+
if (!existsSync(path)) continue
|
|
32
|
+
try {
|
|
33
|
+
raw = JSON.parse(readFileSync(path, 'utf8'))
|
|
34
|
+
} catch {
|
|
35
|
+
// Повний CLI повідомить про невалідний JSON; standalone helpers лишаються safe-by-default.
|
|
36
|
+
}
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
const git = raw && typeof raw.git === 'object' && !Array.isArray(raw.git) ? raw.git : {}
|
|
40
|
+
const baseBranch = branchName(git.baseBranch) ?? DEFAULT_BASE_BRANCH
|
|
41
|
+
const configuredRelease = Array.isArray(git.releaseBranches) ? git.releaseBranches.map(branchName).filter(Boolean) : []
|
|
42
|
+
const releaseBranches = configuredRelease.length > 0 ? [...new Set(configuredRelease)] : [DEFAULT_BASE_BRANCH]
|
|
43
|
+
const integrationBranches = [...new Set([baseBranch, ...releaseBranches])]
|
|
44
|
+
return { baseBranch, releaseBranches, integrationBranches, protectedBranches: integrationBranches }
|
|
45
|
+
}
|
|
@@ -6,8 +6,8 @@ import { performance } from 'node:perf_hooks'
|
|
|
6
6
|
import { env } from 'node:process'
|
|
7
7
|
|
|
8
8
|
import { createProgressReporter } from '../../../scripts/lib/lint-surface/progress.mjs'
|
|
9
|
+
import { readGitPolicy } from '../../../scripts/lib/git-policy.mjs'
|
|
9
10
|
|
|
10
|
-
const BASE_REF = 'origin/main'
|
|
11
11
|
const LLM_TIERS = ['min', 'max']
|
|
12
12
|
const REVIEW_BATCH_SIZE = 10
|
|
13
13
|
const PROMPT_TEXT_LIMIT = 12_000
|
|
@@ -30,6 +30,11 @@ function noop() {
|
|
|
30
30
|
// Навмисно порожньо: caller не запросив progress output.
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** @param {string} cwd корінь репозиторію @returns {string} remote ref базової гілки */
|
|
34
|
+
function policyBaseRef(cwd) {
|
|
35
|
+
return `origin/${readGitPolicy(cwd).baseBranch}`
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* Форматує elapsed time без удаваної точності.
|
|
35
40
|
* @param {number} startedAt початок у мілісекундах
|
|
@@ -178,10 +183,10 @@ function branchName(ref) {
|
|
|
178
183
|
* @param {Map<string,string>} worktreeCommits checkout HEAD OID→path
|
|
179
184
|
* @returns {Array<{ref:string, oid:string, date:string, worktree:string|null,aliases:string[]}>} refs
|
|
180
185
|
*/
|
|
181
|
-
export function dedupeRefs(refs, worktrees, worktreeCommits = new Map()) {
|
|
186
|
+
export function dedupeRefs(refs, worktrees, worktreeCommits = new Map(), protectedBranches = ['main']) {
|
|
182
187
|
const byOid = new Map()
|
|
183
188
|
for (const item of refs) {
|
|
184
|
-
if (item.ref === 'refs/remotes/origin/HEAD' || branchName(item.ref)
|
|
189
|
+
if (item.ref === 'refs/remotes/origin/HEAD' || protectedBranches.includes(branchName(item.ref))) continue
|
|
185
190
|
const existing = byOid.get(item.oid)
|
|
186
191
|
const worktree = worktrees.get(item.ref) ?? worktreeCommits.get(item.oid) ?? existing?.worktree ?? null
|
|
187
192
|
const isRemote = item.ref.startsWith('refs/remotes/origin/')
|
|
@@ -282,8 +287,10 @@ export function conflictFiles(text) {
|
|
|
282
287
|
*/
|
|
283
288
|
export function inventoryRepository(cwd, deps = {}) {
|
|
284
289
|
const spawnFn = deps.spawnFn ?? spawnSync
|
|
290
|
+
const policy = readGitPolicy(cwd)
|
|
291
|
+
const baseRef = policyBaseRef(cwd)
|
|
285
292
|
git(['fetch', '--prune', 'origin'], cwd, spawnFn)
|
|
286
|
-
git(['rev-parse', '--verify',
|
|
293
|
+
git(['rev-parse', '--verify', baseRef], cwd, spawnFn)
|
|
287
294
|
|
|
288
295
|
const worktreeState = parseWorktreeState(git(['worktree', 'list', '--porcelain'], cwd, spawnFn).stdout)
|
|
289
296
|
const refLines = git(
|
|
@@ -299,7 +306,8 @@ export function inventoryRepository(cwd, deps = {}) {
|
|
|
299
306
|
return { ref, oid, date }
|
|
300
307
|
}),
|
|
301
308
|
worktreeState.branches,
|
|
302
|
-
worktreeState.commits
|
|
309
|
+
worktreeState.commits,
|
|
310
|
+
policy.protectedBranches
|
|
303
311
|
)
|
|
304
312
|
const prInventory = openPullRequests(cwd, spawnFn)
|
|
305
313
|
const prs = prInventory.items
|
|
@@ -308,16 +316,16 @@ export function inventoryRepository(cwd, deps = {}) {
|
|
|
308
316
|
const branches = refs.map(item => {
|
|
309
317
|
const name = branchName(item.ref)
|
|
310
318
|
const merged =
|
|
311
|
-
git(['merge-base', '--is-ancestor', item.ref,
|
|
319
|
+
git(['merge-base', '--is-ancestor', item.ref, baseRef], cwd, spawnFn, {
|
|
312
320
|
allowFailure: true
|
|
313
321
|
}).status === 0
|
|
314
322
|
const novelCommitIds = merged
|
|
315
323
|
? []
|
|
316
|
-
: git(['rev-list', '--right-only', '--cherry-pick', '--no-merges', `${
|
|
324
|
+
: git(['rev-list', '--right-only', '--cherry-pick', '--no-merges', `${baseRef}...${item.ref}`], cwd, spawnFn)
|
|
317
325
|
.stdout.split('\n')
|
|
318
326
|
.filter(Boolean)
|
|
319
327
|
.toReversed()
|
|
320
|
-
const counts = git(['rev-list', '--left-right', '--count', `${
|
|
328
|
+
const counts = git(['rev-list', '--left-right', '--count', `${baseRef}...${item.ref}`], cwd, spawnFn)
|
|
321
329
|
.stdout.trim()
|
|
322
330
|
.split(WHITESPACE_RE)
|
|
323
331
|
.map(Number)
|
|
@@ -325,13 +333,13 @@ export function inventoryRepository(cwd, deps = {}) {
|
|
|
325
333
|
const state = branchState({ merged, novelCommitIds, pr, worktree: item.worktree })
|
|
326
334
|
const changedFiles =
|
|
327
335
|
state === 'review'
|
|
328
|
-
? git(['diff', '--name-status', `${
|
|
336
|
+
? git(['diff', '--name-status', `${baseRef}...${item.ref}`], cwd, spawnFn)
|
|
329
337
|
.stdout.split('\n')
|
|
330
338
|
.filter(Boolean)
|
|
331
339
|
.slice(0, 200)
|
|
332
340
|
: []
|
|
333
341
|
const mergeTree =
|
|
334
|
-
state === 'review' ? git(['merge-tree',
|
|
342
|
+
state === 'review' ? git(['merge-tree', baseRef, item.ref], cwd, spawnFn, { allowFailure: true }).stdout : ''
|
|
335
343
|
return {
|
|
336
344
|
source: `${SOURCE_BRANCH_PREFIX}${item.ref}`,
|
|
337
345
|
ref: item.ref,
|
|
@@ -364,7 +372,7 @@ export function inventoryRepository(cwd, deps = {}) {
|
|
|
364
372
|
}
|
|
365
373
|
})
|
|
366
374
|
|
|
367
|
-
return { base:
|
|
375
|
+
return { base: baseRef, baseBranch: policy.baseBranch, branches, stashes, warnings }
|
|
368
376
|
}
|
|
369
377
|
|
|
370
378
|
/**
|
|
@@ -629,7 +637,7 @@ function chooseBranch(title, cwd, spawnFn) {
|
|
|
629
637
|
|
|
630
638
|
/**
|
|
631
639
|
* Створює керований worktree та детерміновано пересаджує його branch на
|
|
632
|
-
*
|
|
640
|
+
* policy base ref без зміни вихідного checkout.
|
|
633
641
|
* @param {string} title PR title
|
|
634
642
|
* @param {string} source source id
|
|
635
643
|
* @param {string} cwd корінь
|
|
@@ -637,11 +645,12 @@ function chooseBranch(title, cwd, spawnFn) {
|
|
|
637
645
|
* @returns {{branch:string,cwd:string}} worktree
|
|
638
646
|
*/
|
|
639
647
|
function createReconcileWorktree(title, source, cwd, spawnFn) {
|
|
648
|
+
const baseRef = policyBaseRef(cwd)
|
|
640
649
|
const branch = chooseBranch(title, cwd, spawnFn)
|
|
641
650
|
run('npx', ['@7n/mt', 'worktree', 'create', branch, `git-reconcile: ${source}`], cwd, spawnFn)
|
|
642
651
|
const worktreeCwd = join(cwd, '.worktrees', branch.replaceAll('/', '-'))
|
|
643
|
-
git(['switch', '--detach',
|
|
644
|
-
git(['branch', '-f', branch,
|
|
652
|
+
git(['switch', '--detach', baseRef], worktreeCwd, spawnFn)
|
|
653
|
+
git(['branch', '-f', branch, baseRef], worktreeCwd, spawnFn)
|
|
645
654
|
git(['switch', branch], worktreeCwd, spawnFn)
|
|
646
655
|
return { branch, cwd: worktreeCwd }
|
|
647
656
|
}
|
|
@@ -718,11 +727,12 @@ export function finishCherryPick(cwd, spawnFn = spawnSync) {
|
|
|
718
727
|
* @returns {boolean} чи є що переносити в PR
|
|
719
728
|
*/
|
|
720
729
|
export function hasChangesFromBase(cwd, spawnFn = spawnSync) {
|
|
730
|
+
const baseRef = policyBaseRef(cwd)
|
|
721
731
|
return (
|
|
722
|
-
git(['diff', '--quiet', `${
|
|
732
|
+
git(['diff', '--quiet', `${baseRef}...HEAD`], cwd, spawnFn, {
|
|
723
733
|
allowFailure: true
|
|
724
734
|
}).status !== 0 ||
|
|
725
|
-
git(['diff', '--quiet',
|
|
735
|
+
git(['diff', '--quiet', baseRef, '--'], cwd, spawnFn, {
|
|
726
736
|
allowFailure: true
|
|
727
737
|
}).status !== 0 ||
|
|
728
738
|
git(['ls-files', '--others', '--exclude-standard'], cwd, spawnFn).stdout.trim().length > 0
|
|
@@ -795,13 +805,13 @@ export function sourceDirectories(paths) {
|
|
|
795
805
|
}
|
|
796
806
|
|
|
797
807
|
/**
|
|
798
|
-
* Збирає tracked і untracked code directories відносно
|
|
808
|
+
* Збирає tracked і untracked code directories відносно policy base ref.
|
|
799
809
|
* @param {string} cwd worktree
|
|
800
810
|
* @param {typeof spawnSync} spawnFn інжект
|
|
801
811
|
* @returns {string[]} directories
|
|
802
812
|
*/
|
|
803
813
|
function changedPaths(cwd, spawnFn) {
|
|
804
|
-
const tracked = git(['diff', '--name-only',
|
|
814
|
+
const tracked = git(['diff', '--name-only', policyBaseRef(cwd), '--'], cwd, spawnFn).stdout.split('\n').filter(Boolean)
|
|
805
815
|
const untracked = git(['ls-files', '--others', '--exclude-standard'], cwd, spawnFn).stdout.split('\n').filter(Boolean)
|
|
806
816
|
return [...new Set([...tracked, ...untracked])]
|
|
807
817
|
}
|
|
@@ -874,7 +884,7 @@ function ensureWorktreeDependencies(cwd, spawnFn) {
|
|
|
874
884
|
}
|
|
875
885
|
|
|
876
886
|
/**
|
|
877
|
-
* Фіксує test baseline на
|
|
887
|
+
* Фіксує test baseline на чистій policy base гілці до перенесення source.
|
|
878
888
|
* @param {string} cwd worktree
|
|
879
889
|
* @param {typeof spawnSync} spawnFn інжект
|
|
880
890
|
* @returns {{tests:{status:number,stdout:string,stderr:string}|null}} baseline
|
|
@@ -889,7 +899,7 @@ export function captureBehaviorBaseline(cwd, spawnFn = spawnSync) {
|
|
|
889
899
|
}
|
|
890
900
|
|
|
891
901
|
/**
|
|
892
|
-
* Повторно використовує test baseline
|
|
902
|
+
* Повторно використовує test baseline однієї policy base гілки між PR-групами.
|
|
893
903
|
* Залежності все одно встановлюються в кожному окремому worktree.
|
|
894
904
|
* @param {string} cwd worktree
|
|
895
905
|
* @param {Map<string,object>} cache кеш за OID бази
|
|
@@ -898,7 +908,7 @@ export function captureBehaviorBaseline(cwd, spawnFn = spawnSync) {
|
|
|
898
908
|
*/
|
|
899
909
|
export function captureCachedBehaviorBaseline(cwd, cache, spawnFn = spawnSync) {
|
|
900
910
|
ensureWorktreeDependencies(cwd, spawnFn)
|
|
901
|
-
const baseOid = git(['rev-parse',
|
|
911
|
+
const baseOid = git(['rev-parse', policyBaseRef(cwd)], cwd, spawnFn).stdout.trim()
|
|
902
912
|
if (cache.has(baseOid)) return { baseline: cache.get(baseOid), cached: true }
|
|
903
913
|
const baseline = captureBehaviorBaseline(cwd, spawnFn)
|
|
904
914
|
cache.set(baseOid, baseline)
|
|
@@ -910,7 +920,7 @@ export function captureCachedBehaviorBaseline(cwd, cache, spawnFn = spawnSync) {
|
|
|
910
920
|
* Саме ці докази вирішують, чи приймати min-результат або ескалювати на max.
|
|
911
921
|
* @param {string} cwd worktree
|
|
912
922
|
* @param {typeof spawnSync} spawnFn інжект
|
|
913
|
-
* @param {{tests:{status:number,stdout:string,stderr:string}|null}|null} [baseline] стан
|
|
923
|
+
* @param {{tests:{status:number,stdout:string,stderr:string}|null}|null} [baseline] стан policy base гілки
|
|
914
924
|
* @param {(stage:string)=>void} [onProgress] stage callback
|
|
915
925
|
* @returns {{ok:boolean,error?:string}} validation
|
|
916
926
|
*/
|
|
@@ -972,7 +982,7 @@ async function resolveConflict(args) {
|
|
|
972
982
|
const { runner, source, worktreeCwd, deps, spawnFn, log, onProgress = noop } = args
|
|
973
983
|
const unresolved = unresolvedFiles(worktreeCwd, spawnFn)
|
|
974
984
|
const prompt = [
|
|
975
|
-
`У worktree ${worktreeCwd} JS уже застосував ${source} до
|
|
985
|
+
`У worktree ${worktreeCwd} JS уже застосував ${source} до свіжої policy base гілки.`,
|
|
976
986
|
`Розв'яжи лише змістові конфлікти: ${unresolved.join(', ')}.`,
|
|
977
987
|
'Порівняй current main та намір перенесеної зміни; не використовуй ours/theirs механічно.',
|
|
978
988
|
'Збережи актуальну поведінку main, перенеси лише відсутню корисну частину.',
|
|
@@ -1036,7 +1046,7 @@ async function applySource(args) {
|
|
|
1036
1046
|
async function finalizeBehavior(args) {
|
|
1037
1047
|
const { runner, source, rationale, worktreeCwd, baseline, deps, spawnFn, log, onProgress = noop } = args
|
|
1038
1048
|
const prompt = [
|
|
1039
|
-
`JS переніс ${source} на
|
|
1049
|
+
`JS переніс ${source} на свіжу policy base гілку у ${worktreeCwd}.`,
|
|
1040
1050
|
`Очікувана користь: ${rationale}`,
|
|
1041
1051
|
'Перевір реальний diff і call sites. Доведи лише перенесену поведінку до готовності:',
|
|
1042
1052
|
'- додай/онови regression test, якщо це bug fix;',
|
|
@@ -1140,7 +1150,9 @@ async function createPullRequest(args) {
|
|
|
1140
1150
|
onProgress('delta lint')
|
|
1141
1151
|
const finalGates = validateFinalProjectGates(worktree.cwd, spawnFn)
|
|
1142
1152
|
if (!finalGates.ok) throw new Error(finalGates.error)
|
|
1143
|
-
|
|
1153
|
+
const baseRef = policyBaseRef(worktree.cwd)
|
|
1154
|
+
const baseBranch = readGitPolicy(worktree.cwd).baseBranch
|
|
1155
|
+
git(['diff', '--check', `${baseRef}...HEAD`], worktree.cwd, spawnFn)
|
|
1144
1156
|
onProgress('push')
|
|
1145
1157
|
git(['push', '-u', 'origin', worktree.branch], worktree.cwd, spawnFn)
|
|
1146
1158
|
|
|
@@ -1148,10 +1160,10 @@ async function createPullRequest(args) {
|
|
|
1148
1160
|
const body = [
|
|
1149
1161
|
`Джерело: \`${source}\`.`,
|
|
1150
1162
|
'',
|
|
1151
|
-
group.rationale ?? candidate.rationale ??
|
|
1163
|
+
group.rationale ?? candidate.rationale ?? `Корисну поведінку перенесено на актуальний ${baseBranch}.`,
|
|
1152
1164
|
'',
|
|
1153
1165
|
'Перевірки:',
|
|
1154
|
-
|
|
1166
|
+
`- \`git diff --check ${baseRef}...HEAD\``,
|
|
1155
1167
|
'- scoped code lint/tests та domain lint для non-code paths',
|
|
1156
1168
|
'- `npx @7n/rules lint changelog --no-fix`',
|
|
1157
1169
|
verification ? `- LLM behavioral verification: ${verification.slice(0, 1000)}` : ''
|
|
@@ -1160,7 +1172,7 @@ async function createPullRequest(args) {
|
|
|
1160
1172
|
.join('\n')
|
|
1161
1173
|
createdPr = run(
|
|
1162
1174
|
'gh',
|
|
1163
|
-
['pr', 'create', '--base',
|
|
1175
|
+
['pr', 'create', '--base', baseBranch, '--head', worktree.branch, '--title', group.title, '--body', body],
|
|
1164
1176
|
worktree.cwd,
|
|
1165
1177
|
spawnFn
|
|
1166
1178
|
).stdout.trim()
|