@7n/rules 1.49.22 → 1.49.24
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.24] - 2026-07-27
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- git-reconcile формує PR descriptions із пріоритетом бізнес-контексту та архітектури за validated final diff
|
|
8
|
+
|
|
9
|
+
## [1.49.23] - 2026-07-27
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Treat reconcile diffs containing only release entries as patch-equivalent
|
|
14
|
+
|
|
3
15
|
## [1.49.22] - 2026-07-27
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/package.json
CHANGED
|
@@ -31,7 +31,8 @@ LLM отримує лише bounded-завдання:
|
|
|
31
31
|
|
|
32
32
|
1. semantic triage кандидатів, які JS не може оцінити за Git-фактами;
|
|
33
33
|
2. розв'язання змістових конфліктів і перевірку перенесеної поведінки у вже
|
|
34
|
-
підготовленому worktree
|
|
34
|
+
підготовленому worktree;
|
|
35
|
+
3. бізнесовий та архітектурний опис PR за bounded-фактами фінального diff.
|
|
35
36
|
|
|
36
37
|
LLM не видаляє refs, не створює worktree, не push-ить і не відкриває PR.
|
|
37
38
|
LLM виконує лише narrow tests, потрібні під час правок; full repository tests,
|
|
@@ -55,6 +56,8 @@ heartbeat з elapsed time. Формат однаковий у TTY/CI, тому c
|
|
|
55
56
|
|
|
56
57
|
- triage — повноту verdicts, schema, groups і commit OID;
|
|
57
58
|
- worktree — відсутність conflict markers та `git diff --check`;
|
|
59
|
+
- PR description — JSON schema, evidence paths із реального diff і перевагу
|
|
60
|
+
business/architecture змісту над behavior/risk details;
|
|
58
61
|
- поведінку — repository test script відносно test baseline чистої
|
|
59
62
|
`origin/<baseBranch>` і changelog gate. Red baseline приймається лише для
|
|
60
63
|
розпізнаних Vitest failures, якщо після перенесення не додалось нових.
|
|
@@ -91,6 +94,13 @@ transport. Після провалу `max` джерело fail-closed лишає
|
|
|
91
94
|
- Перед push обов'язково проходять фінальний tree-diff guard, domain lint для
|
|
92
95
|
non-code paths, changelog і `git diff --check`; code changes додатково
|
|
93
96
|
проходять scoped docs/lint та tests.
|
|
97
|
+
- Перед push JS передає min-моделі bounded final diff, commit metadata,
|
|
98
|
+
triage rationale і behavioral verification. Модель повертає лише validated
|
|
99
|
+
JSON, а JS рендерить PR body із видимими секціями «Навіщо»,
|
|
100
|
+
«Бізнес-результат», «Архітектура», «Поведінка» та «Ризики та сумісність»;
|
|
101
|
+
source і evidence paths лишаються у collapsed technical details.
|
|
102
|
+
- Невалідний PR description повторюється на max; повторний провал fail-closed
|
|
103
|
+
зберігає worktree і не push-ить гілку з misleading описом.
|
|
94
104
|
- Canonical fixers охоплюють code і non-code directories; після механічного
|
|
95
105
|
виправлення фінальні gates обов'язково запускаються повторно без fix.
|
|
96
106
|
- Behavioral LLM не викликається для змін без code paths; test baseline
|
|
@@ -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: fc877b0d
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 100
|
|
9
9
|
---
|
|
@@ -20,11 +20,15 @@ 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
|
+
Після фінальних gates `collectPullRequestFacts` збирає bounded final diff, commit metadata, changed paths, triage rationale та behavioral verification. `describePullRequest` викликає min-модель, а `validatePullRequestDescription` перевіряє JSON schema, factual evidence paths і перевагу business/architecture змісту; residual failure повторюється на max. `renderPullRequestBody` детерміновано ставить секції «Навіщо», «Бізнес-результат» та «Архітектура» перед поведінковими деталями, а source і evidence paths ховає в технічний `<details>`.
|
|
24
|
+
|
|
23
25
|
`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
26
|
|
|
25
27
|
`commitPendingChanges` створює додатковий commit лише для staged remediation. Чистий index після перенесення branch-source є валідним, коли корисні commits уже присутні в `HEAD`. Порожній PR check rollup вважається непідтвердженим, а не успішним.
|
|
26
28
|
|
|
27
|
-
`
|
|
29
|
+
`hasOnlyChangeEntries` відсікає tree diff, у якому лишилися тільки `*/.changes/*.md`: такий результат вважається `patch-equivalent`, не створює PR і дозволяє cleanup source branch.
|
|
30
|
+
|
|
31
|
+
`pruneForensicDependencies` прибирає лише відновлюваний `node_modules` зі збереженого forensic worktree. `cleanupSource` видаляє лише точний доказово безпечний branch/stash. Cleanup review-source дозволений після `drop` або коли всі його групи завершились `pr-created` чи `patch-equivalent`; `failed` і всі `pr-checks-*` outcomes лишають source та forensic worktree.
|
|
28
32
|
|
|
29
33
|
## Публічний API
|
|
30
34
|
|
|
@@ -32,11 +36,13 @@ docgen:
|
|
|
32
36
|
- `createPhaseProgress` — формує ANSI-free progress snapshots і heartbeat.
|
|
33
37
|
- `parseWorktrees`, `dedupeRefs`, `conflictFiles`, `inventoryRepository` — збирають і нормалізують Git inventory.
|
|
34
38
|
- `buildTriagePrompt`, `parseDecisionEnvelope`, `validateTriageOutcome` — задають і перевіряють bounded triage contract.
|
|
39
|
+
- `collectPullRequestFacts`, `validatePullRequestDescription`, `describePullRequest`, `renderPullRequestBody` — готують, перевіряють і формують business/architecture narrative фінального PR.
|
|
35
40
|
- `callRunner`, `callWithValidatedFallback` — викликають runner за схемою `min → validation → max`.
|
|
36
41
|
- `captureBehaviorBaseline`, `captureCachedBehaviorBaseline` — фіксують і кешують test baseline.
|
|
37
42
|
- `validateBehaviorState`, `validateFinalProjectGates`, `remediateBehaviorState` — виконують behavioral та canonical gates.
|
|
38
43
|
- `commitPendingChanges` — комітить лише staged remediation, не вимагаючи порожнього commit для вже перенесених commits.
|
|
39
|
-
- `
|
|
44
|
+
- `hasOnlyChangeEntries` — розпізнає release-metadata-only diff, який не потребує PR.
|
|
45
|
+
- `pruneForensicDependencies` — звільняє відновлювані dependencies без втрати Git evidence.
|
|
40
46
|
- `classifyPullRequestChecks`, `verifyPullRequestReadiness` — класифікують CI відносно base commit.
|
|
41
47
|
- `formatOutcomeCounts`, `formatReport` — формують точний deterministic summary.
|
|
42
48
|
- `cleanupSource`, `runGitReconcileOrchestrator` — виконують безпечний cleanup і координують повний flow.
|
|
@@ -46,9 +52,12 @@ docgen:
|
|
|
46
52
|
- Довгі child processes не блокують event loop і progress heartbeat.
|
|
47
53
|
- Baseline test Promise дедуплікується між concurrent PR-групами одного base OID.
|
|
48
54
|
- Canonical fixers охоплюють code/non-code scope, після чого gates повторюються без fix.
|
|
55
|
+
- PR description спирається лише на bounded final facts, а evidence paths завжди належать реальному diff.
|
|
56
|
+
- Business/architecture narrative має не меншу вагу, ніж behavior/risk details; повторно невалідний опис блокує push.
|
|
49
57
|
- PR вважається створеним успішно лише після terminal green checks.
|
|
50
58
|
- Порожній PR check rollup fail-closed зберігає forensic worktree.
|
|
51
59
|
- Forensic worktree зберігає Git evidence без накопичення `node_modules`.
|
|
60
|
+
- Diff лише з `.changes` не створює PR і не блокує cleanup source branch.
|
|
52
61
|
- CI regression, baseline-red або непідтверджений стан завершують orchestration non-zero і зберігають forensic refs/worktree.
|
|
53
62
|
- Cleanup не видаляє protected, open-PR, kept, failed або `pr-checks-*` sources.
|
|
54
63
|
- `spawnSync.error`, зокрема `ENOENT`, зберігається в command diagnostics.
|
|
@@ -13,6 +13,7 @@ import { readGitPolicy } from '../../../scripts/lib/git-policy.mjs'
|
|
|
13
13
|
const LLM_TIERS = ['min', 'max']
|
|
14
14
|
const REVIEW_BATCH_SIZE = 10
|
|
15
15
|
const PROMPT_TEXT_LIMIT = 12_000
|
|
16
|
+
const PR_DIFF_TEXT_LIMIT = 24_000
|
|
16
17
|
const PROGRESS_HEARTBEAT_MS = 30_000
|
|
17
18
|
const PR_CHECK_TIMEOUT_MS = 15 * 60_000
|
|
18
19
|
const DEFAULT_PR_CONCURRENCY = 3
|
|
@@ -25,7 +26,14 @@ const REF_HEADS_RE = /^refs\/heads\//
|
|
|
25
26
|
const REF_ORIGIN_RE = /^refs\/remotes\/origin\//
|
|
26
27
|
const RENAME_DELETE_CONFLICT_RE = /^CONFLICT \(rename\/delete\): .+? renamed to (.+?) in .+?, but deleted/
|
|
27
28
|
const SOURCE_CODE_RE = /\.(?:js|mjs|ts|vue|rs|py)$/
|
|
29
|
+
const CHANGE_ENTRY_RE = /(^|\/)\.changes\/[^/]+\.md$/
|
|
28
30
|
const WHITESPACE_RE = /\s+/
|
|
31
|
+
const PR_DESCRIPTION_ARRAY_FIELDS = [
|
|
32
|
+
'businessOutcomes',
|
|
33
|
+
'architectureChanges',
|
|
34
|
+
'behaviorChanges',
|
|
35
|
+
'risksAndCompatibility'
|
|
36
|
+
]
|
|
29
37
|
const ACP_PROGRESS_ENV = 'N_LLM_ACP_PROGRESS'
|
|
30
38
|
const REF_INVENTORY_FORMAT = ['%(refname)', '%00', '%(object', 'name)', '%00', '%(committer', 'date:iso-strict)'].join(
|
|
31
39
|
''
|
|
@@ -766,6 +774,184 @@ export function validateTriageOutcome(outcome, candidates) {
|
|
|
766
774
|
return { ok: true, value: envelope }
|
|
767
775
|
}
|
|
768
776
|
|
|
777
|
+
/**
|
|
778
|
+
* Збирає bounded факти з фінального diff для grounded бізнесового й
|
|
779
|
+
* архітектурного опису PR без повторного repository exploration моделлю.
|
|
780
|
+
* @param {object} args контекст готового PR
|
|
781
|
+
* @returns {object} факти опису PR
|
|
782
|
+
*/
|
|
783
|
+
export function collectPullRequestFacts(args) {
|
|
784
|
+
const { cwd, baseRef, source, title, rationale = '', verification = '', spawnFn = spawnSync } = args
|
|
785
|
+
const range = `${baseRef}...HEAD`
|
|
786
|
+
const changedPaths = git(['diff', '--name-only', range], cwd, spawnFn).stdout.split('\n').filter(Boolean)
|
|
787
|
+
const diff = git(['diff', '--no-ext-diff', '--unified=2', range], cwd, spawnFn).stdout
|
|
788
|
+
return {
|
|
789
|
+
source,
|
|
790
|
+
title,
|
|
791
|
+
rationale,
|
|
792
|
+
verification,
|
|
793
|
+
baseRef,
|
|
794
|
+
commits: git(['log', '--format=%h%x09%s', range], cwd, spawnFn).stdout.split('\n').filter(Boolean),
|
|
795
|
+
changedPaths,
|
|
796
|
+
diffStat: git(['diff', '--stat', range], cwd, spawnFn).stdout.trim(),
|
|
797
|
+
diff: diff.length > PR_DIFF_TEXT_LIMIT ? `${diff.slice(0, PR_DIFF_TEXT_LIMIT)}\n[diff truncated by JS]` : diff
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Формує bounded prompt, який забороняє implementation-changelog і вимагає
|
|
803
|
+
* business/architecture narrative лише з підготовлених JS-фактів.
|
|
804
|
+
* @param {object} facts фінальні Git та behavioral факти
|
|
805
|
+
* @returns {string} промпт
|
|
806
|
+
*/
|
|
807
|
+
export function buildPullRequestDescriptionPrompt(facts) {
|
|
808
|
+
return [
|
|
809
|
+
'Ти формуєш лише зміст PR description за вже зібраними JS-фактами.',
|
|
810
|
+
'Не запускай команди, не редагуй файли, не створюй PR і не вигадуй відсутній контекст.',
|
|
811
|
+
'Поясни передусім: навіщо зміна потрібна, який дає продуктово-операційний результат, як змінює responsibilities, boundaries, contracts або data/control flow.',
|
|
812
|
+
'Не переказуй diff по функціях і рядках. Називай implementation detail лише коли він є важливим architecture contract.',
|
|
813
|
+
'Не вигадуй клієнтів, фінансові метрики, deployment status або гарантії. Якщо бізнес-контекст обмежений, чесно сформулюй підтверджений operational/developer outcome.',
|
|
814
|
+
'Кожне твердження обґрунтуй facts; evidencePaths мають бути точними changedPaths.',
|
|
815
|
+
'Business context разом із businessOutcomes та architectureChanges має бути не коротшим за behaviorChanges разом із risksAndCompatibility.',
|
|
816
|
+
'Поверни лише JSON object без markdown:',
|
|
817
|
+
'{"businessContext":"...","businessOutcomes":["..."],"architectureChanges":["..."],"behaviorChanges":["..."],"risksAndCompatibility":["..."],"evidencePaths":["path/from/changedPaths"]}',
|
|
818
|
+
JSON.stringify(facts)
|
|
819
|
+
].join('\n\n')
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Перевіряє структуру, factual anchors і перевагу business/architecture
|
|
824
|
+
* змісту перед дрібними деталями реалізації.
|
|
825
|
+
* @param {{text:string}} outcome LLM output
|
|
826
|
+
* @param {{changedPaths:string[]}} facts фінальні Git-факти
|
|
827
|
+
* @returns {{ok:boolean,error?:string,value?:object}} validation
|
|
828
|
+
*/
|
|
829
|
+
export function validatePullRequestDescription(outcome, facts) {
|
|
830
|
+
const description = parseDecisionEnvelope(outcome.text)
|
|
831
|
+
if (!description) return { ok: false, error: 'відсутній JSON object опису PR' }
|
|
832
|
+
if (
|
|
833
|
+
typeof description.businessContext !== 'string' ||
|
|
834
|
+
description.businessContext.trim().length < 40 ||
|
|
835
|
+
description.businessContext.length > 1200 ||
|
|
836
|
+
description.businessContext.includes('\n')
|
|
837
|
+
) {
|
|
838
|
+
return { ok: false, error: 'businessContext має бути змістовним однорядковим текстом' }
|
|
839
|
+
}
|
|
840
|
+
for (const field of PR_DESCRIPTION_ARRAY_FIELDS) {
|
|
841
|
+
const values = description[field]
|
|
842
|
+
if (
|
|
843
|
+
!Array.isArray(values) ||
|
|
844
|
+
values.length === 0 ||
|
|
845
|
+
values.length > 5 ||
|
|
846
|
+
values.some(
|
|
847
|
+
value => typeof value !== 'string' || value.trim().length < 15 || value.length > 600 || value.includes('\n')
|
|
848
|
+
)
|
|
849
|
+
) {
|
|
850
|
+
return { ok: false, error: `${field} має містити 1-5 змістовних однорядкових пунктів` }
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
const changedPaths = new Set(facts.changedPaths)
|
|
854
|
+
if (
|
|
855
|
+
!Array.isArray(description.evidencePaths) ||
|
|
856
|
+
description.evidencePaths.length === 0 ||
|
|
857
|
+
description.evidencePaths.length > 12 ||
|
|
858
|
+
description.evidencePaths.some(path => typeof path !== 'string' || !changedPaths.has(path))
|
|
859
|
+
) {
|
|
860
|
+
return { ok: false, error: 'evidencePaths мають бути непорожнім subset фактичних changedPaths' }
|
|
861
|
+
}
|
|
862
|
+
const focusedLength =
|
|
863
|
+
description.businessContext.length +
|
|
864
|
+
description.businessOutcomes.join('').length +
|
|
865
|
+
description.architectureChanges.join('').length
|
|
866
|
+
const detailLength = description.behaviorChanges.join('').length + description.risksAndCompatibility.join('').length
|
|
867
|
+
if (focusedLength < detailLength) {
|
|
868
|
+
return { ok: false, error: 'business та architecture зміст коротший за behavior/risk details' }
|
|
869
|
+
}
|
|
870
|
+
return { ok: true, value: description }
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Форматує validated narrative items як Markdown bullets.
|
|
875
|
+
* @param {string[]} values пункти секції
|
|
876
|
+
* @returns {string} Markdown
|
|
877
|
+
*/
|
|
878
|
+
function markdownBullets(values) {
|
|
879
|
+
return values.map(value => `- ${value.trim()}`).join('\n')
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Рендерить стабільний PR body: business та architecture секції видимі
|
|
884
|
+
* першими, а source/evidence залишаються у forensic details.
|
|
885
|
+
* @param {object} args validated description та deterministic facts
|
|
886
|
+
* @param {object} args.description validated narrative
|
|
887
|
+
* @param {object} args.facts deterministic Git facts
|
|
888
|
+
* @returns {string} Markdown PR body
|
|
889
|
+
*/
|
|
890
|
+
export function renderPullRequestBody({ description, facts }) {
|
|
891
|
+
const inline = value => String(value).trim().split(WHITESPACE_RE).join(' ')
|
|
892
|
+
const evidence = description.evidencePaths.map(path => `- \`${path.replaceAll('`', '\\`')}\``).join('\n')
|
|
893
|
+
return [
|
|
894
|
+
'## Навіщо',
|
|
895
|
+
'',
|
|
896
|
+
description.businessContext.trim(),
|
|
897
|
+
'',
|
|
898
|
+
'## Бізнес-результат',
|
|
899
|
+
'',
|
|
900
|
+
markdownBullets(description.businessOutcomes),
|
|
901
|
+
'',
|
|
902
|
+
'## Архітектура',
|
|
903
|
+
'',
|
|
904
|
+
markdownBullets(description.architectureChanges),
|
|
905
|
+
'',
|
|
906
|
+
'## Поведінка',
|
|
907
|
+
'',
|
|
908
|
+
markdownBullets(description.behaviorChanges),
|
|
909
|
+
'',
|
|
910
|
+
'## Ризики та сумісність',
|
|
911
|
+
'',
|
|
912
|
+
markdownBullets(description.risksAndCompatibility),
|
|
913
|
+
'',
|
|
914
|
+
'## Перевірки',
|
|
915
|
+
'',
|
|
916
|
+
`- \`git diff --check ${facts.baseRef}...HEAD\``,
|
|
917
|
+
'- scoped code lint/tests та domain lint для non-code paths',
|
|
918
|
+
'- `npx @7n/rules lint changelog --no-fix`',
|
|
919
|
+
...(facts.verification ? [`- LLM behavioral verification: ${inline(facts.verification).slice(0, 1000)}`] : []),
|
|
920
|
+
'',
|
|
921
|
+
'<details>',
|
|
922
|
+
'<summary>Технічні докази перенесення</summary>',
|
|
923
|
+
'',
|
|
924
|
+
`Джерело: \`${facts.source.replaceAll('`', '\\`')}\`.`,
|
|
925
|
+
'',
|
|
926
|
+
evidence,
|
|
927
|
+
'',
|
|
928
|
+
'</details>'
|
|
929
|
+
].join('\n')
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Генерує validated PR narrative через min→validation→max над фінальним diff.
|
|
934
|
+
* @param {object} args контекст готового worktree
|
|
935
|
+
* @returns {Promise<string>} deterministic Markdown body
|
|
936
|
+
*/
|
|
937
|
+
export async function describePullRequest(args) {
|
|
938
|
+
const { runner, cwd, baseRef, source, title, rationale, verification, deps, spawnFn, log, onProgress = noop } = args
|
|
939
|
+
const collectFacts = deps.collectPullRequestFacts ?? collectPullRequestFacts
|
|
940
|
+
const facts = collectFacts({ cwd, baseRef, source, title, rationale, verification, spawnFn })
|
|
941
|
+
const outcome = await callWithValidatedFallback({
|
|
942
|
+
runner,
|
|
943
|
+
prompt: buildPullRequestDescriptionPrompt(facts),
|
|
944
|
+
cwd,
|
|
945
|
+
deps,
|
|
946
|
+
log,
|
|
947
|
+
label: `PR description ${source}`,
|
|
948
|
+
onAttempt: ({ tier }) => onProgress('PR description', tier),
|
|
949
|
+
validate: result => validatePullRequestDescription(result, facts)
|
|
950
|
+
})
|
|
951
|
+
if (!outcome.ok) throw new Error(`LLM PR description: ${outcome.error}`)
|
|
952
|
+
return renderPullRequestBody({ description: outcome.validation.value, facts })
|
|
953
|
+
}
|
|
954
|
+
|
|
769
955
|
/**
|
|
770
956
|
* Перетворює довільний title/ref на branch slug.
|
|
771
957
|
* @param {string} value title/ref
|
|
@@ -1008,6 +1194,16 @@ export function sourceDirectories(paths) {
|
|
|
1008
1194
|
return [...new Set(paths.filter(path => SOURCE_CODE_RE.test(path)).map(path => dirname(path)))].toSorted()
|
|
1009
1195
|
}
|
|
1010
1196
|
|
|
1197
|
+
/**
|
|
1198
|
+
* Визначає технічний залишок, який містить лише release entries. Такі файли
|
|
1199
|
+
* не доводять окремої корисної поведінки й не мають породжувати PR.
|
|
1200
|
+
* @param {string[]} paths змінені шляхи відносно policy base
|
|
1201
|
+
* @returns {boolean} чи всі зміни лежать безпосередньо у `.changes/`
|
|
1202
|
+
*/
|
|
1203
|
+
export function hasOnlyChangeEntries(paths) {
|
|
1204
|
+
return paths.length > 0 && paths.every(path => CHANGE_ENTRY_RE.test(path))
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1011
1207
|
/**
|
|
1012
1208
|
* Збирає tracked і untracked code directories відносно policy base ref.
|
|
1013
1209
|
* @param {string} cwd worktree
|
|
@@ -1022,6 +1218,40 @@ function changedPaths(cwd, spawnFn) {
|
|
|
1022
1218
|
return [...new Set([...tracked, ...untracked])]
|
|
1023
1219
|
}
|
|
1024
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* @param {string} cwd worktree
|
|
1223
|
+
* @param {typeof spawnSync} spawnFn інжект
|
|
1224
|
+
* @returns {boolean} чи tree diff складається лише з release entries
|
|
1225
|
+
*/
|
|
1226
|
+
function hasOnlyChangeEntriesFromBase(cwd, spawnFn) {
|
|
1227
|
+
return hasOnlyChangeEntries(changedPaths(cwd, spawnFn))
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Прибирає no-op або change-only worktree до дорогих behavioral/CI gates.
|
|
1232
|
+
* @param {object} args контекст materialized worktree
|
|
1233
|
+
* @returns {{status:string,branch:string,rationale?:string}|null} terminal outcome
|
|
1234
|
+
*/
|
|
1235
|
+
function discardPatchEquivalentWorktree(args) {
|
|
1236
|
+
const { worktree, rootCwd, spawnFn, onProgress, validated = false } = args
|
|
1237
|
+
if (!hasChangesFromBase(worktree.cwd, spawnFn)) {
|
|
1238
|
+
onProgress('remove no-op worktree')
|
|
1239
|
+
removeReconcileWorktree(worktree, rootCwd, spawnFn)
|
|
1240
|
+
return { status: 'patch-equivalent', branch: worktree.branch }
|
|
1241
|
+
}
|
|
1242
|
+
if (!hasOnlyChangeEntriesFromBase(worktree.cwd, spawnFn)) return null
|
|
1243
|
+
|
|
1244
|
+
onProgress('remove change-only worktree')
|
|
1245
|
+
removeReconcileWorktree(worktree, rootCwd, spawnFn)
|
|
1246
|
+
return {
|
|
1247
|
+
status: 'patch-equivalent',
|
|
1248
|
+
branch: worktree.branch,
|
|
1249
|
+
rationale: validated
|
|
1250
|
+
? 'Після Git validation лишилися тільки release entries у .changes/'
|
|
1251
|
+
: 'Після перенесення лишилися тільки release entries у .changes/'
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1025
1255
|
/**
|
|
1026
1256
|
* Повертає директорії зміненого коду.
|
|
1027
1257
|
* @param {string} cwd worktree
|
|
@@ -1108,8 +1338,9 @@ export async function remediateBehaviorState(
|
|
|
1108
1338
|
if (validation.remediation !== 'canonical-fixers') return { attempted: false, ok: false }
|
|
1109
1339
|
|
|
1110
1340
|
const longRunner = resolveAsyncSpawn(spawnFn, asyncSpawnFn)
|
|
1111
|
-
const changedDirectories = [
|
|
1112
|
-
|
|
1341
|
+
const changedDirectories = [
|
|
1342
|
+
...new Set([...changedSourceDirectories(cwd, spawnFn), ...changedNonCodeDirectories(cwd, spawnFn)])
|
|
1343
|
+
].toSorted()
|
|
1113
1344
|
for (const path of changedDirectories) {
|
|
1114
1345
|
onProgress(`deterministic fix (${path})`)
|
|
1115
1346
|
const fixed = await runAsync('npx', ['@7n/rules', 'lint', '--path', path], cwd, longRunner, {
|
|
@@ -1458,7 +1689,7 @@ export function classifyPullRequestChecks(prChecks, baseChecks) {
|
|
|
1458
1689
|
}
|
|
1459
1690
|
|
|
1460
1691
|
/**
|
|
1461
|
-
* Видаляє лише
|
|
1692
|
+
* Видаляє лише відновлювані dependencies зі збереженого forensic worktree.
|
|
1462
1693
|
* Git metadata, commits і tracked/untracked зміни не зачіпаються.
|
|
1463
1694
|
* @param {string} worktreeCwd шлях worktree
|
|
1464
1695
|
* @returns {boolean} чи було що прибрати
|
|
@@ -1486,13 +1717,9 @@ export async function verifyPullRequestReadiness(args) {
|
|
|
1486
1717
|
allowFailure: true,
|
|
1487
1718
|
timeoutMs: PR_CHECK_TIMEOUT_MS
|
|
1488
1719
|
})
|
|
1489
|
-
let view = await runAsync(
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
cwd,
|
|
1493
|
-
longRunner,
|
|
1494
|
-
{ allowFailure: true }
|
|
1495
|
-
)
|
|
1720
|
+
let view = await runAsync('gh', ['pr', 'view', url, '--json', 'statusCheckRollup,baseRefOid'], cwd, longRunner, {
|
|
1721
|
+
allowFailure: true
|
|
1722
|
+
})
|
|
1496
1723
|
if (view.status !== 0) {
|
|
1497
1724
|
return { status: 'pr-checks-unverified', error: `Не вдалося прочитати PR checks: ${view.stderr || view.error}` }
|
|
1498
1725
|
}
|
|
@@ -1503,13 +1730,9 @@ export async function verifyPullRequestReadiness(args) {
|
|
|
1503
1730
|
allowFailure: true,
|
|
1504
1731
|
timeoutMs: PR_CHECK_TIMEOUT_MS
|
|
1505
1732
|
})
|
|
1506
|
-
view = await runAsync(
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
cwd,
|
|
1510
|
-
longRunner,
|
|
1511
|
-
{ allowFailure: true }
|
|
1512
|
-
)
|
|
1733
|
+
view = await runAsync('gh', ['pr', 'view', url, '--json', 'statusCheckRollup,baseRefOid'], cwd, longRunner, {
|
|
1734
|
+
allowFailure: true
|
|
1735
|
+
})
|
|
1513
1736
|
if (view.status !== 0) {
|
|
1514
1737
|
return {
|
|
1515
1738
|
status: 'pr-checks-unverified',
|
|
@@ -1635,11 +1858,8 @@ async function createPullRequest(args) {
|
|
|
1635
1858
|
log,
|
|
1636
1859
|
onProgress
|
|
1637
1860
|
})
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
removeReconcileWorktree(worktree, rootCwd, spawnFn)
|
|
1641
|
-
return { status: 'patch-equivalent', branch: worktree.branch }
|
|
1642
|
-
}
|
|
1861
|
+
const appliedOutcome = discardPatchEquivalentWorktree({ worktree, rootCwd, spawnFn, onProgress })
|
|
1862
|
+
if (appliedOutcome) return appliedOutcome
|
|
1643
1863
|
const verification =
|
|
1644
1864
|
changedSourceDirectories(worktree.cwd, spawnFn).length > 0
|
|
1645
1865
|
? await finalizeBehavior({
|
|
@@ -1659,11 +1879,14 @@ async function createPullRequest(args) {
|
|
|
1659
1879
|
if (unresolved.length > 0) throw new Error(`Нерозв'язані конфлікти: ${unresolved.join(', ')}`)
|
|
1660
1880
|
git(['diff', '--check'], worktree.cwd, spawnFn)
|
|
1661
1881
|
git(['add', '-A'], worktree.cwd, spawnFn)
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1882
|
+
const validatedOutcome = discardPatchEquivalentWorktree({
|
|
1883
|
+
worktree,
|
|
1884
|
+
rootCwd,
|
|
1885
|
+
spawnFn,
|
|
1886
|
+
onProgress,
|
|
1887
|
+
validated: true
|
|
1888
|
+
})
|
|
1889
|
+
if (validatedOutcome) return validatedOutcome
|
|
1667
1890
|
onProgress('delta lint')
|
|
1668
1891
|
const finalGates = await passFinalProjectGates({
|
|
1669
1892
|
cwd: worktree.cwd,
|
|
@@ -1676,23 +1899,24 @@ async function createPullRequest(args) {
|
|
|
1676
1899
|
const baseRef = policyBaseRef(worktree.cwd)
|
|
1677
1900
|
const baseBranch = readGitPolicy(worktree.cwd).baseBranch
|
|
1678
1901
|
git(['diff', '--check', `${baseRef}...HEAD`], worktree.cwd, spawnFn)
|
|
1902
|
+
const describePr = deps.describePullRequest ?? describePullRequest
|
|
1903
|
+
const body = await describePr({
|
|
1904
|
+
runner,
|
|
1905
|
+
cwd: worktree.cwd,
|
|
1906
|
+
baseRef,
|
|
1907
|
+
source,
|
|
1908
|
+
title: group.title,
|
|
1909
|
+
rationale: group.rationale ?? candidate.rationale ?? '',
|
|
1910
|
+
verification,
|
|
1911
|
+
deps,
|
|
1912
|
+
spawnFn,
|
|
1913
|
+
log,
|
|
1914
|
+
onProgress
|
|
1915
|
+
})
|
|
1679
1916
|
onProgress('push')
|
|
1680
1917
|
git(['push', '-u', 'origin', worktree.branch], worktree.cwd, spawnFn)
|
|
1681
1918
|
|
|
1682
1919
|
onProgress('create PR')
|
|
1683
|
-
const body = [
|
|
1684
|
-
`Джерело: \`${source}\`.`,
|
|
1685
|
-
'',
|
|
1686
|
-
group.rationale ?? candidate.rationale ?? `Корисну поведінку перенесено на актуальний ${baseBranch}.`,
|
|
1687
|
-
'',
|
|
1688
|
-
'Перевірки:',
|
|
1689
|
-
`- \`git diff --check ${baseRef}...HEAD\``,
|
|
1690
|
-
'- scoped code lint/tests та domain lint для non-code paths',
|
|
1691
|
-
'- `npx @7n/rules lint changelog --no-fix`',
|
|
1692
|
-
verification ? `- LLM behavioral verification: ${verification.slice(0, 1000)}` : ''
|
|
1693
|
-
]
|
|
1694
|
-
.filter(Boolean)
|
|
1695
|
-
.join('\n')
|
|
1696
1920
|
createdPr = run(
|
|
1697
1921
|
'gh',
|
|
1698
1922
|
['pr', 'create', '--base', baseBranch, '--head', worktree.branch, '--title', group.title, '--body', body],
|
|
@@ -2223,12 +2447,7 @@ export async function runGitReconcileOrchestrator(options = {}) {
|
|
|
2223
2447
|
|
|
2224
2448
|
const report = formatReport({ inventory, results })
|
|
2225
2449
|
log(report)
|
|
2226
|
-
const blockingStatuses = new Set([
|
|
2227
|
-
'failed',
|
|
2228
|
-
'pr-checks-regressed',
|
|
2229
|
-
'pr-checks-baseline-red',
|
|
2230
|
-
'pr-checks-unverified'
|
|
2231
|
-
])
|
|
2450
|
+
const blockingStatuses = new Set(['failed', 'pr-checks-regressed', 'pr-checks-baseline-red', 'pr-checks-unverified'])
|
|
2232
2451
|
const ok = results.every(result => !blockingStatuses.has(result.status) && result.incomplete !== true)
|
|
2233
2452
|
return { ok, report, inventory, results }
|
|
2234
2453
|
}
|