@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,207 @@
1
+ # utils_imports.mjs — перевірка кордону `utils/`-каталогів
2
+
3
+ ## Огляд
4
+
5
+ Модуль `npm/rules/js-lint/js/utils_imports.mjs` реалізує одну з перевірок правила `js-lint.mdc`: жоден файл усередині будь-якого каталогу з ім'ям `utils/` не має імпортувати щось за межами цього самого каталогу через відносні шляхи з префіксом `..`.
6
+
7
+ Філософія перевірки:
8
+
9
+ - Каталог `utils/` за конвенцією тримає **generic helpers** — функції без бізнес-логіки, без знання про домен, без залежностей від конфігів конкретного проєкту.
10
+ - Якщо файлу треба сусідній модуль (наприклад, `lib/foo.mjs` чи cross-rule helper) — він мусить переїхати у `lib/`, а не отримувати доступ через `../lib/foo.mjs`.
11
+ - Дозволені імпорт-джерела: `./X`, `./sub/X` (свій каталог чи глибше), bare-package (`oxc-parser`, `@scope/pkg`), Node-builtin (`node:fs`, `fs`).
12
+ - Заборонено будь-який `..`-шлях (`../X`, `../../X`, ...).
13
+
14
+ Перевірка проходить по всьому monorepo: знаходить package-roots, у кожному рекурсивно шукає каталоги `utils/`, з кожного збирає не-тестові джерела (без `tests/` і `__fixtures__/`), парсить їх через `oxc-parser`, витягає всі імпорти (статичні, динамічні, `require(...)`) і логує fail-репорт для кожного імпорту з `..`-префіксом.
15
+
16
+ Файл є точкою входу check-runner-а (CI-чи-локальний прогон): експортує одну async-функцію `check()`, яка повертає exit-code.
17
+
18
+ ## Експорти / API
19
+
20
+ | Експорт | Тип | Призначення |
21
+ | ------- | ----------------------- | ----------------------------------------------------------------------------------------------------- |
22
+ | `check` | `() => Promise<number>` | named export; запускає перевірку від `process.cwd()` і повертає `0` (OK) або `1` (знайдено порушення) |
23
+
24
+ Усе інше — приватні helpers модуля без `export`.
25
+
26
+ ## Функції
27
+
28
+ ### `isIgnored(dir, ignorePaths)`
29
+
30
+ Перевіряє, чи каталог входить у список ignore (точний збіг або префіксне співпадіння).
31
+
32
+ - **Сигнатура:** `function isIgnored(dir: string, ignorePaths: string[]): boolean`
33
+ - **Параметри:**
34
+ - `dir` — абсолютний posix-шлях каталогу.
35
+ - `ignorePaths` — масив абсолютних posix-шляхів, отриманих з `.n-cursor.json` через `loadCursorIgnorePaths`.
36
+ - **Повертає:** `true`, якщо `dir` дорівнює якомусь елементу `ignorePaths` або починається з нього + `/`; інакше `false`.
37
+ - **Side effects:** немає.
38
+
39
+ ### `findUtilsDirs(root, ignorePosix)`
40
+
41
+ Рекурсивно шукає всі каталоги з ім'ям `utils` під `root`.
42
+
43
+ - **Сигнатура:** `async function findUtilsDirs(root: string, ignorePosix: string[]): Promise<string[]>`
44
+ - **Параметри:**
45
+ - `root` — абсолютний шлях кореня обходу (зазвичай корінь package).
46
+ - `ignorePosix` — список абсолютних posix-шляхів, які пропускати.
47
+ - **Повертає:** масив абсолютних шляхів знайдених `utils/`-каталогів. Порядок — результат DFS у тому ж порядку, в якому повертає `readdir`.
48
+ - **Алгоритм:**
49
+ - Вкладена рекурсивна функція `walk(dir)` читає `readdir(dir, { withFileTypes: true })`.
50
+ - Помилка `readdir` (наприклад, нема прав чи каталог зник) проглинається через `try/catch` і дає ранній `return`.
51
+ - Для кожного запису-каталогу:
52
+ - якщо ім'я входить у `SKIP_DIR_NAMES` (`node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`, `__fixtures__`) — скіп;
53
+ - повний шлях конвертується у posix і перевіряється через `isIgnored` — скіп якщо так;
54
+ - якщо ім'я — рівно `utils`, додається у `found` і **не** заходить глибше (вкладені `utils/utils/` не очікуються; навіть якщо є — внутрішній `utils/` усе одно під самим `utils/` і його файли все одно пройдуть як файли зовнішнього `utils/`);
55
+ - інакше — рекурсивно `walk(full)`.
56
+ - **Side effects:** filesystem-чтення.
57
+
58
+ ### `collectUtilsSources(utilsDir)`
59
+
60
+ Збирає всі не-тестові source-файли під `utilsDir`.
61
+
62
+ - **Сигнатура:** `async function collectUtilsSources(utilsDir: string): Promise<string[]>`
63
+ - **Параметри:** `utilsDir` — абсолютний шлях каталогу `utils/`.
64
+ - **Повертає:** масив абсолютних шляхів файлів-джерел.
65
+ - **Фільтри:**
66
+ - Каталоги `tests/`, `__fixtures__/` і будь-що з `SKIP_DIR_NAMES` — пропускаються (тести легально мають імпорти `../X` до свого модуля).
67
+ - Файли мають матчити `JS_SOURCE_RE` (`.mjs`, `.mts`, `.cjs`, `.cts`, `.js`, `.ts`, `.jsx`, `.tsx`).
68
+ - Файли, що матчать `TEST_FILE_RE` (`*.test.*`), — виключаються.
69
+ - **Алгоритм:** аналогічний DFS через вкладену `walk(dir)` з тим самим проглинанням помилок `readdir`.
70
+ - **Side effects:** filesystem-чтення.
71
+
72
+ ### `extractImportSources(source, filePath)`
73
+
74
+ Витягає всі рядкові імпорт-source з тексту файлу.
75
+
76
+ - **Сигнатура:** `function extractImportSources(source: string, filePath: string): string[]`
77
+ - **Параметри:**
78
+ - `source` — текст файлу (UTF-8).
79
+ - `filePath` — шлях до файлу, потрібен для визначення мови парсингу через `langFromPath`.
80
+ - **Повертає:** масив рядків — значення source кожного імпорту в тому вигляді, в якому вони записані в коді (`'./foo'`, `'../bar'`, `'oxc-parser'`, ...).
81
+ - **Що збирає:**
82
+ - **Статичні імпорти** — з `parsed.module.staticImports[*].moduleRequest.value` (oxc-parser API).
83
+ - **Динамічні імпорти** (`import('...')`) — через `dynamicImportModule(node)` під час обходу AST.
84
+ - **CommonJS `require('...')`** — через `requireCallModule(node)`.
85
+ - **Обробка помилок парсингу:** `try/catch` на `parseSync` повертає порожній масив і **не** падає, бо синтаксична помилка — окремий концерн іншої перевірки; ця має запуститись чисто і не блокувати решту.
86
+ - **Side effects:** немає (виклик `parseSync` — CPU-only).
87
+
88
+ ### `check()` (named export)
89
+
90
+ Точка входу. Виконує всю перевірку від поточного робочого каталогу.
91
+
92
+ - **Сигнатура:** `export async function check(): Promise<number>`
93
+ - **Параметри:** немає; використовує `process.cwd()` як корінь monorepo.
94
+ - **Повертає:** `0` — порушень немає; `1` — є хоча б одне (реальний exit-code обчислює `reporter.getExitCode()`).
95
+ - **Покроковий алгоритм:**
96
+ 1. Створити `reporter` через `createCheckReporter()`.
97
+ 2. `root = process.cwd()`.
98
+ 3. Завантажити `ignorePaths` з `.n-cursor.json` (`loadCursorIgnorePaths`) і перевести у posix-варіант (`ignorePosix`).
99
+ 4. Отримати relative-paths package-roots monorepo через `getMonorepoPackageRootDirs(root)`.
100
+ 5. Для кожного package-root знайти всі `utils/`-каталоги (`findUtilsDirs`) і покласти у `Set` (`utilsDirSet`) для де-дуплікації.
101
+ 6. Якщо `utils/`-каталогів немає взагалі — `reporter.pass(...)` з повідомленням про пропуск і повернути exit-code.
102
+ 7. Інакше пройти кожен `utils/`-каталог:
103
+ - зібрати джерела (`collectUtilsSources`);
104
+ - для кожного файлу прочитати контент, витягти імпорти (`extractImportSources`);
105
+ - кожен import з префіксом `..` — `reporter.fail(...)` з relative-шляхом файлу та порушеним import-source, інкремент `violations`;
106
+ - `checkedFiles` рахує всі перевірені файли.
107
+ 8. Якщо `violations === 0` — `reporter.pass(...)` зі статистикою (кількість utils-каталогів і файлів).
108
+ 9. Повернути `reporter.getExitCode()`.
109
+ - **Side effects:**
110
+ - filesystem-чтення (рекурсивне сканування + `readFile`);
111
+ - запис у `reporter` (виводить рядки у stdout/stderr, залежно від реалізації);
112
+ - читає `process.cwd()`.
113
+
114
+ ## Залежності
115
+
116
+ ### Node-builtin
117
+
118
+ - `node:fs/promises` — `readdir`, `readFile`.
119
+ - `node:path` — `join`, `relative`, `sep`.
120
+
121
+ ### npm-пакет
122
+
123
+ - `oxc-parser` — `parseSync` для парсингу JS/TS у AST з достовірною підтримкою сучасних синтаксисів (zero-config).
124
+
125
+ ### Внутрішні модулі проєкту
126
+
127
+ - `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` — фабрика репортера; має методи `pass`, `fail`, `getExitCode`. Уніфікований API для всіх check-функцій.
128
+ - `../../../scripts/lib/load-cursor-config.mjs` → `loadCursorIgnorePaths` — читає `.n-cursor.json` (чи аналог) і повертає масив абсолютних шляхів, які треба пропустити.
129
+ - `../../../scripts/lib/workspaces.mjs` → `getMonorepoPackageRootDirs` — повертає relative-paths коренів пакетів у monorepo (включно з `.`-коренем, якщо це теж пакет).
130
+ - `../../../scripts/utils/ast-scan-utils.mjs`:
131
+ - `langFromPath(filePath)` — мапить розширення файлу у `lang`-параметр для `oxc-parser`.
132
+ - `walkAstWithAncestors(program, ancestors, visitor)` — обхід AST з трекінгом предків.
133
+ - `dynamicImportModule(node)` — повертає рядок source для `import('...')`, або `null`.
134
+ - `requireCallModule(node)` — повертає рядок source для `require('...')`, або `null`.
135
+
136
+ ### Константи модуля
137
+
138
+ | Ім'я | Значення | Призначення |
139
+ | -------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
140
+ | `JS_SOURCE_RE` | `/\.(?:[cm]?[jt]sx?)$/u` | матчить `.mjs`, `.mts`, `.cjs`, `.cts`, `.js`, `.ts`, `.jsx`, `.tsx` |
141
+ | `TEST_FILE_RE` | `/\.test\.[cm]?[jt]sx?$/u` | матчить `*.test.{js,ts,...}` для виключення тестів |
142
+ | `PARENT_RELATIVE_RE` | `/^\.\.(?:\/ | $)/u` | матчить `..` як цілий сегмент (`..` або `../*`); відсіює false-positive типу `..foo` |
143
+ | `SKIP_DIR_NAMES` | `Set(['node_modules', '.git', 'dist', 'coverage', '.turbo', '.next', '__fixtures__'])` | каталоги, які скіпаємо при обходах |
144
+
145
+ ## Потік виконання / Використання
146
+
147
+ ### Інтеграція в перевірочний рантайм
148
+
149
+ Модуль викликається check-runner-ом правила `js-lint.mdc` (зазвичай із `npm/rules/js-lint/js/`). Runner імпортує named export `check` і чекає на її resolved-значення як на process exit-code. Сам файл **не** має top-level executable коду — лише визначення; це дозволяє безпечно імпортувати його у тестах.
150
+
151
+ Типовий виклик (псевдокод):
152
+
153
+ ```mjs
154
+ import { check } from './utils_imports.mjs'
155
+ const exitCode = await check()
156
+ process.exit(exitCode)
157
+ ```
158
+
159
+ ### Сценарій "усе чисто"
160
+
161
+ 1. Runner запускається з кореня monorepo.
162
+ 2. `check()` знаходить `utils/` каталоги в усіх package-roots.
163
+ 3. Для кожного non-test source файлу витягає імпорти.
164
+ 4. Жоден імпорт не починається з `..`.
165
+ 5. `reporter.pass('utils-каталогів: N, перевірено M файлів — domain-bound імпортів немає (js-lint.mdc)')`.
166
+ 6. `getExitCode() → 0`.
167
+
168
+ ### Сценарій "є порушення"
169
+
170
+ 1. Знайдено файл `packages/foo/utils/helper.mjs`.
171
+ 2. У ньому є `import bar from '../lib/bar.mjs'`.
172
+ 3. `PARENT_RELATIVE_RE` матчить `../lib/bar.mjs`.
173
+ 4. `reporter.fail('packages/foo/utils/helper.mjs: заборонений імпорт \'../lib/bar.mjs\' — utils/-файли мають бути generic (js-lint.mdc)')`.
174
+ 5. `violations` інкрементується.
175
+ 6. По завершенню `getExitCode() → 1`.
176
+
177
+ ### Сценарій "немає utils/"
178
+
179
+ 1. У жодному package немає каталогу `utils/`.
180
+ 2. `utilsDirSet.size === 0`.
181
+ 3. `reporter.pass('utils-каталогів немає — перевірку пропущено (js-lint.mdc)')`.
182
+ 4. `getExitCode() → 0`.
183
+
184
+ ### Сценарій "файл із синтаксичною помилкою"
185
+
186
+ 1. `parseSync` кидає виключення.
187
+ 2. `extractImportSources` ловить його у `try/catch` і повертає `[]`.
188
+ 3. Цей файл не дає порушень. Проблему синтаксису ловить інша перевірка.
189
+
190
+ ### Сценарій "ignore-шлях"
191
+
192
+ 1. У `.n-cursor.json` зазначено абсолютний шлях, що покриває певний `utils/`-каталог.
193
+ 2. `findUtilsDirs` через `isIgnored` пропускає його ще на стадії обходу — той `utils/` навіть не потрапляє у `utilsDirSet`.
194
+
195
+ ### Особливості/edge cases
196
+
197
+ - **Тести**: каталог `tests/` усередині `utils/` ігнорується повністю; також ігноруються файли `*.test.{js,ts,...}` будь-де всередині `utils/`. Це свідомо: тести легально імпортують свій модуль через `../X`.
198
+ - **`__fixtures__/`**: ігнорується і в `findUtilsDirs`, і в `collectUtilsSources` — фікстури можуть бути будь-якими.
199
+ - **Bare-imports** (`oxc-parser`, `node:fs`): не відсіюються спеціально, бо просто не матчать `PARENT_RELATIVE_RE`.
200
+ - **Same-dir імпорти** (`./X`): дозволені автоматично з тієї ж причини.
201
+ - **POSIX-шляхи для ignore**: під Windows `sep` — `\\`, тому шляхи нормалізуються у `/`-формат перед порівнянням з ignore-конфігом.
202
+ - **De-duplication** через `Set`: якщо monorepo-структура повертає однаковий `utils/`-шлях двічі (наприклад, `.`-root і назва вкладеного пакета перетинаються), він обробиться лише раз.
203
+ - **Помилки `readdir`** глушаться — недоступний каталог просто пропускається без падіння всієї перевірки.
204
+
205
+ ### Залежність від конвенцій правила `js-lint.mdc`
206
+
207
+ Файл є технічною реалізацією одного з пунктів правила `js-lint.mdc`. Усі повідомлення `reporter.pass/fail` посилаються на `(js-lint.mdc)`, щоб користувач знав, де читати про сам принцип розділу `utils/` ↔ `lib/`.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Нормалізація й класифікація lint-findings (oxlint + eslint) на introduced
3
+ * (рядок у diff від HEAD) vs pre-existing (борг файлу) — беклог #6, варіант A.
4
+ *
5
+ * Формати: oxlint `--format=json` → `{ diagnostics:[{ filename, code, labels:[{span:{line}}] }] }`;
6
+ * eslint `--format=json` → `[{ filePath, messages:[{ ruleId, line, message }] }]`. Шляхи абсолютні.
7
+ */
8
+ import { isAbsolute, relative } from 'node:path'
9
+
10
+ import { isIntroducedLine } from '../../../scripts/lib/diff-added-lines.mjs'
11
+
12
+ /**
13
+ * @param {string} jsonText вивід `oxlint --format=json`
14
+ * @returns {{ file: string, line: number, rule: string, message: string, tool: string }[] | null} findings,
15
+ * або `null` якщо json непарсабельний (краш/обрізаний вивід інструмента — НЕ «чисто»)
16
+ */
17
+ export function parseOxlint(jsonText) {
18
+ let data
19
+ try {
20
+ data = JSON.parse(jsonText)
21
+ } catch {
22
+ return null
23
+ }
24
+ const diags = Array.isArray(data?.diagnostics) ? data.diagnostics : []
25
+ return diags
26
+ .filter(d => d?.filename)
27
+ .map(d => ({
28
+ file: d.filename,
29
+ line: d.labels?.[0]?.span?.line ?? 0,
30
+ rule: d.code ?? '',
31
+ message: d.message ?? '',
32
+ tool: 'oxlint'
33
+ }))
34
+ }
35
+
36
+ /**
37
+ * @param {string} jsonText вивід `eslint --format=json`
38
+ * @returns {{ file: string, line: number, rule: string, message: string, tool: string }[] | null} findings,
39
+ * або `null` якщо json непарсабельний (краш/обрізаний вивід інструмента — НЕ «чисто»)
40
+ */
41
+ export function parseEslint(jsonText) {
42
+ let data
43
+ try {
44
+ data = JSON.parse(jsonText)
45
+ } catch {
46
+ return null
47
+ }
48
+ const results = Array.isArray(data) ? data : []
49
+ const out = []
50
+ for (const r of results) {
51
+ for (const m of r?.messages ?? []) {
52
+ out.push({
53
+ file: r.filePath,
54
+ line: m.line ?? 0,
55
+ rule: m.ruleId ?? '(syntax)',
56
+ message: m.message ?? '',
57
+ tool: 'eslint'
58
+ })
59
+ }
60
+ }
61
+ return out.filter(f => f.file)
62
+ }
63
+
64
+ /**
65
+ * Розділяє findings на introduced / pre-existing за доданими рядками.
66
+ * @param {{ file: string, line: number }[]} findings нормалізовані findings
67
+ * @param {Map<string, Set<number> | string>} addedLines з `addedLinesByFile`
68
+ * @param {string} [cwd] корінь (для нормалізації абсолютних шляхів у relative)
69
+ * @returns {{ introduced: object[], preExisting: object[] }} класифікація
70
+ */
71
+ export function classifyFindings(findings, addedLines, cwd = process.cwd()) {
72
+ const introduced = []
73
+ const preExisting = []
74
+ for (const f of findings) {
75
+ const rel = isAbsolute(f.file) ? relative(cwd, f.file) : f.file
76
+ if (isIntroducedLine(addedLines, rel, f.line)) introduced.push(f)
77
+ else preExisting.push(f)
78
+ }
79
+ return { introduced, preExisting }
80
+ }
81
+
82
+ /**
83
+ * Рядок одного finding: `<rel>:<line> <rule> <message>`.
84
+ * @param {{ file: string, line: number, rule: string, message: string }} f finding
85
+ * @param {string} cwd корінь
86
+ * @returns {string} рядок
87
+ */
88
+ function formatFinding(f, cwd) {
89
+ const rel = isAbsolute(f.file) ? relative(cwd, f.file) : f.file
90
+ return ` ${rel}:${f.line} ${f.rule} ${f.message}`
91
+ }
92
+
93
+ /**
94
+ * Згрупований звіт: 🆕 introduced (виправ) + 🗄 pre-existing (борг файлу).
95
+ * @param {{ introduced: object[], preExisting: object[] }} classified результат `classifyFindings`
96
+ * @param {string} [cwd] корінь
97
+ * @returns {string} текст звіту
98
+ */
99
+ export function renderFindings({ introduced, preExisting }, cwd = process.cwd()) {
100
+ const lines = []
101
+ if (introduced.length > 0) {
102
+ lines.push(` 🆕 introduced (${introduced.length}) — внесено цією зміною, виправ:`)
103
+ for (const f of introduced) lines.push(formatFinding(f, cwd))
104
+ }
105
+ if (preExisting.length > 0) {
106
+ lines.push(` 🗄 pre-existing (${preExisting.length}) — борг файлу, не з цієї зміни:`)
107
+ for (const f of preExisting) lines.push(formatFinding(f, cwd))
108
+ }
109
+ return lines.join('\n')
110
+ }
@@ -2,12 +2,17 @@
2
2
  * Quick-крок lint правила js-lint: oxlint + eslint (з автофіксом).
