@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,411 @@
1
+ # lint.mjs — лінт Kubernetes-маніфестів (kubeconform + kubescape)
2
+
3
+ ## Огляд
4
+
5
+ Модуль `lint.mjs` реалізує підкоманду `lint-k8s` CLI `n-cursor`. Він автоматично знаходить у репозиторії всі дерева Kubernetes-маніфестів за конвенційним сегментом шляху `k8s/`, а потім послідовно валідує їх двома інструментами:
6
+
7
+ 1. **`kubeconform`** — структурна валідація YAML-маніфестів проти OpenAPI-схем Kubernetes; підтримує CRD-схеми з каталогу Datree.
8
+ 2. **`kubescape`** — сканування на misconfiguration / compliance (NSA, MITRE, CIS тощо), з пріоритетом по `kustomize`-білдах (через `kubectl kustomize <dir>`), щоб коректно матчити `namespace`, `podSelector`, network-policies та overlay-структури.
9
+
10
+ Логіка реалізує канон правила `k8s.mdc`:
11
+
12
+ - шукаємо лише `*.yaml` (розширення `.yml` під `k8s` заборонене каноном);
13
+ - виключаємо `.github/` — це домен `ga.mdc`;
14
+ - враховуємо `.cursorignore` для виключення дерев;
15
+ - якщо `*.yaml`-файлів під `k8s` немає — виходимо з кодом `0` без запуску CLI;
16
+ - версія Kubernetes для `kubeconform` (`-kubernetes-version`) синхронізована з `YANNH_PIN` із `rules/k8s/fix.mjs` / `k8s.mdc`.
17
+
18
+ Канонічно публічна форма `runLintK8s` обгорнута в `runStandardLint` (із `scripts/lib/run-standard-lint.mjs`), який забезпечує:
19
+
20
+ - **серіалізацію** через `withLock('lint-k8s')` — щоб уникнути паралельних запусків важких CLI на одній машині (див. `scripts.mdc`, секція «Серіалізація важких CLI-команд»);
21
+ - **дедуплікацію** за станом git-дерева (повторний запуск без змін — no-op).
22
+
23
+ Модуль одночасно є і бібліотекою (експортує допоміжні функції для тестів та реюзу), і CLI-точкою входу (через `isRunAsCli(import.meta.url)`).
24
+
25
+ ## Експорти / API
26
+
27
+ | Експорт | Тип | Призначення |
28
+ | ------------------------------------ | -------------- | -------------------------------------------------------------------------------- |
29
+ | `pathHasK8sSegment(filePath, root?)` | function | Перевіряє, чи має шлях сегмент каталогу `k8s` (відносно `root`, якщо переданий). |
30
+ | `k8sRootFromFile(absFile)` | function | Підіймається вгору від файлу до найближчого предка з назвою `k8s`. |
31
+ | `findK8sRoots(root, ignorePaths?)` | async function | Повертає унікальні сортовані `…/k8s`-корені під `root`, що містять `*.yaml`. |
32
+ | `buildKubescapeExceptionsArgs(root)` | function | Формує `['--exceptions', <abs>]` якщо в корені є `.kubescape-exceptions.json`. |
33
+ | `findKustomizationDirs(dir)` | async function | Знаходить «точки входу» Kustomize (`kustomization.yaml` з `kind` ≠ `Component`). |
34
+ | `runLintK8s` | async function | Публічна CLI-форма: `runStandardLint(import.meta.dirname, runLintK8sSteps)`. |
35
+
36
+ CLI-режим: при прямому виконанні скрипта (`bun npm/rules/k8s/lint/lint.mjs`) встановлюється `process.exitCode = await runLintK8s()`.
37
+
38
+ ### Внутрішні (без `export`) функції
39
+
40
+ | Функція | Роль |
41
+ | ------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
42
+ | `runKubeconform(dirs)` | Запуск `kubeconform` для переданого списку каталогів. |
43
+ | `runKustomizeBuild(kubectlPath, dir)` | `kubectl kustomize <dir>` → `{ status, stdout: Buffer }`. |
44
+ | `runKubescapeManifest(kubescapePath, manifest, exceptionsArgs)` | Скан зібраного маніфесту через тимчасовий файл. |
45
+ | `scanRawK8sDir(kubescapePath, dir, exceptionsArgs)` | Сирий dir-скан kubescape для k8s-кореня без Kustomize. |
46
+ | `scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs, exceptionsArgs)` | Цикл `kustomize build` → `kubescape scan` по всіх `kdirs`. |
47
+ | `runKubescape(dirs, root)` | Оркестратор фази kubescape: Kustomize-білди або fallback на сирий dir-скан. |
48
+ | `runLintK8sSteps()` | Внутрішня послідовність (без локу): пошук дерев → kubeconform → kubescape. |
49
+
50
+ ### Константи модуля
51
+
52
+ | Константа | Значення | Призначення |
53
+ | ---------------------------- | ---------------------------- | ----------------------------------------------------------------- |
54
+ | `KUBESCAPE_EXCEPTIONS_FILE` | `.kubescape-exceptions.json` | Ім'я per-project файлу винятків для kubescape. |
55
+ | `KUSTOMIZATION_FILE` | `kustomization.yaml` | Канонічна назва маніфесту Kustomize (`.yml` заборонено). |
56
+ | `KUBESCAPE_MISSING_HINT` | рядок з URL | Підказка користувачу при відсутності kubescape у PATH. |
57
+ | `PATH_SEPARATOR_RE` | `/[/\\]/u` | Регексп розбиття шляху по `/` або `\`. |
58
+ | `YAML_EXT_RE` | `/\.yaml$/iu` | Регексп фільтра YAML-файлів. |
59
+ | `KUBERNETES_VERSION` | `1.33.9` | Версія схем Kubernetes для kubeconform (узгоджена з `YANNH_PIN`). |
60
+ | `DATREE_CRD_SCHEMA_LOCATION` | URL-шаблон | Додаткова локація схем для CRD-ресурсів (Datree CRDs-catalog). |
61
+
62
+ ## Функції
63
+
64
+ ### `pathHasK8sSegment(filePath, root)`
65
+
66
+ **Сигнатура:** `(filePath: string, root?: string) => boolean`
67
+
68
+ **Параметри:**
69
+
70
+ - `filePath` — абсолютний або відносний шлях до файлу.
71
+ - `root` (необов'язковий) — корінь репо для relativize.
72
+
73
+ **Повертає:** `true`, якщо серед компонентів шляху (відносно `root`, якщо передано) є сегмент `k8s`.
74
+
75
+ **Поведінка / нюанси:**
76
+
77
+ - Без `root` працює напряму з `filePath` — корисно для перевірки відносного шляху.
78
+ - З `root` обов'язково релятивізує: інакше, якщо сам корінь репо містить компонент `k8s` (наприклад `/Users/.../abie/k8s/`), функція повернула б `true` для **усіх** файлів проєкту, включно з `.github/workflows/*.yml`.
79
+ - Бекслеші нормалізуються в `/` через `replaceAll('\\', '/')`.
80
+ - Якщо після relativize рядок порожній — повертає `false`.
81
+
82
+ **Side effects:** немає (чиста функція).
83
+
84
+ ### `k8sRootFromFile(absFile)`
85
+
86
+ **Сигнатура:** `(absFile: string) => string | null`
87
+
88
+ **Параметри:**
89
+
90
+ - `absFile` — абсолютний шлях до YAML-файлу.
91
+
92
+ **Повертає:** абсолютний шлях до найближчого предка з ім'ям `k8s` або `null`, якщо такого сегмента в ланцюжку немає.
93
+
94
+ **Алгоритм:** ітеративно піднімається `dirname → parent` до 64 рівнів вгору; зупиняється, коли `basename(dir) === 'k8s'` або коли `dirname(dir) === dir` (корінь файлової системи).
95
+
96
+ **Side effects:** немає.
97
+
98
+ ### `findK8sRoots(root, ignorePaths)`
99
+
100
+ **Сигнатура:** `async (root: string, ignorePaths?: string[]) => Promise<string[]>`
101
+
102
+ **Параметри:**
103
+
104
+ - `root` — корінь репозиторію.
105
+ - `ignorePaths` (необов'язковий, default `[]`) — абсолютні шляхи каталогів, повністю виключених з обходу (передається у `walkDir`).
106
+
107
+ **Повертає:** Promise з масивом унікальних, відсортованих за `localeCompare` абсолютних шляхів до `…/k8s`-каталогів, у яких знайдено хоча б один `*.yaml`.
108
+
109
+ **Алгоритм:**
110
+
111
+ 1. Викликає `walkDir(root, visitor, ignorePaths)`.
112
+ 2. Для кожного відвіданого `p`:
113
+ - вираховує відносний шлях `rel` (нормалізує бекслеші);
114
+ - **пропускає** все, що під `.github/` (це домен `ga.mdc`);
115
+ - пропускає файли без сегмента `k8s` у шляху;
116
+ - пропускає не-`.yaml` файли;
117
+ - визначає `k8sRoot` через `k8sRootFromFile`; якщо знайдено — додає до `Set`.
118
+ 3. Конвертує `Set` у масив і сортує `localeCompare`.
119
+
120
+ **Side effects:** виконує файлову систему через `walkDir` (read-only обхід).
121
+
122
+ ### `buildKubescapeExceptionsArgs(root)`
123
+
124
+ **Сигнатура:** `(root: string) => string[]`
125
+
126
+ **Параметри:**
127
+
128
+ - `root` — корінь репозиторію (де шукається `.kubescape-exceptions.json`).
129
+
130
+ **Повертає:** `['--exceptions', '<абсолютний шлях>']` якщо файл існує, інакше `[]`.
131
+
132
+ **Side effects:** один синхронний `existsSync`.
133
+
134
+ ### `findKustomizationDirs(dir)`
135
+
136
+ **Сигнатура:** `async (dir: string) => Promise<string[]>`
137
+
138
+ **Параметри:**
139
+
140
+ - `dir` — абсолютний шлях до `…/k8s` (або іншого) каталогу.
141
+
142
+ **Повертає:** Promise з відсортованим списком абсолютних шляхів до каталогів, що містять **білдабельний** `kustomization.yaml` (тобто такий, що `kustomize build` буде здатний рендерити локально).
143
+
144
+ **Семантика «білдабельний»:**
145
+
146
+ - Файл називається саме `kustomization.yaml` (без `.yml` — заборонено каноном).
147
+ - YAML парситься без помилок (інакше — `continue`).
148
+ - Перший документ — об'єкт, у якого `kind !== 'Component'`. `kind: Kustomization` або відсутній `kind` (типово Kustomization) — приймаються; `kind: Component` пропускається, бо Components не білдяться окремо й підключаються через `components:` із overlay.
149
+
150
+ **Алгоритм:**
151
+
152
+ 1. `walkDir(dir, …)` збирає `candidates` — усі шляхи з `basename === 'kustomization.yaml'`.
153
+ 2. Послідовно по `candidates`:
154
+ - `readFile(p, 'utf8')` (помилка → skip);
155
+ - `parse(text)` через пакет `yaml` (помилка → skip);
156
+ - якщо `kind === 'Component'` → skip;
157
+ - інакше — `result.add(dirname(p))`.
158
+ 3. Сортування `localeCompare`.
159
+
160
+ **Side effects:** обхід ФС + читання вмісту YAML; парсинг без винятку назовні.
161
+
162
+ ### `runKubeconform(dirs)` _(внутрішня)_
163
+
164
+ **Сигнатура:** `(dirs: string[]) => number`
165
+
166
+ **Параметри:**
167
+
168
+ - `dirs` — абсолютні шляхи до `…/k8s`-каталогів.
169
+
170
+ **Повертає:** код виходу процесу `kubeconform` (`r.status ?? 1`); `127` якщо kubeconform відсутній (`ENOENT`).
171
+
172
+ **Прапори, що передаються `kubeconform`:**
173
+
174
+ - `-summary` — компактний підсумок наприкінці.
175
+ - `-kubernetes-version 1.33.9` — `KUBERNETES_VERSION`.
176
+ - `-schema-location default` — офіційні схеми Kubernetes.
177
+ - `-schema-location <DATREE_CRD_SCHEMA_LOCATION>` — реєстр CRD-схем Datree.
178
+ - `-ignore-missing-schemas` — пропустити CRD, для яких не знайдено схеми.
179
+ - `…dirs` — список цільових каталогів.
180
+
181
+ **Side effects:** `spawnSync` (stdio inherit) — друкує вихід kubeconform у термінал; на `ENOENT` пише інструкцію встановлення в `stderr`.
182
+
183
+ ### `runKustomizeBuild(kubectlPath, dir)` _(внутрішня)_
184
+
185
+ **Сигнатура:** `(kubectlPath: string, dir: string) => { status: number, stdout: Buffer }`
186
+
187
+ **Поведінка:** запускає `kubectl kustomize <dir>` з `stdio: ['ignore', 'pipe', 'inherit']` — stdout захоплює як буфер, stderr інхеритимо в термінал (щоб помилки збірки одразу були видимі). Використовується `kubectl kustomize` замість окремого бінарника `kustomize`, бо `kubectl` є штатним інструментом, а підкоманда `kustomize` локальна і не вимагає доступу до кластера.
188
+
189
+ **Повертає:** `{ status: r.status ?? 1, stdout: r.stdout ?? Buffer.alloc(0) }`.
190
+
191
+ **Side effects:** дочірній процес з inherit stderr.
192
+
193
+ ### `runKubescapeManifest(kubescapePath, manifest, exceptionsArgs)` _(внутрішня)_
194
+
195
+ **Сигнатура:** `(kubescapePath: string, manifest: Buffer, exceptionsArgs: string[]) => { status: number, enoent: boolean }`
196
+
197
+ **Поведінка:**
198
+
199
+ 1. Створює тимчасову директорію `mkdtempSync(join(tmpdir(), 'nitra-cursor-k8s-'))`.
200
+ 2. Пише `manifest` у файл `manifest.yaml` усередині неї.
201
+ 3. Запускає `kubescape scan <file> --severity-threshold high <...exceptionsArgs>` зі `stdio: 'inherit'`.
202
+ 4. У `finally` гарантовано видаляє створену директорію (`rmSync(dir, { recursive: true, force: true })`).
203
+
204
+ **Чому тимчасовий файл, а не stdin:** `kubescape scan` у v4.x **не читає stdin** — `-` як шлях не розпізнається (`no resources found to scan`), а прапорця `--input`/`--stdin` у CLI немає.
205
+
206
+ **Повертає:** `{ status, enoent }` — `enoent: true` якщо `r.error.code === 'ENOENT'`.
207
+
208
+ **Side effects:** створення/видалення тимчасової директорії, запис файлу, дочірній процес.
209
+
210
+ ### `scanRawK8sDir(kubescapePath, dir, exceptionsArgs)` _(внутрішня)_
211
+
212
+ **Сигнатура:** `(kubescapePath: string, dir: string, exceptionsArgs: string[]) => number`
213
+
214
+ **Поведінка:** сирий dir-скан kubescape для `…/k8s`-кореня без білдабельного `kustomization.yaml`. Друкує лог `run-k8s: kubescape scan <dir> (без kustomization — сирий dir-скан)` і запускає `kubescape scan <dir> --severity-threshold high <...exceptionsArgs>` зі `stdio: 'inherit'`.
215
+
216
+ **Повертає:** `0` при успіху, `127` якщо kubescape зник з PATH (`ENOENT`), інакше `r.status ?? 1`.
217
+
218
+ ### `scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs, exceptionsArgs)` _(внутрішня)_
219
+
220
+ **Сигнатура:** `(kubectlPath: string, kubescapePath: string, kdirs: string[], exceptionsArgs: string[]) => number`
221
+
222
+ **Поведінка:** для кожного `kdir` із `kdirs`:
223
+
224
+ 1. Друкує лог `run-k8s: kubectl kustomize <kdir> | kubescape scan <tmp>`.
225
+ 2. `runKustomizeBuild(kubectlPath, kdir)` — якщо `status !== 0`, негайно повертає цей `status`.
226
+ 3. `runKubescapeManifest(kubescapePath, build.stdout, exceptionsArgs)`:
227
+ - якщо `ks.enoent` — пише `KUBESCAPE_MISSING_HINT` у `stderr` і повертає `127`;
228
+ - якщо `ks.status !== 0` — повертає `ks.status`.
229
+
230
+ **Повертає:** `0` лише якщо всі каталоги пройшли; інакше — код першого невдалого процесу.
231
+
232
+ ### `runKubescape(dirs, root)` _(внутрішня)_
233
+
234
+ **Сигнатура:** `async (dirs: string[], root: string) => Promise<number>`
235
+
236
+ **Алгоритм:**
237
+
238
+ 1. `exceptionsArgs = buildKubescapeExceptionsArgs(root)`; якщо непорожній — лог про використання exceptions-файлу.
239
+ 2. `kubescapePath = ensureTool('kubescape')` — забезпечує наявність бінарника (інсталює, якщо налаштовано).
240
+ 3. `kubectlPath = null` (lazy resolve).
241
+ 4. Для кожного `d` з `dirs`:
242
+ - `kdirs = await findKustomizationDirs(d)`.
243
+ - Якщо `kdirs` порожній → fallback: `scanRawK8sDir(kubescapePath, d, exceptionsArgs)`; помилка → return.
244
+ - Інакше (перший раз): `kubectlPath = resolveCmd('kubectl')`. Якщо `null` → лог про відсутність kubectl + return `127`.
245
+ - `scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs, exceptionsArgs)`; помилка → return.
246
+ 5. Поверне `0`, якщо всі `dirs` пройшли.
247
+
248
+ **Чому через kustomize-білд, а не сирий скан:** збірка нормалізує `namespace` на workload-маніфестах і `base/networkpolicy.yaml` (через `base/kustomization.yaml` `namespace:`), що дає коректний матчинг `podSelector` у control'і C-0260 (`Missing network policy`) і дозволяє kubescape бачити дерево overlays/components зі справжніми ресурсами.
249
+
250
+ **Fallback:** якщо в `…/k8s` немає білдабельного `kustomization.yaml` — сирий dir-скан (не блокувати YAML-only проєкт без Kustomize).
251
+
252
+ ### `runLintK8sSteps()` _(внутрішня)_
253
+
254
+ **Сигнатура:** `async () => Promise<number>`
255
+
256
+ **Поведінка:**
257
+
258
+ 1. `root = process.cwd()`.
259
+ 2. `ignorePaths = await loadCursorIgnorePaths(root)` — підвантажує патерни з `.cursorignore`.
260
+ 3. `dirs = await findK8sRoots(root, ignorePaths)`.
261
+ 4. Якщо `dirs.length === 0` — лог `run-k8s: немає *.yaml під k8s — kubeconform і kubescape пропущено` і `return 0`.
262
+ 5. Лог `run-k8s: каталоги k8s (<n>):` + перелік кожного `d`.
263
+ 6. `kc = runKubeconform(dirs)`; якщо `!= 0` — `return kc`.
264
+ 7. `ks = await runKubescape(dirs, root)`; `return ks`.
265
+
266
+ **Повертає:** код виходу для `process.exitCode` (`0` — успіх або пропуск).
267
+
268
+ ### `runLintK8s` _(експортована CLI-форма)_
269
+
270
+ **Сигнатура:** `() => Promise<number>`
271
+
272
+ **Реалізація:** `runStandardLint(import.meta.dirname, runLintK8sSteps)` — обгортка з канону `scripts.mdc`, що додає:
273
+
274
+ - серіалізацію через `withLock('lint-k8s')` (блокування паралельних запусків);
275
+ - дедуплікацію за станом git-дерева (пропуск повторного запуску без змін).
276
+
277
+ Експорт використовується з `bin/n-cursor.js` як підкоманда `lint-k8s`.
278
+
279
+ ## Залежності
280
+
281
+ ### Node.js builtin
282
+
283
+ | Модуль | Що використовується |
284
+ | -------------------- | ---------------------------------------------------------------------------------------- |
285
+ | `node:child_process` | `spawnSync` для запуску `kubeconform`, `kubectl`, `kubescape`. |
286
+ | `node:fs` | `existsSync` (exceptions-файл), `mkdtempSync`, `rmSync`, `writeFileSync` (tmp-маніфест). |
287
+ | `node:fs/promises` | `readFile` для парсингу `kustomization.yaml`. |
288
+ | `node:os` | `tmpdir()` як база для тимчасової директорії. |
289
+ | `node:path` | `basename`, `dirname`, `join`, `relative`. |
290
+
291
+ ### Зовнішні npm-пакети
292
+
293
+ | Пакет | Використання |
294
+ | ----------------------------- | --------------------------------------------------------------- |
295
+ | `yaml` (named import `parse`) | Парсинг `kustomization.yaml` для відсіювання `kind: Component`. |
296
+
297
+ ### Внутрішні модулі репозиторію
298
+
299
+ | Шлях | Використання |
300
+ | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
301
+ | `../../../scripts/cli-entry.mjs` (`isRunAsCli`) | Детектор «запущено як CLI» (а не імпортовано). |
302
+ | `../../../scripts/lib/ensure-tool.mjs` (`ensureTool`) | Гарантує наявність CLI-інструмента (kubeconform, kubescape) у PATH (інсталює якщо налаштовано). |
303
+ | `../../../scripts/lib/load-cursor-config.mjs` (`loadCursorIgnorePaths`) | Зчитує `.cursorignore` і повертає абсолютні шляхи виключень. |
304
+ | `../../../scripts/utils/resolve-cmd.mjs` (`resolveCmd`) | Знаходить абсолютний шлях до бінарника (для `kubectl`, без installation hook). |
305
+ | `../../../scripts/utils/walkDir.mjs` (`walkDir`) | Рекурсивний обхід ФС із підтримкою ignore-патернів. |
306
+ | `../../../scripts/lib/run-standard-lint.mjs` (`runStandardLint`) | Стандартна обгортка серіалізації + дедупу для lint-команд. |
307
+
308
+ ### Зовнішні CLI-інструменти
309
+
310
+ - **`kubeconform`** — очікується в `PATH`, ставиться через Homebrew (macOS) або релізами з GitHub (`yannh/kubeconform`); у CI — крок установки з `k8s.mdc`.
311
+ - **`kubescape`** — очікується в `PATH`; інструкція встановлення — `https://github.com/kubescape/kubescape#readme`.
312
+ - **`kubectl`** — стандартний інструмент; для підкоманди `kubectl kustomize` доступ до кластера **не потрібен** (рендер локальний).
313
+
314
+ ## Потік виконання / Використання
315
+
316
+ ### CLI-режим (підкоманда `lint-k8s`)
317
+
318
+ ```
319
+ bun run lint-k8s # через root-script (workspace)
320
+ # або
321
+ n-cursor lint-k8s # CLI-обгортка
322
+ # або (наприкінці файлу — прямий запуск)
323
+ bun npm/rules/k8s/lint/lint.mjs
324
+ ```
325
+
326
+ Послідовність:
327
+
328
+ ```
329
+ runLintK8s
330
+ └── runStandardLint(dirname, runLintK8sSteps)
331
+ ├── withLock('lint-k8s') # серіалізація
332
+ ├── (дедуп за git-станом)
333
+ └── runLintK8sSteps
334
+ ├── loadCursorIgnorePaths(cwd)
335
+ ├── findK8sRoots(cwd, ignorePaths)
336
+ │ └── walkDir … pathHasK8sSegment … k8sRootFromFile
337
+ ├── [якщо dirs порожні] → return 0
338
+ ├── runKubeconform(dirs)
339
+ │ └── ensureTool('kubeconform') + spawnSync
340
+ └── runKubescape(dirs, root)
341
+ ├── buildKubescapeExceptionsArgs(root)
342
+ ├── ensureTool('kubescape')
343
+ └── for d of dirs:
344
+ ├── findKustomizationDirs(d)
345
+ ├── [empty] scanRawK8sDir
346
+ └── [else]
347
+ ├── resolveCmd('kubectl') [lazy, один раз]
348
+ └── scanKustomizeK8sDirs
349
+ └── for kdir:
350
+ ├── runKustomizeBuild(kubectl, kdir)
351
+ └── runKubescapeManifest(kubescape, stdout, exceptionsArgs)
352
+ ├── mkdtempSync + writeFileSync
353
+ ├── spawnSync('kubescape scan <tmp> --severity-threshold high …')
354
+ └── finally: rmSync(tmpdir, recursive, force)
355
+ ```
356
+
357
+ ### Імпортний режим (бібліотека)
358
+
359
+ ```js
360
+ import {
361
+ findK8sRoots,
362
+ findKustomizationDirs,
363
+ buildKubescapeExceptionsArgs,
364
+ k8sRootFromFile,
365
+ pathHasK8sSegment,
366
+ runLintK8s
367
+ } from './lint.mjs'
368
+
369
+ const roots = await findK8sRoots(process.cwd())
370
+ ```
371
+
372
+ Чисті помічники (`pathHasK8sSegment`, `k8sRootFromFile`, `buildKubescapeExceptionsArgs`) можна тестувати ізольовано (стек тестів — у сусідньому `tests/`).
373
+
374
+ ### Коди виходу (повертаються через `process.exitCode`)
375
+
376
+ | Код | Значення |
377
+ | ---------- | ----------------------------------------------------------------------------------------------------------- |
378
+ | `0` | Успіх або пропуск (немає `*.yaml` під `k8s`). |
379
+ | `127` | Відсутній зовнішній CLI у PATH: `kubeconform`, `kubescape` або `kubectl`. У stderr — підказка встановлення. |
380
+ | ≠ 0 (інше) | Код невдалого процесу (`kubeconform`, `kubectl kustomize` або `kubescape`). |
381
+ | `1` | Дефолт `r.status ?? 1`, якщо процес завершився без статусу. |
382
+
383
+ ### Файли конфігурації, що впливають на роботу
384
+
385
+ - **`.cursorignore`** у корені — патерни виключення для обходу ФС (через `loadCursorIgnorePaths`).
386
+ - **`.kubescape-exceptions.json`** у корені — точкові винятки control'ів для kubescape (підмішується через `--exceptions <file>`; приклад — виняток C-0012 на ConfigMap з публічним JWT-конфігом; див. `k8s.mdc`).
387
+ - **`kustomization.yaml`** у `…/k8s`-піддеревах — визначає, які каталоги білдяться через `kubectl kustomize`. Файл з `kind: Component` пропускається.
388
+
389
+ ### Конвенції каталогів
390
+
391
+ - Сегмент шляху `k8s/` — маркер дерева Kubernetes-маніфестів.
392
+ - Дозволено лише `.yaml` (не `.yml`) — це канон `k8s.mdc`.
393
+ - `.github/` повністю виключається з обходу (домен `ga.mdc`).
394
+ - Глибина пошуку `k8s`-предка у `k8sRootFromFile` — до 64 рівнів каталогів (захист від нескінченного циклу на дивних ФС).
395
+
396
+ ### Логи у stdout/stderr
397
+
398
+ - `run-k8s: немає *.yaml під k8s — kubeconform і kubescape пропущено` — рання гілка no-op.
399
+ - `run-k8s: каталоги k8s (<n>):` + перелік — стартовий лог із виявленими деревами.
400
+ - `run-k8s: kubescape exceptions — .kubescape-exceptions.json` — якщо exceptions-файл присутній.
401
+ - `run-k8s: kubectl kustomize <kdir> | kubescape scan <tmp>` — Kustomize-pipeline.
402
+ - `run-k8s: kubescape scan <dir> (без kustomization — сирий dir-скан)` — fallback.
403
+ - `stderr`: підказки встановлення kubeconform/kubescape/kubectl при `ENOENT`.
404
+
405
+ ### Side effects (підсумок)
406
+
407
+ - Читання файлової системи (обхід дерев, читання `kustomization.yaml`).
408
+ - Створення/видалення тимчасової директорії у `os.tmpdir()` (`nitra-cursor-k8s-*`) — лише на час сканування одного Kustomize-білду.
409
+ - Запис тимчасового `manifest.yaml` у цю директорію.
410
+ - Запуск дочірніх процесів (`kubeconform`, `kubectl`, `kubescape`) із наслідуванням stdio.
411
+ - Встановлення `process.exitCode` лише в CLI-режимі (`isRunAsCli(import.meta.url)`).
@@ -53,6 +53,8 @@ spec:
53
53
  port: 6379
54
54
  - protocol: TCP
55
55
  port: 8080
56
+ - protocol: TCP
57
+ port: 4222
56
58
  - protocol: TCP
57
59
  port: 4317
58
60
  - protocol: TCP
@@ -57,6 +57,8 @@ spec:
57
57
  port: 6379
58
58
  - protocol: TCP
59
59
  port: 8080
60
+ - protocol: TCP
61
+ port: 4222
60
62
  - protocol: TCP
61
63
  port: 4317
62
64
  - protocol: TCP
@@ -0,0 +1,124 @@
1
+ # `fix.mjs` — точка входу правила `nginx-default-tpl`
2
+
3
+ ## Огляд
4
+
5
+ Модуль `npm/rules/nginx-default-tpl/fix.mjs` є **точкою входу** (entry-point) для правила `nginx-default-tpl` у CLI-інструменті `@nitra/cursor`. Файл реалізує патерн «двох ролей» (dual-role module) для одного rule-файлу:
6
+
7
+ 1. **Library mode** — експортує функцію `run(ctx)`, яку викликає зовнішня orchestration-логіка (наприклад, batch-прогін усіх правил через `n-cursor fix`).
8
+ 2. **Standalone mode** — якщо файл запущено напряму через `bun rules/nginx-default-tpl/fix.mjs`, то відпрацьовує повний CLI-цикл (завантаження конфігурації, whitelist, summary) і завершує процес з відповідним exit-кодом для CI/IDE.
9
+
10
+ Сам файл **не містить специфічної логіки правила** — він є тонкою обгорткою (thin wrapper) над `runStandardRule`, яка делегує виконання стандартному пайплайну: `applies → JS-concerns → policy → mdc-refs`. Конкретна перевірена/виправлювана поведінка правила `nginx-default-tpl` описана в сусідніх артефактах теки (`check-*.mjs`, `.mdc`, конфіги), а цей файл забезпечує лише уніфікований інтерфейс запуску.
11
+
12
+ ## Експорти / API
13
+
14
+ | Експорт | Тип | Призначення |
15
+ | ------- | --------------------------------- | --------------------------------------------------------------------------------------------- |
16
+ | `run` | `function(ctx?): Promise<number>` | Library-API: виконує стандартний пайплайн правила; повертає exit-код (0 — OK, 1 — порушення). |
17
+
18
+ Файл також має **side-effect top-level await** на рівні модуля: при запуску як CLI він викликає `process.exit(await runRuleCli(...))`. Жодних інших іменованих експортів, 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` — спільний результат обходу файлової системи між кількома правилами), щоб не повторювати дорогі операції під час batch-прогону. Якщо `ctx` не передано — `runStandardRule` створює власне ізольоване оточення.
33
+ - **Повертає:** `Promise<number>`
34
+ - `0` — правило не знайшло порушень (або всі порушення були автоматично виправлені).
35
+ - `1` — правило знайшло порушення, які потрібно ескалувати у CI / IDE.
36
+ - **Side effects:**
37
+ - Запускає стандартний пайплайн `runStandardRule`, який залежно від реалізації може **читати файли** проєкту, **писати fix-патчі**, **друкувати summary** у stdout/stderr.
38
+ - Сам по собі `run` процес **не завершує** (`process.exit` викликається лише в standalone-гілці нижче).
39
+ - **Як обчислюється цільова тека:** перший аргумент `import.meta.dirname` — це абсолютний шлях до теки правила (`.../npm/rules/nginx-default-tpl/`). `runStandardRule` за цим шляхом резолвить supporting-артефакти: `meta.json`, `check-*.mjs`, MDC-документ, applies-конфіг тощо. Тому **категорично не можна** замінювати `import.meta.dirname` на CWD або жорстко закодований шлях — це зламає інкапсуляцію правила в монорепо.
40
+
41
+ ### Top-level CLI-блок (не функція, а імперативний side-effect)
42
+
43
+ ```js
44
+ if (isRunAsCli(import.meta.url)) {
45
+ process.exit(await runRuleCli(import.meta.dirname))
46
+ }
47
+ ```
48
+
49
+ - **Умова `isRunAsCli(import.meta.url)`** — детектор того, чи модуль є entry-point (`node fix.mjs` / `bun fix.mjs`), а не імпорт-залежністю. Реалізація утиліти стандартизована в `scripts/lib/run-rule-cli.mjs` (зазвичай порівнює `import.meta.url` з `process.argv[1]`).
50
+ - **`runRuleCli(import.meta.dirname)`** виконує **повний CLI-еквівалент** команди `npx @nitra/cursor fix nginx-default-tpl`:
51
+ - завантаження конфігурації проєкту,
52
+ - застосування whitelist / overrides,
53
+ - друк summary-репорта.
54
+ - **`process.exit(...)`** з результатом `runRuleCli` (number) повертає **exit-code в shell**, щоб CI/IDE могли інтерпретувати «червоний» / «зелений» прогін. Лінт-директиви `n/no-process-exit` і `unicorn/no-process-exit` свідомо вимкнені рядковим коментарем — це задокументоване виключення для **standalone entry-point**, де exit-code є частиною контракту.
55
+ - **Top-level `await`** дозволений у ESM-модулях (`.mjs`) і використовується для уникнення обгортки `(async () => { … })()`.
56
+
57
+ ## Залежності
58
+
59
+ ### Внутрішні (relative imports)
60
+
61
+ | Імпорт | Із | Що використовується |
62
+ | ----------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------- |
63
+ | `isRunAsCli` | `../../scripts/lib/run-rule-cli.mjs` | Детектор entry-point — повертає `true`, якщо поточний модуль викликаний напряму CLI-рантаймом. |
64
+ | `runRuleCli` | `../../scripts/lib/run-rule-cli.mjs` | Standalone-orchestration: повний CLI-цикл одного правила (config + whitelist + summary). |
65
+ | `runStandardRule` | `../../scripts/lib/run-standard-rule.mjs` | Library-pipeline правила: `applies → JS-concerns → policy → mdc-refs`. |
66
+
67
+ Шляхи відносні до `npm/rules/nginx-default-tpl/`; `../../scripts/lib/` резолвиться у `npm/scripts/lib/`.
68
+
69
+ ### Зовнішні / runtime
70
+
71
+ - **Bun / Node.js ESM** — для `import.meta.dirname` потрібен рантайм з підтримкою `import.meta.dirname` (Node ≥ 20.11 або сучасний Bun). На старіших Node цей геттер повертає `undefined`, що зламає резолв шляхів.
72
+ - **`process.exit`** — глобал Node/Bun рантайму; використовується лише у CLI-гілці.
73
+
74
+ ### Типи (JSDoc)
75
+
76
+ `@param {import('../../scripts/lib/run-standard-rule.mjs').RuleContext}` — тип контексту імпортується через JSDoc-тип-імпорт із того ж модуля `runStandardRule`. Це **не runtime-залежність** — лише підказка для TS/IDE.
77
+
78
+ ## Потік виконання / Використання
79
+
80
+ ### Сценарій 1 — Library mode (batch run)
81
+
82
+ ```js
83
+ // Викликається з npm/scripts/run-all-rules.mjs або інших orchestrators
84
+ import { run } from '@nitra/cursor/rules/nginx-default-tpl/fix.mjs'
85
+
86
+ const exitCode = await run({ walkCache })
87
+ if (exitCode !== 0) violatedRules.push('nginx-default-tpl')
88
+ ```
89
+
90
+ - Orchestrator передає спільний `walkCache` (чи інший контекст), щоб уникнути повторного обходу файлової системи між правилами.
91
+ - `run` повертає число — orchestrator сам вирішує, чи робити `process.exit` і коли.
92
+
93
+ ### Сценарій 2 — Standalone (debug / IDE / CI per-rule)
94
+
95
+ ```bash
96
+ bun npm/rules/nginx-default-tpl/fix.mjs
97
+ # еквівалент:
98
+ npx @nitra/cursor fix nginx-default-tpl
99
+ ```
100
+
101
+ - Bun завантажує файл, бачить `isRunAsCli(...) === true`, входить у CLI-гілку.
102
+ - `runRuleCli` сам викликає **той самий** `runStandardRule` під капотом (через `run`-експорт або прямо), але додає завантаження конфігу й whitelist-фільтри.
103
+ - Процес завершується з відповідним exit-кодом — `0` для CI «зелений», `1` для «червоний».
104
+
105
+ ### Логічна послідовність всередині `runStandardRule`
106
+
107
+ Згідно з JSDoc-коментарем у `run`, стандартний пайплайн послідовно виконує чотири фази:
108
+
109
+ 1. **`applies`** — визначення множини файлів, до яких застосовне правило (за glob-патернами з конфігу правила).
110
+ 2. **`JS-concerns`** — JS/TS-специфічні перевірки (синтаксис, AST, imports), якщо релевантно для правила.
111
+ 3. **`policy`** — застосування policy-логіки правила (для `nginx-default-tpl` — перевірка/виправлення дефолт-шаблону nginx-конфігурації).
112
+ 4. **`mdc-refs`** — крос-валідація з `.mdc`-документом правила (наявність refs, узгодженість прикладів тощо).
113
+
114
+ Деталі та точна семантика фаз залежать від реалізації `runStandardRule` й артефактів самого правила (`check-*.mjs`, `mdc`-файл).
115
+
116
+ ### Контракти й нюанси
117
+
118
+ - **Ідемпотентність:** не гарантована на рівні цього wrapper-а — залежить від `runStandardRule`. Зазвичай fix-режим записує патчі на диск і повторний виклик уже повертає `0`.
119
+ - **Concurrency:** глобальне правило монорепо забороняє паралельний запуск кількох правил/лінтерів одночасно (див. CLAUDE.md «Лінт і ESLint (без паралельних запусків)»). У library-режимі orchestrator повинен серіалізувати виклики.
120
+ - **Захищеність від помилок:** wrapper не має власних `try/catch` — будь-який throw з `runStandardRule` чи `runRuleCli` поширюється нагору. У standalone-режимі неперехоплений throw призведе до ненульового exit-коду рантайму (без явного `process.exit(1)`).
121
+
122
+ ## Rebuild Test
123
+
124
+ Документ описує **public-контракт** файлу (експорт `run`, його сигнатуру/повернення/побічні ефекти, CLI-гілку з `process.exit`, відносні залежності й послідовність фаз `applies → JS-concerns → policy → mdc-refs`). Цього достатньо для відновлення семантично-еквівалентного wrapper-файлу: створити `.mjs` модуль з ESM-імпортами трьох утиліт, експортувати `run(ctx)`, який повертає `runStandardRule(import.meta.dirname, ctx)`, і додати CLI-блок з `isRunAsCli` + `process.exit(await runRuleCli(import.meta.dirname))`. Сама бізнес-логіка правила `nginx-default-tpl` тут не міститься — вона делегована стандартному пайплайну й артефактам теки.