@nitra/cursor 3.21.1 → 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 (231) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/CHANGELOG.md +37 -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/js/lint-findings.mjs +110 -0
  74. package/rules/js-lint/js/lint.mjs +86 -15
  75. package/rules/js-lint-ci/docs/fix.md +154 -0
  76. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  77. package/rules/js-mssql/docs/fix.md +128 -0
  78. package/rules/js-mssql/js/docs/deps.md +263 -0
  79. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  80. package/rules/js-run/docs/fix.md +144 -0
  81. package/rules/js-run/js/docs/runtime.md +388 -0
  82. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  83. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  84. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  85. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  86. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  87. package/rules/k8s/docs/fix.md +129 -0
  88. package/rules/k8s/js/docs/manifests.md +344 -0
  89. package/rules/k8s/js/manifests.mjs +6 -2
  90. package/rules/k8s/k8s.mdc +4 -2
  91. package/rules/k8s/lint/docs/lint.md +411 -0
  92. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  93. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  94. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  95. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  96. package/rules/npm-module/docs/fix.md +98 -0
  97. package/rules/npm-module/js/docs/package_structure.md +274 -0
  98. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  99. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  100. package/rules/php/docs/fix.md +107 -0
  101. package/rules/php/js/docs/tooling.md +152 -0
  102. package/rules/php/lint/docs/lint.md +215 -0
  103. package/rules/python/docs/fix.md +163 -0
  104. package/rules/python/js/docs/applies.md +108 -0
  105. package/rules/python/js/docs/tooling.md +153 -0
  106. package/rules/python/lint/docs/lint.md +322 -0
  107. package/rules/rego/docs/fix.md +121 -0
  108. package/rules/rego/js/docs/applies.md +174 -0
  109. package/rules/rego/js/docs/lint.md +118 -0
  110. package/rules/rego/lint/docs/lint.md +204 -0
  111. package/rules/release/docs/change.md +185 -0
  112. package/rules/release/docs/fix.md +119 -0
  113. package/rules/release/docs/release.md +222 -0
  114. package/rules/release/lib/docs/aggregate.md +246 -0
  115. package/rules/release/lib/docs/change-file.md +200 -0
  116. package/rules/release/lib/docs/fallback.md +203 -0
  117. package/rules/rust/docs/fix.md +129 -0
  118. package/rules/rust/js/docs/applies.md +140 -0
  119. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  120. package/rules/security/docs/fix.md +86 -0
  121. package/rules/security/js/docs/lint.md +171 -0
  122. package/rules/security/js/docs/sample_secret.md +190 -0
  123. package/rules/security/js/docs/trufflehog.md +137 -0
  124. package/rules/security/js/lint.mjs +9 -1
  125. package/rules/style-lint/docs/fix.md +155 -0
  126. package/rules/style-lint/js/docs/lint.md +184 -0
  127. package/rules/style-lint/js/docs/tooling.md +194 -0
  128. package/rules/tauri/docs/fix.md +158 -0
  129. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  130. package/rules/tauri/js/docs/tooling.md +228 -0
  131. package/rules/test/coverage/coverage.mjs +15 -3
  132. package/rules/test/docs/fix.md +132 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  135. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  136. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  137. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  138. package/rules/test/js/docs/location.md +136 -0
  139. package/rules/test/js/docs/no-process-chdir.md +160 -0
  140. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  141. package/rules/test/js/docs/stryker_config.md +152 -0
  142. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  143. package/rules/text/docs/fix.md +118 -0
  144. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  145. package/rules/text/js/docs/formatting.md +256 -0
  146. package/rules/text/js/docs/lint.md +122 -0
  147. package/rules/text/lint/docs/lint.md +220 -0
  148. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  149. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  150. package/rules/text/lint/docs/run-v8r.md +197 -0
  151. package/rules/vue/docs/fix.md +127 -0
  152. package/rules/vue/js/docs/packages.md +335 -0
  153. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  154. package/rules/worktree/docs/fix.md +161 -0
  155. package/schemas/rule-meta.json +5 -1
  156. package/scripts/auto-rules.mjs +7 -4
  157. package/scripts/coverage-classify/docs/apply.md +202 -0
  158. package/scripts/coverage-classify/docs/cache.md +203 -0
  159. package/scripts/coverage-classify/docs/index.md +218 -0
  160. package/scripts/coverage-classify/docs/prompt.md +132 -0
  161. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  162. package/scripts/coverage-fix-extract.mjs +122 -0
  163. package/scripts/coverage-fix.mjs +1 -1
  164. package/scripts/dispatcher/docs/graph.md +346 -0
  165. package/scripts/dispatcher/docs/index.md +236 -0
  166. package/scripts/dispatcher/docs/trace.md +296 -0
  167. package/scripts/dispatcher/index.mjs +1 -1
  168. package/scripts/dispatcher/lib/active.mjs +4 -8
  169. package/scripts/dispatcher/lib/commands.mjs +7 -11
  170. package/scripts/dispatcher/lib/docs/active.md +348 -0
  171. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  172. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  173. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  174. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  175. package/scripts/dispatcher/lib/docs/events.md +182 -0
  176. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  177. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  178. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  179. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  180. package/scripts/dispatcher/lib/docs/level.md +335 -0
  181. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  182. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  183. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  184. package/scripts/dispatcher/lib/docs/review.md +255 -0
  185. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  186. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  187. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  188. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  189. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  190. package/scripts/dispatcher/lib/executor.mjs +6 -1
  191. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  192. package/scripts/dispatcher/lib/level.mjs +29 -3
  193. package/scripts/dispatcher/lib/review.mjs +1 -1
  194. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  195. package/scripts/docs/auto-rules.md +376 -0
  196. package/scripts/docs/auto-skills.md +173 -0
  197. package/scripts/docs/build-agents-commands.md +183 -0
  198. package/scripts/docs/cli-entry.md +153 -0
  199. package/scripts/docs/coverage-fix.md +177 -0
  200. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  201. package/scripts/lib/changed-files.mjs +4 -1
  202. package/scripts/lib/diff-added-lines.mjs +85 -0
  203. package/scripts/lib/docs/changed-files.md +149 -0
  204. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  205. package/scripts/lib/docs/check-reporter.md +175 -0
  206. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  207. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  208. package/scripts/lib/docs/ensure-tool.md +254 -0
  209. package/scripts/lib/docs/generated-markdown.md +275 -0
  210. package/scripts/lib/docs/gha-workflow.md +326 -0
  211. package/scripts/lib/docs/inline-template-links.md +303 -0
  212. package/scripts/lib/docs/list-rule-ids.md +156 -0
  213. package/scripts/lib/docs/load-cursor-config.md +147 -0
  214. package/scripts/lib/docs/mirror-parity.md +167 -0
  215. package/scripts/lib/worktree.mjs +26 -0
  216. package/scripts/worktree-cli.mjs +12 -2
  217. package/skills/coverage-fix/SKILL.md +34 -45
  218. package/skills/docgen/SKILL.md +44 -23
  219. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  220. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  221. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  222. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  223. package/skills/docgen/js/docgen-scan.mjs +37 -21
  224. package/skills/llm-patch/SKILL.md +23 -2
  225. package/skills/start-check/SKILL.md +26 -53
  226. package/skills/start-check/js/check.mjs +211 -0
  227. package/skills/taze/SKILL.md +9 -3
  228. package/skills/taze/js/diff.mjs +154 -0
  229. package/types/bin/n-cursor.d.ts +1 -1
  230. package/skills/fix-tests/SKILL.md +0 -119
  231. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,295 @@
