@nitra/cursor 3.22.0 → 3.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/AGENTS.template.md +4 -0
  3. package/CHANGELOG.md +37 -3
  4. package/bin/docs/n-cursor.md +636 -0
  5. package/bin/docs/rename-yaml-extensions.md +207 -0
  6. package/bin/n-cursor.js +30 -3
  7. package/package.json +1 -1
  8. package/rules/abie/docs/fix.md +18 -0
  9. package/rules/abie/js/docs/applies.md +26 -0
  10. package/rules/abie/js/docs/env_dns.md +32 -0
  11. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  12. package/rules/abie/js/docs/hc_pairing.md +35 -0
  13. package/rules/abie/js/docs/ua_http_route.md +28 -0
  14. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  15. package/rules/abie/lib/docs/enabled.md +29 -0
  16. package/rules/abie/lib/docs/env-dns.md +35 -0
  17. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  18. package/rules/abie/lib/docs/http-route.md +44 -0
  19. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  20. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  21. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  22. package/rules/abie/lib/docs/yaml.md +29 -0
  23. package/rules/adr/docs/fix.md +148 -0
  24. package/rules/adr/js/docs/hooks.md +259 -0
  25. package/rules/bun/docs/fix.md +156 -0
  26. package/rules/bun/js/docs/layout.md +393 -0
  27. package/rules/capacitor/docs/fix.md +121 -0
  28. package/rules/capacitor/js/docs/platforms.md +295 -0
  29. package/rules/changelog/changelog.mdc +4 -2
  30. package/rules/changelog/docs/fix.md +174 -0
  31. package/rules/changelog/js/consistency.mjs +114 -13
  32. package/rules/changelog/js/docs/consistency.md +387 -0
  33. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  34. package/rules/ci4/docs/fix.md +179 -0
  35. package/rules/ci4/js/docs/marksman_config.md +128 -0
  36. package/rules/docker/docker.mdc +8 -3
  37. package/rules/docker/docs/fix.md +171 -0
  38. package/rules/docker/js/docs/lint.md +258 -0
  39. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  40. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  41. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  42. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  43. package/rules/docker/lint/docs/lint.md +193 -0
  44. package/rules/efes/docs/fix.md +203 -0
  45. package/rules/feedback/docs/fix.md +140 -0
  46. package/rules/flow/docs/fix.md +152 -0
  47. package/rules/ga/docs/fix.md +158 -0
  48. package/rules/ga/js/docs/lint.md +100 -0
  49. package/rules/ga/js/docs/workflows.md +217 -0
  50. package/rules/ga/lint/docs/lint.md +209 -0
  51. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  52. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  53. package/rules/graphql/docs/fix.md +126 -0
  54. package/rules/graphql/js/docs/tooling.md +264 -0
  55. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  56. package/rules/hasura/docs/fix.md +120 -0
  57. package/rules/hasura/hasura.mdc +14 -0
  58. package/rules/hasura/js/docs/internal_urls.md +326 -0
  59. package/rules/image-avif/docs/fix.md +132 -0
  60. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  61. package/rules/image-compress/docs/fix.md +150 -0
  62. package/rules/image-compress/js/docs/package_setup.md +191 -0
  63. package/rules/js-bun-db/docs/fix.md +148 -0
  64. package/rules/js-bun-db/js/docs/safety.md +231 -0
  65. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  66. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  67. package/rules/js-bun-redis/docs/fix.md +123 -0
  68. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  69. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  70. package/rules/js-lint/docs/fix.md +117 -0
  71. package/rules/js-lint/js/docs/lint.md +250 -0
  72. package/rules/js-lint/js/docs/tooling.md +348 -0
  73. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  74. package/rules/js-lint-ci/docs/fix.md +154 -0
  75. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  76. package/rules/js-mssql/docs/fix.md +128 -0
  77. package/rules/js-mssql/js/docs/deps.md +263 -0
  78. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  79. package/rules/js-run/docs/fix.md +144 -0
  80. package/rules/js-run/js/docs/runtime.md +388 -0
  81. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  82. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  83. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  84. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  85. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  86. package/rules/k8s/docs/fix.md +129 -0
  87. package/rules/k8s/js/docs/manifests.md +344 -0
  88. package/rules/k8s/js/manifests.mjs +6 -2
  89. package/rules/k8s/k8s.mdc +4 -2
  90. package/rules/k8s/lint/docs/lint.md +411 -0
  91. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  92. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  93. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  94. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  95. package/rules/npm-module/docs/fix.md +98 -0
  96. package/rules/npm-module/js/docs/package_structure.md +274 -0
  97. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  98. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  99. package/rules/php/docs/fix.md +107 -0
  100. package/rules/php/js/docs/tooling.md +152 -0
  101. package/rules/php/lint/docs/lint.md +215 -0
  102. package/rules/python/docs/fix.md +163 -0
  103. package/rules/python/js/docs/applies.md +108 -0
  104. package/rules/python/js/docs/tooling.md +153 -0
  105. package/rules/python/lint/docs/lint.md +322 -0
  106. package/rules/rego/docs/fix.md +121 -0
  107. package/rules/rego/js/docs/applies.md +174 -0
  108. package/rules/rego/js/docs/lint.md +118 -0
  109. package/rules/rego/lint/docs/lint.md +204 -0
  110. package/rules/release/docs/change.md +185 -0
  111. package/rules/release/docs/fix.md +119 -0
  112. package/rules/release/docs/release.md +222 -0
  113. package/rules/release/lib/docs/aggregate.md +246 -0
  114. package/rules/release/lib/docs/change-file.md +200 -0
  115. package/rules/release/lib/docs/fallback.md +203 -0
  116. package/rules/rust/docs/fix.md +129 -0
  117. package/rules/rust/js/docs/applies.md +140 -0
  118. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  119. package/rules/security/docs/fix.md +86 -0
  120. package/rules/security/js/docs/lint.md +171 -0
  121. package/rules/security/js/docs/sample_secret.md +190 -0
  122. package/rules/security/js/docs/trufflehog.md +137 -0
  123. package/rules/security/js/lint.mjs +9 -1
  124. package/rules/style-lint/docs/fix.md +155 -0
  125. package/rules/style-lint/js/docs/lint.md +184 -0
  126. package/rules/style-lint/js/docs/tooling.md +194 -0
  127. package/rules/tauri/docs/fix.md +158 -0
  128. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  129. package/rules/tauri/js/docs/tooling.md +228 -0
  130. package/rules/test/coverage/coverage.mjs +15 -3
  131. package/rules/test/docs/fix.md +132 -0
  132. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  135. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  136. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  137. package/rules/test/js/docs/location.md +136 -0
  138. package/rules/test/js/docs/no-process-chdir.md +160 -0
  139. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  140. package/rules/test/js/docs/stryker_config.md +152 -0
  141. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  142. package/rules/text/docs/fix.md +118 -0
  143. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  144. package/rules/text/js/docs/formatting.md +256 -0
  145. package/rules/text/js/docs/lint.md +122 -0
  146. package/rules/text/lint/docs/lint.md +220 -0
  147. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  148. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  149. package/rules/text/lint/docs/run-v8r.md +197 -0
  150. package/rules/vue/docs/fix.md +127 -0
  151. package/rules/vue/js/docs/packages.md +335 -0
  152. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  153. package/rules/worktree/docs/fix.md +161 -0
  154. package/schemas/rule-meta.json +5 -1
  155. package/scripts/auto-rules.mjs +7 -4
  156. package/scripts/coverage-classify/docs/apply.md +202 -0
  157. package/scripts/coverage-classify/docs/cache.md +203 -0
  158. package/scripts/coverage-classify/docs/index.md +218 -0
  159. package/scripts/coverage-classify/docs/prompt.md +132 -0
  160. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  161. package/scripts/coverage-fix-extract.mjs +122 -0
  162. package/scripts/coverage-fix.mjs +1 -1
  163. package/scripts/dispatcher/docs/graph.md +346 -0
  164. package/scripts/dispatcher/docs/index.md +236 -0
  165. package/scripts/dispatcher/docs/trace.md +296 -0
  166. package/scripts/dispatcher/index.mjs +1 -1
  167. package/scripts/dispatcher/lib/active.mjs +4 -8
  168. package/scripts/dispatcher/lib/commands.mjs +7 -11
  169. package/scripts/dispatcher/lib/docs/active.md +348 -0
  170. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  171. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  172. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  173. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  174. package/scripts/dispatcher/lib/docs/events.md +182 -0
  175. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  176. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  177. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  178. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  179. package/scripts/dispatcher/lib/docs/level.md +335 -0
  180. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  181. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  182. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  183. package/scripts/dispatcher/lib/docs/review.md +255 -0
  184. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  185. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  186. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  187. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  188. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  189. package/scripts/dispatcher/lib/executor.mjs +6 -1
  190. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  191. package/scripts/dispatcher/lib/level.mjs +29 -3
  192. package/scripts/dispatcher/lib/review.mjs +1 -1
  193. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  194. package/scripts/docs/auto-rules.md +376 -0
  195. package/scripts/docs/auto-skills.md +173 -0
  196. package/scripts/docs/build-agents-commands.md +183 -0
  197. package/scripts/docs/cli-entry.md +153 -0
  198. package/scripts/docs/coverage-fix.md +177 -0
  199. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  200. package/scripts/lib/changed-files.mjs +4 -1
  201. package/scripts/lib/docs/changed-files.md +149 -0
  202. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  203. package/scripts/lib/docs/check-reporter.md +175 -0
  204. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  205. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  206. package/scripts/lib/docs/ensure-tool.md +254 -0
  207. package/scripts/lib/docs/generated-markdown.md +275 -0
  208. package/scripts/lib/docs/gha-workflow.md +326 -0
  209. package/scripts/lib/docs/inline-template-links.md +303 -0
  210. package/scripts/lib/docs/list-rule-ids.md +156 -0
  211. package/scripts/lib/docs/load-cursor-config.md +147 -0
  212. package/scripts/lib/docs/mirror-parity.md +167 -0
  213. package/scripts/lib/worktree.mjs +26 -0
  214. package/scripts/worktree-cli.mjs +12 -2
  215. package/skills/coverage-fix/SKILL.md +34 -45
  216. package/skills/docgen/SKILL.md +44 -23
  217. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  218. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  219. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  220. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  221. package/skills/docgen/js/docgen-scan.mjs +37 -21
  222. package/skills/llm-patch/SKILL.md +23 -2
  223. package/skills/start-check/SKILL.md +26 -53
  224. package/skills/start-check/js/check.mjs +211 -0
  225. package/skills/taze/SKILL.md +9 -3
  226. package/skills/taze/js/diff.mjs +154 -0
  227. package/types/bin/n-cursor.d.ts +1 -1
  228. package/skills/fix-tests/SKILL.md +0 -119
  229. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,367 @@
