@nitra/cursor 3.22.0 → 3.23.0

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.
Files changed (228) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/CHANGELOG.md +31 -3
  3. package/bin/docs/n-cursor.md +636 -0
  4. package/bin/docs/rename-yaml-extensions.md +207 -0
  5. package/bin/n-cursor.js +30 -3
  6. package/package.json +1 -1
  7. package/rules/abie/docs/fix.md +18 -0
  8. package/rules/abie/js/docs/applies.md +26 -0
  9. package/rules/abie/js/docs/env_dns.md +32 -0
  10. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  11. package/rules/abie/js/docs/hc_pairing.md +35 -0
  12. package/rules/abie/js/docs/ua_http_route.md +28 -0
  13. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  14. package/rules/abie/lib/docs/enabled.md +29 -0
  15. package/rules/abie/lib/docs/env-dns.md +35 -0
  16. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  17. package/rules/abie/lib/docs/http-route.md +44 -0
  18. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  19. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  20. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  21. package/rules/abie/lib/docs/yaml.md +29 -0
  22. package/rules/adr/docs/fix.md +148 -0
  23. package/rules/adr/js/docs/hooks.md +259 -0
  24. package/rules/bun/docs/fix.md +156 -0
  25. package/rules/bun/js/docs/layout.md +393 -0
  26. package/rules/capacitor/docs/fix.md +121 -0
  27. package/rules/capacitor/js/docs/platforms.md +295 -0
  28. package/rules/changelog/changelog.mdc +2 -2
  29. package/rules/changelog/docs/fix.md +174 -0
  30. package/rules/changelog/js/consistency.mjs +114 -13
  31. package/rules/changelog/js/docs/consistency.md +387 -0
  32. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  33. package/rules/ci4/docs/fix.md +179 -0
  34. package/rules/ci4/js/docs/marksman_config.md +128 -0
  35. package/rules/docker/docker.mdc +8 -3
  36. package/rules/docker/docs/fix.md +171 -0
  37. package/rules/docker/js/docs/lint.md +258 -0
  38. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  39. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  40. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  41. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  42. package/rules/docker/lint/docs/lint.md +193 -0
  43. package/rules/efes/docs/fix.md +203 -0
  44. package/rules/feedback/docs/fix.md +140 -0
  45. package/rules/flow/docs/fix.md +152 -0
  46. package/rules/ga/docs/fix.md +158 -0
  47. package/rules/ga/js/docs/lint.md +100 -0
  48. package/rules/ga/js/docs/workflows.md +217 -0
  49. package/rules/ga/lint/docs/lint.md +209 -0
  50. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  51. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  52. package/rules/graphql/docs/fix.md +126 -0
  53. package/rules/graphql/js/docs/tooling.md +264 -0
  54. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  55. package/rules/hasura/docs/fix.md +120 -0
  56. package/rules/hasura/hasura.mdc +14 -0
  57. package/rules/hasura/js/docs/internal_urls.md +326 -0
  58. package/rules/image-avif/docs/fix.md +132 -0
  59. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  60. package/rules/image-compress/docs/fix.md +150 -0
  61. package/rules/image-compress/js/docs/package_setup.md +191 -0
  62. package/rules/js-bun-db/docs/fix.md +148 -0
  63. package/rules/js-bun-db/js/docs/safety.md +231 -0
  64. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  65. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  66. package/rules/js-bun-redis/docs/fix.md +123 -0
  67. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  68. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  69. package/rules/js-lint/docs/fix.md +117 -0
  70. package/rules/js-lint/js/docs/lint.md +250 -0
  71. package/rules/js-lint/js/docs/tooling.md +348 -0
  72. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  73. package/rules/js-lint-ci/docs/fix.md +154 -0
  74. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  75. package/rules/js-mssql/docs/fix.md +128 -0
  76. package/rules/js-mssql/js/docs/deps.md +263 -0
  77. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  78. package/rules/js-run/docs/fix.md +144 -0
  79. package/rules/js-run/js/docs/runtime.md +388 -0
  80. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  81. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  82. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  83. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  84. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  85. package/rules/k8s/docs/fix.md +129 -0
  86. package/rules/k8s/js/docs/manifests.md +344 -0
  87. package/rules/k8s/js/manifests.mjs +6 -2
  88. package/rules/k8s/k8s.mdc +4 -2
  89. package/rules/k8s/lint/docs/lint.md +411 -0
  90. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  91. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  92. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  93. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  94. package/rules/npm-module/docs/fix.md +98 -0
  95. package/rules/npm-module/js/docs/package_structure.md +274 -0
  96. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  97. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  98. package/rules/php/docs/fix.md +107 -0
  99. package/rules/php/js/docs/tooling.md +152 -0
  100. package/rules/php/lint/docs/lint.md +215 -0
  101. package/rules/python/docs/fix.md +163 -0
  102. package/rules/python/js/docs/applies.md +108 -0
  103. package/rules/python/js/docs/tooling.md +153 -0
  104. package/rules/python/lint/docs/lint.md +322 -0
  105. package/rules/rego/docs/fix.md +121 -0
  106. package/rules/rego/js/docs/applies.md +174 -0
  107. package/rules/rego/js/docs/lint.md +118 -0
  108. package/rules/rego/lint/docs/lint.md +204 -0
  109. package/rules/release/docs/change.md +185 -0
  110. package/rules/release/docs/fix.md +119 -0
  111. package/rules/release/docs/release.md +222 -0
  112. package/rules/release/lib/docs/aggregate.md +246 -0
  113. package/rules/release/lib/docs/change-file.md +200 -0
  114. package/rules/release/lib/docs/fallback.md +203 -0
  115. package/rules/rust/docs/fix.md +129 -0
  116. package/rules/rust/js/docs/applies.md +140 -0
  117. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  118. package/rules/security/docs/fix.md +86 -0
  119. package/rules/security/js/docs/lint.md +171 -0
  120. package/rules/security/js/docs/sample_secret.md +190 -0
  121. package/rules/security/js/docs/trufflehog.md +137 -0
  122. package/rules/security/js/lint.mjs +9 -1
  123. package/rules/style-lint/docs/fix.md +155 -0
  124. package/rules/style-lint/js/docs/lint.md +184 -0
  125. package/rules/style-lint/js/docs/tooling.md +194 -0
  126. package/rules/tauri/docs/fix.md +158 -0
  127. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  128. package/rules/tauri/js/docs/tooling.md +228 -0
  129. package/rules/test/coverage/coverage.mjs +15 -3
  130. package/rules/test/docs/fix.md +132 -0
  131. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  132. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  134. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  135. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  136. package/rules/test/js/docs/location.md +136 -0
  137. package/rules/test/js/docs/no-process-chdir.md +160 -0
  138. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  139. package/rules/test/js/docs/stryker_config.md +152 -0
  140. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  141. package/rules/text/docs/fix.md +118 -0
  142. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  143. package/rules/text/js/docs/formatting.md +256 -0
  144. package/rules/text/js/docs/lint.md +122 -0
  145. package/rules/text/lint/docs/lint.md +220 -0
  146. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  147. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  148. package/rules/text/lint/docs/run-v8r.md +197 -0
  149. package/rules/vue/docs/fix.md +127 -0
  150. package/rules/vue/js/docs/packages.md +335 -0
  151. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  152. package/rules/worktree/docs/fix.md +161 -0
  153. package/schemas/rule-meta.json +5 -1
  154. package/scripts/auto-rules.mjs +7 -4
  155. package/scripts/coverage-classify/docs/apply.md +202 -0
  156. package/scripts/coverage-classify/docs/cache.md +203 -0
  157. package/scripts/coverage-classify/docs/index.md +218 -0
  158. package/scripts/coverage-classify/docs/prompt.md +132 -0
  159. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  160. package/scripts/coverage-fix-extract.mjs +122 -0
  161. package/scripts/coverage-fix.mjs +1 -1
  162. package/scripts/dispatcher/docs/graph.md +346 -0
  163. package/scripts/dispatcher/docs/index.md +236 -0
  164. package/scripts/dispatcher/docs/trace.md +296 -0
  165. package/scripts/dispatcher/index.mjs +1 -1
  166. package/scripts/dispatcher/lib/active.mjs +4 -8
  167. package/scripts/dispatcher/lib/commands.mjs +7 -11
  168. package/scripts/dispatcher/lib/docs/active.md +348 -0
  169. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  170. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  171. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  172. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  173. package/scripts/dispatcher/lib/docs/events.md +182 -0
  174. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  175. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  176. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  177. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  178. package/scripts/dispatcher/lib/docs/level.md +335 -0
  179. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  180. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  181. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  182. package/scripts/dispatcher/lib/docs/review.md +255 -0
  183. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  184. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  185. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  186. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  187. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  188. package/scripts/dispatcher/lib/executor.mjs +6 -1
  189. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  190. package/scripts/dispatcher/lib/level.mjs +29 -3
  191. package/scripts/dispatcher/lib/review.mjs +1 -1
  192. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  193. package/scripts/docs/auto-rules.md +376 -0
  194. package/scripts/docs/auto-skills.md +173 -0
  195. package/scripts/docs/build-agents-commands.md +183 -0
  196. package/scripts/docs/cli-entry.md +153 -0
  197. package/scripts/docs/coverage-fix.md +177 -0
  198. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  199. package/scripts/lib/changed-files.mjs +4 -1
  200. package/scripts/lib/docs/changed-files.md +149 -0
  201. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  202. package/scripts/lib/docs/check-reporter.md +175 -0
  203. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  204. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  205. package/scripts/lib/docs/ensure-tool.md +254 -0
  206. package/scripts/lib/docs/generated-markdown.md +275 -0
  207. package/scripts/lib/docs/gha-workflow.md +326 -0
  208. package/scripts/lib/docs/inline-template-links.md +303 -0
  209. package/scripts/lib/docs/list-rule-ids.md +156 -0
  210. package/scripts/lib/docs/load-cursor-config.md +147 -0
  211. package/scripts/lib/docs/mirror-parity.md +167 -0
  212. package/scripts/lib/worktree.mjs +26 -0
  213. package/scripts/worktree-cli.mjs +12 -2
  214. package/skills/coverage-fix/SKILL.md +34 -45
  215. package/skills/docgen/SKILL.md +44 -23
  216. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  217. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  218. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  219. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  220. package/skills/docgen/js/docgen-scan.mjs +37 -21
  221. package/skills/llm-patch/SKILL.md +23 -2
  222. package/skills/start-check/SKILL.md +26 -53
  223. package/skills/start-check/js/check.mjs +211 -0
  224. package/skills/taze/SKILL.md +9 -3
  225. package/skills/taze/js/diff.mjs +154 -0
  226. package/types/bin/n-cursor.d.ts +1 -1
  227. package/skills/fix-tests/SKILL.md +0 -119
  228. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,222 @@
