@nitra/cursor 3.22.0 → 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 (228) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/CHANGELOG.md +31 -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-ci/docs/fix.md +154 -0
  74. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  75. package/rules/js-mssql/docs/fix.md +128 -0
  76. package/rules/js-mssql/js/docs/deps.md +263 -0
  77. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  78. package/rules/js-run/docs/fix.md +144 -0
  79. package/rules/js-run/js/docs/runtime.md +388 -0
  80. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  81. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  82. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  83. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  84. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  85. package/rules/k8s/docs/fix.md +129 -0
  86. package/rules/k8s/js/docs/manifests.md +344 -0
  87. package/rules/k8s/js/manifests.mjs +6 -2
  88. package/rules/k8s/k8s.mdc +4 -2
  89. package/rules/k8s/lint/docs/lint.md +411 -0
  90. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  91. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  92. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  93. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  94. package/rules/npm-module/docs/fix.md +98 -0
  95. package/rules/npm-module/js/docs/package_structure.md +274 -0
  96. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  97. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  98. package/rules/php/docs/fix.md +107 -0
  99. package/rules/php/js/docs/tooling.md +152 -0
  100. package/rules/php/lint/docs/lint.md +215 -0
  101. package/rules/python/docs/fix.md +163 -0
  102. package/rules/python/js/docs/applies.md +108 -0
  103. package/rules/python/js/docs/tooling.md +153 -0
  104. package/rules/python/lint/docs/lint.md +322 -0
  105. package/rules/rego/docs/fix.md +121 -0
  106. package/rules/rego/js/docs/applies.md +174 -0
  107. package/rules/rego/js/docs/lint.md +118 -0
  108. package/rules/rego/lint/docs/lint.md +204 -0
  109. package/rules/release/docs/change.md +185 -0
  110. package/rules/release/docs/fix.md +119 -0
  111. package/rules/release/docs/release.md +222 -0
  112. package/rules/release/lib/docs/aggregate.md +246 -0
  113. package/rules/release/lib/docs/change-file.md +200 -0
  114. package/rules/release/lib/docs/fallback.md +203 -0
  115. package/rules/rust/docs/fix.md +129 -0
  116. package/rules/rust/js/docs/applies.md +140 -0
  117. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  118. package/rules/security/docs/fix.md +86 -0
  119. package/rules/security/js/docs/lint.md +171 -0
  120. package/rules/security/js/docs/sample_secret.md +190 -0
  121. package/rules/security/js/docs/trufflehog.md +137 -0
  122. package/rules/security/js/lint.mjs +9 -1
  123. package/rules/style-lint/docs/fix.md +155 -0
  124. package/rules/style-lint/js/docs/lint.md +184 -0
  125. package/rules/style-lint/js/docs/tooling.md +194 -0
  126. package/rules/tauri/docs/fix.md +158 -0
  127. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  128. package/rules/tauri/js/docs/tooling.md +228 -0
  129. package/rules/test/coverage/coverage.mjs +15 -3
  130. package/rules/test/docs/fix.md +132 -0
  131. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  132. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  134. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  135. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  136. package/rules/test/js/docs/location.md +136 -0
  137. package/rules/test/js/docs/no-process-chdir.md +160 -0
  138. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  139. package/rules/test/js/docs/stryker_config.md +152 -0
  140. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  141. package/rules/text/docs/fix.md +118 -0
  142. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  143. package/rules/text/js/docs/formatting.md +256 -0
  144. package/rules/text/js/docs/lint.md +122 -0
  145. package/rules/text/lint/docs/lint.md +220 -0
  146. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  147. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  148. package/rules/text/lint/docs/run-v8r.md +197 -0
  149. package/rules/vue/docs/fix.md +127 -0
  150. package/rules/vue/js/docs/packages.md +335 -0
  151. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  152. package/rules/worktree/docs/fix.md +161 -0
  153. package/schemas/rule-meta.json +5 -1
  154. package/scripts/auto-rules.mjs +7 -4
  155. package/scripts/coverage-classify/docs/apply.md +202 -0
  156. package/scripts/coverage-classify/docs/cache.md +203 -0
  157. package/scripts/coverage-classify/docs/index.md +218 -0
  158. package/scripts/coverage-classify/docs/prompt.md +132 -0
  159. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  160. package/scripts/coverage-fix-extract.mjs +122 -0
  161. package/scripts/coverage-fix.mjs +1 -1
  162. package/scripts/dispatcher/docs/graph.md +346 -0
  163. package/scripts/dispatcher/docs/index.md +236 -0
  164. package/scripts/dispatcher/docs/trace.md +296 -0
  165. package/scripts/dispatcher/index.mjs +1 -1
  166. package/scripts/dispatcher/lib/active.mjs +4 -8
  167. package/scripts/dispatcher/lib/commands.mjs +7 -11
  168. package/scripts/dispatcher/lib/docs/active.md +348 -0
  169. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  170. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  171. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  172. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  173. package/scripts/dispatcher/lib/docs/events.md +182 -0
  174. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  175. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  176. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  177. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  178. package/scripts/dispatcher/lib/docs/level.md +335 -0
  179. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  180. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  181. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  182. package/scripts/dispatcher/lib/docs/review.md +255 -0
  183. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  184. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  185. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  186. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  187. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  188. package/scripts/dispatcher/lib/executor.mjs +6 -1
  189. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  190. package/scripts/dispatcher/lib/level.mjs +29 -3
  191. package/scripts/dispatcher/lib/review.mjs +1 -1
  192. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  193. package/scripts/docs/auto-rules.md +376 -0
  194. package/scripts/docs/auto-skills.md +173 -0
  195. package/scripts/docs/build-agents-commands.md +183 -0
  196. package/scripts/docs/cli-entry.md +153 -0
  197. package/scripts/docs/coverage-fix.md +177 -0
  198. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  199. package/scripts/lib/changed-files.mjs +4 -1
  200. package/scripts/lib/docs/changed-files.md +149 -0
  201. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  202. package/scripts/lib/docs/check-reporter.md +175 -0
  203. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  204. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  205. package/scripts/lib/docs/ensure-tool.md +254 -0
  206. package/scripts/lib/docs/generated-markdown.md +275 -0
  207. package/scripts/lib/docs/gha-workflow.md +326 -0
  208. package/scripts/lib/docs/inline-template-links.md +303 -0
  209. package/scripts/lib/docs/list-rule-ids.md +156 -0
  210. package/scripts/lib/docs/load-cursor-config.md +147 -0
  211. package/scripts/lib/docs/mirror-parity.md +167 -0
  212. package/scripts/lib/worktree.mjs +26 -0
  213. package/scripts/worktree-cli.mjs +12 -2
  214. package/skills/coverage-fix/SKILL.md +34 -45
  215. package/skills/docgen/SKILL.md +44 -23
  216. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  217. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  218. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  219. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  220. package/skills/docgen/js/docgen-scan.mjs +37 -21
  221. package/skills/llm-patch/SKILL.md +23 -2
  222. package/skills/start-check/SKILL.md +26 -53
  223. package/skills/start-check/js/check.mjs +211 -0
  224. package/skills/taze/SKILL.md +9 -3
  225. package/skills/taze/js/diff.mjs +154 -0
  226. package/types/bin/n-cursor.d.ts +1 -1
  227. package/skills/fix-tests/SKILL.md +0 -119
  228. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,250 @@