3
3
  *
4
4
  * Викликається lint-оркестратором (`n-cursor lint` / `lint-ci`):
5
- * - `files` = масив змінених файлів (quick) → лінтимо лише js-подібні з них;
6
- * - `files` = undefined (ci) лінтимо весь проєкт.
5
+ * - `files` = масив змінених файлів (quick) → лінтимо лише js-подібні з них і
6
+ * КЛАСИФІКУЄМО лишені findings на introduced (рядок у diff від HEAD) vs
7
+ * pre-existing (борг файлу) — беклог #6, варіант A (видимість; блокування без змін);
8
+ * - `files` = undefined (ci) → лінтимо весь проєкт (стрімінг, без класифікації).
7
9
  * Крос-файлові jscpd/knip — окреме правило js-lint-ci (фаза ci).
8
10
  */
9
11
  import { spawnSync } from 'node:child_process'
10
12
 
13
+ import { addedLinesByFile } from '../../../scripts/lib/diff-added-lines.mjs'
14
+ import { classifyFindings, parseEslint, parseOxlint, renderFindings } from './lint-findings.mjs'
15
+
11
16
  const JS_EXT_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx|vue)$/u
12
17
 
13
18
  /**
@@ -20,15 +25,88 @@ export function filterJsFiles(files) {
20
25
  }
21
26
 
22
27
  /**
23
- * @param {string[]} args аргументи інструмента (бінар через bunx)
28
+ * Запуск інструмента (через bunx) зі стрімінгом у термінал.
29
+ * @param {string[]} args аргументи
24
30
  * @param {string} cwd корінь
25
31
  * @returns {number} exit code
26
32
  */
