@nitra/cursor 3.22.0 → 3.23.1

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 (229) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/AGENTS.template.md +4 -0
  3. package/CHANGELOG.md +37 -3
  4. package/bin/docs/n-cursor.md +636 -0
  5. package/bin/docs/rename-yaml-extensions.md +207 -0
  6. package/bin/n-cursor.js +30 -3
  7. package/package.json +1 -1
  8. package/rules/abie/docs/fix.md +18 -0
  9. package/rules/abie/js/docs/applies.md +26 -0
  10. package/rules/abie/js/docs/env_dns.md +32 -0
  11. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  12. package/rules/abie/js/docs/hc_pairing.md +35 -0
  13. package/rules/abie/js/docs/ua_http_route.md +28 -0
  14. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  15. package/rules/abie/lib/docs/enabled.md +29 -0
  16. package/rules/abie/lib/docs/env-dns.md +35 -0
  17. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  18. package/rules/abie/lib/docs/http-route.md +44 -0
  19. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  20. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  21. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  22. package/rules/abie/lib/docs/yaml.md +29 -0
  23. package/rules/adr/docs/fix.md +148 -0
  24. package/rules/adr/js/docs/hooks.md +259 -0
  25. package/rules/bun/docs/fix.md +156 -0
  26. package/rules/bun/js/docs/layout.md +393 -0
  27. package/rules/capacitor/docs/fix.md +121 -0
  28. package/rules/capacitor/js/docs/platforms.md +295 -0
  29. package/rules/changelog/changelog.mdc +4 -2
  30. package/rules/changelog/docs/fix.md +174 -0
  31. package/rules/changelog/js/consistency.mjs +114 -13
  32. package/rules/changelog/js/docs/consistency.md +387 -0
  33. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  34. package/rules/ci4/docs/fix.md +179 -0
  35. package/rules/ci4/js/docs/marksman_config.md +128 -0
  36. package/rules/docker/docker.mdc +8 -3
  37. package/rules/docker/docs/fix.md +171 -0
  38. package/rules/docker/js/docs/lint.md +258 -0
  39. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  40. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  41. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  42. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  43. package/rules/docker/lint/docs/lint.md +193 -0
  44. package/rules/efes/docs/fix.md +203 -0
  45. package/rules/feedback/docs/fix.md +140 -0
  46. package/rules/flow/docs/fix.md +152 -0
  47. package/rules/ga/docs/fix.md +158 -0
  48. package/rules/ga/js/docs/lint.md +100 -0
  49. package/rules/ga/js/docs/workflows.md +217 -0
  50. package/rules/ga/lint/docs/lint.md +209 -0
  51. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  52. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  53. package/rules/graphql/docs/fix.md +126 -0
  54. package/rules/graphql/js/docs/tooling.md +264 -0
  55. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  56. package/rules/hasura/docs/fix.md +120 -0
  57. package/rules/hasura/hasura.mdc +14 -0
  58. package/rules/hasura/js/docs/internal_urls.md +326 -0
  59. package/rules/image-avif/docs/fix.md +132 -0
  60. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  61. package/rules/image-compress/docs/fix.md +150 -0
  62. package/rules/image-compress/js/docs/package_setup.md +191 -0
  63. package/rules/js-bun-db/docs/fix.md +148 -0
  64. package/rules/js-bun-db/js/docs/safety.md +231 -0
  65. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  66. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  67. package/rules/js-bun-redis/docs/fix.md +123 -0
  68. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  69. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  70. package/rules/js-lint/docs/fix.md +117 -0
  71. package/rules/js-lint/js/docs/lint.md +250 -0
  72. package/rules/js-lint/js/docs/tooling.md +348 -0
  73. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  74. package/rules/js-lint-ci/docs/fix.md +154 -0
  75. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  76. package/rules/js-mssql/docs/fix.md +128 -0
  77. package/rules/js-mssql/js/docs/deps.md +263 -0
  78. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  79. package/rules/js-run/docs/fix.md +144 -0
  80. package/rules/js-run/js/docs/runtime.md +388 -0
  81. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  82. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  83. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  84. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  85. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  86. package/rules/k8s/docs/fix.md +129 -0
  87. package/rules/k8s/js/docs/manifests.md +344 -0
  88. package/rules/k8s/js/manifests.mjs +6 -2
  89. package/rules/k8s/k8s.mdc +4 -2
  90. package/rules/k8s/lint/docs/lint.md +411 -0
  91. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  92. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  93. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  94. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  95. package/rules/npm-module/docs/fix.md +98 -0
  96. package/rules/npm-module/js/docs/package_structure.md +274 -0
  97. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  98. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  99. package/rules/php/docs/fix.md +107 -0
  100. package/rules/php/js/docs/tooling.md +152 -0
  101. package/rules/php/lint/docs/lint.md +215 -0
  102. package/rules/python/docs/fix.md +163 -0
  103. package/rules/python/js/docs/applies.md +108 -0
  104. package/rules/python/js/docs/tooling.md +153 -0
  105. package/rules/python/lint/docs/lint.md +322 -0
  106. package/rules/rego/docs/fix.md +121 -0
  107. package/rules/rego/js/docs/applies.md +174 -0
  108. package/rules/rego/js/docs/lint.md +118 -0
  109. package/rules/rego/lint/docs/lint.md +204 -0
  110. package/rules/release/docs/change.md +185 -0
  111. package/rules/release/docs/fix.md +119 -0
  112. package/rules/release/docs/release.md +222 -0
  113. package/rules/release/lib/docs/aggregate.md +246 -0
  114. package/rules/release/lib/docs/change-file.md +200 -0
  115. package/rules/release/lib/docs/fallback.md +203 -0
  116. package/rules/rust/docs/fix.md +129 -0
  117. package/rules/rust/js/docs/applies.md +140 -0
  118. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  119. package/rules/security/docs/fix.md +86 -0
  120. package/rules/security/js/docs/lint.md +171 -0
  121. package/rules/security/js/docs/sample_secret.md +190 -0
  122. package/rules/security/js/docs/trufflehog.md +137 -0
  123. package/rules/security/js/lint.mjs +9 -1
  124. package/rules/style-lint/docs/fix.md +155 -0
  125. package/rules/style-lint/js/docs/lint.md +184 -0
  126. package/rules/style-lint/js/docs/tooling.md +194 -0
  127. package/rules/tauri/docs/fix.md +158 -0
  128. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  129. package/rules/tauri/js/docs/tooling.md +228 -0
  130. package/rules/test/coverage/coverage.mjs +15 -3
  131. package/rules/test/docs/fix.md +132 -0
  132. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  135. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  136. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  137. package/rules/test/js/docs/location.md +136 -0
  138. package/rules/test/js/docs/no-process-chdir.md +160 -0
  139. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  140. package/rules/test/js/docs/stryker_config.md +152 -0
  141. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  142. package/rules/text/docs/fix.md +118 -0
  143. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  144. package/rules/text/js/docs/formatting.md +256 -0
  145. package/rules/text/js/docs/lint.md +122 -0
  146. package/rules/text/lint/docs/lint.md +220 -0
  147. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  148. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  149. package/rules/text/lint/docs/run-v8r.md +197 -0
  150. package/rules/vue/docs/fix.md +127 -0
  151. package/rules/vue/js/docs/packages.md +335 -0
  152. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  153. package/rules/worktree/docs/fix.md +161 -0
  154. package/schemas/rule-meta.json +5 -1
  155. package/scripts/auto-rules.mjs +7 -4
  156. package/scripts/coverage-classify/docs/apply.md +202 -0
  157. package/scripts/coverage-classify/docs/cache.md +203 -0
  158. package/scripts/coverage-classify/docs/index.md +218 -0
  159. package/scripts/coverage-classify/docs/prompt.md +132 -0
  160. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  161. package/scripts/coverage-fix-extract.mjs +122 -0
  162. package/scripts/coverage-fix.mjs +1 -1
  163. package/scripts/dispatcher/docs/graph.md +346 -0
  164. package/scripts/dispatcher/docs/index.md +236 -0
  165. package/scripts/dispatcher/docs/trace.md +296 -0
  166. package/scripts/dispatcher/index.mjs +1 -1
  167. package/scripts/dispatcher/lib/active.mjs +4 -8
  168. package/scripts/dispatcher/lib/commands.mjs +7 -11
  169. package/scripts/dispatcher/lib/docs/active.md +348 -0
  170. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  171. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  172. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  173. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  174. package/scripts/dispatcher/lib/docs/events.md +182 -0
  175. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  176. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  177. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  178. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  179. package/scripts/dispatcher/lib/docs/level.md +335 -0
  180. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  181. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  182. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  183. package/scripts/dispatcher/lib/docs/review.md +255 -0
  184. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  185. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  186. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  187. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  188. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  189. package/scripts/dispatcher/lib/executor.mjs +6 -1
  190. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  191. package/scripts/dispatcher/lib/level.mjs +29 -3
  192. package/scripts/dispatcher/lib/review.mjs +1 -1
  193. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  194. package/scripts/docs/auto-rules.md +376 -0
  195. package/scripts/docs/auto-skills.md +173 -0
  196. package/scripts/docs/build-agents-commands.md +183 -0
  197. package/scripts/docs/cli-entry.md +153 -0
  198. package/scripts/docs/coverage-fix.md +177 -0
  199. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  200. package/scripts/lib/changed-files.mjs +4 -1
  201. package/scripts/lib/docs/changed-files.md +149 -0
  202. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  203. package/scripts/lib/docs/check-reporter.md +175 -0
  204. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  205. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  206. package/scripts/lib/docs/ensure-tool.md +254 -0
  207. package/scripts/lib/docs/generated-markdown.md +275 -0
  208. package/scripts/lib/docs/gha-workflow.md +326 -0
  209. package/scripts/lib/docs/inline-template-links.md +303 -0
  210. package/scripts/lib/docs/list-rule-ids.md +156 -0
  211. package/scripts/lib/docs/load-cursor-config.md +147 -0
  212. package/scripts/lib/docs/mirror-parity.md +167 -0
  213. package/scripts/lib/worktree.mjs +26 -0
  214. package/scripts/worktree-cli.mjs +12 -2
  215. package/skills/coverage-fix/SKILL.md +34 -45
  216. package/skills/docgen/SKILL.md +44 -23
  217. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  218. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  219. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  220. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  221. package/skills/docgen/js/docgen-scan.mjs +37 -21
  222. package/skills/llm-patch/SKILL.md +23 -2
  223. package/skills/start-check/SKILL.md +26 -53
  224. package/skills/start-check/js/check.mjs +211 -0
  225. package/skills/taze/SKILL.md +9 -3
  226. package/skills/taze/js/diff.mjs +154 -0
  227. package/types/bin/n-cursor.d.ts +1 -1
  228. package/skills/fix-tests/SKILL.md +0 -119
  229. 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,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`).
@@ -0,0 +1,144 @@
1
+ # lint.mjs
2
+
3
+ ## Огляд
4
+
5
+ Файл `lint.mjs` — це CI-крок правила `js-lint-ci`, який реалізує крос-файлову статичну перевірку JavaScript-/TypeScript-кодової бази. Він послідовно запускає два інструменти, що працюють лише на рівні всього репозиторію (а не на рівні окремих файлів):
6
+
7
+ 1. **jscpd** — детектор дубльованого коду (copy-paste detector). Знаходить ідентичні або дуже схожі блоки коду між файлами.
8
+ 2. **knip** — детектор «мертвого» коду: невикористаних експортів, файлів, залежностей.
9
+
10
+ Модуль експортує одну функцію `lint`, яка ігнорує переданий список файлів (через природу інструментів — вони сканують усе репо) і повертає ненульовий код виходу при першому ж порушенні. Це класичний «fail-fast» патерн: якщо `jscpd` знайшов клони — `knip` навіть не запускається, репорт першого інструмента залишається на stdout/stderr CI-логу.
11
+
12
+ Файл є частиною контракту правил `n-cursor` для лінтингу: правило з суфіксом `-ci` означає, що воно виконується лише в `lint-ci`-фазі (на CI або при явному виклику), а не в інкрементальному per-file lint-сценарії.
13
+
14
+ ## Експорти / API
15
+
16
+ | Експорт | Тип | Призначення |
17
+ | ------- | ------------------------- | ------------------------------------------------------------------------- |
18
+ | `lint` | `function` (named export) | Єдина точка входу — виконує jscpd → knip і повертає підсумковий exit code |
19
+
20
+ Default-експорту немає. Контракт named-експорту `lint` — обов'язковий для всіх файлів `rules/<rule>/js/lint.mjs` у системі правил `n-cursor`.
21
+
22
+ ## Функції
23
+
24
+ ### `lint(_files, cwd)`
25
+
26
+ #### Сигнатура
27
+
28
+ ```js
29
+ export function lint(_files, cwd = process.cwd()): Promise<number>
30
+ ```
31
+
32
+ #### Параметри
33
+
34
+ | Параметр | Тип | За замовчуванням | Опис |
35
+ | -------- | ----------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36
+ | `_files` | `string[] \| undefined` | — | Список файлів для перевірки. **Ігнорується** — і jscpd, і knip працюють лише по всьому репозиторію. Префікс `_` у назві сигналізує, що аргумент не використовується. Згідно з коментарем-шапкою, функція викликається лише з `lint-ci` і завжди з `undefined`. |
37
+ | `cwd` | `string` | `process.cwd()` | Корінь репозиторію — робоча директорія, з якої запускаються `bunx jscpd .` та `bunx knip`. Усі шляхи всередині інструментів резолвляться відносно `cwd`. |
38
+
39
+ #### Що повертає
40
+
41
+ `Promise<number>` — резолвиться з exit code:
42
+
43
+ - `0` — обидва інструменти завершилися успішно, порушень не знайдено.
44
+ - Ненульове число (`jscpd.status` або `knip.status`) — перший інструмент, що завершився з помилкою, диктує підсумковий код виходу.
45
+ - `1` — fallback, коли `spawnSync` не повернув числовий `status` (наприклад, процес було вбито сигналом і `.status` дорівнює `null`).
46
+
47
+ Хоча функція синхронно виконує дочірні процеси через `spawnSync`, результат загорнуто у `Promise.resolve(...)` — це уніфікує контракт із асинхронними `lint`-функціями інших правил, де можуть використовуватись `spawn` чи інший асинхронний код.
48
+
49
+ #### Логіка крок-за-кроком
50
+
51
+ 1. Запустити `bunx jscpd .` у директорії `cwd` з `stdio: 'inherit'` — увесь вивід інструмента йде безпосередньо в термінал/CI-лог.
52
+ 2. Нормалізувати `jscpd.status`: якщо це число — взяти як є; інакше — `1`.
53
+ 3. Якщо `jc !== 0` — **рано вийти** з цим кодом, не запускаючи knip.
54
+ 4. Інакше — запустити `bunx knip --no-config-hints` (прапор пригнічує підказки про конфіг, щоб лог був чистішим).
55
+ 5. Повернути `Promise.resolve(knip.status ?? 1)` за тим же правилом нормалізації.
56
+
57
+ #### Side effects
58
+
59
+ - **Запуск дочірніх процесів**: `spawnSync('bunx', ...)` блокує поточний потік до завершення процесу. Це навмисно — `lint` повинен дочекатися результату й повернути exit code.
60
+ - **Запис у stdout/stderr**: завдяки `stdio: 'inherit'` весь вивід jscpd і knip потрапляє в термінал викликаючого процесу. Жодних буферизованих рядків `lint` не повертає.
61
+ - **Файлова система — лише читання**: ні jscpd, ні knip із наведеними прапорами не модифікують код; вони генерують репорти в stdout (jscpd за замовчуванням також може писати HTML-репорт у `report/` директорію — це визначається його конфігом, не цим файлом).
62
+ - **Залежності від оточення**: `bunx` повинен бути доступний у `PATH` процесу. Якщо `bunx` відсутній, `spawnSync` поверне `status: null` і `error` — функція віддасть `1`.
63
+
64
+ ## Залежності
65
+
66
+ ### Stdlib (Node.js / Bun)
67
+
68
+ - `node:child_process` — використовується іменований імпорт `spawnSync`. Це синхронний варіант `spawn`, який повертає об'єкт із полями `status`, `signal`, `stdout`, `stderr`, `error` після завершення дочірнього процесу.
69
+
70
+ ### Зовнішні CLI-інструменти (запускаються через `bunx`)
71
+
72
+ - **`jscpd`** — Copy/Paste Detector. Запускається з аргументом `.` (поточна директорія = `cwd`). Конфіг (правила обходу, пороги схожості, ігнор-патерни) інструмент шукає сам у `.jscpd.json`, `package.json#jscpd` тощо.
73
+ - **`knip`** — Detect unused files, dependencies and exports. Запускається з прапором `--no-config-hints`, який вимикає підказки про відсутній/неоптимальний конфіг.
74
+
75
+ ### Runtime-залежності
76
+
77
+ - **`bunx`** — раннер, що йде в комплекті з Bun і виконує бінарники з node_modules або тимчасово завантажує пакет, якщо його немає локально.
78
+
79
+ ### Не імпортується
80
+
81
+ - Жодних інших модулів проекту цей файл не використовує — він свідомо мінімалістичний і не залежить від інфраструктури `n-cursor` (логерів, конфіг-парсерів, fs-хелперів).
82
+
83
+ ## Потік виконання / Використання
84
+
85
+ ### Контекст виклику
86
+
87
+ Функція `lint` із цього файлу викликається механізмом правил `n-cursor` (зокрема, командою `lint-ci`). Конвенція така: для кожного правила `rules/<rule-id>/js/lint.mjs` рушій імпортує named export `lint(files, cwd)` і викликає його. Для правила `js-lint-ci` другий аргумент — корінь репо, а перший — завжди `undefined`, бо це крос-файловий аналіз.
88
+
89
+ ### Псевдокод виклику ззовні
90
+
91
+ ```js
92
+ import { lint } from './npm/rules/js-lint-ci/js/lint.mjs'
93
+
94
+ const code = await lint(undefined, '/path/to/repo')
95
+ if (code !== 0) process.exit(code)
96
+ ```
97
+
98
+ ### Сценарій 1: усе чисто
99
+
100
+ 1. `bunx jscpd .` → exit 0, у логи виводиться репорт без знайдених клонів.
101
+ 2. `bunx knip --no-config-hints` → exit 0, у логи — порожній/чистий звіт.
102
+ 3. `lint` повертає `Promise.resolve(0)`.
103
+ 4. CI-крок проходить.
104
+
105
+ ### Сценарій 2: jscpd знайшов клони
106
+
107
+ 1. `bunx jscpd .` → exit ≠ 0, у логи — список клонованих блоків.
108
+ 2. `lint` рано виходить із цим кодом, **knip не запускається**.
109
+ 3. CI-крок падає. Розробник бачить лише репорт jscpd.
110
+
111
+ ### Сценарій 3: jscpd чистий, knip знайшов мертвий код
112
+
113
+ 1. `bunx jscpd .` → exit 0.
114
+ 2. `bunx knip --no-config-hints` → exit ≠ 0, у логи — список невикористаних експортів/файлів.
115
+ 3. `lint` повертає цей код.
116
+ 4. CI-крок падає з репортом knip.
117
+
118
+ ### Сценарій 4: інструмент крашнувся (сигнал, відсутній bunx)
119
+
120
+ 1. `spawnSync(...).status` повертає `null` (`signal` буде заповнений).
121
+ 2. Перевірка `typeof ... === 'number'` дає `false` → fallback `1`.
122
+ 3. `lint` повертає `1`. CI бачить generic-fail без чіткого репорту — це слабке місце, але навмисно зведене до мінімуму.
123
+
124
+ ### Чому ігнорується `_files`
125
+
126
+ Файлово-орієнтовані аналізатори (eslint, stylelint, ...) працюють і per-file, і crosswise. Натомість `jscpd` і `knip` принципово міжфайлові: щоб знайти дубль або невикористаний експорт, треба бачити **всі** файли одночасно. Тому виконувати їх на підмножині не має сенсу — і per-file шлях для цього правила відсутній. Це підкреслено й у JSDoc, і в підкресленні `_` у назві аргумента.
127
+
128
+ ### Спостережувані виходи
129
+
130
+ - **Логи**: jscpd і knip пишуть напряму в stdio викликаючого процесу через `stdio: 'inherit'`. `lint` нічого не агрегує.
131
+ - **Exit code**: єдиний канал зворотного зв'язку від `lint` до раннера — числовий код.
132
+
133
+ ## Rebuild Test
134
+
135
+ Якщо реалізовувати файл наново за цією документацією, він має:
136
+
137
+ 1. Імпортувати `spawnSync` із `node:child_process`.
138
+ 2. Експортувати named function `lint(_files, cwd = process.cwd())`.
139
+ 3. Запустити `bunx jscpd .` синхронно з `cwd` і `stdio: 'inherit'`.
140
+ 4. Нормалізувати exit (`typeof status === 'number' ? status : 1`) і рано вийти при ≠ 0.
141
+ 5. Інакше — запустити `bunx knip --no-config-hints` за тим же протоколом і повернути нормалізований exit.
142
+ 6. Загорнути результат у `Promise.resolve(...)`.
143
+ 7. Ігнорувати перший аргумент (префікс `_`).
144
+ 8. Не імпортувати жодних інших модулів і не виконувати ніяких записів у файлову систему.