@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,118 @@
1
+ # lint.mjs
2
+
3
+ ## Огляд
4
+
5
+ Файл `npm/rules/rego/js/lint.mjs` — це тонкий ESM-адаптер, що виконує роль CI-кроку (`lint`) для правила `rego` в монорепо. Він не містить власної логіки перевірки чи парсингу, а лише делегує виклик у вже існуючий CLI-модуль правила — `../lint/lint.mjs`. Файл існує задля відповідності уніфікованому контракту CI-кроків (`lint(files?)`), де очікується експорт функції `lint` з певною сигнатурою.
6
+
7
+ Ключова особливість: режим перевірки **per-file** для правила `rego` не підтримується — аналіз завжди виконується на всьому репозиторії (whole-repo). Тому параметр `files`, який зазвичай передається CI-фреймворком у крок `lint`, тут навмисно ігнорується (на що вказує префікс `_` у назві аргументу — `_files`). Це маркер для лінтерів та читачів коду, що значення параметра не використовується.
8
+
9
+ Файл є частиною інфраструктури правил (`.cursor/rules/n-rego.mdc`, `.cursor/rules/n-ci4.mdc`) і служить мостом між уніфікованою CI-сигнатурою кроків та конкретною реалізацією перевірки Rego-файлів.
10
+
11
+ ## Експорти / API
12
+
13
+ Файл експортує одну іменовану асинхронну функцію:
14
+
15
+ | Експорт | Тип | Призначення |
16
+ | ------- | --------------------------------------------------- | ---------------------------------------------------- |
17
+ | `lint` | `async function(files?: string[]): Promise<number>` | CI-крок для лінтингу Rego-файлів; повертає exit code |
18
+
19
+ Default-експорт відсутній. Експорт іменований, що відповідає конвенції CI-кроків у проєкті (де runner імпортує саме `{ lint }`).
20
+
21
+ ## Функції
22
+
23
+ ### `lint(_files)`
24
+
25
+ #### Сигнатура
26
+
27
+ ```js
28
+ export async function lint(_files)
29
+ ```
30
+
31
+ #### Параметри
32
+
33
+ | Параметр | Тип | Обов'язковий | Опис |
34
+ | -------- | ----------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | `_files` | `string[] \| undefined` | ні | Список файлів для перевірки. **Ігнорується** — `rego`-лінтер не має per-file режиму та виконує whole-repo аналіз. Префікс `_` сигналізує, що параметр свідомо не використовується. |
36
+
37
+ #### Повертає
38
+
39
+ `Promise<number>` — exit code від `runLintRego()`:
40
+
41
+ - `0` — лінтинг пройшов без помилок;
42
+ - ненульове значення — виявлено порушення або стався збій під час перевірки.
43
+
44
+ Конкретні значення коду визначає реалізація `runLintRego` у `../lint/lint.mjs` — цей файл лише транслює його далі.
45
+
46
+ #### Side effects
47
+
48
+ Сама функція `lint` не має прямих побічних ефектів — вона не пише у файлову систему, не звертається до мережі, не модифікує глобальний стан і не виводить нічого у консоль. Усі побічні ефекти (виклики `opa`/`regal`, читання Rego-файлів, друк діагностики, можливі тимчасові файли тощо) реалізовані всередині `runLintRego` із сусіднього модуля `../lint/lint.mjs` та повністю інкапсульовані там.
49
+
50
+ #### Винятки
51
+
52
+ Функція не має власних `try`/`catch`. Якщо `runLintRego()` відхилить проміс (`reject`) або викине синхронну помилку, виняток прозоро прокинеться вгору до викликача (зазвичай — CI-runner, який очікує саме на exit code).
53
+
54
+ ## Залежності
55
+
56
+ ### Внутрішні (relative)
57
+
58
+ | Імпорт | Шлях | Призначення |
59
+ | ------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `runLintRego` | `../lint/lint.mjs` | Реалізація whole-repo лінтингу Rego-файлів — фактично виконує перевірку та повертає exit code. Цей файл є єдиною залежністю модуля. |
61
+
62
+ ### Зовнішні
63
+
64
+ Зовнішніх npm-залежностей файл напряму не використовує. Усі сторонні інструменти (наприклад, `opa`, `regal` тощо) транзитивно підключаються через `runLintRego`.
65
+
66
+ ### Runtime
67
+
68
+ ESM-модуль (`.mjs`), потребує Node.js / Bun з підтримкою native ESM та `async/await`.
69
+
70
+ ## Потік виконання / Використання
71
+
72
+ ### Послідовність дій
73
+
74
+ 1. CI-runner (наприклад, фреймворк CI-кроків `n-ci4`) виявляє файл як крок `lint` правила `rego`.
75
+ 2. Runner викликає `lint(files)` — передає або список змінених файлів, або `undefined`, залежно від режиму запуску.
76
+ 3. Функція ігнорує аргумент `_files` і одразу викликає `runLintRego()` без параметрів.
77
+ 4. `runLintRego()` повертає `Promise<number>` із exit code; `lint` повертає цей самий проміс (через `return` в `async`-функції — фактично awaits та віддає значення).
78
+ 5. Runner отримує exit code і інтерпретує його як результат CI-кроку.
79
+
80
+ ### Контрактна позиція в системі правил
81
+
82
+ Файл реалізує уніфікований інтерфейс CI-кроку `lint(files?)`, очікуваний фреймворком правил репозиторію (`n-ci4`). Завдяки тонкому шару адаптації:
83
+
84
+ - сам runner може бути неуважним до особливостей правила (per-file vs whole-repo);
85
+ - логіка перевірки залишається інкапсульованою у `../lint/lint.mjs` і може використовуватися як з CI, так і з CLI напряму;
86
+ - зміна реалізації `runLintRego` не вимагає правок цього файла.
87
+
88
+ ### Приклад використання
89
+
90
+ ```js
91
+ import { lint } from './npm/rules/rego/js/lint.mjs'
92
+
93
+ // CI-режим: список файлів є, але буде проігнорований
94
+ const exitCode = await lint(['policy/a.rego', 'policy/b.rego'])
95
+ process.exit(exitCode)
96
+
97
+ // Або без аргументів — поведінка ідентична
98
+ const code2 = await lint()
99
+ process.exit(code2)
100
+ ```
101
+
102
+ ### Що НЕ робить цей файл
103
+
104
+ - Не парсить аргументи командного рядка — це не CLI-entrypoint.
105
+ - Не фільтрує файли за розширенням — фільтрація (за потреби) — справа `runLintRego`.
106
+ - Не агрегує помилки кількох інструментів — повертає лише підсумковий exit code.
107
+ - Не пише у `stdout`/`stderr` — увесь вивід походить із `runLintRego`.
108
+
109
+ ## Rebuild Test
110
+
111
+ Документація достатня, щоб переписати файл з нуля без перегляду оригіналу:
112
+
113
+ - Модуль ESM (`.mjs`).
114
+ - Імпорт: `runLintRego` із `../lint/lint.mjs`.
115
+ - Один іменований експорт — `async function lint(_files)`.
116
+ - Тіло: `return runLintRego()` (виклик без аргументів).
117
+ - JSDoc: параметр `_files` — `string[] | undefined`, описаний як ігнорований (whole-repo аналіз); повертає `Promise<number>` (exit code).
118
+ - Коментар верхнього рівня: пояснює, що це CI-крок `rego`, делегує у CLI правила, per-file режиму немає, `files` ігнорується.
@@ -0,0 +1,204 @@
1
+ # lint.mjs — Лінт Rego-полісі (`opa` + `regal` + опційний `conftest`)
2
+
3
+ ## Огляд
4
+
5
+ Модуль реалізує лінт-крок для Rego-полісі пакета `@nitra/cursor`, який живуть у каталозі
6
+ `npm/rules/<id>/policy/<concern>/`. Він послідовно запускає три інструменти й повертає код
7
+ виходу першого, що впав:
8
+
9
+ 1. `opa check --strict` — компіляція Rego з типами та строгим режимом (ловить мертвий код,
10
+ неоднозначні правила, незадекларовані змінні).
11
+ 2. `regal lint` — статичний лінтер стилю/ідіоматичності Rego (v0-синтаксис, неявні set-rules,
12
+ відхилення від `rego.v1`, плюс правила категорій bugs/idiomatic/performance — див.
13
+ `https://docs.styra.com/regal`).
14
+ 3. `conftest verify` — опційно: виконує `test_*` правила у `*_test.rego` (юніт-тести
15
+ полісі). Якщо `conftest` відсутній у `PATH`, крок пропускається без помилки (з
16
+ повідомленням, як його встановити).
17
+
18
+ Бінарники `opa` й `regal` резолвляться через `ensureTool` (`PATH` → локальний кеш → автоматичне
19
+ встановлення через `brew`/`scoop`/GitHub Release → hard-fail). `conftest` шукається лише в
20
+ `PATH` через `resolveCmd` без авто-install.
21
+
22
+ Канон патерну `lint-*` (серіалізація через `runStandardLint`, без прямого `withLock`) описаний
23
+ у `.cursor/rules/scripts.mdc`, секція «Серіалізація важких CLI-команд». Публічна CLI-форма
24
+ обгорнута в `withLock('lint-rego')` з дедуплікацією за станом git-дерева.
25
+
26
+ Файл одночасно є ESM-модулем (експортує функції) і CLI-точкою входу: якщо запущений напряму
27
+ (`isRunAsCli`), виконує `await runLintRego()` і виставляє `process.exitCode`.
28
+
29
+ ## Експорти / API
30
+
31
+ Модуль експортує два символи:
32
+
33
+ | Експорт | Тип | Призначення |
34
+ | ------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | `runLintRegoSteps` | named function | Внутрішня форма без локу: запускає три кроки в заданому `cwd`. Призначена для тестів у тимчасових каталогах, де потрібен fresh-прогон без дедуплікації. |
36
+ | `runLintRego` | named const (arrow function) | Публічна CLI-форма: серіалізує виконання через `withLock('lint-rego')` + дедуп за станом git-дерева (через `runStandardLint`). |
37
+
38
+ Також модуль має side-effect при прямому запуску: якщо `isRunAsCli(import.meta.url)` повертає
39
+ `true`, виконується `await runLintRego()` із записом результату в `process.exitCode`.
40
+
41
+ ## Функції
42
+
43
+ ### `runStep(bin, args, cwd)`
44
+
45
+ Внутрішня (не експортована) допоміжна функція. Запускає процес із успадкованим `stdio`, щоб
46
+ вивід виглядав як прямий виклик у shell, і пре-логує команду користувачу.
47
+
48
+ - **Сигнатура:** `runStep(bin: string, args: string[], cwd: string) => number`
49
+ - **Параметри:**
50
+ - `bin` — абсолютний шлях до бінарника (`opa`, `regal`, `conftest`).
51
+ - `args` — масив аргументів командного рядка.
52
+ - `cwd` — робочий каталог для дочірнього процесу.
53
+ - **Повертає:** код виходу (`0` — OK). Якщо `spawnSync` не зміг запустити бінарник
54
+ (`result.error`), повертає `1`.
55
+ - **Side effects:**
56
+ - Друкує рядок `▶ <bin> <args...>` у `stdout` (логування команди).
57
+ - У випадку помилки запуску пише `❌ Не вдалося запустити <bin>: <message>\n` у `stderr`.
58
+ - Породжує дочірній процес через `spawnSync` з `stdio: 'inherit'` і `env: process.env`
59
+ (наслідування поточного середовища).
60
+
61
+ ### `runLintRegoSteps(cwd?)`
62
+
63
+ Експортована функція, що виконує послідовність кроків лінту без локу.
64
+
65
+ - **Сигнатура:** `runLintRegoSteps(cwd?: string) => number`
66
+ - **Параметри:**
67
+ - `cwd` — робочий каталог (за замовчуванням `process.cwd()`).
68
+ - **Повертає:** число — код виходу.
69
+ - `0` — усі кроки OK або жодної цілі не знайдено (skip).
70
+ - Ненульове — код виходу першого кроку, що впав (раннє повернення).
71
+ - **Алгоритм:**
72
+ 1. Резолвить `root = resolve(cwd)`.
73
+ 2. `opa = ensureTool('opa')` — гарантує наявність `opa` (інакше `ensureTool` hard-fail).
74
+ 3. `regal = ensureTool('regal')` — те саме для `regal`.
75
+ 4. Фільтрує `LINT_TARGETS` за наявністю на диску (через `existsSync`). Якщо порожньо —
76
+ повертає `0` (skip).
77
+ 5. `runStep(opa, ['check', '--strict', ...targets], root)` — якщо `!== 0`, повертає цей код.
78
+ 6. `runStep(regal, ['lint', ...targets], root)` — якщо `!== 0`, повертає цей код.
79
+ 7. `conftest = resolveCmd('conftest')` — якщо `null`, друкує інформативне повідомлення про
80
+ пропуск кроку й рекомендацію зі встановлення, повертає `0`.
81
+ 8. Інакше `runStep(conftest, ['verify', ...targets.flatMap(t => ['-p', t])], root)` —
82
+ повертає його код виходу.
83
+ - **Side effects:** виконує `ensureTool` (може встановити бінарник або hard-fail), породжує
84
+ дочірні процеси з успадкованим `stdio`, пише в `stdout`/`stderr`.
85
+
86
+ ### `runLintRego()`
87
+
88
+ Експортована публічна CLI-форма (arrow-const).
89
+
90
+ - **Сигнатура:** `runLintRego() => Promise<number>`
91
+ - **Параметри:** немає.
92
+ - **Повертає:** `Promise<number>` — код виходу.
93
+ - **Поведінка:** делегує в `runStandardLint(import.meta.dirname, () => runLintRegoSteps())`.
94
+ `runStandardLint` забезпечує:
95
+ - Серіалізацію через `withLock('lint-rego')` (іменем серіалізатора виступає назва каталогу
96
+ `lint`, отримана з `import.meta.dirname` — конвенція `runStandardLint`).
97
+ - Дедуплікацію проти попереднього прогону за станом git-дерева.
98
+ - **Side effects:** через делегування — ті самі, що в `runLintRegoSteps`, плюс файлові
99
+ side-effects лок-файлу й кешу станів від `runStandardLint`.
100
+
101
+ ### CLI-вхід (на верхньому рівні модуля)
102
+
103
+ ```js
104
+ if (isRunAsCli(import.meta.url)) {
105
+ process.exitCode = await runLintRego()
106
+ }
107
+ ```
108
+
109
+ - Виконується лише при прямому запуску модуля як скрипта (а не при імпорті).
110
+ - Очікує проміс `runLintRego()` і записує отриманий код у `process.exitCode` (не викликає
111
+ `process.exit` явно, щоб лог-флаш не обрізався).
112
+
113
+ ## Залежності
114
+
115
+ ### Стандартна бібліотека Node.js
116
+
117
+ - `node:child_process` → `spawnSync` — синхронний запуск дочірніх процесів із `stdio: 'inherit'`.
118
+ - `node:fs` → `existsSync` — перевірка наявності каталогів-цілей.
119
+ - `node:path` → `resolve` — нормалізація абсолютних шляхів.
120
+
121
+ ### Внутрішні модулі (відносні шляхи від `npm/rules/rego/lint/lint.mjs`)
122
+
123
+ - `../../../scripts/cli-entry.mjs` → `isRunAsCli` — детектор «запущений напряму як CLI».
124
+ - `../../../scripts/lib/ensure-tool.mjs` → `ensureTool` — резолв бінарників (`PATH` → кеш →
125
+ авто-install brew/scoop/GitHub Release → hard-fail).
126
+ - `../../../scripts/utils/resolve-cmd.mjs` → `resolveCmd` — м'який пошук команди в `PATH`
127
+ (повертає шлях або `null`, без авто-install і без hard-fail).
128
+ - `../../../scripts/lib/run-standard-lint.mjs` → `runStandardLint` — обгортка з локом і
129
+ дедуплікацією для лінт-кроків.
130
+
131
+ ### Зовнішні бінарники (системні)
132
+
133
+ - `opa` — обов'язковий, авто-install через `ensureTool` (Open Policy Agent CLI).
134
+ - `regal` — обов'язковий, авто-install через `ensureTool` (Styra Regal lint CLI).
135
+ - `conftest` — опційний, лише з `PATH` (без авто-install). За відсутності — `verify`
136
+ пропускається.
137
+
138
+ ### Константи модуля
139
+
140
+ - `LINT_TARGETS = ['npm/rules']` — список відносних шляхів-цілей, що передаються в усі три
141
+ інструменти. Перед запуском фільтрується за `existsSync`.
142
+
143
+ ## Потік виконання / Використання
144
+
145
+ ### Як модуль (програмний імпорт)
146
+
147
+ ```js
148
+ import { runLintRego, runLintRegoSteps } from './npm/rules/rego/lint/lint.mjs'
149
+
150
+ // CLI-форма (з локом і дедуплікацією) — рекомендована для оркестрації
151
+ const code = await runLintRego()
152
+
153
+ // Внутрішня форма (без локу) — для тестів у тимчасових каталогах
154
+ const codeFresh = runLintRegoSteps('/tmp/fixture-cwd')
155
+ ```
156
+
157
+ ### Як CLI (прямий запуск)
158
+
159
+ ```sh
160
+ node npm/rules/rego/lint/lint.mjs
161
+ ```
162
+
163
+ При прямому запуску модуль викликає `runLintRego()` і виставляє `process.exitCode` відповідно
164
+ до результату. Це дозволяє оркестратору лінту (наприклад, кореневому `bun run lint`)
165
+ підхопити цей файл як один із кроків і отримати правильний код виходу процесу.
166
+
167
+ ### Логічний потік (один прогон `runLintRegoSteps`)
168
+
169
+ 1. Резолв `cwd → root`.
170
+ 2. `ensureTool('opa')` — повертає шлях до бінарника або hard-fail (з повідомленням
171
+ `ensureTool`).
172
+ 3. `ensureTool('regal')` — те саме.
173
+ 4. Перевірка цілей: `LINT_TARGETS.filter(existsSync)`. Порожньо → `return 0` (skip).
174
+ 5. `opa check --strict <targets...>` → `runStep`. `!== 0` → early-return з цим кодом.
175
+ 6. `regal lint <targets...>` → `runStep`. `!== 0` → early-return з цим кодом.
176
+ 7. `resolveCmd('conftest')`:
177
+ - `null` → інформативний `console.log` із рекомендацією встановлення, `return 0`.
178
+ - інакше → `conftest verify -p <t1> -p <t2> ...` через `runStep`; повертається його код.
179
+ 8. Повернутий код піднімається до `runLintRego` → у `runStandardLint` → у `process.exitCode`
180
+ (для CLI-режиму).
181
+
182
+ ### Семантика помилок і пропусків
183
+
184
+ - **Усі цілі відсутні** → skip із кодом `0` (немає чого лінтити на ранніх стадіях/у мінімальних
185
+ фікстурах).
186
+ - **`opa`/`regal` відсутні** → hard-fail усередині `ensureTool` (без авто-install або з
187
+ невдалим авто-install).
188
+ - **`opa check` або `regal lint` повернули `!== 0`** → раннє повернення з цим кодом; наступні
189
+ кроки не виконуються.
190
+ - **`conftest` відсутній у `PATH`** → лог-нотатка про пропуск, фінальний код `0` (вважається
191
+ не помилкою — юніт-тести полісі опційні в локальному середовищі; у CI рекомендовано
192
+ встановлювати `conftest`).
193
+ - **`spawnSync` не зміг запустити бінарник** (`result.error`) → лог `❌ Не вдалося запустити
194
+ ...` у `stderr`, `runStep` повертає `1`.
195
+
196
+ ### Контекст у проєкті
197
+
198
+ - Цілі лінту — каталог `npm/rules` пакета `@nitra/cursor`, де живуть Rego-полісі у
199
+ `npm/rules/<id>/policy/<concern>/`. Усі три інструменти приймають один шлях і рекурсивно
200
+ знаходять `.rego`, ігноруючи інші розширення (наприклад, `target.json` чи template-фіх).
201
+ - `opa` додатково потрібен VS Code-розширенню `tsandall.opa` (LSP, format-on-save через
202
+ `opa fmt`) — деталі в `mdc/rego.mdc`.
203
+ - Канон патерну `lint-*` із серіалізацією через `runStandardLint` (а не прямий `withLock`) —
204
+ див. `.cursor/rules/scripts.mdc`, секція «Серіалізація важких CLI-команд».
@@ -0,0 +1,185 @@
1
+ # `change.mjs` — створення одного change-файлу
2
+
3
+ ## Огляд
4
+
5
+ Модуль реалізує CLI-команду `n-cursor change`, яка створює **один** change-файл у каталозі `<ws>/.changes/<timestamp>-<rand>.md` усередині конкретного workspace монорепо. Файл містить мінімальний YAML-frontmatter (`bump`, `section`) та текст опису зміни.
6
+
7
+ Призначення — замінити ручне редагування `CHANGELOG.md` під час feature-флоу: розробник (або агент) додає декларативний запис про зміну, а агрегація у фінальний `CHANGELOG.md` відбувається пізніше в CI (відповідно до правила `n-changelog.mdc` v3.0).
8
+
9
+ Файл експонує дві функції:
10
+
11
+ - `writeChange(...)` — програмний API для запису одного change-файлу;
12
+ - `runChangeCli(args)` — обгортка для запуску з аргументів командного рядка.
13
+
14
+ Серіалізація, парсинг, валідація та формування імені файлу делеговані сусідньому модулю `./lib/change-file.mjs`.
15
+
16
+ ## Експорти / API
17
+
18
+ | Експорт | Тип | Призначення |
19
+ | -------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
20
+ | `writeChange` | `async function` | Створює один change-файл у `<ws>/.changes/` та повертає його шлях відносно workspace. |
21
+ | `runChangeCli` | `async function` | Парсить CLI-аргументи (`--bump`, `--section`, `--message`, `--ws`), викликає `writeChange` і повертає exit-код для процесу. |
22
+
23
+ Модуль не має експортованих констант чи класів, лише ці дві функції. `default`-експорт відсутній — використовуються лише іменовані експорти.
24
+
25
+ ## Функції
26
+
27
+ ### `writeChange({ bump, section, message, ws, cwd })`
28
+
29
+ Програмний рівень: створює один change-файл і повертає його шлях, придатний для логування.
30
+
31
+ **Сигнатура:**
32
+
33
+ ```js
34
+ async function writeChange({
35
+ bump, // string: 'major' | 'minor' | 'patch'
36
+ section, // string: 'Added' | 'Changed' | 'Fixed' | 'Removed'
37
+ message, // string: текст опису (буде обрізаний trim'ом)
38
+ ws = '.', // string: відносний шлях workspace від cwd (за замовчуванням поточний)
39
+ cwd = process.cwd(), // string: корінь репозиторію
40
+ }): Promise<string>
41
+ ```
42
+
43
+ **Параметри:**
44
+
45
+ - `bump` _(string, обов'язковий)_ — рівень semver-бампу. Дозволені значення: `major`, `minor`, `patch` (перевіряється у `parseChangeFile`).
46
+ - `section` _(string, обов'язковий)_ — секція Keep a Changelog. Дозволені значення: `Added`, `Changed`, `Fixed`, `Removed`.
47
+ - `message` _(string, обов'язковий)_ — людиночитаний опис зміни. Якщо `null/undefined`, перетворюється на порожній рядок; далі застосовується `trim()`. Порожній (після обрізки) опис призведе до помилки валідації.
48
+ - `ws` _(string, необов'язковий, default `.`)_ — шлях workspace відносно `cwd`. Дозволяє писати change-файли в конкретний підпроект монорепо.
49
+ - `cwd` _(string, необов'язковий, default `process.cwd()`)_ — корінь репозиторію. Винесений у параметр для тестованості.
50
+
51
+ **Повертає:** `Promise<string>` — відносний шлях створеного файлу від кореня workspace (`ws`), у форматі `.changes/<timestamp>-<rand>.md`. Зауважте: шлях **не** включає сам `ws` — це робить його зручним для подальшого логування `join(ws, rel)`.
52
+
53
+ **Side effects:**
54
+
55
+ 1. Серіалізує запис через `serializeChangeFile({ bump, section, description })`.
56
+ 2. **Валідує** серіалізований вміст через `parseChangeFile(content)` — це гарантує, що файл, який буде записаний, є валідним відповідно до тих самих правил, що використовуються при подальшому читанні. Невалідні `bump`/`section` чи порожній опис призведуть до `throw Error(...)` зі зрозумілим повідомленням українською.
57
+ 3. Створює каталог `<cwd>/<ws>/.changes/` (рекурсивно, `mkdir(..., { recursive: true })`) — не падає, якщо каталог уже існує.
58
+ 4. Генерує унікальне ім'я файлу через `newChangeFileName()` (timestamp + випадковий hex-суфікс, що захищає від колізій паралельних агентів у тій самій мілісекунді).
59
+ 5. Записує файл на диск (`writeFile`). Якщо файл випадково існує — буде перезаписаний (хоча колізія за іменем практично виключена через rand-суфікс).
60
+
61
+ **Виняткові ситуації:**
62
+
63
+ - Помилка валідації `parseChangeFile` (невалідні `bump`/`section`, порожній опис) — кидає `Error` із текстом помилки українською.
64
+ - Помилки FS (наприклад, відсутність прав на створення каталогу) — кидаються як стандартні `Error` із Node.js.
65
+
66
+ ### `runChangeCli(args)`
67
+
68
+ CLI-обгортка: розбирає аргументи, викликає `writeChange` і повертає exit-код. Не викликає `process.exit` — це робить виклична сторона (`bin`-скрипт).
69
+
70
+ **Сигнатура:**
71
+
72
+ ```js
73
+ async function runChangeCli(args: string[]): Promise<number>
74
+ ```
75
+
76
+ **Параметри:**
77
+
78
+ - `args` _(string[], обов'язковий)_ — масив аргументів CLI (зазвичай `process.argv.slice(2)`).
79
+
80
+ **Розпізнавані прапори** (парсер мінімалістичний, не використовує бібліотеку):
81
+
82
+ - `--bump <major|minor|patch>` — обов'язковий.
83
+ - `--section <Added|Changed|Fixed|Removed>` — обов'язковий.
84
+ - `--message "<опис>"` — обов'язковий.
85
+ - `--ws <шлях>` — необов'язковий, default `.`.
86
+
87
+ Кожен прапор шукається через `args.indexOf(flag)`; значення береться з наступного елемента (`args[i + 1]`). Якщо прапора немає, або він є останнім — значення `undefined`. Через таку реалізацію значення `--message`, що починається з тире (наприклад, `--message "--foo"`), розбереться коректно, але кілька прапорів з одним іменем не підтримуються (береться перше входження).
88
+
89
+ **Повертає:** `Promise<number>` — exit-код:
90
+
91
+ - `0` — успіх; у `stdout` пишеться `✅ <ws>/<rel>` (де `rel` — шлях, повернений `writeChange`).
92
+ - `1` — помилка:
93
+ - Якщо бракує одного з обов'язкових прапорів (`--bump`, `--section`, `--message`) — у `stderr` пишеться рядок-підказка з повним usage-описом українською.
94
+ - Якщо `writeChange` кинув виняток — у `stderr` пишеться `❌ <message>` (для `Error` — `error.message`, для всього іншого — `String(error)`).
95
+
96
+ **Side effects:**
97
+
98
+ - Пише в `console.log` (`stdout`) на успіх.
99
+ - Пише в `console.error` (`stderr`) на помилку.
100
+ - Через `writeChange` — створює каталог і файл (див. вище).
101
+
102
+ ## Залежності
103
+
104
+ ### Зовнішні (Node.js core)
105
+
106
+ - `node:fs/promises`:
107
+ - `mkdir` — рекурсивне створення `<ws>/.changes/`.
108
+ - `writeFile` — запис вмісту файлу.
109
+ - `node:path`:
110
+ - `join` — побудова абсолютних і відносних шляхів (платформонезалежно).
111
+
112
+ ### Внутрішні (сусідній модуль `./lib/change-file.mjs`)
113
+
114
+ - `CHANGES_DIR` _(string-константа `'.changes'`)_ — назва підкаталогу всередині workspace.
115
+ - `newChangeFileName()` — генератор унікального імені файлу (`<Date.now()>-<3byte-hex>.md`).
116
+ - `parseChangeFile(text)` — парсер + валідатор; використовується **тут** для валідації перед записом. Кидає `Error` зі зрозумілим текстом при невалідних `bump`, `section` або порожньому описі.
117
+ - `serializeChangeFile({ bump, section, description })` — формує текст change-файлу: `---\nbump: ...\nsection: ...\n---\n<description>\n`.
118
+
119
+ Валідні значення `bump` — `major|minor|patch`; валідні значення `section` — `Added|Changed|Fixed|Removed` (експортовані як `VALID_BUMPS` та `VALID_SECTIONS` у `change-file.mjs`).
120
+
121
+ ### Глобальні
122
+
123
+ - `process.cwd()` — використовується як default для `cwd` у `writeChange`.
124
+ - `console.log`, `console.error` — для CLI-виводу.
125
+
126
+ ## Потік виконання / Використання
127
+
128
+ ### Сценарій 1: програмний виклик `writeChange`
129
+
130
+ ```js
131
+ import { writeChange } from './change.mjs'
132
+
133
+ const rel = await writeChange({
134
+ bump: 'patch',
135
+ section: 'Fixed',
136
+ message: 'Fix off-by-one у валідаторі change-файлів',
137
+ ws: 'npm/rules/release'
138
+ })
139
+ // rel === '.changes/1735000000000-a1b2c3.md'
140
+ ```
141
+
142
+ Послідовність кроків усередині:
143
+
144
+ 1. `description = (message ?? '').trim()`.
145
+ 2. `content = serializeChangeFile({ bump, section, description })` — формується текст із frontmatter.
146
+ 3. `parseChangeFile(content)` — перевіряє коректність (`bump`, `section`, непорожній опис); кидає `Error` за невалідних даних.
147
+ 4. `mkdir(join(cwd, ws, '.changes'), { recursive: true })` — створює каталог за потреби.
148
+ 5. `name = newChangeFileName()` — `Date.now() + 3-byte hex`.
149
+ 6. `writeFile(join(dir, name), content)` — атомарно записує файл.
150
+ 7. Повертає `join('.changes', name)` — шлях відносно `ws`.
151
+
152
+ ### Сценарій 2: запуск через CLI
153
+
154
+ ```bash
155
+ n-cursor change \
156
+ --bump patch \
157
+ --section Fixed \
158
+ --message "Fix off-by-one у валідаторі change-файлів" \
159
+ --ws npm/rules/release
160
+ ```
161
+
162
+ Послідовність кроків усередині `runChangeCli`:
163
+
164
+ 1. Витягуються чотири значення через локальну функцію `get(flag)`.
165
+ 2. Якщо бракує `--bump`, `--section` або `--message` — друкується usage-рядок у `stderr`, повертається `1`.
166
+ 3. У `try/catch` викликається `writeChange({ bump, section, message, ws })`.
167
+ 4. На успіх — `console.log('✅ <ws>/<rel>')`, return `0`.
168
+ 5. На виняток — `console.error('❌ <message>')`, return `1`.
169
+
170
+ ### Інтеграція з feature-флоу
171
+
172
+ Файл є частиною release-інфраструктури (`npm/rules/release/`). Створені цією командою change-файли пізніше зчитуються через `readChangeFiles(ws)` з `./lib/change-file.mjs` під час релізу, агрегуються та конвертуються в записи `CHANGELOG.md`. Це усуває конфлікти злиття в `CHANGELOG.md` при паралельних feature-гілках: кожен PR додає **новий** файл із унікальним іменем замість редагування спільного.
173
+
174
+ ### Анти-колізія паралельних агентів
175
+
176
+ Унікальність імені файлу гарантується комбінацією:
177
+
178
+ - `Date.now()` — мілісекундний timestamp (порядок створення зберігається лексикографічно).
179
+ - `randomBytes(3).toString('hex')` — 6 шістнадцяткових символів випадковості (≈ 16M варіантів) у межах однієї мілісекунди.
180
+
181
+ Це робить безпечним одночасний запис із різних worktree чи паралельних агентів `n-cursor` без блокувань і без координації через FS.
182
+
183
+ ## Rebuild Test
184
+
185
+ Документація відображає реальний стан файлу `change.mjs` (60 рядків, версія на момент створення документації): два іменовані експорти `writeChange` та `runChangeCli`, валідація через `parseChangeFile` після серіалізації, default `ws = '.'`, default `cwd = process.cwd()`, обов'язкові CLI-прапори `--bump`/`--section`/`--message`, опціональний `--ws`, exit-коди `0`/`1`, повідомлення українською з префіксами `✅`/`❌`. Залежності — `node:fs/promises` (`mkdir`, `writeFile`), `node:path` (`join`), `./lib/change-file.mjs` (`CHANGES_DIR`, `newChangeFileName`, `parseChangeFile`, `serializeChangeFile`).
@@ -0,0 +1,119 @@
1
+ # fix.mjs — точка входу правила `release`
2
+
3
+ ## Огляд
4
+
5
+ Файл `npm/rules/release/fix.mjs` — це **точка входу (entry-point)** для правила `release` у системі правил репозиторію. Він виконує дві ролі:
6
+
7
+ 1. **Library mode** — експортує асинхронну функцію `run(ctx)`, яку викликають інші скрипти/оркестратори через `import + run(ctx)`. Делегує виконання у стандартний раннер правил `runStandardRule`.
8
+ 2. **CLI mode** — якщо файл запущено напряму як standalone-скрипт (`node fix.mjs` або через bun), він викликає `runRuleCli` й завершує процес кодом виходу (`process.exit`), щоб CI/IDE могли орієнтуватися на exit-code.
9
+
10
+ Сам файл не містить жодної бізнес-логіки правила: всі перевірки (`applies → JS-concerns → policy → mdc-refs`) живуть у супровідних модулях каталогу `npm/rules/release/`, а оркестрацію виконує `runStandardRule`. Це — лише тонкий адаптер між іменем правила (визначається як ім'я каталогу через `import.meta.dirname`) і загальним механізмом запуску правил.
11
+
12
+ ## Експорти / API
13
+
14
+ | Експорт | Тип | Призначення |
15
+ | ------- | --------------------------------- | --------------------------------------------------------------------------------------- |
16
+ | `run` | `function(ctx?): Promise<number>` | Library-mode запуск правила. Повертає exit-code: `0` — без порушень, `1` — є порушення. |
17
+
18
+ Інших публічних експортів (типів, констант, default-експорту) файл не оголошує.
19
+
20
+ ## Функції
21
+
22
+ ### `run(ctx)`
23
+
24
+ ```js
25
+ export function run(ctx) {
26
+ return runStandardRule(import.meta.dirname, ctx)
27
+ }
28
+ ```
29
+
30
+ - **Сигнатура:** `run(ctx?: RuleContext): Promise<number>`
31
+ - **Параметри:**
32
+ - `ctx` (необов'язковий) — об'єкт типу `RuleContext`, імпортований із `../../scripts/lib/run-standard-rule.mjs`. Передається через виклик і несе спільний стан прогону (наприклад, `walkCache` для повторного використання обходу файлової системи між декількома правилами в одному прогоні). Якщо `ctx` не передано, стандартний раннер працює без зовнішнього кешу.
33
+ - **Повертає:** `Promise<number>` — exit-code правила:
34
+ - `0` — порушень не знайдено;
35
+ - `1` — знайдені порушення (їх вже надруковано/зібрано раннером).
36
+ - **Side effects:** Безпосередньо у тілі `run` сайд-ефектів немає — функція просто проксує виклик у `runStandardRule`. Усі реальні ефекти (читання файлів, обчислення `applies`, друк звітів, mdc-refs-перевірки) залежать від реалізації `runStandardRule` і відповідних check-модулів каталогу правила.
37
+ - **Як визначає ім'я правила:** через `import.meta.dirname` — абсолютний шлях до каталогу `npm/rules/release/`. `runStandardRule` сам витягне з нього базове ім'я (`release`), знайде сусідні check-модулі (`check-*.mjs`, `applies.mjs`, `policy.*` тощо) й оркеструє конвеєр.
38
+
39
+ ### CLI-блок (модульний top-level код)
40
+
41
+ ```js
42
+ if (isRunAsCli(import.meta.url)) {
43
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
44
+ process.exit(await runRuleCli(import.meta.dirname))
45
+ }
46
+ ```
47
+
48
+ - **Не є функцією**, але це частина public-API файлу як CLI-скрипта.
49
+ - **Умова виконання:** `isRunAsCli(import.meta.url)` повертає `true` лише коли файл є точкою входу процесу (а не імпортований як модуль). Це класичний еквівалент Node-патерна `require.main === module` для ESM.
50
+ - **Дія:** Дочекатися `runRuleCli(import.meta.dirname)` (top-level `await`) і завершити процес із отриманим exit-code через `process.exit`.
51
+ - **ESLint:** Спеціально дозволено `n/no-process-exit` і `unicorn/no-process-exit` — standalone-entry-point має повертати CI-сумісний код.
52
+ - **Side effects:** Завершує Node-процес. Через top-level `await` блокує закриття модульного завантаження, поки CLI-прогін не завершиться.
53
+
54
+ ## Залежності
55
+
56
+ Файл імпортує **дві** утиліти з локальної бібліотеки скриптів репозиторію (відносні шляхи до `npm/scripts/lib/`):
57
+
58
+ - `../../scripts/lib/run-rule-cli.mjs`
59
+ - **`isRunAsCli(metaUrl: string): boolean`** — перевіряє, чи модуль викликано як CLI, а не як `import`.
60
+ - **`runRuleCli(ruleDir: string): Promise<number>`** — CLI-обгортка над раннером: парсить аргументи, налаштовує оточення прогону як standalone і повертає exit-code.
61
+ - `../../scripts/lib/run-standard-rule.mjs`
62
+ - **`runStandardRule(ruleDir: string, ctx?: RuleContext): Promise<number>`** — стандартний конвеєр правила: дізнається, до яких файлів воно застосовується (`applies`), запускає JS-concerns (зазвичай файли `check-*.mjs`), policy-перевірки і `mdc-refs` (узгодженість з `.mdc`-документами), збирає й виводить порушення.
63
+ - **Тип `RuleContext`** — структура контексту прогону. Цей файл лише re-references його в JSDoc-`@param`.
64
+
65
+ Жодних інших імпортів (зовнішніх npm-пакетів, Node core-модулів, динамічних `import()`) немає. Глобально використовується лише `process` (Node-built-in) — у CLI-блоці для `process.exit`.
66
+
67
+ ## Потік виконання / Використання
68
+
69
+ ### Library mode (виклик з іншого модуля)
70
+
71
+ ```js
72
+ import { run } from './npm/rules/release/fix.mjs'
73
+
74
+ const exitCode = await run(ctx) // ctx — опційний
75
+ if (exitCode !== 0) {
76
+ // знайдено порушення правила release
77
+ }
78
+ ```
79
+
80
+ Послідовність:
81
+
82
+ 1. Викликач робить `import { run } from '.../release/fix.mjs'`.
83
+ 2. Виклик `run(ctx)` → `runStandardRule(import.meta.dirname, ctx)`.
84
+ 3. `runStandardRule` визначає каталог правила (`release`), знаходить і запускає сусідні модулі правила (`applies` → JS-concerns → policy → mdc-refs), обмінюючись із викликачем кешем через `ctx` (за наявності).
85
+ 4. Повертається числовий exit-code (`0`/`1`).
86
+
87
+ ### CLI mode (запуск файлу як скрипта)
88
+
89
+ ```bash
90
+ node npm/rules/release/fix.mjs
91
+ # або через bun
92
+ bun run npm/rules/release/fix.mjs
93
+ ```
94
+
95
+ Послідовність:
96
+
97
+ 1. Node/Bun стартує файл як точку входу — `isRunAsCli(import.meta.url)` повертає `true`.
98
+ 2. `await runRuleCli(import.meta.dirname)` запускає CLI-обгортку правила: парсить можливі аргументи/прапорці, ініціалізує стандартне середовище правила, всередині сам викликає аналог `runStandardRule` і повертає exit-code.
99
+ 3. `process.exit(<exitCode>)` завершує процес: CI-конвеєр або IDE-runner отримує `0` (успіх) або `1` (порушення).
100
+
101
+ ### Типове місце у системі правил
102
+
103
+ Файл є членом сім'ї однотипних entry-point'ів `npm/rules/<rule-name>/fix.mjs`: кожне правило репозиторію має такий-самий шаблон («тонкий fix.mjs + сусідні check-модулі»). Це уніфікує запуск як з оркестратора (`bun run lint`, агрегатори, `runStandardRule`-цикли), так і з окремих CI-кроків чи дебагу через IDE.
104
+
105
+ ### Контракт повернення (exit-code)
106
+
107
+ | Значення | Семантика |
108
+ | -------- | ------------------------------------------------------------------------- |
109
+ | `0` | Правило `release` не знайшло порушень для файлів, до яких воно `applies`. |
110
+ | `1` | Принаймні одне порушення (вже зафіксоване й виведене раннером). |
111
+
112
+ Інших кодів файл не вводить — будь-яка деталізація залежить від `runStandardRule` / `runRuleCli`.
113
+
114
+ ## Примітки щодо реалізації
115
+
116
+ - **Чому `import.meta.dirname`, а не явне ім'я правила:** Це дає змогу шаблону `fix.mjs` бути ідентичним для всіх правил без захардкоджування рядкового імені — раннер сам ідентифікує правило за каталогом.
117
+ - **Чому `await` на top-level:** Файл — ESM (`.mjs`), top-level `await` дозволений. У CLI-режимі потрібен синхронно-доступний exit-code до виклику `process.exit`.
118
+ - **Чому виключені два ESLint-правила:** Обидва (`n/no-process-exit`, `unicorn/no-process-exit`) у проєкті заборонені для бібліотечного коду, але для standalone-entry-points exit-code — це і є контракт. Інлайн-коментар із поясненням обов'язковий за стилем репозиторію.
119
+ - **Що файл свідомо не робить:** не парсить CLI-аргументи самостійно, не читає файли, не звертається до мережі, не імпортує бізнес-логіку правила напряму — все це делеговано в `run-standard-rule.mjs` та `run-rule-cli.mjs`.