27
- function run(args, cwd) {
33
+ function runInherit(args, cwd) {
28
34
  const r = spawnSync('bunx', args, { cwd, stdio: 'inherit' })
29
35
  return typeof r.status === 'number' ? r.status : 1
30
36
  }
31
37
 
38
+ /**
39
+ * Авто-фікс-пас: застосовує `--fix`, stdout приглушено (findings перерендеримо
40
+ * класифіковано), stderr — назовні (краші інструмента видимі).
41
+ * @param {string[]} args аргументи
42
+ * @param {string} cwd корінь
43
+ * @returns {number} exit code
44
+ */
45
+ function runFix(args, cwd) {
46
+ const r = spawnSync('bunx', args, { cwd, stdio: ['ignore', 'ignore', 'inherit'] })
47
+ return typeof r.status === 'number' ? r.status : 1
48
+ }
49
+
50
+ /** Запас буфера для json-виводу лінтерів (великі changeset-и > дефолтного ~1MB). */
51
+ const JSON_MAX_BUFFER = 64 * 1024 * 1024
52
+
53
+ /**
54
+ * Репорт-пас: `--format=json`. Повертає exit-код і stdout (щоб відрізнити
55
+ * «чисто/є-порушення» від краху інструмента).
56
+ * @param {string[]} args аргументи
57
+ * @param {string} cwd корінь
58
+ * @returns {{ status: number, stdout: string }} результат
59
+ */
60
+ function runJson(args, cwd) {
61
+ const r = spawnSync('bunx', args, { cwd, encoding: 'utf8', maxBuffer: JSON_MAX_BUFFER })
62
+ return { status: typeof r.status === 'number' ? r.status : 1, stdout: r.stdout ?? '' }
63
+ }
64
+
65
+ /**
66
+ * Full-режим (ci): лінт усього проєкту зі стрімінгом і fail-fast (без класифікації).
67
+ * @param {string} cwd корінь
68
+ * @returns {number} exit code
69
+ */
70
+ function lintFullProject(cwd) {
71
+ const ox = runInherit(['oxlint', '--fix'], cwd)
72
+ if (ox !== 0) return ox
73
+ return runInherit(['eslint', '--fix', '.'], cwd)
74
+ }
75
+
76
+ /**
77
+ * Quick-режим: авто-фікс змінених файлів, тоді класифікація лишених findings
78
+ * на introduced / pre-existing (беклог #6/A). Блокування на будь-якому finding.
79
+ * @param {string[]} js js-подібні змінені файли
80
+ * @param {string} cwd корінь
81
+ * @returns {number} exit code (0 — чисто; 1 — лишились findings)
82
+ */
83
+ function lintChangedClassified(js, cwd) {
84
+ // Фікс-пас обох інструментів (послідовно; обидва — щоб репорт показав повну картину).
85
+ runFix(['oxlint', '--fix', ...js], cwd)
86
+ runFix(['eslint', '--fix', ...js], cwd)
87
+
88
+ // Репорт-пас по ФІНАЛЬНОМУ (пост-фікс) файлу — рядки findings і diff узгоджені.
89
+ const oxRes = runJson(['oxlint', '--format=json', ...js], cwd)
90
+ const esRes = runJson(['eslint', '--format=json', ...js], cwd)
91
+ const ox = parseOxlint(oxRes.stdout)
92
+ const es = parseEslint(esRes.stdout)
93
+
94
+ // Краш інструмента (ненульовий exit + непарсабельний json) НЕ можна тихо пропустити
95
+ // як «чисто» — це регресія проти старого fail-fast. Фейлимо явно.
96
+ if ((ox === null && oxRes.status !== 0) || (es === null && esRes.status !== 0)) {
97
+ process.stderr.write('❌ js-lint: інструмент завершився з помилкою (не lint-порушення) — json не розпарсено\n')
98
+ return 1
99
+ }
100
+
101
+ const findings = [...(ox ?? []), ...(es ?? [])]
102
+ if (findings.length === 0) return 0
103
+
104
+ const classified = classifyFindings(findings, addedLinesByFile(js, cwd), cwd)
105
+ const header = `❌ js-lint: ${findings.length} порушень (introduced ${classified.introduced.length}, pre-existing ${classified.preExisting.length})`
106
+ process.stdout.write(`${header}\n${renderFindings(classified, cwd)}\n`)
107
+ return 1
108
+ }
109
+
32
110
  /**
33
111
  * Запускає oxlint+eslint з автофіксом.
34
112
  * @param {string[] | undefined} files quick: лише ці файли; undefined: весь проєкт
@@ -36,17 +114,10 @@ function run(args, cwd) {
36
114
  * @returns {Promise<number>} 0 — OK, ≠0 — порушення
37
115
  */
38
116
  export function lint(files, cwd = process.cwd()) {
39
- let oxArgs = ['oxlint', '--fix']
40
- let esArgs = ['eslint', '--fix']
41
117
  if (files === undefined) {
42
- esArgs.push('.')
43
- } else {
44
- const js = filterJsFiles(files)
45
- if (js.length === 0) return Promise.resolve(0)
46
- oxArgs = ['oxlint', '--fix', ...js]
47
- esArgs = ['eslint', '--fix', ...js]
118
+ return Promise.resolve(lintFullProject(cwd))
48
119
  }
49
- const ox = run(oxArgs, cwd)
50
- if (ox !== 0) return Promise.resolve(ox)
51
- return Promise.resolve(run(esArgs, cwd))
120
+ const js = filterJsFiles(files)
121
+ if (js.length === 0) return Promise.resolve(0)
122
+ return Promise.resolve(lintChangedClassified(js, cwd))
52
123
  }
@@ -0,0 +1,154 @@
1
+ # fix.mjs — точка входу правила `js-lint-ci`
2
+
3
+ ## Огляд
4
+
5
+ Файл `npm/rules/js-lint-ci/fix.mjs` є **точкою входу** (entry-point) для правила з ідентифікатором `js-lint-ci` у системі `@nitra/cursor`. Він реалізує **подвійну роль**, типову для всіх стандартних правил каталогу `npm/rules/*`:
6
+
7
+ 1. **Library mode** — експортує функцію `run(ctx)`, яку викликає зовнішній CLI-оркестратор (`npx @nitra/cursor fix js-lint-ci` або агрегатор `npx @nitra/cursor fix` для усіх правил).
8
+ 2. **Standalone mode** — якщо файл запущено напряму через `bun rules/js-lint-ci/fix.mjs`, виконується повний еквівалент CLI-команди `npx @nitra/cursor fix js-lint-ci` (з завантаженням конфігу, whitelist, summary та exit-кодом для CI).
9
+
10
+ Сам файл не містить жодної доменно-специфічної логіки правила — вся механіка делегована у спільну бібліотечну функцію `runStandardRule`, яка реалізує стандартний конвеєр стандартного правила:
11
+
12
+ ```
13
+ applies → JS-concerns → policy → mdc-refs
14
+ ```
15
+
16
+ Тобто: спершу перевіряється, чи правило застосовне до файлу (`applies`), потім виконуються специфічні для JS перевірки (`JS-concerns`), далі — політика (`policy`) та робота з посиланнями `.mdc` (`mdc-refs`).
17
+
18
+ ## Експорти / API
19
+
20
+ | Експорт | Тип | Призначення |
21
+ | ------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
22
+ | `run` | `function (ctx?) => Promise<number>` | Library-точка входу правила. Запускає стандартний конвеєр правила у каталозі правила, повертає exit-код (0 — OK, 1 — порушення). |
23
+
24
+ Інших іменованих чи default-експортів файл не містить.
25
+
26
+ ### Сигнатура `run`
27
+
28
+ ```js
29
+ export function run(ctx)
30
+ ```
31
+
32
+ #### Параметри
33
+
34
+ - `ctx` (необов’язковий) — об’єкт контексту прогону типу `RuleContext` (визначення в модулі `../../scripts/lib/run-standard-rule.mjs`). Через цей контекст передаються спільні для одного запуску артефакти, такі як кеш обходу файлової системи (`walkCache`) тощо. Якщо контекст відсутній, `runStandardRule` створює власний.
35
+
36
+ #### Повертає
37
+
38
+ - `Promise<number>` — асинхронно резолвиться у **exit-код**:
39
+ - `0` — порушень немає, правило пройшло успішно;
40
+ - `1` — виявлено порушення (правило завершилось зі статусом FAIL).
41
+
42
+ #### Side effects
43
+
44
+ Сам по собі `run` не має прямих побічних ефектів, але через делегування у `runStandardRule` ініціює:
45
+
46
+ - читання конфігураційних файлів правила (зокрема `meta.json`, файлів `applies/*`, `policy/*`, `mdc-refs/*` тощо — згідно конвенції стандартного правила);
47
+ - обхід файлів проєкту відповідно до `applies`-патернів;
48
+ - виконання JS-перевірок (наприклад, запуск ESLint у відповідному режимі) та політики;
49
+ - запис до stdout/stderr діагностики, summary та результатів;
50
+ - може кешувати/читати з `walkCache` всередині `ctx`.
51
+
52
+ ## Функції
53
+
54
+ ### `run(ctx)`
55
+
56
+ ```js
57
+ export function run(ctx) {
58
+ return runStandardRule(import.meta.dirname, ctx)
59
+ }
60
+ ```
61
+
62
+ - **Сигнатура:** `run(ctx?: RuleContext): Promise<number>`.
63
+ - **Параметри:**
64
+ - `ctx` — опційний контекст прогону (див. вище).
65
+ - **Повертає:** `Promise<number>` — exit-код прогону правила (0/1).
66
+ - **Реалізація:** єдиний виклик `runStandardRule(import.meta.dirname, ctx)`. Перший аргумент `import.meta.dirname` — абсолютний шлях до каталогу, у якому розташований цей файл (`.../npm/rules/js-lint-ci/`). Таким чином `runStandardRule` дізнається, **яке саме правило** виконувати: всі його артефакти (`meta.json`, `applies`, `policy`, `mdc-refs` тощо) лежать поряд з `fix.mjs`.
67
+ - **Side effects:** делеговані у `runStandardRule` (див. секцію _Експорти / API_ вище).
68
+
69
+ ### Standalone-блок (top-level `if`)
70
+
71
+ ```js
72
+ if (isRunAsCli(import.meta.url)) {
73
+ process.exit(await runRuleCli(import.meta.dirname))
74
+ }
75
+ ```
76
+
77
+ - Це не функція, а **умовний top-level statement**, що виконується лише коли модуль завантажено як головний (а не як імпортований модуль).
78
+ - **Умова:** `isRunAsCli(import.meta.url)` — повертає `true`, якщо поточний модуль є точкою входу процесу (тобто запущено `bun rules/js-lint-ci/fix.mjs`, а не `import` з іншого файлу).
79
+ - **Дія:** виконує `await runRuleCli(import.meta.dirname)` — повний CLI-сценарій (config-loading, whitelist, summary), а потім завершує процес `process.exit(<exit-code>)` з тим самим кодом, що повернув `runRuleCli` (0 або 1) — це критично для CI/IDE, які орієнтуються на код виходу.
80
+ - **Side effects:** завершення процесу (`process.exit`), вся I/O `runRuleCli`. Виклики `process.exit` тут спеціально дозволені директивою:
81
+ ```js
82
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
83
+ ```
84
+
85
+ ## Залежності
86
+
87
+ ### Внутрішні (модулі того ж пакета)
88
+
89
+ - `../../scripts/lib/run-rule-cli.mjs` — імпортуються:
90
+ - `isRunAsCli(metaUrl)` — детектор того, що поточний ESM-модуль запущено як CLI-entry;
91
+ - `runRuleCli(dirname)` — full standalone CLI-runner правила, що дзеркалить поведінку `npx @nitra/cursor fix <id>` (config-loading + whitelist + summary).
92
+ - `../../scripts/lib/run-standard-rule.mjs` — імпортується:
93
+ - `runStandardRule(dirname, ctx?)` — стандартний бібліотечний конвеєр правила (`applies → JS-concerns → policy → mdc-refs`).
94
+
95
+ ### Зовнішні (поза репозиторієм)
96
+
97
+ - Стандартні Node/Bun-глобали: `process` (для `process.exit`), `import.meta.dirname`, `import.meta.url`.
98
+ - Прямих залежностей від npm-пакетів у самому файлі немає (вони — транзитивні через `run-rule-cli.mjs` / `run-standard-rule.mjs`).
99
+
100
+ ### Типи (через JSDoc)
101
+
102
+ - `import('../../scripts/lib/run-standard-rule.mjs').RuleContext` — імпорт типу для параметра `ctx`.
103
+
104
+ ## Потік виконання / Використання
105
+
106
+ ### Сценарій 1. Library mode (виклик з оркестратора)
107
+
108
+ Виконується, коли інший модуль імпортує цей файл та викликає `run(ctx)`:
109
+
110
+ ```js
111
+ import { run } from '@nitra/cursor/rules/js-lint-ci/fix.mjs'
112
+
113
+ const exitCode = await run(ctx)
114
+ if (exitCode !== 0) {
115
+ // правило виявило порушення
116
+ }
117
+ ```
118
+
119
+ Послідовність:
120
+
121
+ 1. Оркестратор передає (опційно) спільний `ctx` (наприклад, з `walkCache`).
122
+ 2. `run` викликає `runStandardRule(import.meta.dirname, ctx)`.
123
+ 3. `runStandardRule` зчитує конфіг правила з каталогу `npm/rules/js-lint-ci/` і послідовно проганяє ланцюжок:
124
+ - `applies` — визначає список файлів, до яких застосовне правило;
125
+ - `JS-concerns` — JS-специфічні перевірки;
126
+ - `policy` — політика правила;
127
+ - `mdc-refs` — звірення з посиланнями `.mdc`.
128
+ 4. Повертається `Promise<number>` з exit-кодом.
129
+
130
+ У цьому сценарії `process.exit` **не** викликається — exit-код повертається у викликача, який сам вирішує, що з ним робити (наприклад, агрегує з кодами інших правил).
131
+
132
+ ### Сценарій 2. Standalone mode (прямий запуск)
133
+
134
+ Виконується командою:
135
+
136
+ ```sh
137
+ bun npm/rules/js-lint-ci/fix.mjs
138
+ ```
139
+
140
+ Послідовність:
141
+
142
+ 1. ESM-модуль завантажується як головний, `import.meta.url` дорівнює URL процесу.
143
+ 2. Виконується top-level `if (isRunAsCli(import.meta.url))` — умова істинна.
144
+ 3. Запускається `await runRuleCli(import.meta.dirname)` — повний CLI-сценарій (config, whitelist, summary), еквівалентний `npx @nitra/cursor fix js-lint-ci`.
145
+ 4. `process.exit(<code>)` завершує процес з отриманим exit-кодом (0/1) — для коректної інтеграції з CI та IDE.
146
+
147
+ > Експортована функція `run` у цьому сценарії **не** викликається напряму — `runRuleCli` сам інкапсулює всю CLI-логіку, включно з потрібними викликами `runStandardRule` всередині.
148
+
149
+ ### Чому існують обидві ролі
150
+
151
+ - **Library `run`** потрібна, щоб агрегатор (`npx @nitra/cursor fix` без id або фоновий runner) міг прогнати багато правил у спільному контексті — з кешуванням обходу ФС, єдиним підсумком тощо, без породження окремого процесу на кожне правило.
152
+ - **Standalone-блок** потрібен, щоб правило було самодостатнім: розробник може запустити його в IDE «як файл» і отримати повноцінний CLI-сценарій з коректним exit-кодом. Це особливо зручно для дебагу окремого правила без переходу через головний CLI пакета.
153
+
154
+ Файл свідомо тримається **мінімальним**: він є лише адаптером (entry-point), уся доменна логіка — у бібліотечних функціях `runStandardRule` та `runRuleCli`. Це уніфікує всі правила з каталогу `npm/rules/*` — їхні `fix.mjs` мають однакову структуру і відрізняються лише шляхом каталогу, у якому лежать (через `import.meta.dirname`).