1
+ # `release.mjs` — оркестратор реліз-процесу `n-cursor release`
2
+
3
+ ## Огляд
4
+
5
+ Модуль `npm/rules/release/release.mjs` — це ядро команди `n-cursor release`. Він агрегує per-workspace change-файли (накопичені у `CHANGES_DIR` кожного воркспейсу) у:
6
+
7
+ 1. version-bump у маніфесті пакета (`package.json` для npm-пакетів або `pyproject.toml` для Python),
8
+ 2. новий розділ у `CHANGELOG.md` відповідного воркспейсу,
9
+ 3. git-коміт зі стандартизованим subject `release: <name@version>, ...`,
10
+ 4. git-теги у форматі `<name>@<version>` для кожного зрелізованого пакета,
11
+ 5. фізичне видалення «спожитих» change-файлів,
12
+ 6. `git push --follow-tags`.
13
+
14
+ Модуль розрахований на запуск у CI на гілці `main` (варіант A з ADR `n-cursor-release-design`). Сам він **нічого не публікує** у реєстри (npm/PyPI) — цим займаються окремі CI-кроки, орієнтовані на створені теги.
15
+
16
+ Підтримуються:
17
+
18
+ - monorepo (root має суб-воркспейси) — root-пакет автоматично пропускається, релізиться лише кожен суб-воркспейс окремо;
19
+ - single-package репо (`workspaces === ['.']`) — релізиться сам root.
20
+
21
+ Якщо явних change-файлів у воркспейсі немає, але в історії з останнього тегу `<name>@<version>` є коміти, модуль робить **fallback-синтез** запису з commit log (через `synthesizeChangeFromCommits`).
22
+
23
+ ## Експорти / API
24
+
25
+ | Символ | Тип | Призначення |
26
+ | ----------------------------- | ---------------- | ------------------------------------------------------------------------------------------------- |
27
+ | `release(opts?)` | `async function` | Програмний API: виконує повний реліз-цикл і повертає масив зрелізованих пакетів. |
28
+ | `runReleaseCli(_args, opts?)` | `async function` | CLI-обгортка: запускає `release`, друкує підсумок у stdout/stderr, повертає exit-код (`0` / `1`). |
29
+
30
+ Внутрішні (не експортовані) функції-помічники: `writeManifestVersion`, `prependWorkspaceChangelog`, `collectChangeFiles`.
31
+
32
+ Внутрішні константи: `SEMVER_LINE_RE`, `PY_VERSION_LINE_RE` — regex для in-place заміни рядка `version` у відповідних типах маніфесту.
33
+
34
+ ## Функції
35
+
36
+ ### `writeManifestVersion(cwd, manifest, newVersion)` (внутрішня)
37
+
38
+ Записує нову version у маніфест пакета, зберігаючи форматування файлу.
39
+
40
+ - **Сигнатура:** `async (cwd: string, manifest: PackageManifest, newVersion: string) => Promise<void>`
41
+ - **Параметри:**
42
+ - `cwd` — абсолютний шлях кореня репо (root проекту);
43
+ - `manifest` — об'єкт маніфесту з типу `PackageManifest`, що містить поля `ws` (відносний шлях до воркспейсу, або `.` для root), `manifestRel` (відносний шлях до файлу маніфесту в межах воркспейсу) і `kind` (`'npm'` чи інший — трактується як Python);
44
+ - `newVersion` — новий рядок версії (SemVer для npm, PEP 440 / SemVer для Python — не валідується тут).
45
+ - **Повертає:** `Promise<void>` після успішного запису.
46
+ - **Side effects:**
47
+ - читає файл маніфесту з диска;
48
+ - перезаписує його зі зміненим рядком версії;
49
+ - **обирає regex за типом маніфесту**: `SEMVER_LINE_RE` для `'npm'`, `PY_VERSION_LINE_RE` інакше;
50
+ - **кидає `Error`**, якщо в файлі не знайдено патерн рядка `version` (тобто `text.replace(...)` не змінив текст).
51
+
52
+ ### `prependWorkspaceChangelog(cwd, ws, sectionBlock)` (внутрішня)
53
+
54
+ Доклеює (prepend) новий блок CHANGELOG до початку `<ws>/CHANGELOG.md`; якщо файл не існує — створює.
55
+
56
+ - **Сигнатура:** `async (cwd: string, ws: string, sectionBlock: string) => Promise<void>`
57
+ - **Параметри:**
58
+ - `cwd` — корінь репо;
59
+ - `ws` — відносний шлях воркспейсу від `cwd` (наприклад, `'npm'`, `'.'`);
60
+ - `sectionBlock` — готовий markdown-блок нового розділу (формат сформований `aggregateWorkspace`).
61
+ - **Повертає:** `Promise<void>`.
62
+ - **Side effects:** читає (за наявності) і пише `<cwd>/<ws>/CHANGELOG.md`. Логіку конкатенації керує `prependChangelogSection` з `./lib/aggregate.mjs` — модуль `release.mjs` лише викликає її, не дублюючи правил вставки.
63
+
64
+ ### `collectChangeFiles(cwd, manifest, runGit)` (внутрішня)
65
+
66
+ Збирає всі change-записи для воркспейсу: спочатку явні файли з `CHANGES_DIR`, інакше — fallback-синтез з історії комітів.
67
+
68
+ - **Сигнатура:** `async (cwd: string, manifest: PackageManifest, runGit: (args: string[]) => Promise<string | null>) => Promise<Array<{ file: string | null, entry: { bump: string, section: string, description: string } }>>`
69
+ - **Параметри:**
70
+ - `cwd` — корінь репо;
71
+ - `manifest` — маніфест воркспейсу;
72
+ - `runGit` — git-раннер; повертає stdout як рядок, або `null`, якщо команда зафейлилася.
73
+ - **Повертає:** масив об'єктів `{ file, entry }`:
74
+ - `file` — ім'я change-файлу у `CHANGES_DIR` (для подальшого видалення), або `null` для синтезованого запису;
75
+ - `entry` — нормалізований опис зміни: `{ bump, section, description }` (типи `bump`/`section` визначаються форматом change-файлу і `synthesizeChangeFromCommits`).
76
+ - **Поведінка:**
77
+ 1. Викликає `readChangeFiles(manifest.ws, cwd)`; якщо результат непустий — повертає його (явні мають пріоритет).
78
+ 2. Інакше: якщо у маніфесті немає `name` — повертає `[]` (без імені неможливо знайти попередній тег для fallback).
79
+ 3. Інакше викликає `synthesizeChangeFromCommits(name, ws, { runGit })`; якщо нічого не синтезувалося — `[]`.
80
+ 4. Якщо синтезовано — друкує в stderr попередження `⚠️ <ws>: немає change-файлів — синтезовано запис із комітів (fallback)` і повертає `[{ file: null, entry: synthesized }]`.
81
+ - **Side effects:** виклики git через `runGit` (всередині `synthesizeChangeFromCommits`); `console.warn` при fallback.
82
+
83
+ ### `release(opts?)` (експорт)
84
+
85
+ Основний програмний вхід — виконує повний реліз-цикл для всіх релевантних воркспейсів.
86
+
87
+ - **Сигнатура:**
88
+ ```
89
+ async (opts?: {
90
+ cwd?: string,
91
+ date?: string,
92
+ runGit?: (args: string[]) => Promise<string | null>,
93
+ }) => Promise<Array<{ ws: string, name: string | null, newVersion: string }>>
94
+ ```
95
+ - **Параметри `opts` (усі необов'язкові):**
96
+ - `cwd` — корінь репо; за замовчуванням `process.cwd()`;
97
+ - `date` — рядок у форматі `YYYY-MM-DD` для дати релізу в CHANGELOG; за замовчуванням сьогоднішня UTC-дата (`new Date().toISOString().slice(0, 10)`);
98
+ - `runGit` — інжектований git-раннер; за замовчуванням `defaultRunGit(cwd)` (виконує справжні git-команди у `cwd`).
99
+ - **Повертає:** масив зрелізованих пакетів, кожен — `{ ws, name, newVersion }`. Якщо релізити нічого не було — повертає `[]` (без коміту/тегу/пушу).
100
+ - **Алгоритм:**
101
+ 1. Отримує список воркспейсів через `getMonorepoProjectRootDirs(cwd)`.
102
+ 2. Визначає, чи це monorepo: `subWorkspaces = workspaces.filter(w => w !== '.')`, `isMonorepoRoot = subWorkspaces.length > 0`.
103
+ 3. Для кожного `ws`:
104
+ - якщо `ws === '.'` і `isMonorepoRoot` — пропустити (root у monorepo не релізиться сам по собі);
105
+ - читає маніфест через `readPackageManifest(ws, cwd)`; якщо манfest відсутній або без `version` — пропустити;
106
+ - збирає change-записи через `collectChangeFiles`;
107
+ - викликає `aggregateWorkspace({ currentVersion, changeFiles, date })` — якщо повертає `null` (нема чого релізити, всі записи відфільтровано) — пропустити;
108
+ - інакше: записує нову версію у маніфест (`writeManifestVersion`), prepend новий розділ у `CHANGELOG.md` (`prependWorkspaceChangelog`), видаляє кожен спожитий change-файл (`rm` у `<cwd>/<ws>/<CHANGES_DIR>/<file>`), додає запис у `released`, а якщо є `manifest.name` — формує тег `<name>@<newVersion>`.
109
+ 4. Якщо `released.length > 0`:
110
+ - формує subject коміту: список тегів через `, `, або (якщо тегів нема — наприклад, у пакетів без `name`) — `<ws>@<newVersion>`-значення;
111
+ - `git add -A`;
112
+ - `git commit -m "release: <subject>"` — якщо `runGit` повертає `null` (коміт не вдався), кидає `Error('release: git commit не вдався — теги та push скасовано')`;
113
+ - для кожного тегу — `git tag <tag>`;
114
+ - `git push --follow-tags`.
115
+ - **Side effects:**
116
+ - читання/запис файлів маніфестів і `CHANGELOG.md`;
117
+ - видалення change-файлів з диска (`rm`);
118
+ - виконання git-команд через `runGit` (включно з push до remote);
119
+ - `console.warn` при fallback (через `collectChangeFiles`).
120
+ - **Помилки:** кидаються через `throw new Error(...)` у двох місцях:
121
+ - не знайдено патерн `version` у маніфесті (`writeManifestVersion`);
122
+ - не вдався `git commit` (`runGit` повернув `null`).
123
+
124
+ ### `runReleaseCli(_args, opts?)` (експорт)
125
+
126
+ CLI-фасад: викликає `release(opts)`, друкує підсумок, мапить помилки на exit-код.
127
+
128
+ - **Сигнатура:** `async (_args: string[], opts?: { cwd?: string, date?: string, runGit?: ... }) => Promise<number>`
129
+ - **Параметри:**
130
+ - `_args` — позиційні CLI-аргументи (поточна імплементація опцій з CLI не приймає, тому ігнорується; параметр підкреслений `_` для лінту);
131
+ - `opts` — ті самі опції, що в `release` (використовується тестами для інжекції `cwd`/`date`/`runGit`).
132
+ - **Повертає:** `Promise<number>` — exit-код:
133
+ - `0` — успіх (включно з випадком «немає що релізити»);
134
+ - `1` — будь-яка помилка з `release`.
135
+ - **Поведінка:**
136
+ - якщо `released.length === 0` — друкує `release: немає змін для релізу`;
137
+ - інакше для кожного запису — `console.log` рядок `✅ <name або ws>@<newVersion>`;
138
+ - при exception — `console.error("❌ <message>")` і повертає `1`. Підтримує і `Error` (бере `.message`), і не-`Error`-значення (приводить через `String(...)`).
139
+
140
+ ## Залежності
141
+
142
+ ### Стандартна бібліотека Node.js
143
+
144
+ - `node:fs` — `existsSync` (для перевірки існування `CHANGELOG.md`);
145
+ - `node:fs/promises` — `readFile`, `writeFile`, `rm`;
146
+ - `node:path` — `join`.
147
+
148
+ ### Внутрішні модулі проекту
149
+
150
+ - `../changelog/lib/package-manifest.mjs`:
151
+ - `getMonorepoProjectRootDirs(cwd)` — повертає список воркспейсів (включно з `.`);
152
+ - `readPackageManifest(ws, cwd)` — читає маніфест воркспейсу;
153
+ - тип `PackageManifest` (через JSDoc-імпорт).
154
+ - `./lib/aggregate.mjs`:
155
+ - `aggregateWorkspace({ currentVersion, changeFiles, date })` — обчислює `newVersion` і `sectionBlock` CHANGELOG за зібраними change-записами; повертає `null`, якщо немає змін до релізу;
156
+ - `prependChangelogSection(existing, sectionBlock)` — формує новий вміст `CHANGELOG.md` зі вставкою блоку на початок.
157
+ - `./lib/change-file.mjs`:
158
+ - константа `CHANGES_DIR` — назва теки з change-файлами всередині воркспейсу;
159
+ - `readChangeFiles(ws, cwd)` — читає всі change-файли воркспейсу.
160
+ - `./lib/fallback.mjs`:
161
+ - `defaultRunGit(cwd)` — фабрика git-раннера з прив'язкою до `cwd`;
162
+ - `synthesizeChangeFromCommits(name, ws, { runGit })` — синтезує change-запис із commit-історії з останнього тегу `<name>@*`.
163
+
164
+ ### Зовнішні залежності
165
+
166
+ Жодних npm-пакетів — лише вбудовані модулі Node.js та внутрішні модулі проекту.
167
+
168
+ ## Потік виконання / Використання
169
+
170
+ ### CLI-використання (через диспатчер `n-cursor`)
171
+
172
+ `runReleaseCli` під'єднується до точки входу `n-cursor release`. Зазвичай викликається в CI на `main` після злиття PR:
173
+
174
+ ```bash
175
+ n-cursor release
176
+ ```
177
+
178
+ Exit-код `0` — навіть якщо нічого не зрелізовано (в стандартний випадок «нічого не змінилось»). Exit-код `1` — фейл (помилка запису маніфесту, фейл `git commit`, тощо).
179
+
180
+ ### Програмне використання (у тестах / інших скриптах)
181
+
182
+ ```js
183
+ import { release, runReleaseCli } from './release.mjs'
184
+
185
+ // 1) Прямий виклик
186
+ const released = await release({
187
+ cwd: '/abs/path/to/repo',
188
+ date: '2026-06-03',
189
+ runGit: async args => '...stdout...' // або null при помилці
190
+ })
191
+
192
+ // 2) Через CLI-фасад
193
+ const code = await runReleaseCli([], { cwd, date, runGit })
194
+ process.exit(code)
195
+ ```
196
+
197
+ ### Послідовність кроків у `release()`
198
+
199
+ 1. **Дискавер воркспейсів** — `getMonorepoProjectRootDirs(cwd)`.
200
+ 2. **Класифікація** — root пропускається у monorepo (де є суб-воркспейси).
201
+ 3. **Для кожного воркспейсу:**
202
+ - читання маніфесту;
203
+ - збір change-файлів (явні → fallback-синтез з комітів);
204
+ - агрегація → `newVersion` + `sectionBlock`;
205
+ - запис маніфесту;
206
+ - prepend CHANGELOG;
207
+ - видалення «спожитих» change-файлів;
208
+ - реєстрація запису і (за наявності `name`) тегу.
209
+ 4. **Якщо є зрелізоване хоча б одне:**
210
+ - формування subject коміту;
211
+ - `git add -A` → `git commit -m "release: <subject>"`;
212
+ - перевірка успішності коміту (інакше throw);
213
+ - проставляння всіх тегів;
214
+ - `git push --follow-tags`.
215
+ 5. **Повернення масиву** зрелізованих пакетів.
216
+
217
+ ### Інваріанти
218
+
219
+ - Жодних версій/тегів/коміту, якщо немає реальних змін у жодному воркспейсі — масив `released` залишається порожнім, і блок git взагалі не виконується.
220
+ - Якщо хоча б у одному воркспейсі не вдалось оновити маніфест — функція кидає виняток до `git add -A`; часткові зміни на диску можуть залишитися (виклик не транзакційний — це відповідальність CI/runner-а відкотити робоче дерево).
221
+ - Якщо `git commit` зафейлився — теги не проставляються і push не виконується; кидається явна помилка.
222
+ - Усі git-операції проходять через інжектований `runGit`, що дає змогу тестувати функцію без реальних git-викликів.
@@ -0,0 +1,246 @@
1
+ # aggregate.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `aggregate.mjs` забезпечує **агрегацію change-файлів одного workspace** у дві основні сутності:
6
+
7
+ 1. Новий рядок версії згідно semver-правил (`x.y.z` → бамп `major|minor|patch`).
8
+ 2. Markdown-секцію для файлу `CHANGELOG.md`, побудовану у форматі [Keep a Changelog 1.1.0](https://keepachangelog.com/uk/1.1.0/), де новіша версія додається зверху.
9
+
10
+ Модуль є **чистим обчислювальним шаром** — він **не має побічних ефектів**: не читає й не пише файли, не викликає `git`, не звертається до мережі чи процесів. Усі операції зводяться до парсингу/рендерингу рядків та арифметики на номерах версії. Відповідальність за читання `CHANGELOG.md` з диску, виконання `git`-команд, видалення спожитих change-файлів та оновлення `package.json` (чи аналогічного маніфесту) лежить на викликачі — за конвенцією проєкту це `release.mjs`.
11
+
12
+ Через відсутність побічних ефектів модуль легко тестується юніт-тестами без mock’ів файлової системи й використовується як деталізована «бібліотечна» частина CLI `n-cursor release` для монорепо.
13
+
14
+ ## Експорти / API
15
+
16
+ | Експорт | Тип | Призначення |
17
+ | ----------------------------------------------------------- | ------- | -------------------------------------------------------------------------------- |
18
+ | `bumpVersion(version, bump)` | функція | Інкремент semver-рядка згідно з рівнем бампа |
19
+ | `maxBump(bumps)` | функція | Найвищий пріоритет бампа у списку (`major` > `minor` > `patch`) |
20
+ | `renderChangelogSection(version, date, entries)` | функція | Рендер однієї версійної секції у markdown |
21
+ | `prependChangelogSection(existingText, sectionBlock)` | функція | Вставка нової секції зверху наявного `CHANGELOG.md` |
22
+ | `aggregateWorkspace({ currentVersion, changeFiles, date })` | функція | Високорівневе об’єднання change-файлів workspace у `newVersion` + `sectionBlock` |
23
+
24
+ Усі експорти — **іменовані** (`export function …`). Default-export відсутній.
25
+
26
+ ## Функції
27
+
28
+ ### `bumpVersion(version, bump)`
29
+
30
+ **Сигнатура:** `bumpVersion(version: string, bump: 'major'|'minor'|'patch'): string`
31
+
32
+ **Параметри:**
33
+
34
+ - `version` — поточна версія у форматі `x.y.z`, де `x`, `y`, `z` — невід’ємні цілі (валідується регулярним виразом `^(\d+)\.(\d+)\.(\d+)$`).
35
+ - `bump` — рівень бампа: `'major'`, `'minor'` або `'patch'`. Будь-яке інше значення (включно з `undefined`, помилковим написанням) поведеться як `'patch'` (через гілку `return` без явної перевірки).
36
+
37
+ **Повертає:** новий рядок версії згідно з правилами semver:
38
+
39
+ - `major` → `(major + 1).0.0`
40
+ - `minor` → `major.(minor + 1).0`
41
+ - `patch` (за замовчуванням у `else`-гілці) → `major.minor.(patch + 1)`
42
+
43
+ **Помилки:** `Error('aggregate: невалідний semver «<version>»')`, якщо вхідний рядок не відповідає формату `x.y.z`.
44
+
45
+ **Side effects:** немає.
46
+
47
+ **Приклади:**
48
+
49
+ - `bumpVersion('1.2.3', 'major')` → `'2.0.0'`
50
+ - `bumpVersion('1.2.3', 'minor')` → `'1.3.0'`
51
+ - `bumpVersion('1.2.3', 'patch')` → `'1.2.4'`
52
+ - `bumpVersion('0.0.1', 'major')` → `'1.0.0'`
53
+
54
+ ### `maxBump(bumps)`
55
+
56
+ **Сигнатура:** `maxBump(bumps: string[]): string`
57
+
58
+ **Параметри:**
59
+
60
+ - `bumps` — непорожній (за контрактом виклику) список рядків-рівнів бампа. Допускаються лише значення зі `VALID_BUMPS` (`['major', 'minor', 'patch']`), але функція не валідує їх явно.
61
+
62
+ **Повертає:** найвищий рівень бампа з масиву за пріоритетом `major > minor > patch`. Якщо у `bumps` немає жодного з `VALID_BUMPS`, повертається `'patch'` (через оператор `??`).
63
+
64
+ **Реалізація:** використовує `VALID_BUMPS.find(level => bumps.includes(level))`. Оскільки `VALID_BUMPS` упорядкований як `['major', 'minor', 'patch']`, `find` поверне **перший** наявний у `bumps` рівень — тобто максимальний.
65
+
66
+ **Side effects:** немає.
67
+
68
+ **Приклади:**
69
+
70
+ - `maxBump(['patch', 'minor'])` → `'minor'`
71
+ - `maxBump(['patch', 'major', 'minor'])` → `'major'`
72
+ - `maxBump(['patch'])` → `'patch'`
73
+ - `maxBump([])` → `'patch'` (через fallback `?? 'patch'`)
74
+
75
+ ### `renderChangelogSection(version, date, entries)`
76
+
77
+ **Сигнатура:** `renderChangelogSection(version: string, date: string, entries: Array<{ section: string, description: string }>): string`
78
+
79
+ **Параметри:**
80
+
81
+ - `version` — нова версія (рядок `x.y.z`).
82
+ - `date` — дата релізу у форматі `YYYY-MM-DD` (не валідується функцією).
83
+ - `entries` — масив записів change-файлів, де кожен запис має ключі `section` (один із `VALID_SECTIONS`) та `description`. Поле `bump` тут уже не використовується.
84
+
85
+ **Повертає:** markdown-рядок, який починається з заголовка `## [<version>] - <date>\n`. Далі для кожного значення з `VALID_SECTIONS` (`Added`, `Changed`, `Fixed`, `Removed`) — за наявності записів — додається блок:
86
+
87
+ ```
88
+ \n### <section>\n\n- <description1>\n- <description2>\n
89
+ ```
90
+
91
+ Порядок секцій у виводі **фіксований** і відповідає порядку у `VALID_SECTIONS`, незалежно від порядку записів на вході. Усередині секції рядки додаються у тому ж порядку, у якому йшли у `entries` (стабільність забезпечується `Array.prototype.filter`).
92
+
93
+ Секції без записів пропускаються (`continue`).
94
+
95
+ **Side effects:** немає.
96
+
97
+ **Приклад:**
98
+
99
+ Вхід:
100
+
101
+ - `version = '1.3.0'`
102
+ - `date = '2026-06-03'`
103
+ - `entries = [{ section: 'Added', description: 'нова опція X' }, { section: 'Fixed', description: 'падіння на Y' }]`
104
+
105
+ Вихід:
106
+
107
+ ```
108
+ ## [1.3.0] - 2026-06-03
109
+
110
+ ### Added
111
+
112
+ - нова опція X
113
+
114
+ ### Fixed
115
+
116
+ - падіння на Y
117
+ ```
118
+
119
+ ### `prependChangelogSection(existingText, sectionBlock)`
120
+
121
+ **Сигнатура:** `prependChangelogSection(existingText: string, sectionBlock: string): string`
122
+
123
+ **Параметри:**
124
+
125
+ - `existingText` — наявний вміст `CHANGELOG.md` (може бути порожнім рядком, або починатися з пробільних символів).
126
+ - `sectionBlock` — попередньо зрендерений markdown-блок (зазвичай результат `renderChangelogSection`).
127
+
128
+ **Повертає:** оновлений текст `CHANGELOG.md` у форматі Keep a Changelog (новіше зверху).
129
+
130
+ **Логіка:**
131
+
132
+ 1. `existingText.trimStart()` — обрізаються провідні пробільні символи.
133
+ 2. Якщо отриманий текст **не** починається з заголовка `# Changelog`, повертається свіжий документ: `# Changelog\n\n<sectionBlock>` (тобто все попереднє вмонтоване як «без заголовка» відкидається — викликач має самостійно дбати про коректний вхідний файл).
134
+ 3. Інакше:
135
+ - `head` — перший рядок до `\n` (зазвичай `# Changelog`, може містити додаткові символи у тому ж рядку).
136
+ - `rest` — решта тексту після першого `\n`, із обрізаним початком (`trimStart`).
137
+ - Повертається конкатенація `head + '\n\n' + sectionBlock + '\n' + rest`.
138
+ 4. Якщо у тексті немає жодного `\n` (тобто текст складається тільки із заголовка-рядка), `rest = ''`, що дає коректний результат із порожньою «рештою».
139
+
140
+ **Side effects:** немає.
141
+
142
+ **Приклади:**
143
+
144
+ - Порожній `existingText`:
145
+ - вихід: `# Changelog\n\n<sectionBlock>`.
146
+ - `existingText = '# Changelog\n\n## [1.0.0] - 2026-01-01\n…'`:
147
+ - вихід: `# Changelog\n\n<sectionBlock>\n## [1.0.0] - 2026-01-01\n…`.
148
+ - `existingText = 'тут нічого корисного'`:
149
+ - вихід: `# Changelog\n\n<sectionBlock>` (попередній вміст відкидається).
150
+
151
+ ### `aggregateWorkspace({ currentVersion, changeFiles, date })`
152
+
153
+ **Сигнатура:**
154
+
155
+ ```
156
+ aggregateWorkspace({
157
+ currentVersion: string,
158
+ changeFiles: Array<{ file: string, entry: { bump: string, section: string, description: string } }>,
159
+ date: string,
160
+ }): { newVersion: string, sectionBlock: string, consumedFiles: string[] } | null
161
+ ```
162
+
163
+ **Параметри (іменований об’єкт):**
164
+
165
+ - `currentVersion` — поточна версія маніфесту workspace (`x.y.z`).
166
+ - `changeFiles` — масив об’єктів, що відповідає виходу `readChangeFiles` з `change-file.mjs`. Кожен елемент має `file` (ім’я файлу `.md`) та `entry` (розпарсений frontmatter + опис).
167
+ - `date` — рядок дати `YYYY-MM-DD` для секції CHANGELOG.
168
+
169
+ **Повертає:**
170
+
171
+ - `null`, якщо `changeFiles.length === 0` (явна ознака «нема чого релізити»).
172
+ - Інакше — об’єкт:
173
+ - `newVersion` — результат `bumpVersion(currentVersion, maxBump(<усі bumps>))`.
174
+ - `sectionBlock` — результат `renderChangelogSection(newVersion, date, <усі entries>)`.
175
+ - `consumedFiles` — імена change-файлів (`c.file`), які мають бути видалені викликачем після успішного запису маніфесту й `CHANGELOG.md`.
176
+
177
+ **Side effects:** немає. Функція суто комбінує попередньо описані `bumpVersion`, `maxBump`, `renderChangelogSection`.
178
+
179
+ **Контракт із викликачем:** саме виклик `release.mjs` (або тести) відповідає за:
180
+
181
+ - запис `CHANGELOG.md` (зазвичай через `prependChangelogSection(readFileSync('CHANGELOG.md'), sectionBlock)`);
182
+ - оновлення поля `version` у `package.json`;
183
+ - видалення файлів зі списку `consumedFiles` із `<ws>/.changes/`;
184
+ - git-операції (commit / tag / push).
185
+
186
+ ## Залежності
187
+
188
+ ### Внутрішні (цей модуль імпортує)
189
+
190
+ - `./change-file.mjs`:
191
+ - `VALID_BUMPS` — `Object.freeze(['major', 'minor', 'patch'])` — порядок використовується у `maxBump` для пріоритету «найвищого».
192
+ - `VALID_SECTIONS` — `Object.freeze(['Added', 'Changed', 'Fixed', 'Removed'])` — порядок секцій у markdown-виводі `renderChangelogSection`.
193
+
194
+ ### Зовнішні
195
+
196
+ Немає. Жодних імпортів зі стандартної бібліотеки Node.js (`node:fs`, `node:path`, `node:crypto` тощо) або сторонніх пакетів. Це підкреслює статус модуля як «pure compute».
197
+
198
+ ### Внутрішні константи (не експортуються)
199
+
200
+ - `SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/` — валідація формату `x.y.z`. Розрізняє лише три цілі числа, **не** підтримує pre-release / build-metadata.
201
+ - `CHANGELOG_HEADER = '# Changelog'` — очікуваний заголовок `CHANGELOG.md` (Keep a Changelog convention).
202
+
203
+ ## Потік виконання / Використання
204
+
205
+ Типовий потік релізу одного workspace (з боку викликача `release.mjs`):
206
+
207
+ 1. **Збір change-файлів.** Викликач звертається до `readChangeFiles(ws, cwd)` з `change-file.mjs`, отримуючи `Array<{ file, entry }>` — усі `.md` із `<ws>/.changes/`, відсортовані за іменем (тобто де-факто за `timestamp`).
208
+ 2. **Отримання поточної версії.** Викликач читає `package.json` (або інший маніфест) і дістає `currentVersion`.
209
+ 3. **Дата.** Викликач формує `date = new Date().toISOString().slice(0, 10)` або еквівалент.
210
+ 4. **Агрегація.** Виклик `aggregateWorkspace({ currentVersion, changeFiles, date })`:
211
+ - Якщо повертає `null` — викликач пропускає workspace (нема `change-файлів`).
212
+ - Якщо повертає об’єкт — отримуємо `newVersion`, `sectionBlock`, `consumedFiles`.
213
+ 5. **Оновлення `CHANGELOG.md`.** Викликач читає поточний `CHANGELOG.md` (або порожній рядок, якщо файлу нема), застосовує `prependChangelogSection(existingText, sectionBlock)` і записує результат на диск.
214
+ 6. **Оновлення маніфесту.** Викликач замінює `"version"` у `package.json` на `newVersion`.
215
+ 7. **Видалення change-файлів.** Викликач видаляє файли зі списку `consumedFiles` з `<ws>/.changes/`.
216
+ 8. **Git-операції.** Викликач виконує `git add`, `git commit`, опційно `git tag` згідно з політикою релізу.
217
+
218
+ ### Чому split: `aggregate.mjs` без I/O, `release.mjs` з I/O
219
+
220
+ - **Тестованість.** Юніт-тести викликають експорти `aggregate.mjs` із чистих вхідних даних — без файлових моків, без часу.
221
+ - **Чітка межа відповідальності.** Помилки парсингу / валідації лежать у `change-file.mjs`, помилки I/O / git — у `release.mjs`, а помилки «бізнес-логіки» semver/CHANGELOG — тут.
222
+ - **Передбачуваність.** Жоден виклик функцій модуля не може зіпсувати ФС чи git-стан, навіть за хибних даних — упаде лише `throw` із описом проблеми.
223
+
224
+ ### Особливості / гарантії
225
+
226
+ - **Стабільний порядок секцій** у виводі CHANGELOG: завжди `Added` → `Changed` → `Fixed` → `Removed` (порядок із `VALID_SECTIONS`), навіть якщо у вхідному масиві records записи перемішані.
227
+ - **Пріоритет бампа** `major > minor > patch` гарантується порядком елементів у `VALID_SECTIONS`/`VALID_BUMPS`, а не явним порівнянням рядків.
228
+ - **Fallback `maxBump`** — якщо у `bumps` немає жодного з відомих значень, повертається `'patch'`. Однак на практиці `change-file.mjs::parseChangeFile` уже валідує `bump`, тому до `maxBump` доходять лише валідні значення.
229
+ - **Idempotency `prependChangelogSection`** не гарантується — функція не перевіряє, чи така версія вже існує у файлі. Викликач має сам слідкувати, щоб не дублювати реліз.
230
+ - **Формат semver** обмежений `x.y.z` без префіксів (`v1.2.3` спричинить `Error`) та без pre-release/build (`1.2.3-rc.1` теж спричинить `Error`).
231
+
232
+ ## Rebuild Test
233
+
234
+ За цією документацією має бути можливо переписати `aggregate.mjs` з нуля, отримавши функціонально-еквівалентний модуль, який:
235
+
236
+ 1. Імпортує `VALID_BUMPS` і `VALID_SECTIONS` з `./change-file.mjs`.
237
+ 2. Експортує функцію `bumpVersion(version, bump)`, що валідує semver через регулярний вираз `^(\d+)\.(\d+)\.(\d+)$`, кидає `Error('aggregate: невалідний semver «…»')` за невалідним входом і повертає інкрементовану версію за правилами `major/minor/patch`, з default-гілкою `patch` для будь-якого іншого значення `bump`.
238
+ 3. Експортує функцію `maxBump(bumps)`, яка шукає у масиві `bumps` перший елемент із `VALID_BUMPS` (тобто за пріоритетом `major > minor > patch`) і повертає `'patch'`, якщо нічого не знайдено.
239
+ 4. Експортує функцію `renderChangelogSection(version, date, entries)`, що повертає рядок із заголовком `## [<version>] - <date>\n`, далі — секції у порядку `VALID_SECTIONS`, кожна з заголовком `### <section>` та bullet-списком `- <description>`, із порожніми рядками-роздільниками згідно з форматом Keep a Changelog. Секції без записів пропускаються.
240
+ 5. Експортує функцію `prependChangelogSection(existingText, sectionBlock)`, яка:
241
+ - Якщо `existingText.trimStart()` не починається з `# Changelog`, повертає `# Changelog\n\n<sectionBlock>`.
242
+ - Інакше відокремлює перший рядок (`head`) і решту (`rest`, із `trimStart`) і повертає `head + '\n\n' + sectionBlock + '\n' + rest`.
243
+ 6. Експортує функцію `aggregateWorkspace({ currentVersion, changeFiles, date })`, яка:
244
+ - Повертає `null`, якщо `changeFiles` порожній.
245
+ - Інакше повертає `{ newVersion, sectionBlock, consumedFiles }`, де `newVersion = bumpVersion(currentVersion, maxBump(<усі c.entry.bump>))`, `sectionBlock = renderChangelogSection(newVersion, date, <усі c.entry>)`, `consumedFiles = <усі c.file>`.
246
+ 7. Не виконує жодного I/O й не залежить від `node:*`.