1
+ # platforms.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `platforms.mjs` реалізує **check** для правила `capacitor.mdc`: перевіряє, що проєкт-застосунок на базі **Capacitor** відповідає політикам монорепо Nitra. Файл експортує асинхронну функцію `check(cwd)`, яка повертає **exit-код** (0 — ok, 1 — fail), а також низку допоміжних чистих функцій для розбору **npm**-діапазонів версій, обходу `package.json` та пошуку `Podfile` у каталозі `ios/`.
6
+
7
+ Перевірка послідовно виконує три блоки логіки:
8
+
9
+ 1. **Виявлення Capacitor у репозиторії.** Capacitor вважається задіяним, якщо в корені присутній один із файлів `capacitor.config.json` / `capacitor.config.ts` / `capacitor.config.mjs`, **або** хоч у одному `package.json` (рекурсивний обхід дерева з пропуском типових каталогів) задекларовано пакет із префіксом `@capacitor/` у будь-якому блоці залежностей (`dependencies`, `devDependencies`, `optionalDependencies`, `peerDependencies`). Якщо ознак немає — check одразу повертає **pass** і код **0**, не вимагаючи жодних дій.
10
+ 2. **Перевірка мінімальної версії `@capacitor/core` ≥ 8.** Для кожного `package.json`, який оголошує `@capacitor/core`, обчислюється **нижня межа major** з рядка npm-діапазону. Якщо вона менша за `MIN_CAPACITOR_MAJOR = 8`, або діапазон не вдалося визначити (`*`, `latest`, `x`, незрозумілий синтаксис) — це fail; повідомлення підказує задати, наприклад, `^8.0.0`. Якщо `capacitor.config.*` знайдено, а `@capacitor/core` у дереві відсутній — теж fail.
11
+ 3. **iOS / Podfile.** За політикою `capacitor.mdc` iOS-частина Capacitor-застосунку повинна збиратися лише через **SPM**, без CocoaPods. Рекурсивний обхід `ios/` (пропускаючи `Pods`, `build`, `DerivedData`) шукає перший `Podfile`. Якщо `Podfile` є, він вважається порушенням, **окрім** випадку коли в кореневому `package.json` або в `capacitor.config.{json,ts,mjs}` присутній об’єкт `nitra` із прапором `iosCocoaPodsBecausePluginsLackSpm: true` або `iosCocoaPodsAllowed: true`. Якщо каталогу `ios/` немає — умова не застосовується.
12
+
13
+ Усі повідомлення про результат виводяться через `createCheckReporter()` зі спільного хелпера `scripts/lib/check-reporter.mjs`.
14
+
15
+ ## Експорти / API
16
+
17
+ Файл експортує іменовані символи (без default export):
18
+
19
+ - `capacitorSegmentMinMajor(segment)` — мінімальний **major** для одного OR-сегмента npm-діапазону.
20
+ - `capacitorVersionRangeMinMajor(versionRange)` — мінімальний **major** для повного діапазону, з підтримкою `||`.
21
+ - `isCapacitorCoreVersionAtLeast8(versionRange, min?)` — булеан-предикат: чи нижня межа діапазону `≥ min` (за замовчуванням `MIN_CAPACITOR_MAJOR`).
22
+ - `recordCapacitorFromOnePackageJson(absPath, root, out)` — асинхронно прочитати один `package.json` і поповнити акумулятор.
23
+ - `collectCapacitorDataFromAllPackageJson(root, out)` — рекурсивно обійти всі `package.json` у репозиторії, агрегуючи `byPath` і `anyCapacitor`.
24
+ - `hasCapacitorConfigInRoot(root)` — синхронно перевірити наявність `capacitor.config.{json,ts,mjs}` у корені.
25
+ - `isCapacitorRelevantForCheck(root, anyCapacitor)` — чи варто застосовувати правила Capacitor взагалі.
26
+ - `walkIosForPodfileSkipPods(root, dir, onPodfileRelative)` — рекурсивний пошук `Podfile` під `ios/` із пропуском службових тек.
27
+ - `findFirstPodfileUnderIosExcludingPods(root)` — повертає **найкоротший** posix-relative шлях до першого знайденого `Podfile` (або `null`).
28
+ - `nitrAObjectAllowsIosCocoaPods(o)` — предикат для об’єкта `nitra`: чи дозволено CocoaPods.
29
+ - `check(cwd?)` — головна функція **check**; повертає **exit-код**.
30
+
31
+ Внутрішні (неекспортовані) функції: `firstVersionMajorFromNpmValue`, `reportOneCapacitorCoreRange`, `recordCapacitorFromDependencyObject`, `extractNitraObjectBodySource`, `nitraObjectBodyStringAllowsCocoaPodsExempt`, `pathJsonShowsNitraCocoapodsExempt`, `capacitorConfigTsMjsNitraCocoapodsExempt`, `isIosCocoaPodsExemptByNitraConfig`.
32
+
33
+ Константи модульного рівня:
34
+
35
+ - `MIN_CAPACITOR_MAJOR = 8` — мінімальний допустимий **major** Capacitor.
36
+ - `IGNORED_DIRS_FOR_PACKAGE_JSON` — `Set` каталогів, які пропускаються при обході (`node_modules`, `.git`, `dist`, `coverage`, `Pods`, `.turbo`, `.next`, `build`).
37
+ - Регулярки: `NPM_OR_PARTS_RE` (`\s*\|\|\s*`), `NPM_HYPHEN_RANGE_RE` (`^(.+?)\s+-\s+(.+)$`), `FIRST_VERSION_NUM_RE` (`^(?:v)?(\d+)`), `PREFIX_GEQ_RE` (`^>=\s*`), `PREFIX_GT_RE` (`^>\s*`), `STRIP_CARET_TILDE_EQ_RE` (`^[=^~]+\s*`), `RE_NITRA_CONFIG_OBJECT_LEAD_IN` (початок блоку `nitra: {` у TS/MJS), `RE_COCOAPODS_EXEMPT_SPM` (`iosCocoaPodsBecausePluginsLackSpm: true`), `RE_COCOAPODS_EXEMPT_ALLOW` (`iosCocoaPodsAllowed: true`).
38
+
39
+ ## Функції
40
+
41
+ ### `capacitorSegmentMinMajor(segment)`
42
+
43
+ - **Сигнатура:** `(segment: string) => number | null`
44
+ - **Параметри:** `segment` — одна частина npm-діапазону (без `||` всередині).
45
+ - **Повертає:** мінімальний **major**, який задовольняється сегментом; `null`, якщо сегмент — `*`, `x` (case-insensitive) або `latest`, або якщо вхід не рядок чи порожній.
46
+ - **Логіка:**
47
+ - Не-рядок або порожній рядок після `trim()` → `null`.
48
+ - `*`, `x` (нижній регістр), `latest` → `null` (невизначена нижня межа).
49
+ - Префікс `<` або `<=` → `0` (теоретично може допускати дуже старі major-и).
50
+ - Префікс `>` (але не `>=`) → витягає число з решти рядка через `firstVersionMajorFromNpmValue`.
51
+ - Дефіс-діапазон `a - b` (`NPM_HYPHEN_RANGE_RE`) → бере major лівої межі.
52
+ - Префікси `^`, `~`, `=` → знімає їх і повертає major першого числа.
53
+ - Префікс `>=` → знімає його і повертає major.
54
+ - Інакше — повертає major першого числа в рядку.
55
+ - **Side effects:** немає (чиста функція).
56
+
57
+ ### `firstVersionMajorFromNpmValue(t)` _(внутрішня)_
58
+
59
+ - **Сигнатура:** `(t: string) => number | null`
60
+ - **Параметри:** `t` — фрагмент рядка версії без префікса операторів.
61
+ - **Повертає:** перше ціле число (major), знайдене регуляркою `FIRST_VERSION_NUM_RE` (опційний префікс `v`); `null`, якщо число не знайдено або рядок порожній.
62
+ - **Side effects:** немає.
63
+
64
+ ### `capacitorVersionRangeMinMajor(versionRange)`
65
+
66
+ - **Сигнатура:** `(versionRange: string) => number | null`
67
+ - **Параметри:** `versionRange` — повне значення поля для `@capacitor/core` з `package.json`.
68
+ - **Повертає:** найменший (нижній) **major** серед усіх OR-частин; `null`, якщо хоча б одна частина — `*` / `latest` / `x` / нерозпізнана (тобто діапазон вважається небезпечним).
69
+ - **Логіка:** розбиває за `||`, кожну частину пропускає через `capacitorSegmentMinMajor`, ранній вихід із `null` при першій невизначеній; інакше — мінімум серед усіх отриманих чисел.
70
+ - **Side effects:** немає.
71
+
72
+ ### `isCapacitorCoreVersionAtLeast8(versionRange, min = MIN_CAPACITOR_MAJOR)`
73
+
74
+ - **Сигнатура:** `(versionRange: string, min?: number) => boolean`
75
+ - **Параметри:** `versionRange` — рядок версії; `min` — нижній поріг major (за замовчуванням **8**).
76
+ - **Повертає:** `true`, якщо нижня межа діапазону визначена і `>= min`; інакше — `false` (зокрема для `*`, `latest`).
77
+ - **Side effects:** немає.
78
+
79
+ ### `reportOneCapacitorCoreRange(fail, pass, rel, range)` _(внутрішня)_
80
+
81
+ - **Сигнатура:** `(fail: (m: string) => void, pass: (m: string) => void, rel: string, range: string) => void`
82
+ - **Параметри:** `fail`, `pass` — друк-колбеки reporter; `rel` — posix-relative шлях `package.json`; `range` — значення `@capacitor/core`.
83
+ - **Повертає:** `void`.
84
+ - **Side effects:** викликає `pass(...)` або `fail(...)` зі сформованим повідомленням, у якому згадано `MIN_CAPACITOR_MAJOR` та рекомендацію `^8.0.0`.
85
+
86
+ ### `recordCapacitorFromDependencyObject(rel, obj, out)` _(внутрішня)_
87
+
88
+ - **Сигнатура:** `(rel: string, obj: Record<string, unknown>, out: { byPath: Map<string, string>, anyCapacitor: boolean }) => void`
89
+ - **Параметри:** `rel` — relative-шлях `package.json`; `obj` — один із блоків залежностей; `out` — акумулятор.
90
+ - **Повертає:** `void`.
91
+ - **Side effects:**
92
+ - Виставляє `out.anyCapacitor = true`, якщо знайдено будь-який ключ, що починається з `@capacitor/`.
93
+ - Якщо ключ — рівно `@capacitor/core` і значення — непорожній рядок, кладе пару `rel → range` в `out.byPath`. Повторні записи з різних блоків залежностей перезаписують одне одного (останній блок перемагає в порядку `dependencies → devDependencies → optionalDependencies → peerDependencies`).
94
+
95
+ ### `recordCapacitorFromOnePackageJson(absPath, root, out)`
96
+
97
+ - **Сигнатура:** `(absPath: string, root: string, out: { byPath: Map<string, string>, anyCapacitor: boolean }) => Promise<void>`
98
+ - **Параметри:** `absPath` — абсолютний шлях до `package.json`; `root` — корінь репозиторію; `out` — акумулятор.
99
+ - **Повертає:** `Promise<void>`.
100
+ - **Side effects:**
101
+ - Читає файл через `readFile(absPath, 'utf8')`; будь-яка помилка вводу/виводу мовчазно повертає керування (файл пропускається).
102
+ - Парсить JSON; помилка парсингу — теж мовчазне пропускання.
103
+ - Обчислює posix-relative шлях відносно `root` (з заміною `\` на `/`); якщо `relative()` повертає порожній рядок — підставляє `absPath`.
104
+ - Для кожного блоку залежностей, що є об’єктом (не масив, не `null`/`undefined`), викликає `recordCapacitorFromDependencyObject`.
105
+
106
+ ### `collectCapacitorDataFromAllPackageJson(root, out)`
107
+
108
+ - **Сигнатура:** `(root: string, out: { byPath: Map<string, string>, anyCapacitor: boolean }) => Promise<void>`
109
+ - **Параметри:** `root` — корінь обходу; `out` — акумулятор (буде ініціалізовано).
110
+ - **Повертає:** `Promise<void>`.
111
+ - **Side effects:**
112
+ - На початку **скидає** `out.anyCapacitor = false`; якщо `out.byPath` уже існує — викликає `.clear()`, інакше створює нову `Map`.
113
+ - Внутрішня функція `walk(dir)` робить `readdir(dir, { withFileTypes: true })`; помилку каталогу мовчки ігнорує.
114
+ - Для кожного запису: якщо це каталог і його імені немає в `IGNORED_DIRS_FOR_PACKAGE_JSON` — рекурсивно входить; якщо це файл `package.json` — викликає `recordCapacitorFromOnePackageJson`.
115
+ - Інші типи файлів (`isFile` зі значенням false, симлінки тощо) пропускаються — записи з `entry.isDirectory() === false && entry.isFile() === false` не оброблюються.
116
+
117
+ ### `hasCapacitorConfigInRoot(root)`
118
+
119
+ - **Сигнатура:** `(root: string) => boolean`
120
+ - **Параметри:** `root` — корінь репозиторію.
121
+ - **Повертає:** `true`, якщо хоча б один із файлів `capacitor.config.json`, `capacitor.config.ts`, `capacitor.config.mjs` існує в корені.
122
+ - **Side effects:** виконує `existsSync` (синхронний доступ до файлової системи).
123
+
124
+ ### `isCapacitorRelevantForCheck(root, anyCapacitor)`
125
+
126
+ - **Сигнатура:** `(root: string, anyCapacitor: boolean) => boolean`
127
+ - **Параметри:** `root` — корінь; `anyCapacitor` — чи зустрічався `@capacitor/` у `package.json`.
128
+ - **Повертає:** `true`, якщо є capacitor-конфіг у корені **або** `anyCapacitor === true`.
129
+ - **Side effects:** виклик `hasCapacitorConfigInRoot` (`existsSync`).
130
+
131
+ ### `walkIosForPodfileSkipPods(root, dir, onPodfileRelative)`
132
+
133
+ - **Сигнатура:** `(root: string, dir: string, onPodfileRelative: (rel: string) => void) => Promise<boolean>`
134
+ - **Параметри:** `root` — корінь репозиторію; `dir` — поточний каталог обходу; `onPodfileRelative` — колбек із posix-relative шляхом знайденого `Podfile`.
135
+ - **Повертає:** `Promise<boolean>` — `true`, якщо в дереві знайдено принаймні один `Podfile` (повернення відбувається при **першому** знайденому всередині поточного `dir` або в його підкаталогах).
136
+ - **Логіка:**
137
+ - `readdir(dir, { withFileTypes: true })`; помилка → `false`.
138
+ - Пропускає підкаталоги/файли з іменами `Pods`, `build`, `DerivedData` (порівняння за іменем).
139
+ - Якщо запис — файл із іменем `Podfile`: викликає колбек із posix-relative шляхом і повертає `true` (ранній вихід — інші записи поточного каталогу не оглядаються).
140
+ - Якщо запис — каталог: рекурсивно входить; якщо нащадок повернув `true`, передається ланцюжком назовні.
141
+ - **Side effects:** дисковий I/O (`readdir`), виклики `onPodfileRelative`.
142
+
143
+ ### `findFirstPodfileUnderIosExcludingPods(root)`
144
+
145
+ - **Сигнатура:** `(root: string) => Promise<string | null>`
146
+ - **Параметри:** `root` — корінь репозиторію.
147
+ - **Повертає:** posix-relative шлях до першого виявленого `Podfile` під `ios/` (а саме — **найкоротший** із тих, що були передані в колбек у межах того самого виклику обходу) або `null`, якщо `ios/` немає чи в ньому не знайдено `Podfile` поза `Pods/`.
148
+ - **Логіка:** перевіряє існування `ios/` через `existsSync`. Викликає `walkIosForPodfileSkipPods` з колбеком, який утримує лише шлях із мінімальною довжиною рядка (`rel.length < first.length`). Через ранній вихід `walkIosForPodfileSkipPods` зазвичай колбек спрацьовує один раз; вибір «найкоротшого» — захист на випадок зміни поведінки обходу.
149
+ - **Side effects:** дисковий I/O.
150
+
151
+ ### `nitrAObjectAllowsIosCocoaPods(o)`
152
+
153
+ - **Сигнатура:** `(o: unknown) => boolean`
154
+ - **Параметри:** `o` — кандидат у об’єкт `nitra`.
155
+ - **Повертає:** `true`, якщо `o` — звичайний об’єкт (не `null`, не масив) і містить `iosCocoaPodsBecausePluginsLackSpm === true` або `iosCocoaPodsAllowed === true`; інакше — `false`.
156
+ - **Side effects:** немає.
157
+ - **Зауваження:** назва функції написана з нестандартною капіталізацією `nitrA` — використовується саме так на місці виклику.
158
+
159
+ ### `extractNitraObjectBodySource(source)` _(внутрішня)_
160
+
161
+ - **Сигнатура:** `(source: string) => string | null`
162
+ - **Параметри:** `source` — текст файлу `capacitor.config.ts` або `capacitor.config.mjs`.
163
+ - **Повертає:** підрядок `{ ... }`, що відповідає тілу об’єкта після `nitra:` / `"nitra":` / `'nitra':` (перше входження), збалансованому за фігурними дужками; `null`, якщо вхід не знайдено або не вдалося збалансувати дужки.
164
+ - **Логіка:** `RE_NITRA_CONFIG_OBJECT_LEAD_IN.exec(source)` знаходить початок; далі вручну лічильник `d` балансу `{`/`}` від першої `{` до зустрічної `}` на нульовому рівні.
165
+ - **Side effects:** немає.
166
+ - **Обмеження:** не парсить TS/MJS повноцінно; ігнорує можливі `{` / `}` усередині рядків чи коментарів, що теоретично може дати хибний баланс на нетипових вхідних даних. Для штатних `capacitor.config.*` цього достатньо.
167
+
168
+ ### `nitraObjectBodyStringAllowsCocoaPodsExempt(objectBody)` _(внутрішня)_
169
+
170
+ - **Сигнатура:** `(objectBody: string) => boolean`
171
+ - **Параметри:** `objectBody` — текст тіла об’єкта `nitra`.
172
+ - **Повертає:** `true`, якщо в підрядку є `iosCocoaPodsBecausePluginsLackSpm: true` або `iosCocoaPodsAllowed: true` (регулярки `RE_COCOAPODS_EXEMPT_SPM`, `RE_COCOAPODS_EXEMPT_ALLOW`).
173
+ - **Side effects:** немає.
174
+
175
+ ### `pathJsonShowsNitraCocoapodsExempt(absPath)` _(внутрішня)_
176
+
177
+ - **Сигнатура:** `(absPath: string) => Promise<boolean>`
178
+ - **Параметри:** `absPath` — повний шлях до JSON-файла (`package.json` або `capacitor.config.json`).
179
+ - **Повертає:** `true`, якщо файл існує, валідно парситься як JSON, і його ключ `nitra` задовольняє `nitrAObjectAllowsIosCocoaPods`.
180
+ - **Логіка:** `existsSync` → `readFile` → `JSON.parse`. Будь-яка помилка читання/парсингу повертає `false`.
181
+ - **Side effects:** дисковий I/O.
182
+
183
+ ### `capacitorConfigTsMjsNitraCocoapodsExempt(root)` _(внутрішня)_
184
+
185
+ - **Сигнатура:** `(root: string) => Promise<boolean>`
186
+ - **Параметри:** `root` — корінь репозиторію.
187
+ - **Повертає:** `true`, якщо `capacitor.config.ts` або `capacitor.config.mjs` (у такій послідовності) існує та містить блок `nitra: { ... }` з прапором винятку.
188
+ - **Логіка:** для кожного імені викликає `existsSync`, читає вміст, через `extractNitraObjectBodySource` дістає тіло і перевіряє `nitraObjectBodyStringAllowsCocoaPodsExempt`. Знайдено → `true`; інакше після обох — `false`.
189
+ - **Side effects:** дисковий I/O. Винятки `readFile` не перехоплюються, тому пошкоджений файл може кинути помилку наверх (єдина функція в файлі, що не загортає `readFile` у `try/catch`).
190
+
191
+ ### `isIosCocoaPodsExemptByNitraConfig(root)` _(внутрішня)_
192
+
193
+ - **Сигнатура:** `(root: string) => Promise<boolean>`
194
+ - **Параметри:** `root` — корінь репозиторію.
195
+ - **Повертає:** `true`, якщо знайдено валідний виняток `nitra` у `package.json`, або в `capacitor.config.json`, або в `capacitor.config.{ts,mjs}` (перевіряється в такому порядку, з раннім виходом).
196
+ - **Side effects:** дисковий I/O.
197
+
198
+ ### `check(cwd = process.cwd())`
199
+
200
+ - **Сигнатура:** `(cwd?: string) => Promise<number>`
201
+ - **Параметри:** `cwd` — корінь репозиторію для перевірки; за замовчуванням `process.cwd()`.
202
+ - **Повертає:** exit-код від `reporter.getExitCode()`: **0** — усі повідомлення лише `pass`; **1** — було щонайменше одне `fail`.
203
+ - **Side effects:**
204
+ - Створює reporter через `createCheckReporter()` і викликає `pass(...)` / `fail(...)` для друку повідомлень користувачу.
205
+ - Викликає `collectCapacitorDataFromAllPackageJson(root, acc)` — рекурсивний дисковий обхід усіх `package.json`.
206
+ - Викликає `findFirstPodfileUnderIosExcludingPods(root)` — обхід `ios/`.
207
+ - Викликає `isIosCocoaPodsExemptByNitraConfig(root)` лише якщо `podfileRel !== null` (мінімізує читання конфігів).
208
+ - **Шлях виконання:**
209
+ 1. `acc = { byPath: new Map(), anyCapacitor: false }`; зібрати дані з усіх `package.json`.
210
+ 2. Якщо `isCapacitorRelevantForCheck(root, anyCapacitor) === false` — `pass('Capacitor не виявлено …')` і вихід **0**.
211
+ 3. Інакше `pass('Проєкт з ознаками Capacitor — застосовую capacitor.mdc')`.
212
+ 4. Якщо `byPath.size === 0` (є конфіг, але немає `@capacitor/core` у дереві) — `fail` з підказкою додати `^8.0.0`. Інакше — для кожної пари `[rel, range]` викликати `reportOneCapacitorCoreRange`.
213
+ 5. Подія iOS:
214
+ - Якщо `findFirstPodfileUnderIosExcludingPods(root)` повернув `null` і `ios/` існує — `pass('ios/ без Podfile поза Pods/ …')`.
215
+ - Якщо `ios/` не існує — `pass('каталог ios/ не знайдено …')`.
216
+ - Якщо `Podfile` знайдено і `isIosCocoaPodsExemptByNitraConfig(root) === true` — `pass(... — дозволено виняток ...)`.
217
+ - Інакше — `fail(... використовуй лише SPM ...)`.
218
+ 6. Повернути `getExitCode()`.
219
+
220
+ ## Залежності
221
+
222
+ **Стандартна бібліотека Node.js:**
223
+
224
+ - `node:fs` — `existsSync` (синхронна перевірка існування файлу/каталогу).
225
+ - `node:fs/promises` — `readdir` (з `withFileTypes: true`), `readFile` (utf-8).
226
+ - `node:path` — `join`, `relative`.
227
+
228
+ **Локальні модулі:**
229
+
230
+ - `../../../scripts/lib/check-reporter.mjs` — `createCheckReporter` (фабрика об’єкта з `pass`, `fail`, `getExitCode`; формує консольний звіт і веде стан коду виходу).
231
+
232
+ **Зовнішні npm-пакети:** немає.
233
+
234
+ **Системні припущення:**
235
+
236
+ - Виклик відбувається з кореня репозиторію (або `cwd` передано явно).
237
+ - Файлова система — POSIX-сумісна або Windows (всі шляхи нормалізуються через `replaceAll('\\', '/')` до posix-форми у звітах).
238
+
239
+ ## Потік виконання / Використання
240
+
241
+ Файл призначений для виклику як check-функція в межах CLI чи runner правил `npm/rules/capacitor`. Типовий сценарій:
242
+
243
+ 1. Зовнішній runner імпортує `check` із `platforms.mjs`.
244
+ 2. Викликає `await check()` або `await check(repoRoot)`.
245
+ 3. Під час виконання у консоль друкуються рядки звіту через `createCheckReporter()` (pass/fail).
246
+ 4. Повернений Promise розв’язується числом `0` або `1`, яке runner передає в `process.exit(...)`.
247
+
248
+ Приклад фактичного виклику (наприклад, у CLI-обгортці):
249
+
250
+ ```js
251
+ import { check } from './platforms.mjs'
252
+
253
+ const exitCode = await check(process.cwd())
254
+ process.exit(exitCode)
255
+ ```
256
+
257
+ Окремі експортовані функції зручні для модульних тестів і для повторного використання в інших правилах:
258
+
259
+ - `capacitorVersionRangeMinMajor`, `isCapacitorCoreVersionAtLeast8`, `capacitorSegmentMinMajor` — суто строкові утиліти без I/O; тести подають синтетичні діапазони.
260
+ - `recordCapacitorFromOnePackageJson` і `collectCapacitorDataFromAllPackageJson` — інтеграційні утиліти над `package.json`; вимагають реальної або змодельованої файлової системи (через тимчасові теки тощо).
261
+ - `walkIosForPodfileSkipPods`, `findFirstPodfileUnderIosExcludingPods` — обхід `ios/`.
262
+ - `nitrAObjectAllowsIosCocoaPods` — чистий предикат для об’єкта `nitra`.
263
+ - `hasCapacitorConfigInRoot`, `isCapacitorRelevantForCheck` — швидкі синхронні перевірки наявності конфігів.
264
+
265
+ **Поведінкові інваріанти:**
266
+
267
+ - Жоден `package.json`, який не валідується як JSON, не призводить до помилки `check`; такі файли мовчки пропускаються.
268
+ - Каталоги з `IGNORED_DIRS_FOR_PACKAGE_JSON` ніколи не оглядаються, у тому числі `node_modules` — тобто аналізуються лише власні `package.json` репозиторію та воркспейсів, але не вкладені пакети залежностей.
269
+ - Обхід `ios/` ніколи не входить у `Pods`, `build`, `DerivedData` — це принципово для коректної політики (SPM-only): артефакти CocoaPods, що могли залишитися від попередніх збірок, не повинні впливати на result.
270
+ - Рішення «диапазон допустимий» приймається консервативно: будь-яка невизначеність (`*`, `latest`, неможливість витягти число) трактується як **не** допустимо.
271
+ - Виняток для CocoaPods читається з трьох джерел у строгому порядку: `package.json` → `capacitor.config.json` → `capacitor.config.{ts,mjs}` (з раннім `true`).
272
+
273
+ ## Rebuild Test
274
+
275
+ Зібрані з цього документа ключові факти, достатні для відтворення поведінки:
276
+
277
+ - Експортується `check(cwd?)` із поверненням `Promise<number>` (0/1) на основі `createCheckReporter`.
278
+ - Capacitor вважається релевантним, якщо в корені є `capacitor.config.{json,ts,mjs}` **АБО** у будь-якому `package.json` (рекурсивно, ігноруючи `node_modules`, `.git`, `dist`, `coverage`, `Pods`, `.turbo`, `.next`, `build`) є залежність з префіксом `@capacitor/` у `dependencies` / `devDependencies` / `optionalDependencies` / `peerDependencies`.
279
+ - Мінімальний допустимий major Capacitor — **8** (`MIN_CAPACITOR_MAJOR`).
280
+ - Алгоритм обчислення мінімального major npm-діапазону:
281
+ - Розбити за `\s*\|\|\s*` на сегменти.
282
+ - У сегменті: `*` / `x` (low) / `latest` → невизначено (`null`), що означає fail для всього діапазону.
283
+ - `<` / `<=` → 0.
284
+ - `>` (не `>=`) → major першого числа після оператора.
285
+ - `a - b` (з `\s+-\s+`) → major лівої межі.
286
+ - `^`, `~`, `=` → major першого числа після префікса.
287
+ - `>=` → major першого числа після префікса.
288
+ - Інакше — major першого числа в сегменті.
289
+ - Регулярка для першого числа: `^(?:v)?(\d+)` (опційний `v`).
290
+ - Результат для діапазону — мінімум серед сегментів; якщо будь-який сегмент дав `null`, повертається `null`.
291
+ - `isCapacitorCoreVersionAtLeast8(range)` ⇔ `capacitorVersionRangeMinMajor(range) >= 8`.
292
+ - iOS-перевірка: знайти перший `Podfile` під `ios/`, пропускаючи `Pods`, `build`, `DerivedData`. Якщо знайдено — fail, **окрім** випадку, коли `package.json.nitra` або `capacitor.config.json.nitra` (як JSON-об’єкт) має `iosCocoaPodsBecausePluginsLackSpm === true` або `iosCocoaPodsAllowed === true`, **або** в `capacitor.config.ts` / `capacitor.config.mjs` фрагмент тіла об’єкта `nitra: { ... }` містить `iosCocoaPodsBecausePluginsLackSpm: true` чи `iosCocoaPodsAllowed: true`.
293
+ - Якщо `ios/` немає — iOS-блок повністю пропускається (`pass` із поясненням).
294
+ - Якщо capacitor-конфіг є, а `@capacitor/core` не знайдено в жодному `package.json` — fail (треба додати залежність з версією `^8.0.0`).
295
+ - Помилки I/O і JSON у `package.json` / JSON-конфігах не падають check, а трактуються як «нічого не знайдено» в цій точці.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: CHANGELOG.md в кожному workspace, з двома моделями бази порівняння (npm і Python)
3
- version: '3.2'
3
+ version: '3.3'
4
4
  alwaysApply: true
5
5
  ---
6
6
 
@@ -16,7 +16,7 @@ alwaysApply: true
16
16
 
17
17
  **Інверсія (change-файл не потрібен):** лише `docs/` / `doc/`; синхронізований із `@nitra/cursor` інструментарій (`.cursor/`, `.claude/`); лише `.gitignore`. **Корінь монорепо** (воркспейс `.` за наявності підпакетів) не перевіряється взагалі — отже й кореневі `AGENTS.md` / `CLAUDE.md` та bump `@nitra/cursor` у `devDependencies`. Окремого «релізного кроку» у feature-флоу немає — `version`/`CHANGELOG.md` змінює лише CI.
18
18
 
19
- **Pre-commit (людина):** `hk` у цьому репо також запускає `check changelog` при змінах під `npm/**` — агент не покладайся лише на commit hook; виконай кроки 1–3 **до** фінальної відповіді.
19
+ **Pre-commit (людина):** `hk` у цьому репо запускає `fix changelog` при змінах під `npm/**` в **autofix-режимі** (env `N_CURSOR_CHANGELOG_AUTOFIX=1`): за відсутності change-файлу хук **сам** його створює (дефолти `patch`/`Changed`, опис = subject останнього коміту) і ставить у git-індекс, тож коміт не падає. Це лише підстраховка — агент не покладайся на неї: виконуй кроки 1–3 **до** фінальної відповіді, бо автоген ставить осмислений опис лише випадково (subject попереднього коміту), і `bump`/`section`/текст майже завжди треба відредагувати. Autofix-режим також **не робить мережевих викликів** (`npm view` / PyPI fetch пропускаються) — реєстрова drift-перевірка `version` у хуці не виконується (її ловить CI / ручний `fix`). Поза хуком (CI, ручний `fix`/`check`) autofix вимкнено — поведінка лишається fail-on-missing з повною drift-перевіркою.
20
20
 
21
21
  ---
22
22
 
@@ -0,0 +1,174 @@
1
+ # fix.mjs — точка входу правила `changelog`
2
+
3
+ ## Огляд
4
+
5
+ Файл `npm/rules/changelog/fix.mjs` — тонкий **entry-point** для правила `changelog` у складі пакета `@nitra/cursor`. Він виконує дві ролі одночасно:
6
+
7
+ 1. **Library mode** — експортує функцію `run(ctx)`, яку викликає зовнішній CLI-оркестратор (`scripts/cli/fix.mjs` або аналог), коли користувач запускає `npx @nitra/cursor fix changelog` (чи прогін усіх правил `fix`). У цьому режимі `runStandardRule` отримує контекст із кешем обходу файлової системи (`walkCache`), shared summary тощо.
8
+ 2. **Standalone mode** — якщо файл запущено напряму (`bun npm/rules/changelog/fix.mjs`), виконується повноцінний CLI-сценарій із завантаженням конфігу, whitelist-фільтрацією й friendly summary — тобто еквівалент `npx @nitra/cursor fix changelog`.
9
+
10
+ Сам файл **не містить бізнес-логіки** правила: вся перевірка/автофікс ділиться між суб-модулями (`applies` → `JS-concerns` → `policy` → `mdc-refs`), які підтягуються конвенційно через `runStandardRule(import.meta.dirname, ctx)`. Поведінка повністю детермінована директорією, в якій лежить `fix.mjs` (`import.meta.dirname` вказує на `npm/rules/changelog/`).
11
+
12
+ Це шаблонний файл-обгортка — він повторюється для **кожного** правила в `npm/rules/<rule-id>/fix.mjs`. Зміни тут зазвичай небажані; натомість конкретна перевірка живе у сусідніх теках (`js/`, `lib/`, `meta.json`, `changelog.mdc`).
13
+
14
+ ## Експорти / API
15
+
16
+ | Експорт | Тип | Призначення |
17
+ | ------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
18
+ | `run` | `function (ctx?: RuleContext) => Promise<number>` | Library-функція для виклику з зовнішнього CLI; повертає exit-code `0` (OK) або `1` (є порушення). |
19
+
20
+ Сторонніх іменованих експортів немає. Default export відсутній.
21
+
22
+ ### Side-effect при імпорті
23
+
24
+ Файл містить **top-level умовний блок**:
25
+
26
+ ```js
27
+ if (isRunAsCli(import.meta.url)) {
28
+ process.exit(await runRuleCli(import.meta.dirname))
29
+ }
30
+ ```
31
+
32
+ — якщо модуль виконується як точка входу процесу (`process.argv[1]` відповідає `import.meta.url`), він **завершить процес** через `process.exit(...)`. У звичайному library-імпорті (`import { run } from '.../fix.mjs'`) `isRunAsCli` повертає `false`, отже `process.exit` НЕ викликається — імпорт безпечний.
33
+
34
+ Зверніть увагу на `await` на top-level — файл потребує підтримки top-level await (ESM, Node ≥ 14.8 / Bun).
35
+
36
+ ## Функції
37
+
38
+ ### `run(ctx)`
39
+
40
+ ```js
41
+ export function run(ctx) {
42
+ return runStandardRule(import.meta.dirname, ctx)
43
+ }
44
+ ```
45
+
46
+ - **Сигнатура:** `run(ctx?: RuleContext): Promise<number>`
47
+ - **Параметри:**
48
+ - `ctx` _(optional)_ — об'єкт `RuleContext`, тип якого імпортується з `../../scripts/lib/run-standard-rule.mjs`. Зазвичай несе спільний стан між кількома правилами: кеш обходу файлів (`walkCache`), accumulator для summary, прапорці dry-run/auto-fix тощо. Якщо `ctx` не передано — `runStandardRule` створить дефолтний контекст всередині.
49
+ - **Повертає:** `Promise<number>` — exit-code правила:
50
+ - `0` — порушень немає (або всі автоматично виправлені);
51
+ - `1` — є невиправні порушення / помилки.
52
+ - **Side effects:**
53
+ - Делегує всю роботу до `runStandardRule(dir, ctx)`. Це може включати: обхід файлової системи (`walkdir`), читання/запис файлів-учасників правила (auto-fix), вивід у `stdout`/`stderr` через спільний логер, мутацію `ctx.walkCache` тощо.
54
+ - **Не** викликає `process.exit` напряму.
55
+ - **Помилки:** будь-який reject від `runStandardRule` пробрасується нагору (caller відповідає за `try/catch`).
56
+
57
+ ### Анонімний CLI-блок (top-level)
58
+
59
+ ```js
60
+ if (isRunAsCli(import.meta.url)) {
61
+ process.exit(await runRuleCli(import.meta.dirname))
62
+ }
63
+ ```
64
+
65
+ - **Тригер:** виконується **лише** коли файл — точка входу процесу. Використовує `isRunAsCli(import.meta.url)` для надійного визначення (порівняння `import.meta.url` з `process.argv[1]` через `pathToFileURL`).
66
+ - **Дія:** делегує до `runRuleCli(import.meta.dirname)` — повноцінного CLI-обгортача, який:
67
+ - завантажує конфіг (`.cursor/cursor.json` чи аналог);
68
+ - застосовує whitelist/blacklist;
69
+ - збирає friendly summary;
70
+ - запускає `runStandardRule` всередині.
71
+ - **Завершення:** `process.exit(<exit-code>)` — пробрасує код у shell для CI/IDE-інтеграцій.
72
+ - **ESLint disables:**
73
+ - `n/no-process-exit` та `unicorn/no-process-exit` свідомо вимкнено коментарем — standalone entry-point **зобов'язаний** повертати exit-code для CI/IDE.
74
+
75
+ ## Залежності
76
+
77
+ ### Внутрішні (relative imports)
78
+
79
+ | Модуль | Що використано | Призначення |
80
+ | ----------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
81
+ | `../../scripts/lib/run-rule-cli.mjs` | `isRunAsCli`, `runRuleCli` | Хелпери standalone-режиму: детект "запущено як CLI" та повноцінна CLI-обгортка. |
82
+ | `../../scripts/lib/run-standard-rule.mjs` | `runStandardRule` | Універсальний раннер, що виконує "стандартну" послідовність етапів правила: `applies → JS-concerns → policy → mdc-refs`. Також експортує тип `RuleContext` (через JSDoc-`import`). |
83
+
84
+ Шлях `../../` веде з `npm/rules/changelog/` до `npm/scripts/lib/`.
85
+
86
+ ### Сусідні артефакти правила `changelog`
87
+
88
+ Не імпортуються напряму з цього файлу, але **використовуються `runStandardRule`** конвенційно (за іменами файлів у тій самій теці):
89
+
90
+ - `npm/rules/changelog/meta.json` — метадані правила (id, опис, теги, прапорці `worktree`/`policy`).
91
+ - `npm/rules/changelog/changelog.mdc` — людинозрозумілий зміст правила (Cursor MDC).
92
+ - `npm/rules/changelog/js/` — JS-concerns (перевірки/фікси для коду).
93
+ - `npm/rules/changelog/lib/` — допоміжні модулі правила.
94
+
95
+ ### Зовнішні (Node/runtime)
96
+
97
+ - `import.meta.url` — стандарт ESM, для детекту CLI-режиму.
98
+ - `import.meta.dirname` — потребує Node ≥ 20.11 або Bun (Bun підтримує). Передається в обидва раннери як корінь правила.
99
+ - `process.exit` — глобал Node/Bun.
100
+ - Top-level `await` — ESM-фіча, потребує сумісного runtime.
101
+
102
+ ## Потік виконання / Використання
103
+
104
+ ### Library mode (типовий — виклик з оркестратора `fix`)
105
+
106
+ ```text
107
+ npx @nitra/cursor fix
108
+
109
+
110
+ scripts/cli/fix.mjs (orchestrator)
111
+ │ для кожного правила з whitelist:
112
+
113
+ import { run } from 'npm/rules/<id>/fix.mjs'
114
+
115
+
116
+ run(ctx)
117
+
118
+
119
+ runStandardRule(import.meta.dirname, ctx)
120
+
121
+
122
+ applies → JS-concerns → policy → mdc-refs
123
+
124
+
125
+ Promise<0 | 1>
126
+ ```
127
+
128
+ Приклад програмного виклику:
129
+
130
+ ```js
131
+ import { run } from '@nitra/cursor/npm/rules/changelog/fix.mjs'
132
+
133
+ const code = await run({ walkCache: new Map(), summary: [] })
134
+ if (code !== 0) {
135
+ // є порушення — обробити в caller
136
+ }
137
+ ```
138
+
139
+ ### Standalone mode (ручний запуск/debug)
140
+
141
+ ```bash
142
+ bun npm/rules/changelog/fix.mjs
143
+ # еквівалент:
144
+ npx @nitra/cursor fix changelog
145
+ ```
146
+
147
+ Послідовність:
148
+
149
+ 1. ESM-модуль завантажується як entry-point.
150
+ 2. Виконуються імпорти.
151
+ 3. Експорт `run` реєструється (але ніхто не викликає).
152
+ 4. Виконується умова `isRunAsCli(import.meta.url)` → `true`.
153
+ 5. `await runRuleCli(import.meta.dirname)` — завантажує конфіг, фільтрує whitelist (тут — лише поточне правило, бо `dirname` фіксує id), запускає `runStandardRule`, друкує summary.
154
+ 6. `process.exit(<code>)` — повертає exit-code у shell.
155
+
156
+ ### Чому два режими в одному файлі?
157
+
158
+ - **DRY** — не дублювати entry-point на кожне правило.
159
+ - **DevEx** — розробник може дебажити одне правило: `bun npm/rules/<id>/fix.mjs`.
160
+ - **CI** — оркестратор `fix.mjs` імпортує `run` гуртом, ділить кеш обходу між правилами для прискорення.
161
+
162
+ ## Rebuild Test (контекстна незалежність)
163
+
164
+ Файл можна відтворити, маючи лише цю документацію:
165
+
166
+ 1. Створіть `npm/rules/changelog/fix.mjs`.
167
+ 2. Імпортуйте `isRunAsCli` та `runRuleCli` з `../../scripts/lib/run-rule-cli.mjs`.
168
+ 3. Імпортуйте `runStandardRule` з `../../scripts/lib/run-standard-rule.mjs`.
169
+ 4. Експортуйте функцію `run(ctx)`, яка повертає результат `runStandardRule(import.meta.dirname, ctx)`.
170
+ 5. Додайте JSDoc до `run` з типом `RuleContext` (через `import('../../scripts/lib/run-standard-rule.mjs').RuleContext`) і `Promise<number>` на повернення.
171
+ 6. Внизу додайте умовний блок `if (isRunAsCli(import.meta.url)) { process.exit(await runRuleCli(import.meta.dirname)) }`.
172
+ 7. Поряд із `process.exit(...)` додайте eslint-disable коментар для `n/no-process-exit` та `unicorn/no-process-exit` із обґрунтуванням "standalone entry-point має повертати exit-code для CI/IDE".
173
+
174
+ Результат має бути ідентичним за поведінкою оригіналу: library-імпорт безпечний, standalone-запуск завершує процес коректним exit-code.