@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,160 @@
1
+ # `no-process-chdir.mjs`
2
+
3
+ ## Огляд
4
+
5
+ Модуль `no-process-chdir.mjs` — concern-перевірка (check) з набору правил `test.mdc`, яка **забороняє виклик `process.chdir(...)` у тестових файлах** (`*.test.js`, `*.test.mjs`) проєкту.
6
+
7
+ Мотивація закладена у заголовному JSDoc файлу та повторена в повідомленнях звіту:
8
+
9
+ - `process.chdir` — це **process-wide мутація** робочого каталогу.
10
+ - Vitest за замовчуванням використовує `pool: 'threads'`, у якому workers ділять **один процес**. Це означає, що паралельний `test file` може перехопити `cwd` сусіда посеред FS- або `git`-операції.
11
+ - Зафіксований реальний інцидент: `git init` + `git commit` із tmp-фікстури `withTmpCwd` потрапили у реальний робочий репозиторій і створили rogue-commit з автором `test <test@test>`.
12
+ - Канонічна заміна: `withTmpDir(async dir => …)` зі `scripts/utils/test-helpers.mjs` (без `chdir`), плюс **явні** `cwd: dir` у child-процесах та `await check(dir)` для concern-функцій.
13
+
14
+ Перевірка реалізована як один експортований асинхронний `check(cwd)`, придатний для запуску з runner-а check-репортера. Вона рекурсивно знаходить тестові файли (через `walkDir`), читає їх вміст і шукає викликний паттерн `process.chdir(`. На збігах формується список offenders, для кожного з яких репортер видає `fail`-повідомлення з шляхом і номером рядка; за відсутності збігів видається одне `pass`.
15
+
16
+ Регулярний вираз навмисно вимагає **відкривну дужку**, тому згадки `process.chdir` у коментарях/документації (наприклад, фраза «не використовуй `process.chdir`») **не тригерять** падіння.
17
+
18
+ ## Експорти / API
19
+
20
+ | Експорт | Тип | Призначення |
21
+ | ------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
+ | `check` | `async (cwdParam?: string) => Promise<number>` | Іменований експорт. Виконує перевірку всіх тестових файлів у заданому корені й повертає exit-код для CI: `0` — чисто, `1` — знайдено `process.chdir(` у тесті. |
23
+
24
+ Інших публічних експортів файл не містить. Модуль використовує ESM (`import`/`export`).
25
+
26
+ Окрім експорту, у модулі визначені **внутрішні** елементи (не експортуються):
27
+
28
+ - Константа `CHDIR_CALL_RE` — `RegExp`.
29
+ - Функція `isTestFile(absPath)` — допоміжна (file-private).
30
+
31
+ ## Функції
32
+
33
+ ### `isTestFile(absPath)`
34
+
35
+ Внутрішня (не експортована) функція-предикат.
36
+
37
+ - **Сигнатура:** `function isTestFile(absPath: string): boolean`
38
+ - **Параметри:**
39
+ - `absPath` (`string`) — абсолютний шлях до файлу-кандидата (передається з `walkDir`).
40
+ - **Повертає:** `boolean` — `true`, якщо `basename(absPath)` закінчується на `.test.mjs` або `.test.js`; інакше `false`.
41
+ - **Side effects:** немає (чиста функція; читає лише вхідний рядок через `basename` з `node:path`).
42
+ - **Логіка:**
43
+ 1. Бере `basename(absPath)` через `node:path`.
44
+ 2. Перевіряє суфікси `.test.mjs` і `.test.js` через `String.prototype.endsWith`.
45
+ 3. Повертає диз'юнкцію цих перевірок.
46
+
47
+ ### `check(cwdParam = process.cwd())`
48
+
49
+ Головна (і єдина) експортована функція concern-у.
50
+
51
+ - **Сигнатура:** `async function check(cwdParam?: string): Promise<number>`
52
+ - **Параметри:**
53
+ - `cwdParam` (`string`, опціональний) — корінь репозиторію/проєкту, який потрібно перевіряти. Якщо не передано — використовується `process.cwd()`. У документації заголовка та в повідомленнях про інцидент рекомендується **завжди передавати явний `dir`** з тестового середовища (через `await check(dir)`), щоб не залежати від фактичного `process.cwd()` процесу-runner-а.
54
+ - **Повертає:** `Promise<number>` — exit-код, отриманий від `reporter.getExitCode()`:
55
+ - `0` — у жодному тестовому файлі не знайдено виклику `process.chdir(`.
56
+ - `1` — знайдено хоча б один виклик; усі offenders повідомлені як `fail`.
57
+ - **Side effects:**
58
+ - Звертається до файлової системи: рекурсивний обхід через `walkDir` та `readFile` для кожного знайденого тестового файлу.
59
+ - Викликає функції `pass`/`fail` репортера (`createCheckReporter`), які типово друкують повідомлення у `stdout`/`stderr` (поведінка інкапсульована у `check-reporter`).
60
+ - Читає `.n-cursor.json` (через `loadCursorIgnorePaths`) для отримання списку ігнорованих шляхів.
61
+ - Не змінює файлову систему й не змінює `cwd` процесу.
62
+ - **Алгоритм:**
63
+ 1. Створює репортер: `const reporter = createCheckReporter()`; деструктурує `pass` і `fail`.
64
+ 2. Зберігає `cwd = cwdParam` (локально, без побічних ефектів на процес).
65
+ 3. Завантажує список ignore-шляхів: `ignorePaths = await loadCursorIgnorePaths(cwd)`.
66
+ 4. Готує локальний акумулятор `testFiles: string[]` і викликає `walkDir(cwd, callback, ignorePaths)`. У callback-у — якщо `isTestFile(absPath)`, додає шлях у масив.
67
+ 5. Готує акумулятор `offenders: Array<{file: string, line: number}>`.
68
+ 6. Для кожного `absPath` із `testFiles`:
69
+ - Читає вміст: `body = await readFile(absPath, 'utf8')`.
70
+ - Якщо у тілі немає збігу з `CHDIR_CALL_RE` — `continue` (швидкий вихід без побудови `lines`).
71
+ - Інакше розбиває тіло на рядки `body.split('\n')` і для кожного рядка `(i, line)` перевіряє `CHDIR_CALL_RE.test(line)`. На збігу — `offenders.push({ file: relative(cwd, absPath), line: i + 1 })` (номер рядка — 1-базований).
72
+ 7. Якщо `offenders.length === 0` — викликає `pass(`Жоден з ${testFiles.length} тестових файлів не викликає process.chdir() (test.mdc)`)` і повертає `reporter.getExitCode()`.
73
+ 8. Інакше — для кожного offender викликає `fail` з повідомленням формату `${file}:${line}: process.chdir() у тесті заборонений — використовуй withTmpDir(async dir => …) + явні join(dir, …) + cwd: dir (test.mdc)` і у фіналі повертає `reporter.getExitCode()`.
74
+
75
+ ## Константи і регулярні вирази
76
+
77
+ ### `CHDIR_CALL_RE`
78
+
79
+ ```js
80
+ const CHDIR_CALL_RE = /process\.chdir\s*\(/u
81
+ ```
82
+
83
+ - **Призначення:** виявити **викликний** паттерн `process.chdir(`.
84
+ - **Семантика:**
85
+ - `process\.chdir` — буквальне ім'я виклику (крапка екранована).
86
+ - `\s*` — допускає будь-яку кількість пробільних символів між ідентифікатором і дужкою.
87
+ - `\(` — обов'язкова відкривна дужка; саме вона відрізняє виклик від згадки в коментарі/документації.
88
+ - Прапор `u` — Unicode-режим.
89
+ - **Наслідки дизайну:**
90
+ - Згадки `process.chdir` у JSDoc/коментарях без `(` **не** дають збігу (це навмисно і задокументовано).
91
+ - Регулярка лінійна, без backtracking-ризиків; виконується спочатку проти всього `body` (швидкий short-circuit), потім — порядково для локалізації номера рядка.
92
+
93
+ ## Залежності
94
+
95
+ ### Зовнішні (стандартна бібліотека Node)
96
+
97
+ - `node:fs/promises` → іменований імпорт `readFile` — асинхронне читання тестових файлів у режимі `'utf8'`.
98
+ - `node:path` → іменовані імпорти:
99
+ - `basename` — у `isTestFile` для перевірки суфікса.
100
+ - `relative` — для перетворення абсолютного шляху offender-а в шлях, відносний до `cwd`, перед публікацією у `fail`-повідомленні.
101
+
102
+ ### Внутрішні (репозиторій)
103
+
104
+ Усі — відносні шляхи від `npm/rules/test/js/no-process-chdir.mjs`:
105
+
106
+ - `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` — фабрика репортера; повертає об'єкт з методами `pass`, `fail` і `getExitCode()`. Інкапсулює формат виведення й підрахунок exit-коду.
107
+ - `../../../scripts/lib/load-cursor-config.mjs` → `loadCursorIgnorePaths` — повертає список ігнорованих шляхів із `.n-cursor.json` (поле `ignore`), які треба передати у `walkDir` як другий рівень фільтрації (на додаток до вбудованих скіпів самого `walkDir`).
108
+ - `../../../scripts/utils/walkDir.mjs` → `walkDir` — рекурсивний обхід дерева; за заголовним коментарем модуля вбудовано пропускає `node_modules`, `.git`, `dist`, `build`, `.venv`, `venv`. Викликає callback з абсолютним шляхом для кожного відвіданого файлу.
109
+
110
+ ## Потік виконання / Використання
111
+
112
+ ### Запуск як частина check-набору
113
+
114
+ Модуль не виконується автоматично при імпорті — це **бібліотечний** файл concern-а: він експортує `check`, який має бути викликаний runner-ом (наприклад, агрегатором правил `test.mdc`). Типовий виклик:
115
+
116
+ ```js
117
+ import { check } from './npm/rules/test/js/no-process-chdir.mjs'
118
+
119
+ const code = await check(process.cwd())
120
+ process.exit(code)
121
+ ```
122
+
123
+ Або з явним коренем (як рекомендовано у заголовному JSDoc — для concern-функцій передавати `dir`, а не покладатися на process-wide `cwd`):
124
+
125
+ ```js
126
+ await check(dir) // dir отриманий з withTmpDir або з runner-а
127
+ ```
128
+
129
+ ### Послідовність кроків під час одного виклику
130
+
131
+ 1. `createCheckReporter()` створює пристрій для накопичення pass/fail повідомлень.
132
+ 2. `loadCursorIgnorePaths(cwd)` читає `.n-cursor.json` (поле `ignore`) у корені проєкту.
133
+ 3. `walkDir(cwd, callback, ignorePaths)` обходить дерево, пропускаючи стандартні теки (`node_modules`, `.git`, `dist`, `build`, `.venv`, `venv`) і шляхи з ignore-списку. Колбек збирає шляхи, що задовольняють `isTestFile`.
134
+ 4. Для кожного зібраного тестового файлу:
135
+ - Швидкий тест регуляркою проти повного тексту — для більшості файлів ціна перевірки = `O(N)` без подальшої роботи.
136
+ - Якщо є збіг, файл розбивається на рядки, і для кожного рядка з виявленим паттерном до `offenders` додається `{file, line}` (шлях нормалізовано через `relative(cwd, absPath)`, номер рядка — 1-базований).
137
+ 5. Формування результату:
138
+ - Порожній `offenders` → одна `pass`-нотатка з кількістю просканованих файлів.
139
+ - Непорожній — окремий `fail` на кожен `{file, line}` з підказкою про канонічну заміну.
140
+ 6. Повертається `reporter.getExitCode()` — `0` або `1`.
141
+
142
+ ### Поведінка у crash-/inconsistent-сценаріях
143
+
144
+ - `readFile` з режимом `'utf8'` для бінарних/неіснуючих файлів кине помилку — обхідник `walkDir` має фільтрувати по типу файлу/доступу, але всередині `check` блоків `try/catch` немає, тож помилка спливе у викликача.
145
+ - `loadCursorIgnorePaths`/`walkDir`/`createCheckReporter` помилки також пробрасуються у викликача.
146
+ - Модуль не модифікує глобальний стан і не змінює `cwd` процесу.
147
+
148
+ ### Як виглядає звіт
149
+
150
+ - **Pass:** `Жоден з N тестових файлів не викликає process.chdir() (test.mdc)` (де `N` — кількість просканованих тест-файлів).
151
+ - **Fail** (по одному рядку на кожен збіг): `path/to/file.test.mjs:123: process.chdir() у тесті заборонений — використовуй withTmpDir(async dir => …) + явні join(dir, …) + cwd: dir (test.mdc)`.
152
+
153
+ ### Канонічна заміна `process.chdir` у тестах
154
+
155
+ Згідно з повідомленнями репортера і коментарем у заголовку:
156
+
157
+ - Використовувати `withTmpDir(async dir => …)` зі `scripts/utils/test-helpers.mjs`.
158
+ - У FS-операціях формувати шляхи через `join(dir, …)` явно.
159
+ - У викликах child-процесів передавати `cwd: dir` як опцію.
160
+ - У concern-функціях — приймати `cwd`/`dir` параметром і не покладатися на `process.cwd()`.
@@ -0,0 +1,271 @@
1
+ # no-relative-fs-path.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль реалізує AST-based перевірку, яка забороняє передавати **relative-path**
6
+ аргументи у функції модулів `node:fs` / `node:fs/promises` (а також у тестові
7
+ helper-функції, що вимагають абсолютних шляхів) усередині JS-тестів
8
+ (`*.test.mjs` / `*.test.js`).
9
+
10
+ Мотивація задокументована у самому файлі та у правилі `test.mdc`, секція
11
+ «Заборона `process.chdir` у тестах»: після видалення хелпера `withTmpCwd` усі
12
+ тести отримують `dir` параметром і повинні будувати **абсолютні** шляхи через
13
+ `join(dir, …)`. Якщо хтось забуде префікс і напише, наприклад,
14
+ `writeFile('foo.json', …)` або `copyFile(src, 'foo.json')`, relative-path
15
+ зарезолвиться у `process.cwd()` (= `npm/`), що призведе до запису тестової
16
+ фікстури у production tree. Реальний інцидент v1.28.0
17
+ (`tests/check-rule-fixtures.test.mjs` із викликами
18
+ `copyFile(src, 'values-dev.ini')` та
19
+ `copyFile(src, 'default.conf.template')`) створив файли `npm/values-dev.ini` і
20
+ `npm/default.conf.template` поза `dir`.
21
+
22
+ Сканер парсить кожен тестовий файл через `oxc-parser` (через утиліту
23
+ `parseProgramOrNull`), обходить AST і шукає `CallExpression`, де callee
24
+ збігається з відомою FS-функцією, а path-аргумент є **string literal** (або
25
+ template literal без виразів), що НЕ починається з:
26
+
27
+ - `/`, `\\` — POSIX/Windows absolute;
28
+ - `file:`, `http:`, `https:`, `data:` — URL-схема (для `new URL(...)`);
29
+ - Windows drive letter `C:\…` або `C:/…`;
30
+ - та НЕ є template literal зі вставленим виразом `${…}` (такі вважаються
31
+ обчисленими через `join`/`resolve` і пропускаються).
32
+
33
+ Виклики, у яких path-аргумент НЕ literal (наприклад, `join(...)`,
34
+ `BinaryExpression`, `Identifier`, `MemberExpression`), пропускаються —
35
+ припускається, що це абсолютний шлях.
36
+
37
+ Перевіряються лише файли, що відповідають `**/*.test.{js,mjs}`. Обхід дерева
38
+ використовує загальний `walkDir` з його скіп-правилами та користувацькими
39
+ `ignore`-шляхами з `.n-cursor.json`.
40
+
41
+ ## Експорти / API
42
+
43
+ | Експорт | Тип | Призначення |
44
+ | ------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------- |
45
+ | `check(cwdParam?)` | `async function` | Точка входу перевірки. Сканує `*.test.{mjs,js}` під `cwd`, повертає exit code `0` (чисто) або `1` (є порушення). |
46
+
47
+ Усі решта функцій (`extractRelativeLiteralPath`, `isRelativeString`,
48
+ `extractFsFunctionName`, `isTestFile`, `findOffendersInBody`,
49
+ `computeLineOffsets`, `offsetToLineFromCache`) та константи
50
+ (`FS_PATH_ARG_POSITIONS`, `ABSOLUTE_PREFIXES`) — module-private, не
51
+ експортуються.
52
+
53
+ ## Функції
54
+
55
+ ### `extractRelativeLiteralPath(arg)`
56
+
57
+ - **Сигнатура:** `(arg: object) => string | null`
58
+ - **Параметри:**
59
+ - `arg` — AST node аргументу виклику (Literal, TemplateLiteral тощо) або
60
+ `undefined`.
61
+ - **Повертає:** значення relative-path (рядок) — якщо аргумент є
62
+ string-літералом або template literal без виразів і його значення є
63
+ відносним; `null` — якщо аргумент відсутній, обчислюваний або абсолютний.
64
+ - **Логіка:**
65
+ - `arg.type === 'Literal' && typeof arg.value === 'string'` →
66
+ `isRelativeString(arg.value) ? arg.value : null`.
67
+ - `arg.type === 'TemplateLiteral' && expressions.length === 0` →
68
+ конкатенує `quasis[i].value.cooked` і так само перевіряє через
69
+ `isRelativeString`.
70
+ - Будь-який інший вузол → `null` (не аналізується).
71
+ - **Side effects:** немає.
72
+
73
+ ### `isRelativeString(s)`
74
+
75
+ - **Сигнатура:** `(s: string) => boolean`
76
+ - **Параметри:** `s` — рядок-шлях.
77
+ - **Повертає:** `true` — якщо рядок виглядає як relative path; `false` — якщо
78
+ абсолютний, URL, Windows-drive-letter або порожній.
79
+ - **Логіка:**
80
+ - Порожній рядок → `false` (не path).
81
+ - Якщо починається з будь-якого з `ABSOLUTE_PREFIXES`
82
+ (`/`, `\\`, `file:`, `http:`, `https:`, `data:`) → `false`.
83
+ - Якщо відповідає `^[A-Za-z]:[\\/]/u` (наприклад, `C:\foo`, `C:/foo`) →
84
+ `false`.
85
+ - Інакше → `true`.
86
+ - **Side effects:** немає.
87
+
88
+ ### `extractFsFunctionName(callee)`
89
+
90
+ - **Сигнатура:** `(callee: object) => string | null`
91
+ - **Параметри:** `callee` — AST callee node (Identifier або MemberExpression).
92
+ - **Повертає:** ім'я FS-функції з `FS_PATH_ARG_POSITIONS`, якщо callee
93
+ розпізнано; інакше `null`.
94
+ - **Логіка:**
95
+ - `Identifier` → перевіряє `callee.name` у `FS_PATH_ARG_POSITIONS`.
96
+ - `MemberExpression`, не `computed`, `property.type === 'Identifier'` →
97
+ бере `callee.property.name` (це покриває `fs.writeFile`, `fsp.writeFile`,
98
+ `fs.promises.writeFile` тощо) і перевіряє у мапі.
99
+ - Інакше → `null`.
100
+ - **Side effects:** немає.
101
+
102
+ ### `isTestFile(absPath)`
103
+
104
+ - **Сигнатура:** `(absPath: string) => boolean`
105
+ - **Параметри:** `absPath` — абсолютний шлях файлу.
106
+ - **Повертає:** `true`, якщо `basename(absPath)` закінчується на
107
+ `.test.mjs` або `.test.js`; інакше `false`.
108
+ - **Side effects:** немає.
109
+
110
+ ### `findOffendersInBody(body)`
111
+
112
+ - **Сигнатура:**
113
+ `(body: string) => Array<{ line: number, fn: string, path: string, argPos: number }>`
114
+ - **Параметри:** `body` — вміст тестового файлу (UTF-8).
115
+ - **Повертає:** масив порушень: `{ line, fn, path, argPos }`, де:
116
+ - `line` — 1-індексований рядок початку аргументу (або виклику, якщо у
117
+ аргументу немає `.start`);
118
+ - `fn` — ім'я FS-функції;
119
+ - `path` — фактичне значення relative-літералу;
120
+ - `argPos` — 0-індексована позиція проблемного аргументу.
121
+ - **Логіка:**
122
+ 1. `parseProgramOrNull(body, 'test.mjs')` — парс через oxc-parser із
123
+ використанням віртуального імені; якщо парс не вдався, повертає `[]`.
124
+ 2. Кешує newline-offsets через `computeLineOffsets`.
125
+ 3. `walkAstWithAncestors(program, [], cb)` — обходить усі вузли.
126
+ Для кожного `CallExpression`:
127
+ - визначає `fnName = extractFsFunctionName(node.callee)`; якщо `null` —
128
+ пропускає;
129
+ - для кожної позиції з `FS_PATH_ARG_POSITIONS.get(fnName)` бере
130
+ `node.arguments[pos]`, перевіряє через `extractRelativeLiteralPath`;
131
+ - якщо `relPath !== null` — обчислює `line` через
132
+ `offsetToLineFromCache(lineOffsets, arg?.start ?? node.start ?? 0)` і
133
+ додає об'єкт у `offenders`.
134
+ - **Side effects:** немає. Парсинг через `parseProgramOrNull` сам по собі
135
+ чистий.
136
+
137
+ ### `computeLineOffsets(body)`
138
+
139
+ - **Сигнатура:** `(body: string) => number[]`
140
+ - **Параметри:** `body` — джерельний рядок.
141
+ - **Повертає:** масив 0-індексованих offset-ів початків рядків
142
+ (елемент `0` — позиція `0`, далі позиція кожного символу після `\n`).
143
+ - **Логіка:** лінійний прохід по символах; на кожному `\n` додає `pos + 1`.
144
+ - **Side effects:** немає.
145
+
146
+ ### `offsetToLineFromCache(offsets, offset)`
147
+
148
+ - **Сигнатура:** `(offsets: number[], offset: number) => number`
149
+ - **Параметри:**
150
+ - `offsets` — кеш із `computeLineOffsets`;
151
+ - `offset` — 0-індекс символу у source.
152
+ - **Повертає:** 1-індексований номер рядка, що містить цей offset.
153
+ - **Логіка:** бінарний пошук правого діапазону (`lo`/`hi` із кроком
154
+ `mid = floor((lo + hi + 1) / 2)`); кінцевий `lo + 1` — номер рядка.
155
+ - **Side effects:** немає.
156
+
157
+ ### `check(cwdParam = process.cwd())`
158
+
159
+ - **Сигнатура:** `async (cwdParam?: string) => Promise<number>`
160
+ - **Параметри:** `cwdParam` — корінь репозиторію (за замовчуванням
161
+ `process.cwd()`).
162
+ - **Повертає:** `0` — порушень немає; `1` — є хоча б одне порушення
163
+ (через `reporter.getExitCode()`).
164
+ - **Логіка:**
165
+ 1. Створює репортер: `createCheckReporter()`, дістає `pass` і `fail`.
166
+ 2. Завантажує користувацькі ignore-шляхи через `loadCursorIgnorePaths(cwd)`
167
+ (читає `.n-cursor.json#ignore`).
168
+ 3. Через `walkDir(cwd, cb, ignorePaths)` збирає у масив `testFiles` усі
169
+ `absPath`, для яких `isTestFile(absPath) === true`.
170
+ 4. Для кожного тестового файлу: читає через
171
+ `readFile(absPath, 'utf8')`, запускає `findOffendersInBody(body)`, до
172
+ кожного знахідки додає `file: relative(cwd, absPath)`.
173
+ 5. Якщо `offenders.length === 0` — викликає
174
+ `pass(\`Жоден з ${testFiles.length} тестових файлів не передає relative-path у FS-функції (test.mdc)\`)`і повертає`reporter.getExitCode()`.
175
+ 6. Інакше — для кожного порушення викликає `fail(...)` із повідомленням
176
+ виду `${file}:${line}: ${fn}() — ${which} '${path}' relative; використовуй join(dir, …) (test.mdc, no-relative-fs-path)`,
177
+ де `which` = `'1-й аргумент'` для `argPos === 0` і
178
+ `'${argPos + 1}-й аргумент'` для решти.
179
+ - **Side effects:**
180
+ - Читає файли з диска (через `readFile` та `walkDir`).
181
+ - Пише у stdout/stderr через репортер (`pass`/`fail`).
182
+ - Не змінює файли.
183
+
184
+ ## Константи
185
+
186
+ ### `FS_PATH_ARG_POSITIONS`
187
+
188
+ `Map<string, number[]>` — імена FS-функцій → масив 0-індексованих позицій
189
+ path-аргументів. Включає:
190
+
191
+ - з одним path-аргументом (`[0]`): `writeFile`, `writeFileSync`, `readFile`,
192
+ `readFileSync`, `appendFile`, `appendFileSync`, `mkdir`, `mkdirSync`,
193
+ `rmdir`, `rmdirSync`, `rm`, `rmSync`, `unlink`, `unlinkSync`, `access`,
194
+ `accessSync`, `stat`, `statSync`, `lstat`, `lstatSync`, `chmod`,
195
+ `chmodSync`, `chown`, `chownSync`, `truncate`, `truncateSync`,
196
+ `existsSync`, `readdir`, `readdirSync`;
197
+ - з двома path-аргументами (`[0, 1]`): `copyFile`, `copyFileSync`, `rename`,
198
+ `renameSync`, `symlink`, `symlinkSync`, `link`, `linkSync`, `cp`, `cpSync`;
199
+ - тестові-хелпери (зайвий захист, тільки 1-й): `writeJson`, `ensureDir`.
200
+
201
+ ### `ABSOLUTE_PREFIXES`
202
+
203
+ `string[]` зі значенням `['/', '\\', 'file:', 'http:', 'https:', 'data:']` —
204
+ префікси, які вважаються «явно абсолютним або URL-шляхом» і виключають
205
+ рядок зі списку relative-path.
206
+
207
+ ## Залежності
208
+
209
+ Зовнішні / стандартні модулі:
210
+
211
+ - `node:fs/promises` → `readFile` — читання тестових файлів.
212
+ - `node:path` → `basename`, `relative` — визначення імені файлу та шляху
213
+ відносно `cwd` для повідомлень.
214
+
215
+ Внутрішні модулі (відносні шляхи у репозиторії):
216
+
217
+ - `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` — стандартний
218
+ репортер перевірок (методи `pass`, `fail`, `getExitCode`).
219
+ - `../../../scripts/lib/load-cursor-config.mjs` → `loadCursorIgnorePaths` —
220
+ читає `.n-cursor.json#ignore` для виключення шляхів.
221
+ - `../../../scripts/utils/ast-scan-utils.mjs` → `parseProgramOrNull`,
222
+ `walkAstWithAncestors` — обгортка над oxc-parser і AST-обхід з
223
+ трекінгом ancestors (тут ancestors не використовуються — `[]`).
224
+ - `../../../scripts/utils/walkDir.mjs` → `walkDir` — рекурсивний обхід
225
+ директорії із загальними skip-правилами та підтримкою `ignorePaths`.
226
+
227
+ ## Потік виконання / Використання
228
+
229
+ Файл реалізує одну з перевірок з папки `npm/rules/test/js/`, що
230
+ викликається через загальний механізм запуску правил (зазвичай
231
+ `bun n-cursor rules` або еквівалентну команду). Загальний потік виклику
232
+ `check(cwd)`:
233
+
234
+ 1. Створення `reporter` через `createCheckReporter()`.
235
+ 2. Завантаження `ignorePaths` з `.n-cursor.json`.
236
+ 3. `walkDir(cwd, ..., ignorePaths)` рекурсивно проходить дерево;
237
+ collector-callback відбирає лише файли, що задовольняють `isTestFile`.
238
+ 4. Для кожного тестового файлу:
239
+ - читається вміст;
240
+ - `parseProgramOrNull` намагається спарсити; при невдачі файл
241
+ мовчазно пропускається (`return []`);
242
+ - `walkAstWithAncestors` обходить AST і кожен `CallExpression`
243
+ перевіряється проти `FS_PATH_ARG_POSITIONS`;
244
+ - кожен виявлений relative-path-літерал перетворюється на запис
245
+ `offender` з `file`/`line`/`fn`/`path`/`argPos`.
246
+ 5. Якщо порушень немає — викликається `pass(...)` і
247
+ повертається `reporter.getExitCode()` (зазвичай `0`).
248
+ 6. Якщо є — кожне порушення емітиться через `fail(...)` із заздалегідь
249
+ сформатованим повідомленням; підсумковий exit code — `1`.
250
+
251
+ Типове використання — як check-функція у конвеєрі правил `test.mdc`
252
+ (адже повідомлення містять згадку `(test.mdc, no-relative-fs-path)`),
253
+ де `cwd` — корінь монорепо. Файл є частиною підкаталогу
254
+ `npm/rules/test/js/` (правила, що відповідають `test.mdc`).
255
+
256
+ Edge-cases:
257
+
258
+ - Невалідний JS → `parseProgramOrNull` повертає `null` → файл пропускається
259
+ без помилки.
260
+ - Шлях обчислюється через `join`/`resolve` → пропускається (припускається
261
+ абсолютний).
262
+ - Template literal зі вставленим виразом (`expressions.length > 0`) →
263
+ пропускається.
264
+ - `existsSync` та інші sync-варіанти аналізуються нарівні з async.
265
+ - Виклики через `fs.promises.X` — обробляються коректно, бо
266
+ `extractFsFunctionName` бере `callee.property.name` і не залежить від
267
+ глибини доступу.
268
+
269
+ Інтеграція з CI: повертає ненульовий код у разі порушень → провалює
270
+ відповідний крок CI; формат повідомлень містить `file:line: …`, що
271
+ розпізнають IDE/CI-інтерфейси.
@@ -0,0 +1,152 @@
1
+ # stryker_config.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `stryker_config` — це концерн (check) правила `test` (відповідає правилу `test.mdc` із набору `.cursor/rules/`). Його роль — гарантувати, що в усіх JS-roots проєкту присутні canonical baseline-файли конфігурації для mutation testing (Stryker) і unit-test runner-а (Vitest), а також що тестові артефакти не потрапляють у git.
6
+
7
+ Концерн виконує такі задачі:
8
+
9
+ - Self-gating: якщо в `.n-cursor.json` правило `js-lint` не включене або явно вимкнене (`disable-rules`), концерн нічого не робить і тихо повертає успіх.
10
+ - Визначення всіх JS-roots проєкту — кожен workspace із `package.json` у monorepo, або сам `cwd` у single-package режимі.
11
+ - Для кожного JS-root перевіряє наявність файлу `stryker.config.mjs`; якщо його немає — копіює canonical baseline з пакета. Для roots із Vue 3 SFC-файлами (`<script setup>`) використовує спеціальний vue-варіант baseline і додатково копіює локальний Stryker-плагін `stryker-vue-macros-ignorer.mjs`.
12
+ - Аналогічно для `vitest.config.js`.
13
+ - Додає в кореневий `.gitignore` патерни `**/reports/stryker/` і `**/coverage/`, щоб тестові артефакти не комітилися.
14
+
15
+ Файл є idempotent: повторні запуски на тому ж дереві не змінюють вже існуючі конфіги і не дублюють `.gitignore`-патерни.
16
+
17
+ ## Експорти / API
18
+
19
+ Модуль експортує одну іменовану асинхронну функцію:
20
+
21
+ - `check(cwd?: string): Promise<number>` — entry-point концерна. Повертає exit code: `0` (успіх або silently skipped), `1` (порушення).
22
+
23
+ Інші функції модуля (`hasVueFiles`, `ensureBaselineFile`) є внутрішніми і не експортуються.
24
+
25
+ ## Функції
26
+
27
+ ### `hasVueFiles(jsRoot)`
28
+
29
+ ```js
30
+ async function hasVueFiles(jsRoot)
31
+ ```
32
+
33
+ - **Параметри:**
34
+ - `jsRoot` (`string`) — абсолютний шлях до workspace-каталогу (JS-root).
35
+ - **Повертає:** `Promise<boolean>` — `true`, якщо в межах `<jsRoot>/src/**/*.vue` (виключаючи `node_modules`, `dist`, `reports`) знайдено хоча б один `.vue`-файл; інакше `false`.
36
+ - **Поведінка:** використовує `node:fs/promises#glob` із patterns `VUE_GLOB_PATTERN = 'src/**/*.vue'` і ignore-списком `VUE_GLOB_IGNORE = ['**/node_modules/**', '**/dist/**', '**/reports/**']`. Як тільки знаходить перший збіг — миттєво повертає `true` через `return` у тілі циклу `for await…of`, тому повний обхід директорії не виконується.
37
+ - **Side effects:** немає (read-only filesystem операція).
38
+
39
+ ### `ensureBaselineFile(reporter, cwd, baselinePath, target, label)`
40
+
41
+ ```js
42
+ async function ensureBaselineFile(reporter, cwd, baselinePath, target, label)
43
+ ```
44
+
45
+ - **Параметри:**
46
+ - `reporter` (`ReturnType<typeof createCheckReporter>`) — check-reporter для логування статусів pass/fail.
47
+ - `cwd` (`string`) — корінь проєкту, використовується для побудови relative-шляхів у повідомленнях.
48
+ - `baselinePath` (`string`) — абсолютний шлях до canonical baseline-файла у пакеті.
49
+ - `target` (`string`) — абсолютний шлях, куди копіювати baseline.
50
+ - `label` (`string`) — людиночитна мітка для логу (наприклад, `"stryker.config.mjs"` чи `"vitest.config.js"`).
51
+ - **Повертає:** `Promise<void>`.
52
+ - **Поведінка:** якщо `target` вже існує (`existsSync`) — логує `reporter.pass` із позначкою "існує" і виходить. Якщо ні — копіює `baselinePath` у `target` через `copyFile` і логує `reporter.pass` із поміткою "створено з canonical baseline ... (test.mdc)".
53
+ - **Side effects:** запис файлу через `copyFile` (тільки якщо target був відсутній); запис у reporter.
54
+ - **Idempotent:** так — повторний виклик на існуючому target не перезаписує файл.
55
+
56
+ ### `check(cwd = process.cwd())`
57
+
58
+ ```js
59
+ export async function check(cwd = process.cwd())
60
+ ```
61
+
62
+ - **Параметри:**
63
+ - `cwd` (`string`, опціональний) — корінь проєкту. За замовчуванням `process.cwd()` для CLI-сумісності.
64
+ - **Повертає:** `Promise<number>` — exit code (`0` — OK або silently skipped, `1` — порушення).
65
+ - **Side effects:**
66
+ - Читає `.n-cursor.json` через `readNCursorConfigLite`.
67
+ - Викликає `resolveAllJsRoots` для отримання списку JS-roots.
68
+ - Перевіряє існування canonical baseline-файлів у самому пакеті.
69
+ - Копіює `stryker.config.mjs` (canonical або vue-варіант), `vitest.config.js`, а для Vue-roots — `stryker-vue-macros-ignorer.mjs` у кожен JS-root, де відповідний файл відсутній.
70
+ - Додає у кореневий `.gitignore` патерни `**/reports/stryker/` і `**/coverage/`, якщо їх там немає, через `ensureGitignoreEntries` із коментарем-секцією `"Test artifacts: Stryker + coverage (test.mdc)"`.
71
+ - Накопичує статуси у reporter і повертає його exit code.
72
+
73
+ ## Залежності
74
+
75
+ Стандартні модулі Node.js:
76
+
77
+ - `node:fs` — `existsSync` (синхронна перевірка наявності файла).
78
+ - `node:fs/promises` — `copyFile` (копіювання baseline у target), `glob` (пошук `.vue` файлів).
79
+ - `node:path` — `dirname`, `join`, `relative` (побудова абсолютних і relative шляхів).
80
+ - `node:url` — `fileURLToPath` (конвертація `import.meta.url` у POSIX-шлях для `dirname`).
81
+
82
+ Внутрішні залежності пакета:
83
+
84
+ - `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` — фабрика reporter-а з API `pass(msg)`, `fail(msg)`, `getExitCode()`.
85
+ - `../../../scripts/lib/read-n-cursor-config-lite.mjs` → `readNCursorConfigLite` — читання `.n-cursor.json`, повертає об'єкт із полями `rules: string[]`, `disableRules: string[]`.
86
+ - `../../../scripts/utils/ensure-gitignore-entries.mjs` → `ensureGitignoreEntries(cwd, entries, sectionLabel)` — idempotent додавання патернів у кореневий `.gitignore`. Повертає `{ added: string[] }`.
87
+ - `../../../scripts/utils/resolve-js-root.mjs` → `resolveAllJsRoots(cwd)` — повертає масив абсолютних шляхів до всіх JS-roots: усі workspaces із `package.json` у monorepo, або `[cwd]` у single-package.
88
+
89
+ Зовнішні дані-файли (canonical baseline у пакеті `@nitra/cursor`):
90
+
91
+ - `data/stryker_config/stryker.config.baseline.mjs` — стандартний baseline Stryker (vitest-runner + perTest, mutate-патерни на Stryker defaults `src/**/*.{js,mjs,ts,jsx,tsx,cjs}`).
92
+ - `data/stryker_config/stryker.config.vue.baseline.mjs` — vue-варіант baseline; реєструє локальний Ignore-плагін `vue-macros`, щоб Stryker не загортав `defineProps`/`defineEmits`/... у coverage-тернарник (інакше `@vue/compiler-sfc` падає при компіляції SFC).
93
+ - `data/stryker_config/stryker-vue-macros-ignorer.mjs` — сам Stryker-плагін, який копіюється поруч із vue-варіантом baseline і реєструється з нього.
94
+ - `data/vitest_config/vitest.config.baseline.js` — мінімальний baseline Vitest для runner-а.
95
+
96
+ ## Константи модуля
97
+
98
+ - `HERE` — каталог самого `stryker_config.mjs` (`dirname(fileURLToPath(import.meta.url))`).
99
+ - `STRYKER_BASELINE_PATH` — абсолютний шлях до стандартного `stryker.config.baseline.mjs`.
100
+ - `STRYKER_VUE_BASELINE_PATH` — абсолютний шлях до vue-варіанта baseline.
101
+ - `STRYKER_VUE_PLUGIN_PATH` — абсолютний шлях до `stryker-vue-macros-ignorer.mjs` у пакеті.
102
+ - `STRYKER_VUE_PLUGIN_FILENAME = 'stryker-vue-macros-ignorer.mjs'` — ім'я файлу, під яким плагін копіюється у jsRoot.
103
+ - `VITEST_BASELINE_PATH` — абсолютний шлях до canonical `vitest.config.baseline.js`.
104
+ - `TEST_GITIGNORE_ENTRIES = ['**/reports/stryker/', '**/coverage/']` — патерни, які додаються в корений `.gitignore`. Подвійний-зірочка-префікс `**/` забезпечує покриття всіх workspaces у monorepo (єдиний root `.gitignore`).
105
+ - `VUE_GLOB_PATTERN = 'src/**/*.vue'` — scope пошуку `.vue` файлів (відповідає Stryker mutate defaults для `src/`).
106
+ - `VUE_GLOB_IGNORE = ['**/node_modules/**', '**/dist/**', '**/reports/**']` — пропускаються build-артефакти і чужі `node_modules`, щоб не активувати vue-варіант через transitive-deps.
107
+
108
+ ## Потік виконання / Використання
109
+
110
+ Концерн запускається або через CLI пакета `@nitra/cursor` (як один із checks правила `test`), або імпортно з іншого скрипта.
111
+
112
+ Алгоритм `check(cwd)` крок за кроком:
113
+
114
+ 1. Створює `reporter` через `createCheckReporter()`.
115
+ 2. Читає конфіг `.n-cursor.json` через `readNCursorConfigLite(cwd)`.
116
+ 3. **Self-gate:** якщо `js-lint` відсутнє в `config.rules`, або присутнє в `config.disableRules` — повертає `reporter.getExitCode()` без жодних повідомлень (silently skipped). Це навмисна поведінка, щоб не шуміти у проєктах без JS coverage tooling.
117
+ 4. Викликає `resolveAllJsRoots(cwd)`. Якщо повернувся порожній масив — це аномалія (`js-lint` enabled, але немає `package.json`); `reporter.fail` із повідомленням `'test: js-lint enabled, але кореневий package.json не знайдено (test.mdc)'` і повернення exit code.
118
+ 5. Перевіряє існування всіх чотирьох canonical baseline-файлів у пакеті (`STRYKER_BASELINE_PATH`, `STRYKER_VUE_BASELINE_PATH`, `STRYKER_VUE_PLUGIN_PATH`, `VITEST_BASELINE_PATH`). Якщо будь-якого з них немає — `reporter.fail` із вимогою перевстановити `@nitra/cursor` і early-return.
119
+ 6. Для кожного `jsRoot` із `jsRoots`:
120
+ - Визначає `isVueRoot = await hasVueFiles(jsRoot)`.
121
+ - Обирає `strykerBaseline = isVueRoot ? STRYKER_VUE_BASELINE_PATH : STRYKER_BASELINE_PATH`.
122
+ - Через `ensureBaselineFile` копіює `strykerBaseline` у `<jsRoot>/stryker.config.mjs` (якщо відсутній).
123
+ - Якщо `isVueRoot` — додатково через `ensureBaselineFile` копіює `STRYKER_VUE_PLUGIN_PATH` у `<jsRoot>/stryker-vue-macros-ignorer.mjs`.
124
+ - Через `ensureBaselineFile` копіює `VITEST_BASELINE_PATH` у `<jsRoot>/vitest.config.js`.
125
+ 7. Викликає `ensureGitignoreEntries(cwd, TEST_GITIGNORE_ENTRIES, 'Test artifacts: Stryker + coverage (test.mdc)')` — додає в кореневий `.gitignore` патерни `**/reports/stryker/` і `**/coverage/` (якщо їх там немає). Якщо щось дійсно було додане (`added.length > 0`) — `reporter.pass` із перелічуванням доданих патернів.
126
+ 8. Повертає `reporter.getExitCode()` — `0`, якщо жодного `fail` не було, інакше `1`.
127
+
128
+ ### Приклад використання
129
+
130
+ ```js
131
+ import { check } from '@nitra/cursor/rules/test/js/stryker_config.mjs'
132
+
133
+ const exitCode = await check(process.cwd())
134
+ process.exit(exitCode)
135
+ ```
136
+
137
+ Зазвичай безпосередній виклик не потрібен — концерн запускається диспетчером правила `test` (`test.mdc`) у складі команди `n-cursor fix` / `n-cursor check`.
138
+
139
+ ### Спостережувані ефекти у файловій системі
140
+
141
+ Після успішного прогону в кожному JS-root з'являються:
142
+
143
+ - `stryker.config.mjs` (стандартний або vue-варіант, залежно від наявності `.vue` у `src/`).
144
+ - `vitest.config.js`.
145
+ - `stryker-vue-macros-ignorer.mjs` — тільки для Vue-roots.
146
+
147
+ У кореневому `.gitignore` гарантовано присутні:
148
+
149
+ - `**/reports/stryker/`
150
+ - `**/coverage/`
151
+
152
+ Якщо файл уже існував — він не перезаписується, що дозволяє користувачу безпечно кастомізувати baseline під специфіку проєкту.