@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,165 @@
1
+ # discover-checkable-rules.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `discover-checkable-rules.mjs` — це discovery-шар для CLI-команди `fix`. Його завдання — швидко просканувати файлову структуру каталогу `npm/rules/` та виявити «прогонні» правила, тобто правила, у яких є щонайменше один JS-концерн або policy-концерн. Правила, які складаються тільки з декларативних артефактів (`.mdc` + `auto.md`) без жодного прогонного концерну, відсіюються.
6
+
7
+ Виокремлюються два типи концернів:
8
+
9
+ - **JS concerns** — окремі файли `rules/<id>/js/<concern>.mjs`. Кожен файл — один концерн (flat-конвенція).
10
+ - **Policy concerns** — підкаталоги `rules/<id>/policy/<concern>/`, у яких присутній файл `target.json` (поруч із якого зазвичай лежить `<concern>.rego`).
11
+
12
+ Модуль свідомо не парсить вміст `target.json` і не читає JS-файли — це лише швидкий «структурний» скан (шляхи + назви) без I/O-вмісту. Парсинг покладено на runner.
13
+
14
+ Файли-помічники з префіксом `_` (зокрема каталог `_lib/`), тестові файли `*.test.mjs`, а також приховані файли/каталоги (`.`-префікс) ігноруються.
15
+
16
+ Історичний контекст конвенції розкладки JS-концернів:
17
+
18
+ - `js/<concern>/check.mjs` — версії 1.13.80–1.13.89 (вкладений каталог на концерн);
19
+ - `js/<concern>.mjs` — версії 1.13.90+ (flat: концерн = файл, а не каталог).
20
+
21
+ Допоміжні файли, тести, шаблони й дані винесені в окремі топ-level папки правила: `js/_lib/`, `tests/`, `templates/`, `data/`.
22
+
23
+ ## Експорти / API
24
+
25
+ Модуль експортує дві асинхронні функції:
26
+
27
+ | Експорт | Тип | Призначення |
28
+ | ----------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
29
+ | `discoverOneRule(ruleDir, ruleId)` | `async (string, string) => Promise<CheckableRule>` | Будує опис одного правила за заданим шляхом каталогу та id, без обходу `rules/`. |
30
+ | `discoverCheckableRules(bundledRulesDir)` | `async (string) => Promise<CheckableRule[]>` | Сканує цілий каталог `npm/rules/` і повертає масив правил, для яких є JS- або policy-концерни. |
31
+
32
+ Внутрішніми (не експортованими) залишаються функції `listJsConcerns` і `listPolicyConcerns`.
33
+
34
+ ### Типи (JSDoc `@typedef`)
35
+
36
+ ```text
37
+ JsConcern { name: string } // basename файла js/<name>.mjs без розширення
38
+ PolicyConcern { name: string } // imʼя підкаталогу policy/<name>/
39
+ CheckableRule {
40
+ id: string, // = basename каталогу rules/<id>/
41
+ jsConcerns: JsConcern[], // алфавітно
42
+ policyConcerns: PolicyConcern[], // алфавітно
43
+ }
44
+ ```
45
+
46
+ ## Функції
47
+
48
+ ### `listJsConcerns(jsDir)` (internal)
49
+
50
+ - **Сигнатура:** `async function listJsConcerns(jsDir: string): Promise<JsConcern[]>`
51
+ - **Параметри:**
52
+ - `jsDir` — абсолютний шлях до каталогу `rules/<id>/js/`.
53
+ - **Повертає:** масив `JsConcern[]`, відсортований алфавітно за `name` через `Array.prototype.toSorted` із компаратором `localeCompare`.
54
+ - **Логіка:**
55
+ 1. Якщо каталог `jsDir` не існує (`existsSync` повертає `false`) — повертається `[]`.
56
+ 2. Читається вміст через `readdir(jsDir, { withFileTypes: true })` — тобто отримуються `Dirent`-обʼєкти з метаінформацією.
57
+ 3. Пропускаються:
58
+ - сутності, що не є файлом (`!entry.isFile()`);
59
+ - файли без розширення `.mjs`;
60
+ - тестові файли `*.test.mjs`;
61
+ - службові файли з префіксом `_` (наприклад вміст `_lib/`, хоча сам `_lib` як каталог не буде файлом і відсіється раніше);
62
+ - приховані файли з префіксом `.`.
63
+ 4. Для решти файлів обчислюється `name = entry.name.slice(0, -'.mjs'.length)` — basename без розширення.
64
+ - **Side effects:** лише файлові читання (`existsSync`, `readdir`), без запису.
65
+
66
+ ### `listPolicyConcerns(policyDir)` (internal)
67
+
68
+ - **Сигнатура:** `async function listPolicyConcerns(policyDir: string): Promise<PolicyConcern[]>`
69
+ - **Параметри:**
70
+ - `policyDir` — абсолютний шлях до каталогу `rules/<id>/policy/`.
71
+ - **Повертає:** масив `PolicyConcern[]`, відсортований алфавітно за `name`.
72
+ - **Логіка:**
73
+ 1. Якщо `policyDir` не існує — повертається `[]`.
74
+ 2. Читається вміст з `withFileTypes: true`.
75
+ 3. Пропускаються будь-які записи, що не є каталогом, а також приховані каталоги (префікс `.`).
76
+ 4. Для кожного підкаталогу перевіряється наявність файла `target.json` через `existsSync(join(policyDir, entry.name, 'target.json'))`. Якщо `target.json` є — підкаталог зараховується як policy-концерн.
77
+ - **Side effects:** лише файлові читання, без запису.
78
+
79
+ ### `discoverOneRule(ruleDir, ruleId)` (exported)
80
+
81
+ - **Сигнатура:** `export async function discoverOneRule(ruleDir: string, ruleId: string): Promise<CheckableRule>`
82
+ - **Параметри:**
83
+ - `ruleDir` — абсолютний шлях до каталогу правила `rules/<id>/`;
84
+ - `ruleId` — ідентифікатор правила, зазвичай `basename(ruleDir)`. Передається явно, бо функція не виводить його з шляху самостійно.
85
+ - **Повертає:** обʼєкт `CheckableRule` з полями `id`, `jsConcerns`, `policyConcerns`. На відміну від `discoverCheckableRules`, тут не виконується фільтрація «має бути хоч щось» — повертається опис як є (можуть бути порожні масиви концернів).
86
+ - **Логіка:** паралельно (точніше — послідовно `await`-ить) запускає `listJsConcerns(join(ruleDir, 'js'))` і `listPolicyConcerns(join(ruleDir, 'policy'))` та збирає результати в обʼєкт.
87
+ - **Використання:** викликається `runStandardRule`-flow для per-rule entry-point, коли потрібно отримати опис конкретного правила, а не сканувати весь каталог.
88
+ - **Side effects:** лише читання файлової системи.
89
+
90
+ ### `discoverCheckableRules(bundledRulesDir)` (exported)
91
+
92
+ - **Сигнатура:** `export async function discoverCheckableRules(bundledRulesDir: string): Promise<CheckableRule[]>`
93
+ - **Параметри:**
94
+ - `bundledRulesDir` — абсолютний шлях до кореневого каталогу всіх правил (зазвичай `npm/rules/`).
95
+ - **Повертає:** масив `CheckableRule[]`, відсортований алфавітно за `id`. Включаються тільки правила, у яких `jsConcerns.length > 0 || policyConcerns.length > 0`.
96
+ - **Логіка:**
97
+ 1. Якщо `bundledRulesDir` не існує — повертається `[]`.
98
+ 2. Читається вміст каталогу з `withFileTypes: true`.
99
+ 3. Пропускаються сутності, які не є каталогом, та приховані каталоги (префікс `.`).
100
+ 4. Для кожного підкаталогу формується `ruleDir = join(bundledRulesDir, entry.name)` і викликається `discoverOneRule(ruleDir, entry.name)`.
101
+ 5. Правила, у яких немає жодного JS- або policy-концерну (декларативні-only), відсіюються.
102
+ - **Side effects:** лише читання файлової системи (без запису, без мережевих викликів).
103
+
104
+ ## Залежності
105
+
106
+ Виключно зовнішні стандартні модулі Node.js:
107
+
108
+ - `node:fs` → `existsSync` — синхронна перевірка існування шляху;
109
+ - `node:fs/promises` → `readdir` — асинхронне читання вмісту каталогу (з `withFileTypes: true` повертає `Dirent[]`);
110
+ - `node:path` → `join` — кросплатформова конкатенація шляхів.
111
+
112
+ Внутрішніх імпортів з інших модулів проєкту немає. Це робить модуль чистим discovery-шаром без бізнес-логіки, що дозволяє безпечно його тестувати ізольовано (потрібна лише підготовлена файлова структура у тимчасовому каталозі).
113
+
114
+ Зворотні залежності (хто використовує цей модуль): runner-флоу CLI `fix` (зокрема `runStandardRule`), який після discovery читає `target.json` для policy-концернів і виконує JS-концерни.
115
+
116
+ ## Потік виконання / Використання
117
+
118
+ ### Типовий сценарій 1. Повний скан правил для CLI `fix`
119
+
120
+ ```javascript
121
+ import { discoverCheckableRules } from './discover-checkable-rules.mjs'
122
+
123
+ const rules = await discoverCheckableRules('/abs/path/to/npm/rules')
124
+ for (const rule of rules) {
125
+ // rule.id, rule.jsConcerns, rule.policyConcerns
126
+ }
127
+ ```
128
+
129
+ Послідовність дій усередині:
130
+
131
+ 1. `discoverCheckableRules` перевіряє існування кореневого каталогу.
132
+ 2. Перебирає підкаталоги верхнього рівня (= потенційні правила).
133
+ 3. Для кожного підкаталогу викликає `discoverOneRule`, який своєю чергою:
134
+ - сканує `rules/<id>/js/` через `listJsConcerns`;
135
+ - сканує `rules/<id>/policy/` через `listPolicyConcerns`;
136
+ 4. Якщо знайдено хоч один концерн — правило додається у вихідний масив.
137
+ 5. Масив сортується за `id`.
138
+
139
+ ### Типовий сценарій 2. Per-rule entry-point
140
+
141
+ ```javascript
142
+ import { discoverOneRule } from './discover-checkable-rules.mjs'
143
+
144
+ const rule = await discoverOneRule('/abs/path/to/npm/rules/n-js-lint', 'n-js-lint')
145
+ // rule.id === 'n-js-lint'
146
+ // rule.jsConcerns — список JS-концернів у js/
147
+ // rule.policyConcerns — список policy-концернів у policy/
148
+ ```
149
+
150
+ Цей шлях оминає енумерацію всього `rules/`, що корисно коли id правила вже відомий (наприклад, передано аргументом CLI).
151
+
152
+ ### Гарантії та обмеження
153
+
154
+ - **Чистота:** функції — read-only (не пишуть у ФС, не виконують код концернів). Це дозволяє безпечно викликати їх повторно.
155
+ - **Сортування:** усі результати алфавітно відсортовані через `toSorted` із `localeCompare`. Це робить вивід детермінованим між запусками й між платформами (хоч порядок `readdir` залежить від ФС).
156
+ - **Толерантність до відсутніх каталогів:** будь-яка з директорій (`rules/`, `js/`, `policy/`) може бути відсутня — повертається порожній масив без помилки.
157
+ - **Тести/хелпери:** файли з префіксом `_` і `*.test.mjs` гарантовано виключаються із JS-концернів.
158
+ - **Що _не_ робиться:** не валідуються імена концернів, не парситься `target.json`, не перевіряється наявність `<name>.rego` поруч із `target.json`. Це робота runner-а.
159
+
160
+ ### Eage cases
161
+
162
+ - Файл з префіксом `.` у `js/` — пропускається.
163
+ - Підкаталог у `js/` (наприклад `_lib/`) — не є файлом, відсіюється першою перевіркою `entry.isFile()`.
164
+ - Підкаталог у `policy/` без `target.json` — пропускається; наявність `<name>.rego` без `target.json` не зараховується.
165
+ - Порожнє правило (тільки `.mdc`/`auto.md` без `js/` і `policy/`) — не потрапляє у вихід `discoverCheckableRules`, але буде повернене з порожніми масивами в `discoverOneRule`.
@@ -0,0 +1,254 @@
1
+ # ensure-tool.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `ensure-tool.mjs` — єдина точка резолву зовнішніх CLI-залежностей пакета `@nitra/cursor`. Він гарантує, що потрібний бінарник (`hk`, `conftest`, `shellcheck`, `actionlint`, `dotenv-linter`, `opa`, `regal`, `hadolint`, `kubeconform`, `kubescape`) доступний у системі, виконуючи послідовний пошук:
6
+
7
+ 1. У системному `PATH` (через `resolveCmd`).
8
+ 2. У керованому кеші бінарників (`~/.cache/@nitra/cursor/bin/` на Linux/macOS або `%LOCALAPPDATA%\@nitra\cursor\bin\` на Windows).
9
+ 3. Авто-встановлення відповідно до OS (`brew` для macOS, `scoop` для Windows із fallback на GitHub Release, прямий завантажувач GitHub Release для Linux).
10
+ 4. Hard-fail з персоналізованою підказкою, якщо авто-встановлення вимкнено змінною середовища `N_CURSOR_NO_AUTO_INSTALL`.
11
+
12
+ Така архітектура усуває дублювання install-логіки в кожному `lint.mjs` / `fix.mjs`: щоб додати нову зовнішню утиліту, достатньо одного запису в реєстрі `TOOLS`. Додатково модуль експортує `ensureHkInstall`, який реєструє git pre-commit hook через `hk install` (пропускається в CI).
13
+
14
+ Файл написаний для Node.js (ESM), використовує лише стандартну бібліотеку та один локальний хелпер `resolveCmd`.
15
+
16
+ ## Експорти / API
17
+
18
+ | Експорт | Тип | Призначення |
19
+ | ------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------- |
20
+ | `ensureTool(toolId)` | `function` | Резолвить і за потреби встановлює зовнішній CLI. Повертає абсолютний шлях до бінарника або кидає `Error`. |
21
+ | `ensureHkInstall(hkBin)` | `function` | Виконує `hk install` для реєстрації git pre-commit hook. Жодного return value; на помилку лише `console.warn`. |
22
+
23
+ Внутрішні (не експортуються, але формують контракт модуля):
24
+
25
+ - `TOOLS` — реєстр `Record<string, ToolEntry>` із описом install-стратегії для кожного тула.
26
+ - `ToolEntry` — JSDoc-тип, що описує поля одного запису реєстру.
27
+ - Допоміжні функції: `getCacheDir`, `mapArch`, `fetchLatestVersion`, `installFromGithub`, `installViaBrew`, `installViaScoop`, `autoInstall`, `buildHint`.
28
+
29
+ ## Функції
30
+
31
+ ### `getCacheDir()`
32
+
33
+ - **Сигнатура:** `getCacheDir(): string`
34
+ - **Параметри:** немає.
35
+ - **Повертає:** абсолютний шлях до каталогу кешу бінарників.
36
+ - **Логіка:**
37
+ - На `win32` бере `process.env.LOCALAPPDATA` (fallback `homedir()/AppData/Local`) і додає `@nitra/cursor/bin`.
38
+ - На інших платформах повертає `homedir()/.cache/@nitra/cursor/bin`.
39
+ - **Side effects:** немає (тільки читає env / `os.homedir`).
40
+
41
+ ### `mapArch(nodeArch, style)`
42
+
43
+ - **Сигнатура:** `mapArch(nodeArch: 'x64'|'arm64'|string, style: 'hk'|'conftest'|'actionlint'): string`
44
+ - **Параметри:**
45
+ - `nodeArch` — значення `process.arch`.
46
+ - `style` — стиль іменування платформи для release-asset:
47
+ - `'actionlint'` → `amd64` / `arm64`.
48
+ - `'conftest'` → `x86_64` / `arm64`.
49
+ - `'hk'` (та інші release-asset стилі: shellcheck, dotenv-linter) → `x86_64` / `aarch64`.
50
+ - **Повертає:** рядок архітектури, очікуваний у назві release-asset.
51
+ - **Side effects:** немає.
52
+
53
+ ### `fetchLatestVersion(repo, curlBin)`
54
+
55
+ - **Сигнатура:** `fetchLatestVersion(repo: string, curlBin: string): string`
56
+ - **Параметри:**
57
+ - `repo` — репозиторій у форматі `owner/repo`.
58
+ - `curlBin` — абсолютний шлях до бінарника `curl`.
59
+ - **Повертає:** версію останнього релізу без префікса `v` (наприклад `0.4.1`).
60
+ - **Поведінка:** через `spawnSync` викликає `curl -sSL -H "Accept: application/vnd.github+json" https://api.github.com/repos/<repo>/releases/latest`, парсить JSON, бере поле `tag_name`, прибирає префікс `v` за допомогою регексу `TAG_V_PREFIX_RE`.
61
+ - **Помилки:**
62
+ - `curl failed: ...` — `r.error` ненульове.
63
+ - `curl exit <status>: ...` — ненульовий exit-код.
64
+ - `GitHub API response is not JSON: ...` — некоректний JSON.
65
+ - `GitHub API: tag_name missing for <repo>` — у відповіді немає `tag_name`.
66
+ - **Side effects:** мережевий HTTP-запит до GitHub API.
67
+
68
+ ### `installFromGithub(toolId, entry, cacheDir)`
69
+
70
+ - **Сигнатура:** `installFromGithub(toolId: string, entry: ToolEntry, cacheDir: string): string`
71
+ - **Параметри:**
72
+ - `toolId` — ключ у `TOOLS`.
73
+ - `entry` — `ToolEntry` для цього тула.
74
+ - `cacheDir` — абсолютний шлях до каталогу кешу.
75
+ - **Повертає:** абсолютний шлях до встановленого бінарника.
76
+ - **Послідовність дій:**
77
+ 1. Резолвить `curl` та `tar` у PATH; за відсутності — кидає `Error`.
78
+ 2. Через `fetchLatestVersion` отримує актуальну версію.
79
+ 3. Формує назву asset через `entry.asset(ver)` і URL `https://github.com/<github>/releases/download/v<ver>/<asset>`.
80
+ 4. Створює `cacheDir` (`mkdirSync` з `recursive: true`).
81
+ 5. Завантажує asset через `curl -sSL -o <archivePath> <downloadUrl>`.
82
+ 6. Якщо `entry.archive === false` — перейменовує завантажений файл у `<cacheDir>/<toolId>`, виставляє права `0o755` і повертає шлях.
83
+ 7. Інакше викликає `tar` із прапорцем `-xJf` (для `.tar.xz`) або `-xzf` (для `.tar.gz`) для розпакування в `cacheDir`.
84
+ 8. Визначає реальний шлях до бінарника через `entry.binFinder(ver)` або просто `toolId`. Перевіряє існування файлу.
85
+ 9. Опціонально видаляє завантажений архів через `rm` (м’яко: якщо `rm` не знайдено, продовжує).
86
+ - **Помилки:**
87
+ - `curl не знайдено в PATH — потрібен для завантаження <toolId>`.
88
+ - `tar не знайдено в PATH — потрібен для встановлення <toolId>`.
89
+ - `Завантаження <toolId> не вдалось: ...` / `curl exit <status> при завантаженні <toolId>: ...`.
90
+ - `tar failed for <toolId>: ...` / `tar exit <status> для <toolId>: ...`.
91
+ - `Бінарник <toolId> не знайдено після розпакування: <binPath>`.
92
+ - **Side effects:** мережа, файлова система (створення/перейменування файлів, chmod, видалення архіву).
93
+
94
+ ### `installViaBrew(toolId, entry)`
95
+
96
+ - **Сигнатура:** `installViaBrew(toolId: string, entry: ToolEntry): string`
97
+ - **Параметри:** `toolId` і `entry` як у попередній функції.
98
+ - **Повертає:** абсолютний шлях до бінарника після встановлення (через повторний `resolveCmd(toolId)`).
99
+ - **Послідовність дій:**
100
+ 1. Резолвить `brew` у PATH; на відсутність кидає `brew не знайдено в PATH. Встанови Homebrew: https://brew.sh`.
101
+ 2. Виконує `brew install <entry.brew>` із `stdio: 'inherit'` (інтерактивний прогрес).
102
+ 3. Перевіряє exit-код і `error` об’єкт.
103
+ 4. Після успіху повторно резолвить `toolId` у PATH; якщо й тоді нема — кидає `... не знайдено в PATH після brew install`.
104
+ - **Side effects:** виклик зовнішнього `brew install` (мережа, мутація системного стану на macOS).
105
+
106
+ ### `installViaScoop(toolId, entry)`
107
+
108
+ - **Сигнатура:** `installViaScoop(toolId: string, entry: ToolEntry): string`
109
+ - **Параметри:** як вище.
110
+ - **Повертає:** абсолютний шлях до бінарника після встановлення.
111
+ - **Поведінка:** дзеркало `installViaBrew`, але для `scoop install`.
112
+ - Якщо `entry.scoop === null` — кидає `... недоступний у Scoop. Встанови вручну: https://github.com/<repo>/releases`.
113
+ - Якщо `scoop` не у PATH — кидає `scoop не знайдено в PATH. Встанови Scoop: https://scoop.sh`.
114
+ - **Side effects:** виклик зовнішнього `scoop install`.
115
+
116
+ ### `autoInstall(toolId, entry, cacheDir)`
117
+
118
+ - **Сигнатура:** `autoInstall(toolId: string, entry: ToolEntry, cacheDir: string): string`
119
+ - **Поведінка диспетчера за платформою:**
120
+ - `darwin` → `installViaBrew`.
121
+ - `win32` → пробує `installViaScoop`, на будь-який throw — fallback на `installFromGithub` (наприклад, для `dotenv-linter` чи `regal`, де `scoop: null`).
122
+ - Інше (Linux) → `installFromGithub`.
123
+ - **Повертає:** абсолютний шлях до бінарника.
124
+ - **Side effects:** делегує своїм внутрішнім installer-ам.
125
+
126
+ ### `buildHint(toolId, entry)`
127
+
128
+ - **Сигнатура:** `buildHint(toolId: string, entry: ToolEntry): string`
129
+ - **Параметри:** як вище.
130
+ - **Повертає:** багаторядкове повідомлення для error message при заблокованому авто-installі.
131
+ - **Формат:**
132
+ - Перший рядок — `❌ <toolId> не знайдено в PATH і авто-встановлення відключено (N_CURSOR_NO_AUTO_INSTALL).`.
133
+ - Другий рядок — ` Встанови:`.
134
+ - Далі — OS-specific підказка:
135
+ - macOS: ` macOS: brew install <entry.brew>`.
136
+ - Windows: ` Windows: scoop install <entry.scoop>` (якщо доступний) та ` або: https://github.com/<repo>/releases`.
137
+ - Linux: ` Linux: https://github.com/<repo>/releases`.
138
+ - **Side effects:** немає.
139
+
140
+ ### `ensureTool(toolId)` _(export)_
141
+
142
+ - **Сигнатура:** `ensureTool(toolId: string): string`
143
+ - **Параметри:** `toolId` — ключ у реєстрі `TOOLS` (`'hk'`, `'conftest'`, `'shellcheck'`, `'actionlint'`, `'dotenv-linter'`, `'opa'`, `'regal'`, `'hadolint'`, `'kubeconform'`, `'kubescape'`).
144
+ - **Повертає:** абсолютний шлях до бінарника.
145
+ - **Послідовність резолву:**
146
+ 1. **Валідація** — якщо `TOOLS[toolId]` відсутній, кидає `ensureTool: невідомий тул '<toolId>'`.
147
+ 2. **PATH** — `resolveCmd(toolId)`; якщо знайдено — повертає одразу.
148
+ 3. **Кеш** — `join(getCacheDir(), toolId)`; якщо файл існує — повертає його шлях. Зауваження: перевірка спрощена і не враховує `entry.binFinder` (для cached binaries `installFromGithub` уже клав фінальний бінарник у відоме місце або кеш просто не містить такого файлу — у такому разі переходимо до install).
149
+ 4. **Авто-install** — якщо змінна середовища `N_CURSOR_NO_AUTO_INSTALL` не виставлена, викликає `autoInstall(toolId, entry, cacheDir)`.
150
+ 5. **Hard-fail** — кидає `Error(buildHint(toolId, entry))`.
151
+ - **Помилки:** будь-яка з помилок `autoInstall` / `installFrom*` піднімається вгору; додатково — `невідомий тул` та `❌ ... не знайдено в PATH`.
152
+ - **Side effects:** мережа, файлова система, виклик зовнішніх install-команд (`brew`, `scoop`, `curl`, `tar`, `rm`).
153
+
154
+ ### `ensureHkInstall(hkBin)` _(export)_
155
+
156
+ - **Сигнатура:** `ensureHkInstall(hkBin: string): void`
157
+ - **Параметри:**
158
+ - `hkBin` — абсолютний шлях до бінарника `hk` (зазвичай отриманий через `ensureTool('hk')`).
159
+ - **Повертає:** `void`.
160
+ - **Логіка:**
161
+ - Якщо `process.env.CI` truthy — функція виходить без дії (не реєструємо hook у CI).
162
+ - Інакше виконує `spawnSync(hkBin, ['install'], { stdio: 'inherit' })`.
163
+ - На `r.error` або ненульовий `r.status` виводить попередження через `console.warn` (не кидає!).
164
+ - **Side effects:** запис git pre-commit hook у `.git/hooks/pre-commit` (через `hk`), вивід у stdout/stderr.
165
+
166
+ ## Залежності
167
+
168
+ ### Стандартна бібліотека Node.js
169
+
170
+ - `node:child_process` — `spawnSync` для синхронного запуску `curl`, `tar`, `brew`, `scoop`, `rm`, `hk`.
171
+ - `node:fs` — `chmodSync`, `existsSync`, `mkdirSync`, `renameSync`.
172
+ - `node:os` — `homedir` для побудови шляху кешу.
173
+ - `node:path` — `join` для конструювання шляхів.
174
+ - `node:process` — `arch`, `env`, `platform`.
175
+
176
+ ### Внутрішні
177
+
178
+ - `../utils/resolve-cmd.mjs` → `resolveCmd(cmd)` — кросплатформне резолвлення абсолютного шляху до бінарника в PATH (Linux/macOS — like `which`, Windows — like `where`).
179
+
180
+ ### Зовнішні CLI (виконуються через `spawnSync`)
181
+
182
+ - `curl` — завантаження GitHub API і release-asset.
183
+ - `tar` — розпакування `.tar.gz` / `.tar.xz`.
184
+ - `rm` — м’яке видалення завантаженого архіву (необов’язково).
185
+ - `brew` — install на macOS.
186
+ - `scoop` — install на Windows.
187
+ - `hk` — реєстрація git pre-commit hook у `ensureHkInstall`.
188
+
189
+ ### Змінні середовища
190
+
191
+ - `N_CURSOR_NO_AUTO_INSTALL` — якщо встановлена, блокує авто-install і змушує `ensureTool` кидати помилку з install-hint.
192
+ - `CI` — якщо truthy, `ensureHkInstall` нічого не робить.
193
+ - `LOCALAPPDATA` (Windows) — використовується для побудови шляху кешу.
194
+
195
+ ## Потік виконання / Використання
196
+
197
+ ### Типовий сценарій `ensureTool('shellcheck')` на Linux x64
198
+
199
+ 1. Викликається `ensureTool('shellcheck')`.
200
+ 2. `resolveCmd('shellcheck')` — не знайдено в `PATH`.
201
+ 3. `getCacheDir()` повертає `/home/<user>/.cache/@nitra/cursor/bin`.
202
+ 4. Перевірка `/home/<user>/.cache/@nitra/cursor/bin/shellcheck` — не існує.
203
+ 5. `N_CURSOR_NO_AUTO_INSTALL` не виставлено → `autoInstall(...)`.
204
+ 6. На Linux диспетчер викликає `installFromGithub('shellcheck', entry, cacheDir)`:
205
+ - `fetchLatestVersion('koalaman/shellcheck', curl)` → наприклад `0.10.0`.
206
+ - asset name = `shellcheck-v0.10.0.linux.x86_64.tar.xz`.
207
+ - URL = `https://github.com/koalaman/shellcheck/releases/download/v0.10.0/<asset>`.
208
+ - `mkdirSync(cacheDir, { recursive: true })`.
209
+ - `curl -sSL -o <cacheDir>/<asset> <url>`.
210
+ - `tar -xJf <asset> -C <cacheDir>` (бо `.tar.xz`).
211
+ - `binFinder('0.10.0')` → `shellcheck-v0.10.0/shellcheck`.
212
+ - Перевірка `existsSync(<cacheDir>/shellcheck-v0.10.0/shellcheck)`.
213
+ - `rm <archivePath>`.
214
+ - Повертає `<cacheDir>/shellcheck-v0.10.0/shellcheck`.
215
+ 7. Викликач отримує абсолютний шлях і запускає `spawnSync(bin, [...args])`.
216
+
217
+ ### Сценарій блокування авто-installу
218
+
219
+ ```js
220
+ process.env.N_CURSOR_NO_AUTO_INSTALL = '1'
221
+ ensureTool('hk')
222
+ // throws Error із багаторядковим hint, наприклад на macOS:
223
+ // ❌ hk не знайдено в PATH і авто-встановлення відключено (N_CURSOR_NO_AUTO_INSTALL).
224
+ // Встанови:
225
+ // macOS: brew install hk
226
+ ```
227
+
228
+ ### Сценарій реєстрації git hook
229
+
230
+ ```js
231
+ import { ensureTool, ensureHkInstall } from './lib/ensure-tool.mjs'
232
+
233
+ const hkBin = ensureTool('hk') // PATH → кеш → brew/scoop/github
234
+ ensureHkInstall(hkBin) // git hook у .git/hooks/pre-commit
235
+ ```
236
+
237
+ У CI (`process.env.CI=true`) другий виклик стає no-op, що зручно для pipeline-ів, де hooks не потрібні.
238
+
239
+ ### Розширення реєстру
240
+
241
+ Щоб додати новий тул `foo`:
242
+
243
+ 1. Підбрати `brew`-формулу та `scoop`-пакет (або `null`, якщо відсутній).
244
+ 2. Знайти GitHub-репо релізів і визначити `archStyle` (`hk` / `conftest` / `actionlint`).
245
+ 3. Описати `asset(ver)` та, якщо потрібно, `binFinder(ver)` (коли бінарник лежить не в корені архіву).
246
+ 4. Виставити `archive: false` для прямого бінарника без архіву.
247
+ 5. Додати запис у `TOOLS`. Жодних змін у викликачах не потрібно — `ensureTool('foo')` запрацює одразу.
248
+
249
+ ### Гарантії та інваріанти
250
+
251
+ - **Ідемпотентність:** повторний виклик `ensureTool` повертає шлях за O(1) після першого install — спочатку PATH, потім кеш.
252
+ - **Hard-fail:** на будь-яку нерозв’язну помилку install кидається `Error` із описовим повідомленням; немає silent fallback на «обірваний» бінарник.
253
+ - **Кросплатформність:** єдиний публічний API для трьох ОС; OS-specific деталі інкапсульовані всередині модуля.
254
+ - **Безпека для CI:** `ensureHkInstall` ніколи не змінює git-репозиторій під CI, навіть якщо `hk` доступний.