@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,303 @@
1
+ # inline-template-links.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `inline-template-links.mjs` — допоміжна утиліта для **build-кроку обробки `.mdc`-правил**. Її роль: у текстовому вмісті `.mdc`-документа знайти Markdown-посилання, які ведуть на template-файли (шлях містить сегмент `/template/` або `/templates/`), і **замінити** ці посилання на **інлайн fenced-блоки** з фактичним вмістом target-файлу.
6
+
7
+ Простими словами: замість того щоб у згенерованому правилі читач бачив посилання `[конфіг](./templates/package.json.snippet.json)`, він побачить безпосередньо назву реального файлу (`package.json`) і fenced-блок із його вмістом — це робить правило «самодостатнім», без потреби клікати по лінках.
8
+
9
+ Особливості:
10
+
11
+ - Працює асинхронно (`async`), бо читає файли через `node:fs/promises`.
12
+ - Робить **fail-loud** валідацію: якщо посилання вказує на неіснуючий файл — кидає `Error` (а не мовчки пропускає), щоб автор правила одразу побачив проблему.
13
+ - «Розгортає» спеціальні суфікси `.snippet.<ext>` / `.deny.<ext>` / `.contains.<ext>` до імені реального target-файлу, який вони описують (наприклад `package.json.snippet.json` → `package.json`).
14
+ - Безпечно щодо ReDoS: усі regexp — статичні літерали з обмеженням довжини, без `new RegExp(variable)` із користувацьких даних.
15
+
16
+ Модуль експортує єдину функцію `inlineTemplateLinks(text, ruleDir)`.
17
+
18
+ ## Експорти / API
19
+
20
+ | Експорт | Тип | Призначення |
21
+ | --------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
22
+ | `inlineTemplateLinks` | `async function(text: string, ruleDir: string): Promise<string>` | Замінює Markdown-посилання на template-файли в `.mdc`-тексті на інлайн fenced-блоки з фактичним вмістом цих файлів. |
23
+
24
+ Усі інші імена в модулі (`langFromExt`, `normalizeTargetName`, константи `MD_LINK_RE`, `TEMPLATE_SEGMENT_RE`, `SLOT_SUFFIX_RES`) — **внутрішні** (не експортуються).
25
+
26
+ ## Внутрішні константи
27
+
28
+ ### `MD_LINK_RE`
29
+
30
+ ```js
31
+ ;/\[([^\]]{1,200})\]\((\.\/[^)]{1,500})\)/g
32
+ ```
33
+
34
+ Глобальний regexp, який ловить **Markdown-посилання вигляду `[label](./path)`** із обов'язковим префіксом `./` у href. Group 1 — текст посилання (до 200 символів), group 2 — шлях (до 500 символів, що починається з `./`). Обмеження довжин — захист від ReDoS / pathological input.
35
+
36
+ ### `TEMPLATE_SEGMENT_RE`
37
+
38
+ ```js
39
+ ;/\/templates?\//
40
+ ```
41
+
42
+ Перевіряє, чи шлях містить сегмент `/template/` або `/templates/`. Тільки такі посилання вважаються «template-посиланнями» і підлягають заміні; інші Markdown-лінки залишаються недоторканими.
43
+
44
+ ### `SLOT_SUFFIX_RES`
45
+
46
+ Масив із трьох **статичних** regexp:
47
+
48
+ ```js
49
+ ;[/^(.+)\.snippet\.[^.]+$/, /^(.+)\.deny\.[^.]+$/, /^(.+)\.contains\.[^.]+$/]
50
+ ```
51
+
52
+ Кожен ловить ім'я файлу з суфіксом-«слотом»: `<name>.snippet.<ext>`, `<name>.deny.<ext>`, `<name>.contains.<ext>`. Group 1 — це ім'я реального target-файлу (без суфікса слоту і без власного розширення). Коментар у коді явно зазначає: regexp-літерали статичні, без `RegExp(variable)`.
53
+
54
+ ## Функції
55
+
56
+ ### `langFromExt(filePath)` — internal
57
+
58
+ Сигнатура:
59
+
60
+ ```js
61
+ function langFromExt(filePath: string): string
62
+ ```
63
+
64
+ Параметри:
65
+
66
+ - `filePath` — рядок зі шляхом до файлу (досить навіть базового імені, бо використовується лише розширення).
67
+
68
+ Повертає:
69
+
70
+ - Рядок-ідентифікатор мови для Markdown fenced-блока:
71
+ - `'json'` — якщо розширення `.json`;
72
+ - `'toml'` — якщо `.toml`;
73
+ - `'yaml'` — якщо `.yml` або `.yaml`;
74
+ - `''` (порожній рядок) — для будь-яких інших розширень.
75
+
76
+ Side effects: жодних — чиста функція над рядком.
77
+
78
+ Призначення: визначити, який мовний таг ставити після відкривального ` ``` ` у згенерованому fenced-блоці, щоб підсвічування синтаксису працювало коректно.
79
+
80
+ ### `normalizeTargetName(fileBasename)` — internal
81
+
82
+ Сигнатура:
83
+
84
+ ```js
85
+ function normalizeTargetName(fileBasename: string): string
86
+ ```
87
+
88
+ Параметри:
89
+
90
+ - `fileBasename` — базове ім'я файлу (без шляху), наприклад `package.json.snippet.json`.
91
+
92
+ Повертає:
93
+
94
+ - Якщо ім'я **збігається з одним із regexp у `SLOT_SUFFIX_RES`** (тобто має суфікс `.snippet.<ext>`, `.deny.<ext>` або `.contains.<ext>`) — повертається **group 1** першого збігу (ім'я без слоту). Приклади:
95
+ - `package.json.snippet.json` → `package.json`
96
+ - `eslint.config.js.deny.js` → `eslint.config.js`
97
+ - `Caddyfile.contains.txt` → `Caddyfile`
98
+ - Якщо жоден з regexp не збігся — повертається оригінальне `fileBasename` без змін.
99
+
100
+ Side effects: відсутні.
101
+
102
+ Призначення: для template-файлу з суфіксом-слотом відновити **реальне ім'я target-файлу**, на який цей template посилається; саме це ім'я потім підставляється як заголовок перед fenced-блоком у згенерованому Markdown.
103
+
104
+ Коментар над функцією у вихіднику прямо описує цю поведінку: «Strip `.<slot>.<ext>` suffix (slot ∈ snippet/deny/contains) to recover the real target file name».
105
+
106
+ ### `inlineTemplateLinks(text, ruleDir)` — **exported**
107
+
108
+ Сигнатура:
109
+
110
+ ```js
111
+ export async function inlineTemplateLinks(
112
+ text: string,
113
+ ruleDir: string,
114
+ ): Promise<string>
115
+ ```
116
+
117
+ Параметри:
118
+
119
+ - `text` — вміст `.mdc`-файлу (повний текст) як рядок.
120
+ - `ruleDir` — **абсолютний** шлях до директорії правила (наприклад `.../npm/rules/security/`). Усі відносні href із `./` резолвляться **відносно цього каталогу**.
121
+
122
+ Повертає:
123
+
124
+ - `Promise<string>` — трансформований текст, у якому всі **template-посилання** замінено на блоки виду:
125
+
126
+ ````text
127
+ `<targetName>`:
128
+
129
+ ```<lang>
130
+ <contents>
131
+ ````
132
+
133
+ ```
134
+
135
+ де `targetName` — результат `normalizeTargetName(basename(absPath))`, `lang` — результат `langFromExt(absPath)`, а `contents` — вміст файлу після `.trim()`.
136
+
137
+ ```
138
+
139
+ - Якщо у тексті немає жодного template-посилання — повертається **той самий `text` без змін** (early-exit).
140
+
141
+ Алгоритм роботи:
142
+
143
+ 1. Знаходимо **всі** збіги `MD_LINK_RE` у `text` через `text.matchAll(...)`.
144
+ 2. Фільтруємо їх: залишаємо лише ті, у яких href (group 2) містить `/template/` або `/templates/` (через `TEMPLATE_SEGMENT_RE.test(m[2])`).
145
+ 3. Якщо після фільтрації збігів **немає** — повертаємо `text` як є.
146
+ 4. Кладемо стартовий результат `result = text`.
147
+ 5. Для кожного збігу послідовно (`for ... of`, з `await` на читанні файлу):
148
+ 1. Деструктуруємо: `const [fullMatch, , href] = match` (label не використовується, тому позиція пропущена).
149
+ 2. Будуємо відносний шлях: `relPath = href.slice(2)` — обрізаємо префікс `./` (його гарантує regexp).
150
+ 3. Збираємо абсолютний шлях: `absPath = join(ruleDir, relPath)`.
151
+ 4. Перевіряємо існування: `existsSync(absPath)`. Якщо файлу немає — **кидаємо** `Error`:
152
+
153
+ ```text
154
+ inlineTemplateLinks: file not found: <absPath> (referenced from .mdc)
155
+ ```
156
+
157
+ Жодного fallback / тихого пропуску — це fail-loud за дизайном.
158
+
159
+ 5. Читаємо файл: `raw = await readFile(absPath, 'utf8')`, далі `contents = raw.trim()` (прибираємо хвостові пробіли / переноси).
160
+ 6. Обчислюємо `lang = langFromExt(absPath)`.
161
+ 7. Обчислюємо `targetName = normalizeTargetName(basename(absPath))`.
162
+ 8. Формуємо `replacement` — backtick-екранований заголовок, порожній рядок і fenced-блок із `lang`:
163
+
164
+ ```js
165
+ ;`\`${targetName}\`:\n\n\`\`\`${lang}\n${contents}\n\`\`\``
166
+ ```
167
+
168
+ 9. Робимо заміну: `result = result.replace(fullMatch, () => replacement)`. Передача **callback-форми** в `.replace` критично важлива: інакше спецсимволи у `replacement` (наприклад `$&`, `$1` із вмісту шаблону) трактувалися б як backreferences і зламали б вивід.
169
+
170
+ 6. Повертаємо `result`.
171
+
172
+ Side effects:
173
+
174
+ - **Читання** файлів із диска (синхронна перевірка `existsSync` + асинхронне `readFile`).
175
+ - **Кидання `Error`** при відсутності target-файлу — це навмисна поведінка («fail loud — user must know»), а не баг.
176
+ - Запису на диск або мережевих викликів **не робить**.
177
+
178
+ Складність та обмеження:
179
+
180
+ - Цикл лінійний за кількістю template-посилань у тексті; для кожного — один `existsSync` і один `readFile`.
181
+ - Файли читаються **послідовно** (через `await` у тілі `for...of`), а не паралельно через `Promise.all`. Це осмислений вибір: правил, як правило, мало, а послідовність робить порядок помилок передбачуваним.
182
+ - Заміна виконується через простий `result.replace(fullMatch, ...)` — перший збіг `fullMatch` у `result`. Якщо однакове Markdown-посилання трапляється кілька разів — модифікується лише перше входження (фактичний `matchAll` дасть і інші входження, але кожен з них має той самий `fullMatch`, і їх теж замінить — по одному за крок ітерації; для повних дублікатів це працює коректно).
183
+
184
+ ## Залежності
185
+
186
+ ### Стандартна бібліотека Node.js
187
+
188
+ - `node:fs` → `existsSync` — синхронна перевірка наявності файлу перед читанням.
189
+ - `node:fs/promises` → `readFile` — асинхронне читання вмісту target-файлу як UTF-8.
190
+ - `node:path` → `basename`, `extname`, `join` — робота з шляхами:
191
+ - `extname` — у `langFromExt` для визначення мови;
192
+ - `basename` — для отримання базового імені файлу, з якого `normalizeTargetName` витягне target-ім'я;
193
+ - `join` — для побудови абсолютного шляху від `ruleDir` + `relPath`.
194
+
195
+ ### Зовнішні залежності
196
+
197
+ Жодних npm-пакетів. Модуль працює лише на Node.js стандарті.
198
+
199
+ ### Споживачі модуля
200
+
201
+ Файл лежить у `npm/scripts/lib/` поряд із іншими допоміжними утилітами для збірки правил, тому очікувані споживачі — build-скрипти у `npm/scripts/`, які генерують підсумкові `.mdc`-документи для cursor-rules / Claude-rules. Експортована функція `inlineTemplateLinks` викликається на проміжній стадії пайплайна обробки тексту `.mdc`-файлу разом із `ruleDir`, обчисленим від шляху до самого `.mdc`.
202
+
203
+ ## Потік виконання / Використання
204
+
205
+ Типовий сценарій інтеграції в build-скрипт:
206
+
207
+ ```js
208
+ import { readFile, writeFile } from 'node:fs/promises'
209
+ import { dirname } from 'node:path'
210
+
211
+ import { inlineTemplateLinks } from './lib/inline-template-links.mjs'
212
+
213
+ const mdcPath = '/abs/path/to/npm/rules/security/n-security.mdc'
214
+ const original = await readFile(mdcPath, 'utf8')
215
+
216
+ const ruleDir = dirname(mdcPath) // важливо: каталог, де лежить .mdc
217
+ const transformed = await inlineTemplateLinks(original, ruleDir)
218
+
219
+ await writeFile(mdcPath, transformed, 'utf8')
220
+ ```
221
+
222
+ Що відбувається крок-за-кроком на прикладі.
223
+
224
+ Вхідний `.mdc`-фрагмент (`ruleDir = .../npm/rules/security/`):
225
+
226
+ ```text
227
+ Snippet вимоги до `package.json` — див. [тут](./templates/package.json.snippet.json).
228
+ ```
229
+
230
+ Файл `.../npm/rules/security/templates/package.json.snippet.json`:
231
+
232
+ ```json
233
+ {
234
+ "scripts": {
235
+ "lint": "eslint ."
236
+ }
237
+ }
238
+ ```
239
+
240
+ Що зробить `inlineTemplateLinks`:
241
+
242
+ 1. `matchAll(MD_LINK_RE)` знайде один збіг із href `./templates/package.json.snippet.json`.
243
+ 2. `TEMPLATE_SEGMENT_RE` пропустить його (бо є `/templates/`).
244
+ 3. `relPath = 'templates/package.json.snippet.json'`, `absPath = '.../npm/rules/security/templates/package.json.snippet.json'`.
245
+ 4. `existsSync(absPath)` → `true`, файл читається.
246
+ 5. `langFromExt(absPath)` → `'json'`.
247
+ 6. `normalizeTargetName('package.json.snippet.json')` → `'package.json'` (спрацює regexp `/^(.+)\.snippet\.[^.]+$/`).
248
+ 7. `replacement` буде:
249
+
250
+ ````text
251
+ `package.json`:
252
+
253
+ ```json
254
+ {
255
+ "scripts": {
256
+ "lint": "eslint ."
257
+ }
258
+ }
259
+ ````
260
+
261
+ ```
262
+
263
+ ```
264
+
265
+ 8. Результат заміняє оригінальний Markdown-лінк у тексті.
266
+
267
+ Випадки помилок:
268
+
269
+ - Якщо `href` веде на неіснуючий файл — кидається `Error` із повним абсолютним шляхом у повідомленні; build-скрипт має право або впасти, або зловити цю помилку.
270
+ - Якщо у `text` немає Markdown-посилань або жодне з них не містить `/template(s)/` — функція повертає `text` без модифікацій.
271
+ - Якщо template-файл має нерозпізнаване розширення (наприклад `.txt` або `.conf`) — `langFromExt` поверне порожній рядок, і fenced-блок буде без мовного тегу (Markdown це допускає).
272
+ - Якщо ім'я template-файлу **не** має одного з суфіксів `.snippet.<ext>` / `.deny.<ext>` / `.contains.<ext>` — `normalizeTargetName` поверне його як є; це нормальна поведінка для «звичайних» template-файлів, у яких саме ім'я і є target-ім'ям.
273
+
274
+ ## Rebuild Test
275
+
276
+ Якщо видалити цей файл і відтворити його з нуля, мінімально достатній рецепт такий:
277
+
278
+ 1. Створи модуль `inline-template-links.mjs` у `npm/scripts/lib/`.
279
+ 2. Імпортуй з `node:fs` функцію `existsSync`, з `node:fs/promises` — `readFile`, з `node:path` — `basename`, `extname`, `join`.
280
+ 3. Оголоси константи:
281
+ - `MD_LINK_RE = /\[([^\]]{1,200})\]\((\.\/[^)]{1,500})\)/g` — глобальний regexp для Markdown-посилань `[label](./path)`.
282
+ - `TEMPLATE_SEGMENT_RE = /\/templates?\//` — фільтр шляхів, що містять `/template/` чи `/templates/`.
283
+ - `SLOT_SUFFIX_RES` — масив із трьох **статичних** regexp: `/^(.+)\.snippet\.[^.]+$/`, `/^(.+)\.deny\.[^.]+$/`, `/^(.+)\.contains\.[^.]+$/`. Принципово: жодного `new RegExp(variable)` — захист від ReDoS.
284
+ 4. Реалізуй `langFromExt(filePath)`:
285
+ - `extname(filePath)` → за вмістом повернути `'json' | 'toml' | 'yaml' | ''` (для `.yml` теж `'yaml'`).
286
+ 5. Реалізуй `normalizeTargetName(fileBasename)`:
287
+ - Пройди `SLOT_SUFFIX_RES` у заданому порядку; при першому збігу поверни `match[1]`. Інакше — оригінал.
288
+ 6. Експортуй `async function inlineTemplateLinks(text, ruleDir)`:
289
+ - `matchAll(MD_LINK_RE)` → відфільтруй за `TEMPLATE_SEGMENT_RE.test(href)`.
290
+ - Якщо нічого не залишилося — поверни `text`.
291
+ - Для кожного збігу: `relPath = href.slice(2)`, `absPath = join(ruleDir, relPath)`; якщо `!existsSync(absPath)` — `throw new Error('inlineTemplateLinks: file not found: <absPath> (referenced from .mdc)')`.
292
+ - Читай файл `utf8`, роби `.trim()`, обчисли `lang` і `targetName`, побудуй `replacement = \`\\\`${targetName}\\\`:\\n\\n\\\`\\\`\\\`${lang}\\n${contents}\\n\\\`\\\`\\\``.
293
+ - Заміни через `result = result.replace(fullMatch, () => replacement)` (саме callback-форма — щоб уникнути інтерпретації `$&`/`$1` у вмісті template-файлу).
294
+ 7. Поверни `result`.
295
+
296
+ Контракт, який має зберегтися:
297
+
298
+ - Чиста функція над текстом + читання файлів (без записів і без мережі).
299
+ - **Fail-loud** на відсутній target.
300
+ - Підтримка трьох слот-суфіксів: `snippet`, `deny`, `contains`.
301
+ - Підтримка мов підсвічування: `json`, `toml`, `yaml`, інакше — без таргу.
302
+ - Жодного `RegExp(variable)`.
303
+ - Префікс href повинен починатися з `./`, інакше посилання ігнорується (це закладено в `MD_LINK_RE`).
@@ -0,0 +1,156 @@
1
+ # list-rule-ids.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `list-rule-ids.mjs` — невелика бібліотечна утиліта, призначена для перебору директорій-правил у каталозі `npm/rules/` та повернення відсортованого алфавітно списку ідентифікаторів правил, які реально містять виконуваний модуль `fix.mjs`.
6
+
7
+ Логіка модуля побудована навколо архітектурної інваріанти проєкту: після так званої «атомарної міграції» кожне валідне правило **зобов'язане** мати файл `fix.mjs` у власній директорії `rules/<id>/`. Будь-яка піддиректорія без `fix.mjs` вважається або «не-правилом» (службова тека), або заглушкою-чернеткою — і ігнорується. Прихованих директорій (імена, що починаються з `.`) також не існує в результаті.
8
+
9
+ Додатково модуль підтримує опційне точкове фільтрування одним конкретним `id`, що використовується CLI-флагом `--rule abie` для прогону команд у режимі «тільки одне правило».
10
+
11
+ Модуль повністю асинхронний (використовує `node:fs/promises`), не має зовнішніх npm-залежностей і не виконує жодних побічних ефектів окрім читання директорії та перевірки наявності файлу через `existsSync`.
12
+
13
+ ## Експорти / API
14
+
15
+ Файл експортує одну іменовану функцію:
16
+
17
+ | Експорт | Тип | Призначення |
18
+ | ------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `listRuleIds` | `async function` | Повертає `Promise<string[]>` зі списком id правил, відфільтрованим за наявністю `fix.mjs` і опційним рівнянням до конкретного id. |
20
+
21
+ Експорту за замовчуванням немає.
22
+
23
+ ## Функції
24
+
25
+ ### `listRuleIds(bundledRulesDir, filter)`
26
+
27
+ #### Сигнатура
28
+
29
+ ```js
30
+ export async function listRuleIds(bundledRulesDir, filter)
31
+ ```
32
+
33
+ JSDoc-типи:
34
+
35
+ ```
36
+ @param {string} bundledRulesDir — абсолютний шлях до `npm/rules/`
37
+ @param {string} [filter] — id одного правила (через `--rule abie`)
38
+ @returns {Promise<string[]>} — відсортовані алфавітно id
39
+ ```
40
+
41
+ #### Параметри
42
+
43
+ - **`bundledRulesDir`** (`string`, обовʼязковий) — абсолютний шлях до кореневої теки правил, найчастіше `npm/rules/` (наприклад, `/path/to/repo/npm/rules`). Передбачається, що шлях існує і є директорією; модуль не валідує його окремо — будь-яка помилка з `fs.readdir` пробрасується далі промісом.
44
+ - **`filter`** (`string | undefined`, опційний) — точна назва правила, до якої потрібно звузити список. Якщо параметр не передано (`undefined`), повертаються всі знайдені id. Якщо передано, у результат потрапляють лише ті id, що _точно_ рівні `filter`. Часткові збіги, регулярні вирази або кейс-незалежність не підтримуються.
45
+
46
+ #### Повертає
47
+
48
+ `Promise<string[]>` — масив рядків-ідентифікаторів правил, відсортованих алфавітно за допомогою `Array.prototype.toSorted` із компаратором `a.localeCompare(b)`. Сортування _стабільне_ для оригінального методу і коректне для Unicode-локалей.
49
+
50
+ Можливі сценарії результату:
51
+
52
+ - Порожній масив `[]` — якщо в `bundledRulesDir` немає жодної директорії з `fix.mjs`, або якщо `filter` не збігається з жодним наявним id.
53
+ - Масив з одного елемента — типово при заданому `filter`.
54
+ - Повний відсортований список усіх правил — у випадку відсутності `filter`.
55
+
56
+ #### Алгоритм / послідовність кроків
57
+
58
+ 1. Викликається `readdir(bundledRulesDir, { withFileTypes: true })` — отримуємо масив `Dirent`-обʼєктів (а не просто рядків імен), що дає змогу одразу перевіряти тип запису без додаткових `stat`-викликів.
59
+ 2. Залишаються лише записи, для яких одночасно:
60
+ - `e.isDirectory()` — це саме директорія;
61
+ - `!e.name.startsWith('.')` — імʼя не починається з крапки (відсікаються приховані теки на кшталт `.git`, `.cache`, тощо).
62
+ 3. Записи перетворюються на голі імена (`.map(e => e.name)`).
63
+ 4. Фільтрація на наявність `fix.mjs` — для кожного `id` синхронно перевіряється `existsSync(join(bundledRulesDir, id, 'fix.mjs'))`. Директорії без цього файлу відсіюються як «not-a-rule або заглушка».
64
+ 5. Фільтрація за `filter` — якщо параметр визначений, залишаються лише id, рівні `filter`; інакше пропускаються всі.
65
+ 6. Сортування `toSorted((a, b) => a.localeCompare(b))` — повертає новий масив без мутації проміжного.
66
+
67
+ #### Side effects
68
+
69
+ - **Дискові читання**: `readdir` (асинхронний обхід директорії) та `existsSync` (синхронні перевірки наявності файлу для кожного кандидата). Винятки I/O пробрасуються відхиленим промісом.
70
+ - **Жодних записів, мережевих викликів, мутацій глобального стану, логування** — функція є read-only і чистою щодо побічних ефектів окрім згаданих читань ФС.
71
+ - Використання `existsSync` у циклі — це _синхронні_ виклики всередині асинхронної функції; вони виконуються послідовно, але без `await`. Для типового розміру каталогу `npm/rules/` (десятки правил) це непомітна вартість.
72
+
73
+ #### Гранітні випадки
74
+
75
+ - Якщо `bundledRulesDir` не існує — `readdir` відхиляє проміс з `ENOENT`.
76
+ - Якщо в директорії є символічні посилання — поведінка залежить від того, що повертає `Dirent.isDirectory()`; ця функція покладається на штатну семантику Node.js.
77
+ - Якщо `filter` переданий як порожній рядок `''` — він _не_ є `undefined`, тому фільтр буде застосовано (`id === ''`), і результат майже напевно буде порожнім.
78
+ - Файл `fix.mjs` як директорія: малоймовірний випадок, але `existsSync` поверне `true` для будь-якого існуючого запису з цим імʼям; модуль не розрізняє файл і теку.
79
+
80
+ ## Залежності
81
+
82
+ ### Вбудовані модулі Node.js
83
+
84
+ | Модуль | Імпортований символ | Використання |
85
+ | ------------------ | ------------------- | ------------------------------------------------------------ |
86
+ | `node:fs` | `existsSync` | Синхронна перевірка наявності `fix.mjs` у кожному кандидаті. |
87
+ | `node:fs/promises` | `readdir` | Асинхронний обхід `bundledRulesDir` з `withFileTypes: true`. |
88
+ | `node:path` | `join` | Безпечне зʼєднання сегментів шляху до `fix.mjs`. |
89
+
90
+ ### Зовнішні npm-залежності
91
+
92
+ Немає.
93
+
94
+ ### Внутрішні залежності
95
+
96
+ Файл не імпортує жодних інших модулів проєкту. Сам по собі він використовується як бібліотечна функція з інших скриптів у `npm/scripts/` (CLI-обгортки, які приймають флаг `--rule`).
97
+
98
+ ## Потік виконання / Використання
99
+
100
+ ### Контекст у проєкті
101
+
102
+ `listRuleIds` — це фундамент будь-якого скрипту, який «робить щось для кожного правила»: лінт, авто-фікс, генерація звіту, перевірка покриття, нормалізація ADR тощо. Замість того щоб кожному скрипту самостійно перелічувати теки в `npm/rules/`, всі вони викликають цю функцію та отримують однаковий, відсортований, відфільтрований список — це гарантує детермінованість порядку обробки правил.
103
+
104
+ Архітектурно файл підкріплює інваріант «`fix.mjs` обовʼязковий»: будь-яка тека без нього автоматично невидима для всіх споживачів. Це дає змогу тримати у `rules/` допоміжні підтеки (наприклад, шаблони, шаред-файли) без ризику, що вони будуть оброблені як правило.
105
+
106
+ ### Приклад використання
107
+
108
+ ```js
109
+ import { listRuleIds } from './lib/list-rule-ids.mjs'
110
+ import { resolve } from 'node:path'
111
+
112
+ const bundledRulesDir = resolve(import.meta.dirname, '../../rules')
113
+
114
+ // 1. Всі правила.
115
+ const all = await listRuleIds(bundledRulesDir)
116
+ // → ['abie', 'changelog', 'feedback', ...]
117
+
118
+ // 2. Тільки одне правило (з CLI-флагу --rule abie).
119
+ const ruleFlag = process.argv.includes('--rule') ? process.argv[process.argv.indexOf('--rule') + 1] : undefined
120
+
121
+ const subset = await listRuleIds(bundledRulesDir, ruleFlag)
122
+ // → ['abie'] якщо ruleFlag === 'abie' і таке правило існує
123
+ // → [] якщо ruleFlag не відповідає жодному правилу
124
+ ```
125
+
126
+ ### Інтеграція з CLI
127
+
128
+ Типовий патерн виклику в скриптах проєкту:
129
+
130
+ 1. Скрипт парсить `process.argv` (часто через `mri`/`minimist` або вручну), отримує опційний `--rule <id>`.
131
+ 2. Викликає `listRuleIds(rulesDir, ruleFromArgv)` і чекає на результат.
132
+ 3. Ітерується по отриманому масиву і виконує цільову дію (fix/check/коверідж) для кожного id.
133
+
134
+ ### Послідовність виконання при виклику
135
+
136
+ ```
137
+ listRuleIds(dir, filter)
138
+
139
+ ├─ readdir(dir, withFileTypes:true) ── async ──▶ [Dirent...]
140
+
141
+ ├─ .filter(isDirectory && !startsWith('.'))
142
+ ├─ .map(e => e.name)
143
+ ├─ .filter(existsSync(<id>/fix.mjs)) ◀── sync I/O loop
144
+ ├─ .filter(filter === undefined || id === filter)
145
+ ├─ .toSorted(localeCompare)
146
+
147
+
148
+ Promise<string[]> (відсортовані id)
149
+ ```
150
+
151
+ ### Очікувані помилки
152
+
153
+ - `ENOENT` / `ENOTDIR` від `readdir` — якщо переданий `bundledRulesDir` неіснуючий або не є директорією. Викликач має або переконатися в коректності шляху перед викликом, або обгорнути виклик у `try/catch`.
154
+ - `EACCES` — якщо немає прав на читання теки.
155
+
156
+ Усі інші помилки (відсутність окремого `fix.mjs`, відсутність відповідності `filter`) семантично не є помилками — вони лише звужують повернений масив.
@@ -0,0 +1,147 @@
1
+ # load-cursor-config.mjs
2
+
3
+ ## Огляд
4
+
5
+ Утилітарний модуль для читання конфігураційного файлу `.n-cursor.json` з кореня репозиторію. Призначений для використання check-скриптами, що обходять файлову систему й мають виключати певні каталоги зі сканування.
6
+
7
+ Наразі експортує лише одну публічну функцію — `loadCursorIgnorePaths(root)`, яка повертає нормалізовані абсолютні posix-шляхи з масиву `ignore` у конфізі. Якщо конфіг відсутній, пошкоджений, або поле `ignore` має невалідний формат — функція повертає порожній масив без кидання винятків (fail-soft підхід).
8
+
9
+ Модуль свідомо **не валідує** структуру конфігу повністю — це робота окремої перевірки (наприклад, через `v8r`). Тут перевіряється лише наявність і тип поля `ignore` та його елементів.
10
+
11
+ Шлях до конфігу зашитий константою `CONFIG_FILE = '.n-cursor.json'` й читається з кореня репозиторію, переданого через параметр `root`.
12
+
13
+ ## Експорти / API
14
+
15
+ | Експорт | Тип | Призначення |
16
+ | ----------------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
17
+ | `loadCursorIgnorePaths` | `async function (root: string): Promise<string[]>` | Зчитати й нормалізувати список ігнорованих шляхів з `.n-cursor.json` |
18
+
19
+ Внутрішні (не експортовані) сутності:
20
+
21
+ | Сутність | Тип | Призначення |
22
+ | ------------- | -------------------------------------------- | ------------------------------------------------------------------ |
23
+ | `CONFIG_FILE` | `string` (константа `'.n-cursor.json'`) | Ім'я конфіг-файлу в корені репозиторію |
24
+ | `toAbsPosix` | `function (root: string, p: string): string` | Нормалізатор шляху до абсолютного posix-формату без trailing-slash |
25
+
26
+ ## Функції
27
+
28
+ ### `toAbsPosix(root, p)`
29
+
30
+ Внутрішня (приватна для модуля) функція-нормалізатор шляху.
31
+
32
+ - **Сигнатура:** `function toAbsPosix(root: string, p: string): string`
33
+ - **Параметри:**
34
+ - `root` — абсолютний корінь репозиторію (використовується для розв'язання відносних шляхів).
35
+ - `p` — шлях з конфігу; може бути відносним або абсолютним.
36
+ - **Повертає:** абсолютний posix-шлях (роздільник `/`) без жодного завершального `/`.
37
+ - **Алгоритм:**
38
+ 1. Конвертує `p` у рядок через `String(p)` і прибирає пробіли по краях через `trim()`.
39
+ 2. Якщо результат вже абсолютний (`isAbsolute`), використовує його як є; інакше — резолвить відносно `root` через `resolve(root, trimmed)`.
40
+ 3. Замінює нативний роздільник платформи (`sep`) на `/` (через `split(sep).join('/')`) — на Windows це конвертує `\` у `/`.
41
+ 4. У циклі видаляє всі завершальні `/` (нормалізація `foo/bar//` → `foo/bar`).
42
+ - **Side effects:** немає (чиста функція над аргументами).
43
+
44
+ ### `loadCursorIgnorePaths(root)`
45
+
46
+ Публічна async-функція — точка входу модуля.
47
+
48
+ - **Сигнатура:** `async function loadCursorIgnorePaths(root: string): Promise<string[]>`
49
+ - **Параметри:**
50
+ - `root` — абсолютний шлях до кореня репозиторію, де очікується `.n-cursor.json`.
51
+ - **Повертає:** `Promise<string[]>` — масив абсолютних posix-шляхів без trailing-slash; може бути порожнім.
52
+ - **Алгоритм (fail-soft):**
53
+ 1. Обчислює абсолютний шлях до конфіг-файлу через `join(root, CONFIG_FILE)`.
54
+ 2. Якщо файлу немає на диску (`existsSync` повернув `false`) — повертає `[]`.
55
+ 3. Намагається прочитати файл через `readFile(file, 'utf8')` й розпарсити його як JSON. Якщо парсинг кидає виняток (битий JSON, помилка читання тощо) — `catch` повертає `[]`.
56
+ 4. Дістає поле `ignore` з розпарсеного об'єкта через optional chaining (`raw?.ignore`). Якщо поле не є масивом (`Array.isArray` повернув `false`) — повертає `[]`.
57
+ 5. Ітерує по `list`, для кожного елемента:
58
+ - якщо це не рядок — пропускає;
59
+ - тримить пробіли; якщо рядок став порожнім — пропускає;
60
+ - інакше додає в результат `toAbsPosix(root, v)`.
61
+ 6. Повертає накопичений масив `out`.
62
+ - **Side effects:**
63
+ - Синхронний `existsSync` на дисковий файл `<root>/.n-cursor.json`.
64
+ - Асинхронне читання того ж файлу через `readFile` (тільки якщо `existsSync` повернув `true`).
65
+ - Жодних записів, мережевих викликів чи мутацій глобального стану.
66
+ - **Обробка помилок:**
67
+ - Файл не існує → `[]`.
68
+ - Файл існує, але JSON битий або `readFile` падає → `[]` (через `try/catch`).
69
+ - `ignore` не масив, або взагалі відсутнє → `[]`.
70
+ - Нестрингові або порожні після `trim()` елементи масиву → мовчки пропускаються.
71
+
72
+ ## Залежності
73
+
74
+ ### Node.js built-ins
75
+
76
+ | Модуль | Імпортовані сутності | Використання |
77
+ | ------------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
78
+ | `node:fs` | `existsSync` | Синхронна перевірка наявності `.n-cursor.json` перед читанням |
79
+ | `node:fs/promises` | `readFile` | Асинхронне читання вмісту конфіг-файлу як UTF-8 |
80
+ | `node:path` | `isAbsolute`, `join`, `resolve`, `sep` | Перевірка абсолютності, склейка шляху до конфігу, розв'язання відносних шляхів, заміна платформного роздільника на posix |
81
+
82
+ ### Зовнішні залежності
83
+
84
+ Жодних — лише стандартна бібліотека Node.js.
85
+
86
+ ### Конфігураційні артефакти
87
+
88
+ - `.n-cursor.json` у корені репозиторію — джерело правди для `ignore` (опціональне поле, масив рядків).
89
+
90
+ ## Потік виконання / Використання
91
+
92
+ ### Типовий сценарій виклику
93
+
94
+ Викликач (наприклад, check-скрипт) передає абсолютний корінь репозиторію й отримує перелік каталогів, які слід виключити з обходу.
95
+
96
+ ```js
97
+ import { loadCursorIgnorePaths } from './load-cursor-config.mjs'
98
+
99
+ const root = process.cwd() // або інший абсолютний корінь
100
+ const ignored = await loadCursorIgnorePaths(root)
101
+ // ignored: ['/abs/path/.worktrees', '/abs/path/node_modules', ...]
102
+
103
+ // Далі при обході файлів пропускаємо ті, що під будь-яким із ignored:
104
+ function isIgnored(absPosixPath) {
105
+ return ignored.some(ig => absPosixPath === ig || absPosixPath.startsWith(ig + '/'))
106
+ }
107
+ ```
108
+
109
+ ### Очікуваний формат `.n-cursor.json`
110
+
111
+ ```json
112
+ {
113
+ "ignore": [".worktrees", "node_modules", "/abs/path/to/skip"]
114
+ }
115
+ ```
116
+
117
+ Інші поля у файлі допустимі — їх ігнорує цей модуль (валідація схеми — окрема відповідальність).
118
+
119
+ ### Послідовність кроків усередині `loadCursorIgnorePaths`
120
+
121
+ 1. `join(root, '.n-cursor.json')` → шлях до конфігу.
122
+ 2. `existsSync(file)` → якщо `false`, ранній вихід з `[]`.
123
+ 3. `readFile + JSON.parse` у `try/catch` → при будь-якій помилці `[]`.
124
+ 4. `Array.isArray(raw?.ignore)` → якщо `false`, `[]`.
125
+ 5. Лінійний прохід по `ignore`-елементах: фільтр по типу й непорожності, нормалізація через `toAbsPosix(root, v)`.
126
+ 6. Повернення масиву `out`.
127
+
128
+ ### Інваріанти результату
129
+
130
+ - Усі шляхи — **абсолютні**.
131
+ - Усі шляхи — у **posix-форматі** (роздільник `/`), навіть на Windows.
132
+ - Жоден шлях не має **завершального `/`**.
133
+ - Порядок шляхів — відповідає порядку в `ignore` (вхідні дублікати **не дедуплікуються**).
134
+ - За жодних умов функція не кидає винятки — повертає `[]` як єдиний "поганий" результат.
135
+
136
+ ### Тестування (Rebuild Test)
137
+
138
+ Перевірка контрактів модуля:
139
+
140
+ - Файлу немає → `loadCursorIgnorePaths(root)` резолвиться у `[]`.
141
+ - Файл є, але JSON битий → `[]`.
142
+ - Файл є, але `ignore` відсутнє/не масив → `[]`.
143
+ - `ignore` містить нестрингові елементи й порожні рядки — вони відфільтровані; рядкові — нормалізовані до абсолютного posix-шляху без trailing-slash.
144
+ - Відносний шлях у `ignore` (наприклад `"node_modules"`) перетворюється на `<root>/node_modules` у posix-формі.
145
+ - Абсолютний шлях у `ignore` зберігається як є (з можливою конверсією `\` → `/`).
146
+ - На Windows шляхи з `\` конвертуються у `/`.
147
+ - Trailing `/` (один або кілька) видаляються.