1
+ # mssql-pool-scan.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `mssql-pool-scan.mjs` — це набір AST-сканерів, що шукають **небезпечні патерни використання драйвера `mssql`** (Microsoft SQL Server для Node.js) у вихідних файлах JavaScript / TypeScript. Сканери призначені для статичного аналізу й використовуються з правил `js-mssql` (див. `js-mssql.mdc`).
6
+
7
+ Модуль виявляє п'ять класів проблем:
8
+
9
+ 1. **Створення `new sql.ConnectionPool(...)` / `new mssql.ConnectionPool(...)` всередині функції.** Це антипатерн: пул має бути singleton на рівні модуля, а не створюватися на кожен запит/виклик.
10
+ 2. **Небезпечний виклик `query(\`...\`)`** — звичайний `CallExpression` з `TemplateLiteral` як першим аргументом (не tagged template). Це може призвести до **SQL injection**, бо інтерполяція відбувається на рівні JS і в SQL потрапляє вже склеєний рядок.
11
+ 3. **Shared `Request`** — `export const request = pool.request()` (або `const request = pool.request()`), який не можна повторно використовувати між запитами в драйвері `mssql`.
12
+ 4. **Динамічні SQL-списки через `.join(...)`** у `TemplateLiteral` / `TaggedTemplateExpression` у контексті `IN (...)` або `VALUES (...)`. Навіть у tagged template це небезпечно, бо в запит підставляється готовий шматок SQL.
13
+ 5. **`IN (${...})` без числового парсера й/або без guard-перевірки на порожній список.** Навіть у безпечному tagged template значення треба явно приводити до Number/BigInt і відкидати NaN, а перед запитом — перевіряти, що список не порожній (`if (!ids.length) throw ...`).
14
+
15
+ Парсинг виконується через **`oxc-parser`** (`parseSync`). Якщо файл не парситься або містить синтаксичні помилки — кожна функція повертає **порожній масив** (треба спочатку полагодити синтаксис і перезапустити сканування).
16
+
17
+ Файл живе всередині `npm/rules/js-mssql/lib/` й експортує **6 публічних функцій** (5 сканерів + 1 фільтр розширень файлів). Допоміжні функції (перевірки AST-вузлів, трасування Identifier-ів) — приватні.
18
+
19
+ ---
20
+
21
+ ## Експорти / API
22
+
23
+ | Експорт | Тип | Призначення |
24
+ | --------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------- |
25
+ | `findMssqlPerRequestConnectionInText(content, virtualPath?)` | function | Знайти `new sql.ConnectionPool(...)` / `new mssql.ConnectionPool(...)` всередині функцій. |
26
+ | `findUnsafeMssqlQueryTemplateCallInText(content, virtualPath?)` | function | Знайти `obj.query(\`...\`)`— небезпечну інтерполяцію в`query(...)`. |
27
+ | `findSharedMssqlRequestInText(content, virtualPath?)` | function | Знайти shared `Request`: `const request = something.request()`. |
28
+ | `findUnsafeMssqlDynamicSqlListInText(content, virtualPath?)` | function | Знайти `IN (...)` / `VALUES (...)` зі склеєним `.join(',')` у `${...}`. |
29
+ | `findUnsafeMssqlInListUnparsedInText(content, virtualPath?)` | function | Знайти `IN (${expr})`, де `expr` не пройшов числовий парсер. |
30
+ | `findUnsafeMssqlInListMissingEmptyGuardInText(content, virtualPath?)` | function | Знайти `IN (${...})` без guard `if (empty) throw` (або без винесення у змінну). |
31
+ | `isMssqlScanSourceFile(relativePathPosix)` | function | Фільтр файлів для сканування за розширенням. |
32
+
33
+ ### Загальна форма результату сканерів
34
+
35
+ Усі сканери, окрім останнього missing-guard-сканера, повертають масив об'єктів виду:
36
+
37
+ ```
38
+ {
39
+ line: number, // 1-based номер рядка в content
40
+ snippet: string // нормалізований фрагмент вихідного коду (через normalizeSnippet)
41
+ }
42
+ ```
43
+
44
+ Сканер `findUnsafeMssqlInListMissingEmptyGuardInText` додатково повертає поля `reason: 'not_var' | 'missing_guard'` та опційне `name: string` (імʼя Identifier-у, для якого не знайдено guard).
45
+
46
+ `isMssqlScanSourceFile` повертає `boolean`.
47
+
48
+ ### Спільні параметри сканерів
49
+
50
+ - `content: string` — повний вихідний код файлу.
51
+ - `virtualPath: string` — необовʼязковий «віртуальний» шлях файлу (наприклад `pkg/src/db.ts`), потрібний `oxc-parser`, щоб обрати мову (`lang`) через `langFromPath`. За замовчуванням — `'scan.ts'`.
52
+
53
+ ---
54
+
55
+ ## Функції
56
+
57
+ ### Експортовані функції
58
+
59
+ #### `findMssqlPerRequestConnectionInText(content, virtualPath = 'scan.ts')`
60
+
61
+ - **Сигнатура:** `(content: string, virtualPath?: string) => { line: number, snippet: string }[]`
62
+ - **Параметри:**
63
+ - `content` — вихідний код для сканування.
64
+ - `virtualPath` — шлях для вибору мови парсера (`lang`).
65
+ - **Повертає:** масив порушень `{ line, snippet }`. Порожній масив, якщо файл не парситься або немає порушень.
66
+ - **Що шукає:** `NewExpression` виду `new sql.ConnectionPool(...)` або `new mssql.ConnectionPool(...)` (через `isNewConnectionPool`), і лише ті, що **знаходяться всередині будь-якої функції** (перевірка через `ancestors.some(isFunctionNode)`).
67
+ - **Side effects:** немає (чиста функція). Не кидає винятків — парсинг обгорнуто в `try/catch`.
68
+
69
+ #### `findUnsafeMssqlQueryTemplateCallInText(content, virtualPath = 'scan.ts')`
70
+
71
+ - **Сигнатура:** `(content: string, virtualPath?: string) => { line: number, snippet: string }[]`
72
+ - **Параметри:**
73
+ - `content` — вихідний код.
74
+ - `virtualPath` — шлях файлу для вибору `lang`.
75
+ - **Повертає:** масив `{ line, snippet }` для всіх `CallExpression`, що відповідають `<obj>.query(\`...\`)`(метод`.query`без квадратних дужок, перший аргумент —`TemplateLiteral`).
76
+ - **Side effects:** немає.
77
+
78
+ #### `findSharedMssqlRequestInText(content, virtualPath = 'scan.ts')`
79
+
80
+ - **Сигнатура:** `(content: string, virtualPath?: string) => { line: number, snippet: string }[]`
81
+ - **Параметри:** як вище.
82
+ - **Повертає:** масив `{ line, snippet }`. Включає `VariableDeclarator`, у яких:
83
+ - `id` — `Identifier` з імʼям рівно `request`;
84
+ - `init` — `CallExpression` виду `<obj>.request()` (через `isRequestFactoryCall`).
85
+ - **Side effects:** немає.
86
+
87
+ #### `findUnsafeMssqlDynamicSqlListInText(content, virtualPath = 'scan.ts')`
88
+
89
+ - **Сигнатура:** `(content: string, virtualPath?: string) => { line: number, snippet: string }[]`
90
+ - **Параметри:** як вище.
91
+ - **Повертає:** масив `{ line, snippet }`. Виявляє `TemplateLiteral` або `TaggedTemplateExpression` (через `.quasi`), які одночасно:
92
+ - Знаходяться в SQL-контексті списку `IN (...)` / `VALUES (...)` — через `isSqlListContextTemplate` (із `ast-scan-utils`).
93
+ - Містять у `template.expressions` хоча б одну `CallExpression` `.join(...)` — через `isJoinCall`.
94
+ - `line`/`snippet` беруться по координатам `template.start` / `template.end`.
95
+ - **Side effects:** немає.
96
+
97
+ #### `findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan.ts')`
98
+
99
+ - **Сигнатура:** `(content: string, virtualPath?: string) => { line: number, snippet: string }[]`
100
+ - **Параметри:** як вище.
101
+ - **Повертає:** масив `{ line, snippet }`. Виявляє `TemplateLiteral`, у яких:
102
+ - Quasi прямо перед `expressions[i]` закінчується на `IN (` (regex `IN_PLACEHOLDER_END_RE`, `/\bin\s*\(\s*$/iu`).
103
+ - Сам вираз `${...}` **не** є «безпечно числовим» за критеріями `isInListExpressionParsed` (літеральний масив чисел / піддерево з `parseInt|parseFloat|Number|BigInt|+x` / Identifier з безпечним `init`).
104
+ - Вираз — не `.join(...)` (це покривається окремим сканером `findUnsafeMssqlDynamicSqlListInText`).
105
+ - Перед скануванням збирає всі `VariableDeclarator`-и в програмі через `collectVariableDeclarators` — щоб трасувати Identifier до його init у тому ж файлі.
106
+ - `line` рахується по `expr.start` (або, якщо не визначено, по `node.start`); `snippet` — від `node.start` до `node.end`.
107
+ - **Side effects:** немає.
108
+
109
+ #### `findUnsafeMssqlInListMissingEmptyGuardInText(content, virtualPath = 'scan.ts')`
110
+
111
+ - **Сигнатура:** `(content: string, virtualPath?: string) => { line: number, snippet: string, reason: 'not_var' | 'missing_guard', name?: string }[]`
112
+ - **Параметри:** як вище.
113
+ - **Повертає:** масив порушень. На кожен `${expr}` у позиції `IN (...)`:
114
+ - Якщо `expr` — **не** `Identifier`, порушення з `reason: 'not_var'` (значення мали бути винесені у змінну, щоб мати точку для guard).
115
+ - Якщо `expr` — `Identifier` `name`, але в enclosing-блоці перед поточним statement немає guard `if (empty(name)) throw`, — порушення з `reason: 'missing_guard'` і `name: <identifier>`.
116
+ - Якщо guard є, порушення не додається.
117
+ - `line`/`snippet` беруться по `node.start` / `node.end` всього `TemplateLiteral`.
118
+ - **Side effects:** немає.
119
+
120
+ #### `isMssqlScanSourceFile(relativePathPosix)`
121
+
122
+ - **Сигнатура:** `(relativePathPosix: string) => boolean`
123
+ - **Параметри:** `relativePathPosix` — відносний шлях файлу у posix-форматі.
124
+ - **Повертає:** `true`, якщо файл має розширення `.js | .mjs | .cjs | .jsx | .ts | .mts | .cts | .tsx` (regex `SOURCE_FILE_RE` = `/\.([cm]?[jt]sx?)$/`) **і** не закінчується на `.d.ts` (декларації типів виключені).
125
+ - **Side effects:** немає.
126
+
127
+ ### Приватні допоміжні функції
128
+
129
+ #### Локалізація `IN (...)` і числові гарантії
130
+
131
+ - `isZeroLiteral(node)` — `node` є літералом `0` (`NumericLiteral`/`Literal` зі значенням `0`).
132
+ - `isLengthMemberOf(node, name)` — `node` — некомпʼютоване `MemberExpression` `name.length`.
133
+ - `isEmptyListBinaryTest(test, name)` — `BinaryExpression` з оператором з `EMPTY_LIST_BINARY_OPERATORS` = `{ '===', '==', '<=', '<' }`, що порівнює `name.length` із літералом `0`. Для `===` і `==` (`EMPTY_LIST_REVERSED_OPERATORS`) дозволяється зворотний порядок (`0 === name.length`).
134
+ - `isEmptyListTest(test, name)` — тест if-умови виду `!name.length` або `name.length {===,==,<=} 0`, або `name.length < 1`. **Зауваження:** `<` із порівнянням з `0` через `isEmptyListBinaryTest` дає вираз `name.length < 0` (завжди false), що рідко зустрічається на практиці; типовий `length < 1` теж покривається через оператор `<` з правою частиною `1` — але `isZeroLiteral` приймає лише `0`, тож насправді `< 1` **не** розпізнається. (Це поведінка реалізації.)
135
+ - `consequentHasThrow(consequent)` — у consequent if-у (як один `ThrowStatement` або як `BlockStatement.body`) є `ThrowStatement`.
136
+ - `hasEmptyGuardBefore(block, statementIndex, name)` — у `block.body[0..statementIndex-1]` є `IfStatement`, який одночасно перевіряє «список порожній» (`isEmptyListTest`) і кидає (`consequentHasThrow`).
137
+ - `findEnclosingBlockAndStatementIndex(ancestors)` — у списку `ancestors` (зверху-вниз) знаходить найближчу пару `(BlockStatement, indexUnderItsBody)`, де statement з `ancestors[i]` входить у `block.body`.
138
+
139
+ #### Розпізнавання AST-патернів
140
+
141
+ - `isNewConnectionPool(node)` — `new {sql|mssql}.ConnectionPool(...)`.
142
+ - `isUnsafeQueryCallWithTemplateLiteral(node)` — `<obj>.query(\`...\`)`: метод `query`(некомпʼютований), перший аргумент —`TemplateLiteral`.
143
+ - `isRequestFactoryCall(node)` — `<obj>.request()`: будь-яке `CallExpression` з `.request` як методом (некомпʼютованим).
144
+
145
+ #### Гарантії числовості значень для `IN (${...})`
146
+
147
+ - `isLiteralNumericArrayExpression(node)` — `ArrayExpression`, всі елементи якого — `NumericLiteral`/`BigIntLiteral` або generic `Literal` зі значенням типу `number`/`bigint`. Масив має бути непорожнім.
148
+ - `isNumericParseCallExpression(node)` — `CallExpression`, де `callee` — це `Identifier` з імʼям з `NUMERIC_PARSE_FN_NAMES` = `{ parseInt, parseFloat, Number, BigInt }`, або `MemberExpression` з властивістю з того ж списку (наприклад `Number.parseInt(...)`).
149
+ - `subtreeHasNumericParseCall(node)` — рекурсивно обходить піддерево й повертає `true`, якщо знайшов `isNumericParseCallExpression` або `UnaryExpression` з оператором `+`. Поле `parent` пропускається, щоб уникнути нескінченних циклів.
150
+ - `isInListExpressionParsed(expr, declarators, seen=Set)` — головний критерій «безпечно числовий вираз». Повертає `true`, якщо:
151
+ - `expr` — літеральний масив чисел;
152
+ - або в піддереві `expr` є числовий парсер / унарний `+`;
153
+ - або `expr` — `Identifier`, у якого знайдено `VariableDeclarator`-и у файлі, і **кожен** `init` цих декларацій рекурсивно проходить ту ж перевірку. `seen` — анти-цикл (Set уже трасованих імен). Якщо для Identifier немає видимого init (наприклад параметр функції чи import) — повертається `false`.
154
+
155
+ #### Збирачі порушень для `IN (...)` сканерів
156
+
157
+ - `collectVariableDeclarators(programNode)` — обхід AST і збір усіх `VariableDeclarator`-ів.
158
+ - `quasiRawText(q)` — повертає `q.value.raw` або `''`, якщо структура `q` не підходить (захист від нестандартних AST-вузлів).
159
+ - `collectInListUnparsedFromTemplate(node, content, declarators, out)` — для одного `TemplateLiteral`: для кожного `expressions[i]` перевіряє, що `quasis[i].value.raw` закінчується на `IN (` (`IN_PLACEHOLDER_END_RE`), й експресія не є `.join(...)` і не «парсована». Якщо так — додає `{ line, snippet }` у `out`.
160
+ - `collectInListMissingEmptyGuardFromTemplate(node, ancestors, content, out)` — для одного `TemplateLiteral`: для кожного `expressions[i]` після `IN (`:
161
+ - якщо `expr` — не Identifier → порушення `not_var`;
162
+ - якщо `expr` — Identifier, але в enclosing-блоці немає guard перед поточним statement → порушення `missing_guard` з `name`.
163
+
164
+ ### Side effects по модулю в цілому
165
+
166
+ Жодна функція не звертається до файлової системи / мережі та не мутує вхідні дані; усе — pure-функції над рядками й AST. Парсинг `parseSync` із `oxc-parser` — синхронний, його винятки ловляться, і у разі помилки повертається `[]`.
167
+
168
+ ---
169
+
170
+ ## Залежності
171
+
172
+ ### Зовнішні npm-залежності
173
+
174
+ - **`oxc-parser`** — імпорт `parseSync`. Швидкий парсер JS/TS (Rust-based) для побудови AST з вибором мови за `lang` і `sourceType: 'module'`.
175
+
176
+ ### Внутрішні залежності проєкту
177
+
178
+ Усі імпортуються з відносного шляху `../../../scripts/utils/ast-scan-utils.mjs`:
179
+
180
+ - `isFunctionNode(node)` — чи `node` є функціональним AST-вузлом (FunctionDeclaration / FunctionExpression / ArrowFunctionExpression / тощо).
181
+ - `isJoinCall(node)` — чи `node` — це `CallExpression` виду `<x>.join(<sep>)`.
182
+ - `isSqlListContextTemplate(template)` — чи `TemplateLiteral` знаходиться у SQL-контексті списку (`IN (...)` / `VALUES (...)`).
183
+ - `langFromPath(path)` — обчислює `lang` для парсера за розширенням файлу (наприклад `'js'`, `'ts'`, `'tsx'`).
184
+ - `normalizeSnippet(text)` — нормалізує сирий фрагмент коду до однорядкового сніппета (підрізає пробіли/нові рядки).
185
+ - `offsetToLine(content, offset)` — конвертує абсолютний offset у вихідному коді в 1-based номер рядка.
186
+ - `walkAstWithAncestors(root, initialAncestors, visitor)` — обхід AST із трекінгом `ancestors` для кожного вузла.
187
+
188
+ ### Константи модуля
189
+
190
+ - `SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/` — фільтр розширень сорсів.
191
+ - `IN_PLACEHOLDER_END_RE = /\bin\s*\(\s*$/iu` — детекція `... IN ( ` у raw-тексті quasi.
192
+ - `NUMERIC_PARSE_FN_NAMES = new Set(['parseInt', 'parseFloat', 'Number', 'BigInt'])` — імена-«парсери чисел».
193
+ - `EMPTY_LIST_BINARY_OPERATORS = new Set(['===', '==', '<=', '<'])` — оператори у тесті `name.length OP 0`.
194
+ - `EMPTY_LIST_REVERSED_OPERATORS = new Set(['===', '=='])` — оператори, для яких дозволено зворотний порядок `0 OP name.length`.
195
+
196
+ ---
197
+
198
+ ## Потік виконання / Використання
199
+
200
+ ### Спільна каркасна логіка сканера
201
+
202
+ Усі сканери побудовані однаково:
203
+
204
+ 1. Викликати `langFromPath(virtualPath || 'scan.ts')` → отримати `lang`.
205
+ 2. Викликати `parseSync(virtualPath, content, { lang, sourceType: 'module' })` у `try/catch`. У `catch` → `return []`.
206
+ 3. Якщо `result.errors?.length` → `return []` (парсер повернув помилки).
207
+ 4. Викликати `walkAstWithAncestors(result.program, [], visitor)` з тим чи іншим visitor.
208
+ 5. Повернути накопичений масив `out`.
209
+
210
+ ### Особливості окремих сканерів
211
+
212
+ - **`findMssqlPerRequestConnectionInText`** додатково перевіряє, що поточний вузол знаходиться всередині функції: `ancestors.some(isFunctionNode)`. Без цього global-declaration `const pool = new sql.ConnectionPool(...)` помилково попадало б у порушення (а це якраз бажаний патерн).
213
+ - **`findUnsafeMssqlInListUnparsedInText`** перед обходом збирає всі `VariableDeclarator` (`collectVariableDeclarators`), щоб у `isInListExpressionParsed` мати можливість трасувати Identifier → init (рекурсивно). `seen` Set захищає від циклів виду `let a = b; let b = a;`.
214
+ - **`findUnsafeMssqlInListMissingEmptyGuardInText`** використовує `ancestors` усередині visitor: `findEnclosingBlockAndStatementIndex(ancestors)` дає пару `(block, statementIndex)`, після чого `hasEmptyGuardBefore(block, statementIndex, name)` перевіряє наявність guard у тому ж блоці **до** statement, що містить запит.
215
+
216
+ ### Як викликати
217
+
218
+ ```javascript
219
+ import {
220
+ findMssqlPerRequestConnectionInText,
221
+ findSharedMssqlRequestInText,
222
+ findUnsafeMssqlDynamicSqlListInText,
223
+ findUnsafeMssqlInListMissingEmptyGuardInText,
224
+ findUnsafeMssqlInListUnparsedInText,
225
+ findUnsafeMssqlQueryTemplateCallInText,
226
+ isMssqlScanSourceFile
227
+ } from './mssql-pool-scan.mjs'
228
+
229
+ import { readFileSync } from 'node:fs'
230
+
231
+ const relPath = 'pkg/src/db.ts'
232
+ if (!isMssqlScanSourceFile(relPath)) return
233
+
234
+ const content = readFileSync(relPath, 'utf8')
235
+
236
+ const violationsPool = findMssqlPerRequestConnectionInText(content, relPath)
237
+ const violationsQuery = findUnsafeMssqlQueryTemplateCallInText(content, relPath)
238
+ const violationsShared = findSharedMssqlRequestInText(content, relPath)
239
+ const violationsJoin = findUnsafeMssqlDynamicSqlListInText(content, relPath)
240
+ const violationsUnparsed = findUnsafeMssqlInListUnparsedInText(content, relPath)
241
+ const violationsGuard = findUnsafeMssqlInListMissingEmptyGuardInText(content, relPath)
242
+
243
+ // Кожен елемент:
244
+ // { line, snippet } — для перших пʼяти
245
+ // { line, snippet, reason, name? } — для останнього
246
+ ```
247
+
248
+ ### Приклади того, що ловить кожен сканер
249
+
250
+ #### `findMssqlPerRequestConnectionInText`
251
+
252
+ Порушення:
253
+
254
+ ```javascript
255
+ export async function handler() {
256
+ const pool = new sql.ConnectionPool(config) // створення на кожен запит
257
+ await pool.connect()
258
+ }
259
+ ```
260
+
261
+ Не порушення (модульний singleton):
262
+
263
+ ```javascript
264
+ const pool = new sql.ConnectionPool(config) // на рівні модуля
265
+ ```
266
+
267
+ #### `findUnsafeMssqlQueryTemplateCallInText`
268
+
269
+ Порушення:
270
+
271
+ ```javascript
272
+ await pool.request().query(`SELECT * FROM users WHERE id = ${userId}`)
273
+ // це звичайний CallExpression з TemplateLiteral → SQL injection
274
+ ```
275
+
276
+ Не порушення (tagged template):
277
+
278
+ ```javascript
279
+ await pool.request().query`SELECT * FROM users WHERE id = ${userId}`
280
+ ```
281
+
282
+ #### `findSharedMssqlRequestInText`
283
+
284
+ Порушення:
285
+
286
+ ```javascript
287
+ export const request = pool.request()
288
+ // або
289
+ const request = somePool.request()
290
+ ```
291
+
292
+ #### `findUnsafeMssqlDynamicSqlListInText`
293
+
294
+ Порушення:
295
+
296
+ ```javascript
297
+ await sql.query`SELECT * FROM t WHERE id IN (${ids.join(',')})`
298
+ // .join(',') у списку IN/VALUES
299
+ ```
300
+
301
+ #### `findUnsafeMssqlInListUnparsedInText`
302
+
303
+ Порушення:
304
+
305
+ ```javascript
306
+ await sql.query`SELECT * FROM t WHERE id IN (${ids})`
307
+ // expr=ids, init=ids не пройшов parseInt/Number/BigInt/+
308
+ ```
309
+
310
+ Не порушення:
311
+
312
+ ```javascript
313
+ const ids = rawIds.map(x => parseInt(x, 10)).filter(Number.isFinite)
314
+ await sql.query`SELECT * FROM t WHERE id IN (${ids})`
315
+ // у піддереві ids є parseInt → subtreeHasNumericParseCall === true
316
+ ```
317
+
318
+ #### `findUnsafeMssqlInListMissingEmptyGuardInText`
319
+
320
+ Порушення (`not_var` — вираз не Identifier):
321
+
322
+ ```javascript
323
+ await sql.query`SELECT * FROM t WHERE id IN (${rawIds.map(Number)})`
324
+ ```
325
+
326
+ Порушення (`missing_guard` — немає `if (!ids.length) throw`):
327
+
328
+ ```javascript
329
+ async function load(ids) {
330
+ await sql.query`SELECT * FROM t WHERE id IN (${ids})`
331
+ }
332
+ ```
333
+
334
+ Не порушення:
335
+
336
+ ```javascript
337
+ async function load(ids) {
338
+ if (!ids.length) throw new Error('empty')
339
+ await sql.query`SELECT * FROM t WHERE id IN (${ids})`
340
+ }
341
+ ```
342
+
343
+ ### Контракти та обмеження
344
+
345
+ - На синтаксично некоректному файлі **усі** сканери мовчки повертають `[]` — це навмисна стратегія, щоб не блокувати CI на парсинг-помилках (їх виявить лінт).
346
+ - Імпорт-Identifier-и не вважаються «парсованими» (немає видимого `init`), тож `findUnsafeMssqlInListUnparsedInText` для `import { ids } from '...'` дасть порушення — це консервативна поведінка.
347
+ - `isFunctionNode`/`isJoinCall`/`isSqlListContextTemplate` визначені у `ast-scan-utils.mjs`; зміни їхньої поведінки впливають на семантику цього модуля.
348
+ - Параметр `virtualPath` керує тільки вибором мови парсера. Якщо передати `.tsx`, парсер прийме JSX/TSX-синтаксис; для `.js` — звичайний JS і так далі.
349
+
350
+ ### Точки розширення
351
+
352
+ - Додати новий клас порушень → ще одна функція `findX...InText(content, virtualPath)` + один visitor для `walkAstWithAncestors`.
353
+ - Розширити перелік числових парсерів → додати імʼя до `NUMERIC_PARSE_FN_NAMES`.
354
+ - Розширити форми guard-у → доповнити `isEmptyListTest` / `EMPTY_LIST_BINARY_OPERATORS` (наприклад додати `length < 1` через спеціальну гілку, бо поточна реалізація з `isZeroLiteral` цей варіант не покриває).
355
+
356
+ ---
357
+
358
+ ## Rebuild Test
359
+
360
+ Файл `mssql-pool-scan.mjs` можна повністю відтворити з цієї документації за такими опорними точками:
361
+
362
+ - Модуль ESM (`.mjs`), імпортує `parseSync` із `oxc-parser` і 7 утиліт із `../../../scripts/utils/ast-scan-utils.mjs` (`isFunctionNode`, `isJoinCall`, `isSqlListContextTemplate`, `langFromPath`, `normalizeSnippet`, `offsetToLine`, `walkAstWithAncestors`).
363
+ - Експортує 7 функцій (6 сканерів + `isMssqlScanSourceFile`) з підписами, описаними в розділі **Експорти / API** і **Функції**.
364
+ - Внутрішні константи: `SOURCE_FILE_RE`, `IN_PLACEHOLDER_END_RE`, `NUMERIC_PARSE_FN_NAMES`, `EMPTY_LIST_BINARY_OPERATORS`, `EMPTY_LIST_REVERSED_OPERATORS` — з точними значеннями з розділу **Залежності → Константи модуля**.
365
+ - Каркас сканера: `try { parseSync(...) } catch { return [] }`, далі `if (result.errors?.length) return []`, далі `walkAstWithAncestors(result.program, [], visitor)`, далі `return out`.
366
+ - Семантика visitor-ів — як описано в розділі **Функції → Експортовані функції** та **Збирачі порушень для `IN (...)` сканерів**.
367
+ - Допоміжні предикати (`isZeroLiteral`, `isLengthMemberOf`, `isEmptyListBinaryTest`, `isEmptyListTest`, `consequentHasThrow`, `hasEmptyGuardBefore`, `findEnclosingBlockAndStatementIndex`, `isNewConnectionPool`, `isUnsafeQueryCallWithTemplateLiteral`, `isRequestFactoryCall`, `isLiteralNumericArrayExpression`, `isNumericParseCallExpression`, `subtreeHasNumericParseCall`, `collectVariableDeclarators`, `quasiRawText`, `isInListExpressionParsed`, `collectInListUnparsedFromTemplate`, `collectInListMissingEmptyGuardFromTemplate`) — з контрактами, описаними в розділі **Приватні допоміжні функції**.
@@ -0,0 +1,144 @@
1
+ # fix.mjs — точка входу правила `js-run`
2
+
3
+ ## Огляд
4
+
5
+ Файл `npm/rules/js-run/fix.mjs` — це **диспетчер правила** `js-run` у пакеті `@nitra/cursor`. Він виконує дві ролі одночасно:
6
+
7
+ 1. **Library-режим** — експортує функцію `run(ctx)`, яку викликає зовнішній CLI-оркестратор (`npx @nitra/cursor fix <id>`) під час пакетного прогону всіх правил. У цьому режимі модуль не торкається `process.exit` і повертає `Promise<number>` із кодом виходу.
8
+ 2. **Standalone-режим** — якщо файл запущено напряму (`bun rules/js-run/fix.mjs`), він поводиться як повноцінний entry-point: завантажує конфіг, застосовує whitelist, друкує підсумок і завершує процес із відповідним exit-code.
9
+
10
+ Усю фактичну логіку (визначення `applies`, JS-concerns, policy-перевірки, mdc-references) інкапсулює спільна функція `runStandardRule`. `fix.mjs` лише прокидає `import.meta.dirname` (директорія правила) до неї або до CLI-обгортки `runRuleCli`. Файл не містить власних доменних перевірок — він суто структурний і дотримується конвенції «двох ролей `fix.mjs`», прийнятої в `@nitra/cursor` для всіх стандартних правил.
11
+
12
+ Назву правила (`js-run`) система визначає не з вмісту цього файлу, а з імені директорії, в якій він лежить (`npm/rules/js-run/`). Це дозволяє створювати ідентичні `fix.mjs` для багатьох правил, не дублюючи логіку.
13
+
14
+ ## Експорти / API
15
+
16
+ | Експорт | Тип | Призначення |
17
+ | ------- | ---------------------------------------------- | ------------------------------------------------------------------------------------ |
18
+ | `run` | `function(ctx?: RuleContext): Promise<number>` | Library entry-point правила — викликається зовнішнім оркестратором у тому ж процесі. |
19
+
20
+ Додатково в модулі присутній **side-effect блок** на рівні модуля: якщо `isRunAsCli(import.meta.url)` повертає `true`, модуль виконує `await runRuleCli(...)` і завершує процес через `process.exit(...)`. Цей блок не є експортом, але є частиною публічної поведінки файлу.
21
+
22
+ Файл не має default-експорту і не реекспортує нічого зі своїх залежностей.
23
+
24
+ ## Функції
25
+
26
+ ### `run(ctx)`
27
+
28
+ ```js
29
+ export function run(ctx) {
30
+ return runStandardRule(import.meta.dirname, ctx)
31
+ }
32
+ ```
33
+
34
+ - **Сигнатура:** `run(ctx?: RuleContext): Promise<number>`
35
+ - **Параметри:**
36
+ - `ctx` _(необовʼязковий)_ — обʼєкт контексту прогону. Тип імпортується з `../../scripts/lib/run-standard-rule.mjs` як `RuleContext`. Зазвичай містить розділяємий між правилами кеш обходу файлової системи (`walkCache`) та інші cross-rule артефакти, які дозволяють зекономити IO при батч-прогоні. Якщо аргумент не передано, `runStandardRule` створює локальний контекст самостійно.
37
+ - **Повертає:** `Promise<number>` — exit-code прогону:
38
+ - `0` — порушень не знайдено (OK);
39
+ - `1` — знайдено хоча б одне порушення.
40
+ - **Side effects:** делегуються в `runStandardRule`. Можливі: читання файлів проєкту, читання `meta.json` правила, друк діагностики у stdout/stderr, запис у спільні структури `ctx`. Сам `run` `process.exit` не викликає — це принципово для library-режиму, де декілька правил мають викликатись підряд в одному процесі.
41
+ - **Алгоритм:** одна делегація — `runStandardRule(import.meta.dirname, ctx)`. `import.meta.dirname` — абсолютний шлях до директорії, в якій лежить `fix.mjs` (тобто `.../npm/rules/js-run/`). За цим шляхом `runStandardRule` знаходить `meta.json`, підправила в `js/`, `policy/`, а також `.mdc`-документ правила.
42
+
43
+ ### Top-level CLI-блок
44
+
45
+ ```js
46
+ if (isRunAsCli(import.meta.url)) {
47
+ process.exit(await runRuleCli(import.meta.dirname))
48
+ }
49
+ ```
50
+
51
+ - **Умова активації:** `isRunAsCli(import.meta.url)` — повертає `true`, якщо файл запущено напряму як скрипт (`node`/`bun fix.mjs`), а не імпортовано як модуль. Конкретний механізм перевірки інкапсульований у `run-rule-cli.mjs`.
52
+ - **Що робить:** очікує (`await`) завершення `runRuleCli(import.meta.dirname)`, який окрім логіки `runStandardRule` додатково:
53
+ - завантажує конфіг проєкту (whitelist, exclusions);
54
+ - вирівнює аргументи CLI;
55
+ - друкує summary в кінці прогону;
56
+ - повертає exit-code.
57
+ - **Завершення:** `process.exit(<code>)` — обовʼязкове для standalone-режиму, бо CI/IDE очікують саме exit-code, а не повернене значення з `import()`.
58
+ - **ESLint suppression:** коментар `// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit` свідомо вимикає правила, які забороняють `process.exit` у бібліотечному коді. Виключення виправдане роллю файлу як standalone-entry-point — це не лібра у вузькому сенсі.
59
+
60
+ ## Залежності
61
+
62
+ Файл має рівно **дві** внутрішні залежності, обидві з `npm/scripts/lib/`:
63
+
64
+ | Імпорт | Звідки | Що звідти використовується |
65
+ | ----------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
66
+ | `isRunAsCli` | `../../scripts/lib/run-rule-cli.mjs` | Предикат: чи модуль запущено як CLI, а не імпортовано як бібліотека. Приймає `import.meta.url`. |
67
+ | `runRuleCli` | `../../scripts/lib/run-rule-cli.mjs` | Повна CLI-обгортка над `runStandardRule`: конфіг + whitelist + summary + повернення exit-code. Приймає `import.meta.dirname`. |
68
+ | `runStandardRule` | `../../scripts/lib/run-standard-rule.mjs` | Універсальний раннер «стандартного» правила: applies → JS-concerns → policy → mdc-refs. Приймає `(dirname, ctx?)`. |
69
+
70
+ Зовнішніх npm-залежностей файл не має. Опосередковано він залежить від:
71
+
72
+ - структури директорії правила (`meta.json`, підтеки `js/`, `policy/`, `.mdc`-файл);
73
+ - існування файлу `js-run.mdc` поруч (документ правила, на який спирається mdc-refs);
74
+ - API-контракту `RuleContext`, який тримається в `run-standard-rule.mjs`.
75
+
76
+ ## Потік виконання / Використання
77
+
78
+ ### Library-режим (батч-прогон правил)
79
+
80
+ ```js
81
+ // Десь у CLI-оркестраторі @nitra/cursor
82
+ import { run as runJsRun } from '@nitra/cursor/rules/js-run/fix.mjs'
83
+
84
+ const ctx = createSharedContext() // walkCache, тощо
85
+ const code = await runJsRun(ctx) // 0 або 1
86
+ if (code !== 0) failures.push('js-run')
87
+ ```
88
+
89
+ Послідовність:
90
+
91
+ 1. Оркестратор створює спільний `ctx` (один на всі правила в прогоні).
92
+ 2. Імпортує `run` з `fix.mjs` потрібного правила.
93
+ 3. Викликає `run(ctx)` і чекає `Promise<number>`.
94
+ 4. `run` делегує в `runStandardRule(dirname, ctx)`, яка читає `meta.json`, виконує applies → JS-concerns → policy → mdc-refs.
95
+ 5. Результат — exit-code — повертається оркестратору, який агрегує всі коди й вирішує, з чим завершити сам процес.
96
+
97
+ ### Standalone-режим (локальний дебаг або CI per-rule)
98
+
99
+ ```bash
100
+ bun npm/rules/js-run/fix.mjs
101
+ # або
102
+ node npm/rules/js-run/fix.mjs
103
+ ```
104
+
105
+ Послідовність:
106
+
107
+ 1. Інтерпретатор завантажує модуль.
108
+ 2. Виконується top-level код: `isRunAsCli(import.meta.url)` повертає `true` (бо файл — entrypoint).
109
+ 3. Викликається `await runRuleCli(import.meta.dirname)`:
110
+ - читає конфіг проєкту і whitelist;
111
+ - усередині запускає еквівалент `runStandardRule(dirname, ctx)`;
112
+ - друкує summary у stdout.
113
+ 4. Повернений exit-code передається в `process.exit(...)`, який завершує процес з ним же.
114
+ 5. CI/IDE/Husky отримують exit-code і вирішують, чи фейлити крок.
115
+
116
+ ### Розширення / модифікація
117
+
118
+ - Щоб **додати специфічну логіку** для `js-run` за межами «стандартної» воронки — не правити цей файл, а додати чек у `js/` або `policy/` директорію правила. `runStandardRule` сам їх підхопить.
119
+ - Щоб **використати власний контекст** — передавати `ctx` із полем `walkCache: Map<string, FsEntry[]>` для шарингу обходу між правилами.
120
+ - Щоб **тимчасово виключити правило з batch** — оркестратор просто не імпортує `run` цього файлу. Сам файл такої логіки не містить.
121
+
122
+ ### Інваріанти, які слід зберігати при змінах
123
+
124
+ 1. Library-функція `run` **ніколи не викликає** `process.exit` — інакше зламається батч-прогон.
125
+ 2. CLI-блок виконується **тільки** під охороною `isRunAsCli(import.meta.url)` — інакше імпорт зробить exit у чужому процесі.
126
+ 3. У `runStandardRule` і `runRuleCli` передається `import.meta.dirname`, а не `import.meta.url` — це шлях, а не URL. Підміна типу зламає резолвінг `meta.json` і підправил.
127
+ 4. Назва правила береться з імені директорії — не дублюй її рядком у цьому файлі.
128
+
129
+ ## Rebuild Test
130
+
131
+ Виходячи з цього документа можна відновити еквівалентний файл `fix.mjs`:
132
+
133
+ 1. Імпортувати `isRunAsCli` і `runRuleCli` з `../../scripts/lib/run-rule-cli.mjs`.
134
+ 2. Імпортувати `runStandardRule` з `../../scripts/lib/run-standard-rule.mjs`.
135
+ 3. Експортувати функцію `run(ctx)`, яка повертає `runStandardRule(import.meta.dirname, ctx)`.
136
+ 4. Додати top-level `if (isRunAsCli(import.meta.url)) { process.exit(await runRuleCli(import.meta.dirname)) }` з ESLint-suppression-коментарем для `n/no-process-exit` і `unicorn/no-process-exit`.
137
+ 5. JSDoc для `run`: параметр `ctx?: RuleContext` (тип із `run-standard-rule.mjs`), повертає `Promise<number>` (0 — OK, 1 — порушення).
138
+
139
+ Поведінкові ознаки для перевірки відновленого файлу:
140
+
141
+ - `import { run } from './fix.mjs'` працює без побічних ефектів (CLI-блок не активується).
142
+ - `bun fix.mjs` запускає повний CLI-прогон і завершує процес із кодом `0` або `1`.
143
+ - `run(ctx)` повертає `Promise`, який резолвиться у число.
144
+ - Файл не містить більше ніяких експортів і ніяких власних доменних перевірок.