1
+ # lint.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `lint.mjs` реалізує **quick-крок** правила `js-lint`: послідовний запуск двох лінтерів — `oxlint` та `eslint` — обох із прапорцем автофіксу (`--fix`). Модуль безпосередньо викликається lint-оркестратором проєкту (зокрема командами `n-cursor lint` для quick-фази та `lint-ci` для CI-фази).
6
+
7
+ Поведінка має два режими, які перемикаються типом вхідного параметра `files`:
8
+
9
+ - **Quick-режим** (`files` — масив рядків): із переданого списку відбираються лише js-подібні файли за розширенням; якщо таких немає — крок завершується успішно без запуску інструментів; інакше `oxlint`/`eslint` отримують точковий перелік цих файлів.
10
+ - **CI-режим / повний прогін** (`files === undefined`): `oxlint` викликається без шляхів (бінар сам обходить корінь за своєю конфігурацією), `eslint` отримує явну ціль `.` (поточна директорія).
11
+
12
+ Крос-файлові перевірки (наприклад `jscpd`, `knip`) у цей модуль **не входять** — вони реалізовані в окремому правилі `js-lint-ci` і виконуються виключно у CI-фазі.
13
+
14
+ Запуск зовнішніх бінарів робиться через `bunx` (з тулчейну Bun) і успадковує stdio батьківського процесу, тому увесь stdout/stderr лінтерів видно у консолі викликача в реальному часі. Семантика виходу: при ненульовому коді `oxlint` модуль одразу повертає його — `eslint` у такому випадку **не** запускається (fail-fast). Якщо `oxlint` успішний — повертається код виходу `eslint`.
15
+
16
+ ## Експорти / API
17
+
18
+ Модуль є ES-модулем (`.mjs`) із двома іменованими експортами:
19
+
20
+ | Експорт | Тип | Призначення |
21
+ | --------------- | ----------------------------------------------------------------- | ------------------------------------------------------------- |
22
+ | `filterJsFiles` | `(files: string[]) => string[]` | Фільтрує список шляхів, залишаючи лише js-подібні розширення. |
23
+ | `lint` | `(files: string[] \| undefined, cwd?: string) => Promise<number>` | Точка входу quick-кроку: запускає oxlint+eslint з автофіксом. |
24
+
25
+ Default-експорту немає. Внутрішня функція `run` приватна та не реекспортується.
26
+
27
+ ## Функції
28
+
29
+ ### `filterJsFiles(files)`
30
+
31
+ **Сигнатура:** `filterJsFiles(files: string[]): string[]`
32
+
33
+ **Параметри:**
34
+
35
+ - `files` — масив рядків зі шляхами файлів (відносними або абсолютними; модуль шляхи не нормалізує).
36
+
37
+ **Повертає:** новий масив, що містить лише ті елементи `files`, чий шлях завершується одним із розширень:
38
+ `.mjs`, `.cjs`, `.js`, `.jsx`, `.ts`, `.tsx`, `.vue`.
39
+
40
+ Перевірка виконується регулярним виразом `JS_EXT_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx|vue)$/u` — match за кінцем рядка, регістрозалежний, з прапорцем `u` (Unicode). Порядок та дублікати оригінального масиву зберігаються; вхідний масив не мутується (використовується `Array.prototype.filter`).
41
+
42
+ **Side effects:** немає (pure-функція).
43
+
44
+ **Граничні випадки:**
45
+
46
+ - Порожній масив → порожній масив.
47
+ - Файл без розширення або з не-js розширенням → відкидається.
48
+ - `.d.ts` — формально матчиться (хвіст `.ts`), тож потрапить у вихід; модуль не виключає декларацій типів спеціально.
49
+ - Шлях із крапкою у директорії, але без js-розширення наприкінці (наприклад `a.b/c.txt`) → не матчиться (через якір `$`).
50
+
51
+ ### `run(args, cwd)` _(приватна)_
52
+
53
+ **Сигнатура:** `run(args: string[], cwd: string): number`
54
+
55
+ **Параметри:**
56
+
57
+ - `args` — масив аргументів, що передаються `bunx` (перший елемент — назва бінаря лінтера, далі його аргументи/таргети).
58
+ - `cwd` — робоча директорія для дочірнього процесу.
59
+
60
+ **Повертає:** числовий exit code. Якщо `spawnSync` повернув `r.status` числом — повертається саме він; інакше (наприклад процес завершився сигналом і `status === null`) повертається `1`.
61
+
62
+ **Side effects:**
63
+
64
+ - Синхронно (блокуюче) запускає дочірній процес `bunx <args>` через `node:child_process.spawnSync`.
65
+ - Успадковує stdio батьківського процесу (`stdio: 'inherit'`) — тобто весь вивід лінтера йде напряму в термінал/лог поточного процесу.
66
+ - Може записувати файли на диск опосередковано: лінтери викликаються з `--fix` і модифікують вихідний код.
67
+
68
+ **Помилки запуску:** `spawnSync` не кидає виняток за нульового статусу запуску, але у разі помилки спавна повертає об’єкт із `error` та `status === null` — у цьому випадку функція повертає `1` (м’яка інтерпретація як «не OK»).
69
+
70
+ ### `lint(files, cwd?)`
71
+
72
+ **Сигнатура:** `lint(files: string[] | undefined, cwd: string = process.cwd()): Promise<number>`
73
+
74
+ **Параметри:**
75
+
76
+ - `files`:
77
+ - `string[]` — quick-режим: лінтити **лише** ці файли (після фільтрації js-розширень).
78
+ - `undefined` — режим повного прогону: лінтити **весь** проєкт.
79
+ - `cwd` — корінь, у якому виконуються `bunx oxlint` та `bunx eslint`. За замовчуванням — `process.cwd()`.
80
+
81
+ **Повертає:** `Promise<number>`. Резолвиться зі значенням exit code:
82
+
83
+ - `0` — обидва лінтери (або один із них у спеціальному випадку) завершилися успішно.
84
+ - `≠0` — є порушення або помилка запуску; повертається код того лінтера, що першим завершився ненульовим (oxlint має пріоритет).
85
+
86
+ **Логіка побудови аргументів:**
87
+
88
+ 1. Стартові значення: `oxArgs = ['oxlint', '--fix']`, `esArgs = ['eslint', '--fix']`.
89
+ 2. Якщо `files === undefined`:
90
+ - `oxArgs` лишаються без явних таргетів (oxlint обходить проєкт сам).
91
+ - До `esArgs` додається `.` → стає `['eslint', '--fix', '.']`.
92
+ 3. Інакше (масив):
93
+ - Фільтрація через `filterJsFiles(files)`.
94
+ - Якщо фільтрований список порожній — функція **одразу** повертає `Promise.resolve(0)` (немає чого лінтити; oxlint/eslint **не** запускаються).
95
+ - Інакше: `oxArgs = ['oxlint', '--fix', ...js]`, `esArgs = ['eslint', '--fix', ...js]`.
96
+
97
+ **Послідовність виконання:**
98
+
99
+ 1. Синхронно запускається `oxlint` через `run(oxArgs, cwd)` → отримуємо `ox`.
100
+ 2. Якщо `ox !== 0` → негайно повертається `Promise.resolve(ox)` (eslint **не** запускається — fail-fast).
101
+ 3. Якщо `ox === 0` → синхронно запускається `eslint` через `run(esArgs, cwd)`; результат загорнуто у `Promise.resolve(...)` і повернуто.
102
+
103
+ **Тип повертаного значення:** функція синхронна по суті (внутрішні `spawnSync` блокують event-loop), але інтерфейс — `Promise<number>`, що дозволяє оркестратору однаково обробляти async- та sync-кроки.
104
+
105
+ **Side effects:**
106
+
107
+ - Спавнить до двох дочірніх процесів (`bunx oxlint`, `bunx eslint`).
108
+ - Через прапорець `--fix` лінтери модифікують вихідні файли на диску (автофікс).
109
+ - Виводить stdout/stderr лінтерів напряму в консоль батьківського процесу.
110
+
111
+ **Граничні випадки:**
112
+
113
+ - `files = []` → після фільтрації порожній список → resolve(0), запусків немає.
114
+ - `files = ['README.md']` → після фільтрації порожній список → resolve(0), запусків немає.
115
+ - `files = undefined` + порожній проєкт → oxlint/eslint самі обходять корінь і повертають свій код (часто 0).
116
+ - Невказаний `cwd` → `process.cwd()` на момент виклику (а не на момент імпорту).
117
+
118
+ ## Залежності
119
+
120
+ ### Зовнішні (рантайм)
121
+
122
+ | Залежність | Звідки | Призначення |
123
+ | ------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------- |
124
+ | `node:child_process.spawnSync` | Стандартна бібліотека Node.js | Синхронний запуск дочірніх процесів `bunx oxlint` / `bunx eslint` зі спадкуванням stdio. |
125
+ | `bunx` | Тулчейн Bun (PATH) | Резолвер/раннер бінарів пакетів — використовується для виклику `oxlint` та `eslint`. |
126
+ | `oxlint` | Доступний через `bunx` | Швидкий лінтер на Rust; перший етап quick-кроку, виконується з `--fix`. |
127
+ | `eslint` | Доступний через `bunx` | Основний лінтер на JavaScript; другий етап, виконується з `--fix` після успішного `oxlint`. |
128
+
129
+ ### Внутрішні
130
+
131
+ Модуль **не** імпортує локальних модулів проєкту — повністю автономний у межах файлу. Контекст використання задає зовнішній lint-оркестратор (правило `js-lint`), який передає аргументи `files`/`cwd`.
132
+
133
+ ### Споживачі
134
+
135
+ Викликається lint-оркестратором правил `js-lint` (quick-фаза). Модуль `lint-ci` (повний CI) реалізований окремо і додає крос-файлові інструменти (`jscpd`, `knip`) — у поточному файлі їх немає.
136
+
137
+ ## Потік виконання / Використання
138
+
139
+ ### Загальний потік (псевдо-діаграма)
140
+
141
+ ```
142
+ викликач (n-cursor lint / lint-ci)
143
+ |
144
+ v
145
+ lint(files, cwd)
146
+ |
147
+ files === undefined ? --- так ---> oxArgs=['oxlint','--fix']
148
+ | esArgs=['eslint','--fix','.']
149
+ | ні
150
+ v
151
+ js = filterJsFiles(files)
152
+ |
153
+ js.length === 0 ? --- так ---> Promise.resolve(0) (вихід)
154
+ | ні
155
+ v
156
+ oxArgs=['oxlint','--fix',...js]
157
+ esArgs=['eslint','--fix',...js]
158
+ |
159
+ v
160
+ ox = run(oxArgs, cwd) // bunx oxlint ...
161
+ |
162
+ ox !== 0 ? --- так ---> Promise.resolve(ox) (eslint не запускається)
163
+ | ні
164
+ v
165
+ es = run(esArgs, cwd) // bunx eslint ...
166
+ |
167
+ v
168
+ Promise.resolve(es)
169
+ ```
170
+
171
+ ### Приклад: quick-режим (масив файлів)
172
+
173
+ ```js
174
+ import { lint } from './lint.mjs'
175
+
176
+ const changed = [
177
+ 'src/foo.ts',
178
+ 'src/bar.vue',
179
+ 'README.md', // буде відфільтровано
180
+ 'scripts/baz.mjs'
181
+ ]
182
+
183
+ const code = await lint(changed)
184
+ // 1) filterJsFiles → ['src/foo.ts','src/bar.vue','scripts/baz.mjs']
185
+ // 2) bunx oxlint --fix src/foo.ts src/bar.vue scripts/baz.mjs
186
+ // 3) якщо ox === 0 → bunx eslint --fix src/foo.ts src/bar.vue scripts/baz.mjs
187
+ process.exit(code)
188
+ ```
189
+
190
+ ### Приклад: CI-режим (повний прогін)
191
+
192
+ ```js
193
+ import { lint } from './lint.mjs'
194
+
195
+ const code = await lint(undefined, '/path/to/repo')
196
+ // 1) bunx oxlint --fix (oxlint сам обходить проєкт)
197
+ // 2) якщо ox === 0 → bunx eslint --fix .
198
+ process.exit(code)
199
+ ```
200
+
201
+ ### Приклад: масив без js-файлів
202
+
203
+ ```js
204
+ import { lint } from './lint.mjs'
205
+
206
+ const code = await lint(['CHANGELOG.md', 'docs/spec.md'])
207
+ // filterJsFiles → [] → Promise.resolve(0)
208
+ // жодного процесу не спавниться
209
+ ```
210
+
211
+ ### Семантика кодів виходу
212
+
213
+ - `0` — обидва лінтери успішні **або** немає js-файлів для лінтингу в quick-режимі.
214
+ - `ox` (≠0) — `oxlint` знайшов порушення / завершився помилкою; `eslint` пропущено.
215
+ - exit code `eslint` — повертається, якщо `oxlint` був успішний, а `eslint` — ні.
216
+ - `1` (з `run`) — фолбек, коли `spawnSync` не повернув числового `status` (наприклад, процес убитий сигналом).
217
+
218
+ ### Важливі зауваги для викликачів
219
+
220
+ - Виклики `oxlint` та `eslint` **синхронні** (через `spawnSync`) — `Promise<number>` тут лише обгортка для уніфікації API; функція **блокує** event-loop на час лінтингу.
221
+ - Завдяки `stdio: 'inherit'` весь вивід лінтерів видно одразу — додаткове логування зайве.
222
+ - `--fix` модифікує файли на диску; якщо потрібно «чисто перевірити без правок» — цей модуль такого режиму не пропонує, доведеться обійти його або змінити аргументи.
223
+ - За правилами проєкту (див. `n-js-lint` / `n-lint`) **заборонені** паралельні запуски `eslint` — оркестратор має викликати `lint` **послідовно**, без фонових/підзадачних дублів.
224
+ - `oxlint` має пріоритет (fail-fast): якщо він валиться — `eslint` не запускається, тож звіт про помилки у тій самій сесії покаже лише результати oxlint.
225
+
226
+ ## Rebuild Test
227
+
228
+ Описане в цій документації достатньо для відтворення модуля з нуля:
229
+
230
+ 1. ES-модуль `.mjs` без default-експорту.
231
+ 2. Імпорт `spawnSync` з `node:child_process`.
232
+ 3. Константа `JS_EXT_RE` — регекс `^.*\.(mjs|cjs|js|jsx|ts|tsx|vue)$` (формально: `/\.(?:mjs|cjs|js|jsx|ts|tsx|vue)$/u`).
233
+ 4. Експорт `filterJsFiles(files)`: `return files.filter(f => JS_EXT_RE.test(f))`.
234
+ 5. Приватна `run(args, cwd)`: `spawnSync('bunx', args, { cwd, stdio: 'inherit' })`; повертає `r.status` якщо це `number`, інакше `1`.
235
+ 6. Експорт `lint(files, cwd = process.cwd())`:
236
+ - стартові аргументи: `oxArgs=['oxlint','--fix']`, `esArgs=['eslint','--fix']`;
237
+ - якщо `files === undefined` → `esArgs.push('.')`;
238
+ - інакше: `js = filterJsFiles(files)`; якщо `js.length === 0` → `Promise.resolve(0)`;
239
+ інакше переписати: `oxArgs=['oxlint','--fix',...js]`, `esArgs=['eslint','--fix',...js]`;
240
+ - `ox = run(oxArgs, cwd)`; якщо `ox !== 0` → `Promise.resolve(ox)`;
241
+ - інакше → `Promise.resolve(run(esArgs, cwd))`.
242
+
243
+ Поведінкові інваріанти для верифікації:
244
+
245
+ - `filterJsFiles([])` ⇒ `[]`.
246
+ - `filterJsFiles(['a.md','b.ts','c.vue','d.txt'])` ⇒ `['b.ts','c.vue']`.
247
+ - `await lint([])` ⇒ `0`, без побічних спавнів.
248
+ - `await lint(['x.md'])` ⇒ `0`, без побічних спавнів.
249
+ - `await lint(undefined)` ⇒ запускає `bunx oxlint --fix`, далі (якщо OK) `bunx eslint --fix .`.
250
+ - При ненульовому коді `oxlint` `eslint` не запускається.
@@ -0,0 +1,348 @@
1
+ # tooling.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `tooling.mjs` реалізує перевірку (check-частину) правила **`js-lint.mdc`** для проєктів-споживачів пакета `@nitra/cursor`. Він гарантує, що репозиторій налаштований на канонічний JS-toolchain Nitra:
6
+
7
+ - наявність та коректний вміст **flat ESLint config** (`eslint.config.js` або `eslint.config.mjs`) з виклику `getConfig` із `@nitra/eslint-config` та `ignores` для `**/auto-imports.d.ts`;
8
+ - **`.oxlintrc.json`** має збігатися з канонічним JSON у пакеті (`npm/rules/js-lint/js/data/tooling/oxlint-canonical.json`) — `plugins`, `jsPlugins`, `categories`, усі правила з канону, `settings`, `env`, `globals`, `ignorePatterns`; додаткові записи дозволені лише у блоці `rules`;
9
+ - кожен workspace-`package.json` має `"type": "module"`, `engines.node >= 24` та `engines.bun >= 1.3`;
10
+ - існує workflow `.github/workflows/lint-js.yml` (структуру валідує Rego-policy `js_lint.lint_js_yml`), а `.github/workflows/lint.yml` (якщо є) **не дублює** кроки `bunx oxlint` / `bunx eslint` / `jscpd`;
11
+ - існує `knip.json` (якщо відсутній — копіюється канонічний baseline з пакета);
12
+ - у репозиторії **немає** застарілих legacy-конфігів ESLint (`.eslintrc`, `.eslintrc.js`, `.eslintrc.json`, `.eslintrc.yml`).
13
+
14
+ Per-document вимоги (`lint-js` script, мінімальна версія `@nitra/eslint-config ≥ 3.10.0`, root `engines`, `.jscpd.json`, `.vscode/extensions.json`, структура `lint-js.yml`) вже **не** реалізуються тут — їх валідують Rego-policy-пакети `js_lint.*` (`npm/policy/js_lint/package_json/`, `npm/policy/js_lint/lint_js_yml/`). Це усуває дублювання істини між JS-перевіркою та Rego-політиками.
15
+
16
+ Модуль використовує спільний `createCheckReporter` зі скриптів пакета: збирає `pass` / `fail`-повідомлення та повертає числовий exit-код (0 — OK, 1 — є проблеми).
17
+
18
+ ## Експорти / API
19
+
20
+ | Експорт | Тип | Призначення |
21
+ | ------------------------------------------------ | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
+ | `OXLINT_CANONICAL_JSON_PATH` | `string` (named const) | Абсолютний шлях до канонічного `oxlint-canonical.json` у цьому пакеті (`<dirname>/data/tooling/oxlint-canonical.json`). Використовується перевіркою та тестами. |
23
+ | `KNIP_CANONICAL_JSON_PATH` | `string` (named const) | Абсолютний шлях до канонічного `knip-canonical.json` у цьому пакеті. Копіюється у корінь проєкту-споживача, якщо `knip.json` відсутній. |
24
+ | `verifyOxlintRcAgainstCanonical(cfg, canonical)` | `function` (named export) | Чиста функція звірення розпарсеного `.oxlintrc.json` із канонічним JSON. Повертає `{ ok: boolean, failures: string[] }`. |
25
+ | `check(cwd?)` | `async function` (named export) | Точка входу: запускає всі under-the-hood перевірки і повертає `Promise<number>` (0 — все добре, 1 — є fail). |
26
+
27
+ Усі решта функцій (`deepEqualOxlintCanonical`, `asRecordOrEmpty`, `compareOxlintRules`, `compareOxlintIgnorePatterns`, `checkEslintConfig`, `checkPackageJsonTypeModule`, `checkWorkspacePackages`, `checkEnginesNode`, `checkEnginesBun`, `checkPackageJsonJsLint`, `checkOxlintRc`, `checkLintJsWorkflows`, `checkKnipConfig`) — **module-private** (не експортуються).
28
+
29
+ ## Функції
30
+
31
+ ### `deepEqualOxlintCanonical(actual, expected)`
32
+
33
+ **Сигнатура:** `function deepEqualOxlintCanonical(actual: unknown, expected: unknown): boolean`
34
+
35
+ **Параметри:**
36
+
37
+ - `actual` — значення з `.oxlintrc.json` (будь-який тип);
38
+ - `expected` — еталонне значення з канону.
39
+
40
+ **Повертає:** `true`, якщо `actual` повністю збігається з `expected` за правилами канону:
41
+
42
+ - примітиви — `===`;
43
+ - масиви — однакові за `JSON.stringify` (тобто **порядок** теж важливий);
44
+ - об’єкти — **той самий набір ключів** (`expKeys.length === actKeys.length` і всі ключі канону присутні в actual) та рекурсивно рівні значення.
45
+
46
+ **Side effects:** немає (чиста функція).
47
+
48
+ ---
49
+
50
+ ### `asRecordOrEmpty(v)`
51
+
52
+ **Сигнатура:** `function asRecordOrEmpty(v: unknown): Record<string, unknown>`
53
+
54
+ **Параметри:**
55
+
56
+ - `v` — будь-яке значення.
57
+
58
+ **Повертає:** саме значення (із cast-коментарем), якщо це plain-object (`typeof v === 'object'`, не `null`, не масив); інакше — `{}`.
59
+
60
+ **Side effects:** немає.
61
+
62
+ ---
63
+
64
+ ### `compareOxlintRules(expected, actual, failures)`
65
+
66
+ **Сигнатура:** `function compareOxlintRules(expected: unknown, actual: unknown, failures: string[]): void`
67
+
68
+ **Параметри:**
69
+
70
+ - `expected` — канонічний об’єкт `rules`;
71
+ - `actual` — значення `rules` з поточного `.oxlintrc.json`;
72
+ - `failures` — буфер-масив, у який функція **дописує** повідомлення про невідповідності.
73
+
74
+ **Поведінка:** ітерує по ключах канону; якщо `actual[ruleKey] !== expected[ruleKey]` (строге `!==` — отже об’єктні значення правил мають бути тим самим референсом, що рідко зустрічається — на практиці значення правил у oxlint це числа / рядки / прості масиви), додає повідомлення у форматі:
75
+
76
+ ```
77
+ .oxlintrc.json: rules["<key>"] очікується <expected>, зараз <actual>
78
+ ```
79
+
80
+ Додаткові ключі правил у `actual` **дозволені** (не повідомляються).
81
+
82
+ **Повертає:** `undefined`.
83
+
84
+ **Side effects:** мутує `failures`.
85
+
86
+ ---
87
+
88
+ ### `compareOxlintIgnorePatterns(expected, actual, failures)`
89
+
90
+ **Сигнатура:** `function compareOxlintIgnorePatterns(expected: unknown, actual: unknown, failures: string[]): void`
91
+
92
+ **Параметри:**
93
+
94
+ - `expected` — канонічний масив `ignorePatterns`;
95
+ - `actual` — поточний масив `ignorePatterns`;
96
+ - `failures` — буфер для повідомлень.
97
+
98
+ **Поведінка:**
99
+
100
+ - якщо `expected` не є масивом — функція мовчки виходить (канон не задає очікувань);
101
+ - якщо `actual` не є масивом — додає повідомлення про обов’язковість масиву;
102
+ - порівнює як **підмножину**: усі канонічні патерни мають бути в `actual` (через `Set`); відсутні патерни перелічуються одним повідомленням. Додаткові локальні патерни **дозволені**.
103
+
104
+ **Повертає:** `undefined`.
105
+
106
+ **Side effects:** мутує `failures`.
107
+
108
+ ---
109
+
110
+ ### `verifyOxlintRcAgainstCanonical(cfg, canonical)` _(експортовано)_
111
+
112
+ **Сигнатура:** `function verifyOxlintRcAgainstCanonical(cfg: unknown, canonical: unknown): { ok: boolean, failures: string[] }`
113
+
114
+ **Параметри:**
115
+
116
+ - `cfg` — розпарсений корінь `.oxlintrc.json`;
117
+ - `canonical` — розпарсений `oxlint-canonical.json`.
118
+
119
+ **Поведінка:**
120
+
121
+ 1. Якщо `cfg` чи `canonical` не є plain-object — повертає `ok: false` з відповідним повідомленням (для `cfg` — про невалідний корінь; для `canonical` — як «внутрішня помилка»).
122
+ 2. Для кожного ключа канону:
123
+ - `rules` → делегує `compareOxlintRules`;
124
+ - `ignorePatterns` → делегує `compareOxlintIgnorePatterns`;
125
+ - решта ключів → перевіряється через `deepEqualOxlintCanonical`; при невідповідності в `failures` додається повідомлення, що поле має збігатися з каноном пакета `@nitra/cursor`.
126
+ 3. Повертає `{ ok: failures.length === 0, failures }`.
127
+
128
+ **Повертає:** результат-об’єкт із прапором `ok` і масивом fail-повідомлень для подальшого репортинга.
129
+
130
+ **Side effects:** немає (чиста функція; працює з аргументами, не читає файли).
131
+
132
+ ---
133
+
134
+ ### `checkEslintConfig(passFn, failFn, cwd)`
135
+
136
+ **Сигнатура:** `async function checkEslintConfig(passFn: (msg:string)=>void, failFn: (msg:string)=>void, cwd: string): Promise<void>`
137
+
138
+ **Параметри:**
139
+
140
+ - `passFn` — колбек успіху;
141
+ - `failFn` — колбек помилки;
142
+ - `cwd` — корінь репозиторію.
143
+
144
+ **Поведінка:**
145
+
146
+ - шукає `eslint.config.js`, потім `eslint.config.mjs`; якщо жодного — fail і ранній вихід;
147
+ - читає вміст знайденого файла як `utf8` і застосовує три суто **текстові** перевірки наявності підрядків:
148
+ 1. `getConfig` — обов’язковий виклик;
149
+ 2. `@nitra/eslint-config` — обов’язковий імпорт;
150
+ 3. `**/auto-imports.d.ts` — обов’язковий запис у `ignores`.
151
+
152
+ **Повертає:** `Promise<void>`.
153
+
154
+ **Side effects:** читає файли з ФС (`existsSync`, `readFile`); викликає `passFn`/`failFn`.
155
+
156
+ ---
157
+
158
+ ### `checkPackageJsonTypeModule(label, pkg, passFn, failFn)`
159
+
160
+ **Сигнатура:** `function checkPackageJsonTypeModule(label: string, pkg: { type?: string }, passFn: (msg:string)=>void, failFn: (msg:string)=>void): void`
161
+
162
+ **Поведінка:** якщо `pkg.type === 'module'` → `passFn`; інакше → `failFn` з вимогою додати поле.
163
+
164
+ **Side effects:** виклики переданих колбеків.
165
+
166
+ ---
167
+
168
+ ### `checkWorkspacePackages(workspaces, passFn, failFn, cwd)`
169
+
170
+ **Сигнатура:** `async function checkWorkspacePackages(workspaces: unknown[], passFn, failFn, cwd: string): Promise<void>`
171
+
172
+ **Поведінка:** для кожного запису workspaces:
173
+
174
+ - будує шлях `<ws>/package.json`;
175
+ - якщо файл існує — читає та парсить JSON; запускає послідовно `checkPackageJsonTypeModule`, `checkEnginesNode`, `checkEnginesBun`.
176
+
177
+ **Side effects:** читання ФС, виклики колбеків.
178
+
179
+ ---
180
+
181
+ ### `checkEnginesNode(label, pkg, passFn, failFn)`
182
+
183
+ **Сигнатура:** `function checkEnginesNode(label: string, pkg: { engines?: { node?: string } }, passFn, failFn): void`
184
+
185
+ **Поведінка:** дістає `pkg.engines?.node`; через регекс `NON_DIGITS_RE = /\D+/u` бере **перший** числовий токен (наприклад, з `">=24"` → `"24"`); якщо це число ≥ 24 → pass, інакше → fail; якщо поле відсутнє → fail з пропозицією додати `"engines": { "node": ">=24" }`.
186
+
187
+ **Side effects:** виклики колбеків.
188
+
189
+ ---
190
+
191
+ ### `checkEnginesBun(label, pkg, passFn, failFn)`
192
+
193
+ **Сигнатура:** `function checkEnginesBun(label: string, pkg: { engines?: { bun?: string } }, passFn, failFn): void`
194
+
195
+ **Поведінка:** дістає `pkg.engines?.bun`; розбиває рядок по `NON_DIGITS_RE`, фільтрує порожні токени, бере перші два як `[major, minor]`; pass якщо `major > 1` або `major === 1 && minor >= 3`; інакше fail; відсутнє поле → fail з пропозицією додати `"engines": { "bun": ">=1.3" }`.
196
+
197
+ **Side effects:** виклики колбеків.
198
+
199
+ ---
200
+
201
+ ### `checkPackageJsonJsLint(passFn, failFn, cwd)`
202
+
203
+ **Сигнатура:** `async function checkPackageJsonJsLint(passFn, failFn, cwd: string): Promise<void>`
204
+
205
+ **Поведінка:** читає кореневий `package.json` (якщо відсутній — мовчки виходить); бере поле `workspaces` (масив або `[]`) і делегує `checkWorkspacePackages`. Кореневий `package.json` тут **не** валідується — це робить Rego-policy `npm/policy/js_lint/package_json/`.
206
+
207
+ **Side effects:** читання ФС, виклики колбеків.
208
+
209
+ ---
210
+
211
+ ### `checkOxlintRc(passFn, failFn, cwd)`
212
+
213
+ **Сигнатура:** `async function checkOxlintRc(passFn, failFn, cwd: string): Promise<void>`
214
+
215
+ **Поведінка:**
216
+
217
+ 1. Якщо `.oxlintrc.json` відсутній → fail і ранній вихід.
218
+ 2. Спроба JSON-парсингу; при помилці → fail про невалідний JSON.
219
+ 3. Pass-повідомлення про наявність файла.
220
+ 4. Читає канонічний `oxlint-canonical.json` (шлях `OXLINT_CANONICAL_JSON_PATH`); при помилці → fail-повідомлення «внутрішня помилка».
221
+ 5. Викликає `verifyOxlintRcAgainstCanonical(oxCfg, canonical)`; pass якщо `ok`, інакше — повторно викликає `failFn` для кожного повідомлення зі списку `failures`.
222
+
223
+ **Side effects:** читання ФС, виклики колбеків.
224
+
225
+ ---
226
+
227
+ ### `checkLintJsWorkflows(passFn, failFn, cwd)`
228
+
229
+ **Сигнатура:** `async function checkLintJsWorkflows(passFn, failFn, cwd: string): Promise<void>`
230
+
231
+ **Поведінка:**
232
+
233
+ - перевіряє існування `.github/workflows/lint-js.yml` → pass з нагадуванням, що структуру валідує Rego (`js_lint.lint_js_yml`); відсутність → fail;
234
+ - якщо існує `.github/workflows/lint.yml` — читає його як текст і перевіряє, що він **не містить одночасно** трьох підрядків `bunx oxlint`, `bunx eslint`, `jscpd`. Якщо містить — fail (дубль кроків JS-лінта), інакше — pass.
235
+
236
+ **Side effects:** читання ФС, виклики колбеків.
237
+
238
+ ---
239
+
240
+ ### `checkKnipConfig(passFn, failFn, cwd)`
241
+
242
+ **Сигнатура:** `async function checkKnipConfig(passFn, failFn, cwd: string): Promise<void>`
243
+
244
+ **Поведінка:**
245
+
246
+ - якщо `knip.json` існує у корені — pass і вихід;
247
+ - якщо відсутній і канонічний шаблон `KNIP_CANONICAL_JSON_PATH` теж недоступний (`existsSync` повертає false) — fail з пропозицією перевстановити `@nitra/cursor`;
248
+ - інакше — **копіює** канонічний JSON у `cwd/knip.json` через `copyFile` і повідомляє pass.
249
+
250
+ **Side effects:** читання ФС, **запис** нового файла `knip.json`, виклики колбеків. Це єдина функція модуля з write-side-effect.
251
+
252
+ ---
253
+
254
+ ### `check(cwd?)` _(експортовано — публічна точка входу)_
255
+
256
+ **Сигнатура:** `async function check(cwd?: string): Promise<number>` (за замовчуванням `cwd = process.cwd()`).
257
+
258
+ **Поведінка:**
259
+
260
+ 1. Створює репортер через `createCheckReporter()` і отримує `pass` / `fail` колбеки.
261
+ 2. Послідовно `await`-ить:
262
+ - `checkEslintConfig` — flat-config;
263
+ - `checkPackageJsonJsLint` — workspace-`package.json`;
264
+ - `checkOxlintRc` — `.oxlintrc.json` vs canonical;
265
+ - `checkLintJsWorkflows` — `lint-js.yml` + дубль у `lint.yml`;
266
+ - `checkKnipConfig` — `knip.json` (з можливим створенням).
267
+ 3. Перевіряє наявність legacy-файлів у списку `['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']`; для кожного знайденого — `fail`.
268
+ 4. Повертає `reporter.getExitCode()` — `0` якщо не було жодного `fail`, інакше `1`.
269
+
270
+ **Повертає:** `Promise<number>` — exit-код для CLI.
271
+
272
+ **Side effects:** усі ефекти із дочірніх функцій (читання ФС, можливий запис `knip.json`, виведення pass/fail через репортер).
273
+
274
+ ## Залежності
275
+
276
+ ### Стандартна бібліотека Node.js
277
+
278
+ - `node:fs` → `existsSync` — синхронна перевірка наявності файлів.
279
+ - `node:fs/promises` → `copyFile`, `readFile` — асинхронні файлові операції.
280
+ - `node:path` → `dirname`, `join` — побудова шляхів.
281
+ - `node:url` → `fileURLToPath` — перетворення `import.meta.url` для отримання абсолютної директорії модуля.
282
+
283
+ ### Внутрішні
284
+
285
+ - `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` — фабрика репортера зі стандартними `pass` / `fail` колбеками та методом `getExitCode()`.
286
+
287
+ ### Канонічні артефакти даних (читаються/копіюються, але не імпортуються як JS-модулі)
288
+
289
+ - `data/tooling/oxlint-canonical.json` — еталон `.oxlintrc.json`.
290
+ - `data/tooling/knip-canonical.json` — baseline для `knip.json` (копіюється у проєкт-споживач).
291
+
292
+ ### Зовнішні
293
+
294
+ Жодних рантайм-залежностей з `node_modules` модуль не використовує. (Перевірки на наявність `@nitra/eslint-config` виконуються **текстово** як пошук підрядка у файлі ESLint-конфіга.)
295
+
296
+ ## Потік виконання / Використання
297
+
298
+ ### Типовий запуск
299
+
300
+ Модуль використовується інфраструктурою `@nitra/cursor` як check-частина правила `js-lint`. Виклик:
301
+
302
+ ```js
303
+ import { check } from '@nitra/cursor/rules/js-lint/js/tooling.mjs'
304
+
305
+ const exitCode = await check(process.cwd())
306
+ process.exit(exitCode)
307
+ ```
308
+
309
+ або через загальний раннер пакета, який ітерує всі `check-*.mjs` правил.
310
+
311
+ ### Послідовність кроків у середині `check`
312
+
313
+ 1. **ESLint config** → шукаємо `eslint.config.js`/`.mjs`, перевіряємо вміст на `getConfig`, `@nitra/eslint-config`, `**/auto-imports.d.ts`.
314
+ 2. **package.json workspaces** → ітеруємо `workspaces[]`, перевіряємо `type: "module"` + `engines.node` + `engines.bun` у кожному.
315
+ 3. **.oxlintrc.json** → читаємо JSON, парсимо канон із пакета, повне дерев’яне порівняння через `verifyOxlintRcAgainstCanonical` (особливі правила для `rules` і `ignorePatterns`).
316
+ 4. **GitHub workflows** → перевіряємо існування `lint-js.yml`; перевіряємо, що `lint.yml` не дублює `bunx oxlint` + `bunx eslint` + `jscpd`.
317
+ 5. **knip.json** → якщо відсутній, копіюємо канонічний baseline; інакше — лише фіксуємо наявність.
318
+ 6. **Legacy ESLint configs** → fail для будь-якого з `.eslintrc`, `.eslintrc.js`, `.eslintrc.json`, `.eslintrc.yml` у корені.
319
+ 7. Повертаємо `0` або `1` через репортер.
320
+
321
+ ### Розподіл відповідальності з Rego-policies
322
+
323
+ | Що перевіряється | Де живе |
324
+ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
325
+ | Наявність `eslint.config.{js,mjs}` + ключові підрядки | **тут** (`checkEslintConfig`) |
326
+ | `type: "module"`, `engines.{node,bun}` у workspace-пакетах | **тут** (`checkWorkspacePackages`) |
327
+ | Той самий набір полів у кореневому `package.json`, мінімальна версія `@nitra/eslint-config`, `lint-js`-script, `prettier`, `@nitra/prettier-config` | Rego: `npm/policy/js_lint/package_json/`, `npm/policy/text/package_json/` |
328
+ | Збіг `.oxlintrc.json` з канонічним JSON | **тут** (`checkOxlintRc` + `verifyOxlintRcAgainstCanonical`) |
329
+ | Наявність `lint-js.yml`, заборона дубля у `lint.yml` | **тут** (`checkLintJsWorkflows`) |
330
+ | Структура `lint-js.yml` (`actions/checkout@v6`, `persist-credentials: false`, `setup-bun-deps`, заборона `--fix`) | Rego: `npm/policy/js_lint/lint_js_yml/` |
331
+ | Bootstrap `knip.json` (копіювання канону) | **тут** (`checkKnipConfig`) |
332
+ | Заборона legacy `.eslintrc*` | **тут** (`check`, фінальний цикл) |
333
+
334
+ ### Семантика звіту
335
+
336
+ - Кожна перевірка дописує **окремі** повідомлення у репортер; `check` не зупиняється на першому `fail`, а збирає повний набір проблем — це навмисно, щоб CI-користувач бачив усі порушення одним прогоном.
337
+ - Винятки JSON-парсингу `.oxlintrc.json` ловляться локально та трансформуються у fail-повідомлення; невдала зчитка канону також не кидає виняток назовні (стає «внутрішня помилка»). Інші помилки (наприклад, `JSON.parse` для workspace-`package.json`) **не** ловляться і прокидаються через `Promise<number>` як rejection — це сигналізує про серйозну поломку (битий JSON у відомому файлі).
338
+
339
+ ### Rebuild Test
340
+
341
+ Документ описує одиничний файл `tooling.mjs`. Перебудова за документом має містити:
342
+
343
+ - два названих експорти-шляхи (`OXLINT_CANONICAL_JSON_PATH`, `KNIP_CANONICAL_JSON_PATH`), обчислені через `dirname(fileURLToPath(import.meta.url))` + `data/tooling/<name>.json`;
344
+ - регекс `NON_DIGITS_RE = /\D+/u` як модульну константу;
345
+ - експорт `verifyOxlintRcAgainstCanonical(cfg, canonical)` із семантикою «`rules` — підмножина значень, `ignorePatterns` — підмножина елементів, решта полів — повний `deepEqualOxlintCanonical`»;
346
+ - допоміжні приватні функції `deepEqualOxlintCanonical`, `asRecordOrEmpty`, `compareOxlintRules`, `compareOxlintIgnorePatterns` з описаною семантикою (для масивів — порівняння через `JSON.stringify`; для об’єктів — однаковий розмір ключів і рекурсія; primitives — `===`);
347
+ - приватні чекери (`checkEslintConfig`, `checkPackageJsonTypeModule`, `checkWorkspacePackages`, `checkEnginesNode`, `checkEnginesBun`, `checkPackageJsonJsLint`, `checkOxlintRc`, `checkLintJsWorkflows`, `checkKnipConfig`) з зазначеними сигнатурами та повідомленнями;
348
+ - експорт `check(cwd = process.cwd())`, який створює репортер через `createCheckReporter`, послідовно `await`-ить п’ять чекерів у порядку _ESLint → package.json → oxlint → workflows → knip_, потім перевіряє legacy-конфіги ESLint і повертає `reporter.getExitCode()`.