@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.49.22",
3
+ "version": "1.49.24",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -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: 59ea7f47
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
- `pruneForensicDependencies` прибирає лише rebuildable `node_modules` зі збереженого forensic worktree. `cleanupSource` видаляє лише точний доказово безпечний branch/stash. Cleanup review-source дозволений після `drop` або коли всі його групи завершились `pr-created` чи `patch-equivalent`; `failed` і всі `pr-checks-*` outcomes лишають source та forensic worktree.
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
- - `pruneForensicDependencies` — звільняє rebuildable dependencies без втрати Git evidence.
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 = [...new Set([...changedSourceDirectories(cwd, spawnFn), ...changedNonCodeDirectories(cwd, spawnFn)])]
1112
- .toSorted()
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
- * Видаляє лише rebuildable dependencies зі збереженого forensic worktree.
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
- 'gh',
1491
- ['pr', 'view', url, '--json', 'statusCheckRollup,baseRefOid'],
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
- 'gh',
1508
- ['pr', 'view', url, '--json', 'statusCheckRollup,baseRefOid'],
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
- if (!hasChangesFromBase(worktree.cwd, spawnFn)) {
1639
- onProgress('remove no-op worktree')
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
- if (!hasChangesFromBase(worktree.cwd, spawnFn)) {
1663
- onProgress('remove no-op worktree')
1664
- removeReconcileWorktree(worktree, rootCwd, spawnFn)
1665
- return { status: 'patch-equivalent', branch: worktree.branch }
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
  }