@nitra/cursor 12.8.6 → 12.8.8

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 (263) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/rules/abie/main.mdc +9 -5
  4. package/rules/abie/policy/base_deployment_preem/base_deployment_preem.mdc +22 -0
  5. package/rules/abie/policy/clean_merged_ignore_branches/clean_merged_ignore_branches.mdc +19 -0
  6. package/rules/abie/policy/health_check_policy/health_check_policy.mdc +17 -0
  7. package/rules/abie/policy/http_route_base/http_route_base.mdc +9 -0
  8. package/rules/abie/policy/package_json_shared/package_json_shared.mdc +17 -0
  9. package/rules/adr/js/hooks.mdc +32 -0
  10. package/rules/adr/js/madr_format.mdc +96 -0
  11. package/rules/adr/js/settings_policy.mdc +34 -0
  12. package/rules/adr/main.mdc +17 -95
  13. package/rules/adr/policy/settings_json/settings_json.mdc +7 -0
  14. package/rules/adr/policy/settings_local_json/settings_local_json.mdc +7 -0
  15. package/rules/bun/js/bunfig.mdc +12 -0
  16. package/rules/bun/js/layout.mdc +60 -0
  17. package/rules/bun/js/lint.mdc +9 -0
  18. package/rules/bun/js/package_json.mdc +19 -0
  19. package/rules/bun/main.mdc +7 -60
  20. package/rules/bun/policy/bunfig/bunfig.mdc +12 -0
  21. package/rules/bun/policy/package_json/package_json.mdc +14 -0
  22. package/rules/capacitor/js/ios_spm.mdc +69 -0
  23. package/rules/capacitor/js/version.mdc +29 -0
  24. package/rules/capacitor/main.mdc +6 -22
  25. package/rules/capacitor/policy/package_json/package_json.mdc +9 -0
  26. package/rules/changelog/js/agent-workflow.mdc +15 -0
  27. package/rules/changelog/js/changelog-format.mdc +33 -0
  28. package/rules/changelog/js/comparison-models.mdc +40 -0
  29. package/rules/changelog/main.mdc +4 -98
  30. package/rules/ci4/js/marksman_config.mdc +31 -0
  31. package/rules/ci4/js/vscode_extensions.mdc +33 -0
  32. package/rules/ci4/main.mdc +16 -14
  33. package/rules/ci4/policy/vscode_extensions/vscode_extensions.mdc +9 -0
  34. package/rules/docker/js/compile.mdc +44 -0
  35. package/rules/docker/js/hadolint.mdc +50 -0
  36. package/rules/docker/js/mirror.mdc +13 -0
  37. package/rules/docker/js/multistage.mdc +13 -0
  38. package/rules/docker/js/native-addon.mdc +43 -0
  39. package/rules/docker/js/nginx-tag.mdc +7 -0
  40. package/rules/docker/js/nginx-user.mdc +37 -0
  41. package/rules/docker/js/non-root.mdc +39 -0
  42. package/rules/docker/main.mdc +13 -196
  43. package/rules/docker/policy/lint_docker_yml/lint_docker_yml.mdc +14 -0
  44. package/rules/efes/main.mdc +1 -1
  45. package/rules/efes/policy/package_json_shared/package_json_shared.mdc +30 -0
  46. package/rules/ga/js/lint_toolchain.mdc +15 -0
  47. package/rules/ga/js/required_workflows.mdc +35 -0
  48. package/rules/ga/js/vscode.mdc +17 -0
  49. package/rules/ga/js/workflow_common.mdc +108 -0
  50. package/rules/ga/js/workflows.mdc +32 -0
  51. package/rules/ga/js/zizmor.mdc +7 -0
  52. package/rules/ga/main.mdc +16 -119
  53. package/rules/ga/policy/clean_ga_workflows/clean_ga_workflows.mdc +18 -0
  54. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.mdc +22 -0
  55. package/rules/ga/policy/git_ai/git_ai.mdc +19 -0
  56. package/rules/ga/policy/lint_ga/lint_ga.mdc +21 -0
  57. package/rules/ga/policy/vscode_extensions/vscode_extensions.mdc +9 -0
  58. package/rules/ga/policy/vscode_settings/vscode_settings.mdc +9 -0
  59. package/rules/ga/policy/workflow_common/workflow_common.mdc +18 -0
  60. package/rules/ga/policy/zizmor_yml/zizmor_yml.mdc +9 -0
  61. package/rules/graphql/js/tooling.mdc +13 -0
  62. package/rules/graphql/js/vscode_extensions.mdc +13 -0
  63. package/rules/graphql/main.mdc +4 -21
  64. package/rules/graphql/policy/vscode_extensions/vscode_extensions.mdc +9 -0
  65. package/rules/hasura/js/internal_urls.mdc +27 -0
  66. package/rules/hasura/js/migrations.mdc +13 -0
  67. package/rules/hasura/js/svc_hl.mdc +17 -0
  68. package/rules/hasura/main.mdc +6 -30
  69. package/rules/hasura/policy/svc_hl/svc_hl.mdc +15 -0
  70. package/rules/image-avif/js/avif_generation.mdc +26 -0
  71. package/rules/image-avif/js/package_json_optout.mdc +21 -0
  72. package/rules/image-avif/main.mdc +5 -34
  73. package/rules/image-avif/policy/package_json/package_json.mdc +18 -0
  74. package/rules/image-compress/js/package_json.mdc +7 -0
  75. package/rules/image-compress/js/package_setup.mdc +13 -0
  76. package/rules/image-compress/main.mdc +4 -12
  77. package/rules/image-compress/policy/package_json/package_json.mdc +13 -0
  78. package/rules/js/docs/index.md +3 -3
  79. package/rules/js/js/dep-policy.mdc +17 -0
  80. package/rules/js/js/eslint-config.mdc +28 -0
  81. package/rules/js/js/extensions.mdc +8 -0
  82. package/rules/js/js/file-extensions.mdc +12 -0
  83. package/rules/js/js/for-in.mdc +26 -0
  84. package/rules/js/js/jscpd.mdc +42 -0
  85. package/rules/js/js/knip.mdc +15 -0
  86. package/rules/js/js/lint-js-workflow.mdc +58 -0
  87. package/rules/js/js/oxlintrc.mdc +20 -0
  88. package/rules/js/js/package-json.mdc +31 -0
  89. package/rules/js/js/tests.mdc +9 -0
  90. package/rules/js/js/utils-lib-structure.mdc +15 -0
  91. package/rules/js/main.mdc +19 -211
  92. package/rules/js/policy/jscpd/jscpd.mdc +14 -0
  93. package/rules/js/policy/lint_js_yml/lint_js_yml.mdc +14 -0
  94. package/rules/js/policy/package_json/package_json.mdc +15 -0
  95. package/rules/js/policy/vscode_extensions/vscode_extensions.mdc +11 -0
  96. package/rules/js-bun-db/js/bun-sql-migration.mdc +15 -0
  97. package/rules/js-bun-db/js/connection.mdc +42 -0
  98. package/rules/js-bun-db/js/pg-format-identifiers.mdc +102 -0
  99. package/rules/js-bun-db/js/pg-format-shim.mdc +99 -0
  100. package/rules/js-bun-db/js/pg-leftover.mdc +27 -0
  101. package/rules/js-bun-db/js/pg-listen-notify.mdc +51 -0
  102. package/rules/js-bun-db/js/query-safety.mdc +117 -0
  103. package/rules/js-bun-db/js/sql-array.mdc +88 -0
  104. package/rules/js-bun-db/js/unsafe.mdc +65 -0
  105. package/rules/js-bun-db/main.mdc +12 -607
  106. package/rules/js-bun-db/policy/package_json/package_json.mdc +17 -0
  107. package/rules/js-bun-redis/js/imports.mdc +47 -0
  108. package/rules/js-bun-redis/js/package_json.mdc +44 -0
  109. package/rules/js-bun-redis/main.mdc +4 -10
  110. package/rules/js-bun-redis/policy/package_json/package_json.mdc +11 -0
  111. package/rules/js-mssql/js/mssql-in-list.mdc +38 -0
  112. package/rules/js-mssql/js/mssql-pool.mdc +56 -0
  113. package/rules/js-mssql/js/mssql-query-template.mdc +33 -0
  114. package/rules/js-mssql/js/mssql-tvp.mdc +75 -0
  115. package/rules/js-mssql/js/mssql-version.mdc +7 -0
  116. package/rules/js-mssql/main.mdc +10 -198
  117. package/rules/js-mssql/policy/package_json/package_json.mdc +9 -0
  118. package/rules/js-run/js/check-env.mdc +35 -0
  119. package/rules/js-run/js/conn-aliases.mdc +109 -0
  120. package/rules/js-run/js/jsconfig.mdc +20 -0
  121. package/rules/js-run/js/otel-configmap.mdc +6 -0
  122. package/rules/js-run/js/pino.mdc +6 -0
  123. package/rules/js-run/js/project-structure.mdc +11 -0
  124. package/rules/js-run/js/runtime.mdc +14 -0
  125. package/rules/js-run/js/scope.mdc +11 -0
  126. package/rules/js-run/js/settimeout.mdc +11 -0
  127. package/rules/js-run/js/temporal.mdc +5 -0
  128. package/rules/js-run/main.mdc +16 -216
  129. package/rules/js-run/policy/configmap/configmap.mdc +31 -0
  130. package/rules/js-run/policy/jsconfig/jsconfig.mdc +25 -0
  131. package/rules/js-run/policy/package_json/package_json.mdc +38 -0
  132. package/rules/k8s/js/configmap.mdc +41 -0
  133. package/rules/k8s/js/deployment_resources.mdc +49 -0
  134. package/rules/k8s/js/hasura_httproute.mdc +91 -0
  135. package/rules/k8s/js/hpa_apiversion.mdc +27 -0
  136. package/rules/k8s/js/ingress_gateway.mdc +16 -0
  137. package/rules/k8s/js/kustomize_structure.mdc +144 -0
  138. package/rules/k8s/js/lint_k8s.mdc +72 -0
  139. package/rules/k8s/js/multidoc_yaml.mdc +5 -0
  140. package/rules/k8s/js/network_policy.mdc +136 -0
  141. package/rules/k8s/js/schema_modeline.mdc +57 -0
  142. package/rules/k8s/js/service.mdc +44 -0
  143. package/rules/k8s/js/topology_hpa_pdb.mdc +181 -0
  144. package/rules/k8s/main.mdc +29 -834
  145. package/rules/k8s/policy/base_kustomization/base_kustomization.mdc +12 -0
  146. package/rules/k8s/policy/base_manifest/base_manifest.mdc +14 -0
  147. package/rules/k8s/policy/gateway/gateway.mdc +17 -0
  148. package/rules/k8s/policy/hasura_configmap/hasura_configmap.mdc +20 -0
  149. package/rules/k8s/policy/hasura_httproute/hasura_httproute.mdc +16 -0
  150. package/rules/k8s/policy/hpa_pdb/hpa_pdb.mdc +23 -0
  151. package/rules/k8s/policy/kustomization/kustomization.mdc +20 -0
  152. package/rules/k8s/policy/manifest/manifest.mdc +17 -0
  153. package/rules/k8s/policy/network_policy/network_policy.mdc +22 -0
  154. package/rules/k8s/policy/svc_hl_yaml/svc_hl_yaml.mdc +13 -0
  155. package/rules/k8s/policy/svc_yaml/svc_yaml.mdc +12 -0
  156. package/rules/nginx-default-tpl/js/dockerfile.mdc +36 -0
  157. package/rules/nginx-default-tpl/js/http-route.mdc +41 -0
  158. package/rules/nginx-default-tpl/js/ini-keys.mdc +21 -0
  159. package/rules/nginx-default-tpl/js/template-structure.mdc +86 -0
  160. package/rules/nginx-default-tpl/js/vscode.mdc +37 -0
  161. package/rules/nginx-default-tpl/main.mdc +8 -110
  162. package/rules/nginx-default-tpl/policy/vscode_extensions/vscode_extensions.mdc +11 -0
  163. package/rules/nginx-default-tpl/policy/vscode_settings/vscode_settings.mdc +15 -0
  164. package/rules/npm-module/js/docs/index.md +5 -5
  165. package/rules/npm-module/js/docs/rule_meta.md +6 -6
  166. package/rules/npm-module/js/docs/skill_meta.md +8 -8
  167. package/rules/npm-module/js/header_doc_pointer.mdc +18 -0
  168. package/rules/npm-module/js/package_structure.mdc +62 -0
  169. package/rules/npm-module/js/rule_meta.mdc +11 -0
  170. package/rules/npm-module/js/skill_meta.mdc +11 -0
  171. package/rules/npm-module/main.mdc +10 -52
  172. package/rules/npm-module/policy/emit_types_config/emit_types_config.mdc +40 -0
  173. package/rules/npm-module/policy/npm_package_json/npm_package_json.mdc +50 -0
  174. package/rules/npm-module/policy/root_package_json/root_package_json.mdc +37 -0
  175. package/rules/php/js/lint_php_yml.mdc +12 -0
  176. package/rules/php/js/tooling.mdc +66 -0
  177. package/rules/php/main.mdc +5 -66
  178. package/rules/php/policy/lint_php_yml/lint_php_yml.mdc +21 -0
  179. package/rules/python/js/lint_python_yml.mdc +23 -0
  180. package/rules/python/js/pyproject_toml.mdc +32 -0
  181. package/rules/python/js/tooling.mdc +23 -0
  182. package/rules/python/main.mdc +7 -32
  183. package/rules/python/policy/lint_python_yml/lint_python_yml.mdc +12 -0
  184. package/rules/python/policy/pyproject_toml/pyproject_toml.mdc +13 -0
  185. package/rules/rego/js/rego-lint.mdc +31 -0
  186. package/rules/rego/js/vscode_extensions.mdc +11 -0
  187. package/rules/rego/js/vscode_settings.mdc +13 -0
  188. package/rules/rego/main.mdc +10 -22
  189. package/rules/rego/policy/vscode_extensions/vscode_extensions.mdc +11 -0
  190. package/rules/rego/policy/vscode_settings/vscode_settings.mdc +19 -0
  191. package/rules/rust/js/coverage.mdc +28 -0
  192. package/rules/rust/js/lint.mdc +22 -0
  193. package/rules/rust/js/tauri_composition.mdc +8 -0
  194. package/rules/rust/js/vscode_extensions.mdc +12 -0
  195. package/rules/rust/main.mdc +8 -38
  196. package/rules/rust/policy/lint_rust_yml/lint_rust_yml.mdc +12 -0
  197. package/rules/rust/policy/vscode_extensions/vscode_extensions.mdc +9 -0
  198. package/rules/security/js/rego_policies.mdc +15 -0
  199. package/rules/security/js/sample_secret.mdc +19 -0
  200. package/rules/security/js/trufflehog.mdc +21 -0
  201. package/rules/security/main.mdc +7 -34
  202. package/rules/security/policy/lint_security_yml/lint_security_yml.mdc +7 -0
  203. package/rules/security/policy/package_json/package_json.mdc +7 -0
  204. package/rules/style/js/admin-table.mdc +88 -0
  205. package/rules/style/js/colors.mdc +21 -0
  206. package/rules/style/js/gap.mdc +22 -0
  207. package/rules/style/js/quasar-fixes.mdc +32 -0
  208. package/rules/style/js/quasar.mdc +7 -0
  209. package/rules/style/js/tooling.mdc +85 -0
  210. package/rules/style/main.mdc +12 -251
  211. package/rules/style/policy/lint_style_yml/lint_style_yml.mdc +13 -0
  212. package/rules/style/policy/package_json/package_json.mdc +18 -0
  213. package/rules/style/policy/vscode_extensions/vscode_extensions.mdc +13 -0
  214. package/rules/style/policy/vscode_settings/vscode_settings.mdc +19 -0
  215. package/rules/tauri/js/cargo_mutants_config.mdc +39 -0
  216. package/rules/tauri/js/tool_surface.mdc +21 -0
  217. package/rules/tauri/js/tooling.mdc +25 -0
  218. package/rules/tauri/main.mdc +6 -78
  219. package/rules/tauri/policy/vscode_extensions/vscode_extensions.mdc +21 -0
  220. package/rules/test/js/cargo_mutants_config.mdc +18 -0
  221. package/rules/test/js/docs/index.md +7 -7
  222. package/rules/test/js/location.mdc +52 -0
  223. package/rules/test/js/no-console-store-restore.mdc +11 -0
  224. package/rules/test/js/no-process-chdir.mdc +15 -0
  225. package/rules/test/js/no-relative-fs-path.mdc +22 -0
  226. package/rules/test/js/sandbox-aware-test.mdc +28 -0
  227. package/rules/test/js/stryker_config.mdc +26 -0
  228. package/rules/test/js/vitest-config-pool-forks.mdc +33 -0
  229. package/rules/test/main.mdc +16 -184
  230. package/rules/test/policy/package_json/package_json.mdc +16 -0
  231. package/rules/text/js/ci-lint-text.mdc +15 -0
  232. package/rules/text/js/cspell.mdc +81 -0
  233. package/rules/text/js/dotenv-linter.mdc +16 -0
  234. package/rules/text/js/forbidden-prettier.mdc +13 -0
  235. package/rules/text/js/markdownlint.mdc +25 -0
  236. package/rules/text/js/oxfmt.mdc +35 -0
  237. package/rules/text/js/package-json.mdc +26 -0
  238. package/rules/text/js/shellcheck.mdc +18 -0
  239. package/rules/text/js/v8r.mdc +23 -0
  240. package/rules/text/js/vscode.mdc +86 -0
  241. package/rules/text/main.mdc +20 -231
  242. package/rules/text/policy/cspell/cspell.mdc +34 -0
  243. package/rules/text/policy/lint_text/lint_text.mdc +19 -0
  244. package/rules/text/policy/markdownlint/markdownlint.mdc +38 -0
  245. package/rules/text/policy/oxfmtrc/oxfmtrc.mdc +11 -0
  246. package/rules/text/policy/package_json/package_json.mdc +33 -0
  247. package/rules/text/policy/vscode_extensions/vscode_extensions.mdc +13 -0
  248. package/rules/text/policy/vscode_settings/vscode_settings.mdc +13 -0
  249. package/rules/vue/js/composition-api.mdc +82 -0
  250. package/rules/vue/js/nheader-layout.mdc +171 -0
  251. package/rules/vue/js/node-imports.mdc +25 -0
  252. package/rules/vue/js/quasar-ui.mdc +32 -0
  253. package/rules/vue/js/structure.mdc +101 -0
  254. package/rules/vue/js/testing.mdc +32 -0
  255. package/rules/vue/js/tfm-translations.mdc +26 -0
  256. package/rules/vue/js/vite-config.mdc +126 -0
  257. package/rules/vue/js/vite-env.mdc +55 -0
  258. package/rules/vue/js/vue-imports.mdc +25 -0
  259. package/rules/vue/main.mdc +15 -641
  260. package/rules/vue/policy/package_json/package_json.mdc +30 -0
  261. package/scripts/docs/index.md +16 -16
  262. package/scripts/lib/docs/index.md +36 -36
  263. package/scripts/utils/docs/index.md +14 -14
package/rules/js/main.mdc CHANGED
@@ -5,232 +5,40 @@ alwaysApply: false
5
5
  version: '1.30'
6
6
  ---
7
7
 
8
- **oxlint**, **ESLint**, **jscpd**, **knip**. Запуск — **`n-cursor lint js`** (локально; у CI — `--read-only`, без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config`** — версія не нижче канонічного мінімуму зі snippet нижче (semver-поріг, єдине джерело істини) (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint). Dependency-політика CI-етапу: `@e18e/eslint-plugin` і oxlint/eslint/jscpd/knip окремо не додавати.
8
+ **oxlint**, **ESLint**, **jscpd**, **knip**. Запуск — **`n-cursor lint js`** (локально; у CI — `--read-only`, без **`--fix`** для oxlint/eslint). Без **prettier** і **@nitra/prettier-config**.
9
9
 
10
- У кожному **`package.json`** проєкту (корінь і всі workspace-пакети) має бути **`"type": "module"`** — весь код у ESM.
10
+ [js-file-extensions](./js/file-extensions.mdc)
11
11
 
12
- ```json title="package.json"
13
- {
14
- "type": "module",
15
- "devDependencies": {
16
- "@nitra/eslint-config": "^3.10.0"
17
- }
18
- }
19
- ```
12
+ [js-package-json](./js/package-json.mdc)
20
13
 
21
- Канон `type` і мінімальна `@nitra/eslint-config` (semver-поріг `devDependencies`): [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json). Окремого `lint-js` скрипта немає — лінт через **`n-cursor lint js`** (CI — `--read-only`).
14
+ [js-eslint-config](./js/eslint-config.mdc)
22
15
 
23
- ## Розширення нових файлів — `.mjs` / `.cjs`, не `.js`
16
+ [js-oxlintrc](./js/oxlintrc.mdc)
24
17
 
25
- **Нові** JS-файли створюй з явним розширенням модуля:
18
+ [js-extensions](./js/extensions.mdc)
26
19
 
27
- - **`.mjs`** — для ESM (типовий випадок);
28
- - **`.cjs`** — для CommonJS, де він справді потрібен.
20
+ [js-jscpd](./js/jscpd.mdc)
29
21
 
30
- Голий **`.js`** для нового файлу **заборонено**. Розширення `.js` інтерпретується як ESM чи CJS лише за полем `package.json#type`, тож той самий файл читається по-різному залежно від пакета. Явне `.mjs`/`.cjs` робить тип модуля однозначним **без читання `package.json`** — навіть якщо `type` зміниться або файл перемістять в інший пакет. Це доповнює вимогу `"type": "module"` вище: `type` лишається каноном для всього дерева, а розширення нового файлу прибирає залежність від нього.
22
+ [js-knip](./js/knip.mdc)
31
23
 
32
- Стосується **backend і frontend** — будь-який новий вихідний файл: `src/`, тести `*.test.*`, `scripts/`, `src/conn/` тощо.
24
+ [js-dep-policy](./js/dep-policy.mdc)
33
25
 
34
- **Існуючі `.js` лишаються як є** — масово перейменовувати не треба; це конвенція для нового коду. Автоматичної перевірки тут немає: stateless-скан не відрізнить новий файл від існуючого, тож `.js` нікого не фейлить.
26
+ [js-lint-js-workflow](./js/lint-js-workflow.mdc)
35
27
 
36
- У `.vscode/extensions.json` `recommendations` мають містити `dbaeumer.vscode-eslint`, `github.vscode-github-actions`, `oxc.oxc-vscode`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
28
+ [js-utils-lib-structure](./js/utils-lib-structure.mdc)
37
29
 
38
- У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/rules/js/js/data/tooling/oxlint-canonical.json`** (plugins, jsPlugins з **`@e18e/eslint-plugin`**, categories, повний набір **rules** із канону — додаткові записи в **`rules`** дозволені; також **`settings`**, **`env`**, **`globals`**). Поле **`ignorePatterns`** працює як **`rules`**: канонічні патерни з **`oxlint-canonical.json`** (наразі **`**/schema.graphql`**, **`**/auto-imports.d.ts`**) мають бути присутні, додаткові локальні glob-и дозволені. Канон **`oxlint-canonical.json`** — source-of-truth, редагується напряму; у споживачі оновлюється копіюванням файлу з репозиторію пакета. Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.8.0**), oxlint підвантажує його з **`node_modules`**.
30
+ [js-for-in](./js/for-in.mdc)
39
31
 
40
- Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
32
+ [js-tests](./js/tests.mdc)
41
33
 
42
- ```json title=".oxlintrc.json (фрагмент)"
43
- {
44
- "jsPlugins": ["@e18e/eslint-plugin"],
45
- "rules": {
46
- "e18e/prefer-includes": "error"
47
- }
48
- }
49
- ```
34
+ ## Швидкий gate через conftest
50
35
 
51
- У корені проєкту має бути `.jscpd.json`. Мінімум: увімкнути облік `.gitignore`, ненульовий код виходу при знаходженні клонів, консольний звіт. За потреби додай `ignore` (дзеркальні каталоги, шаблони) та `minLines`, щоб відсікти дрібні збіги.
36
+ Rego-пакети у `policy/` запускаються `npx @nitra/cursor fix js` або `conftest`:
52
37
 
53
- Каталог `.claude/worktrees/` (робочі копії, які Claude Code створює через **superpowers:using-git-worktrees**) має ігноруватися: додай його у кореневий `.gitignore` (це штатне місце для не-комітних робочих копій), а в `.jscpd.json` додай `.claude/worktrees/**` у `ignore` як страховку на випадок запуску без `gitignore: true`. Без цього jscpd сканує паралельну копію репо в worktree і фіксує самозбіги між дзеркальними файлами.
38
+ [js-policy-package_json](./policy/package_json/package_json.mdc)
54
39
 
55
- `**/CHANGELOG.md` теж у каноні `ignore`: release-журнали різних пакетів структурно повторюються (заголовки `## [x.y.z] - YYYY-MM-DD`, секції `### Added` / `### Changed` / `### Fixed` за Keep a Changelog), і `jscpd` за `minLines: 25` фіксує їх як клон, хоч це false positive — кожен `CHANGELOG.md` веде свій per-package журнал за каноном `n-changelog`, спільної історії не існує. Без цього в монорепо легко зловити критичне `bun run lint` на парі CHANGELOG-ів довжиною від ~25 рядків.
40
+ [js-policy-jscpd](./policy/jscpd/jscpd.mdc)
56
41
 
57
- ```json title=".jscpd.json"
58
- {
59
- "gitignore": true,
60
- "exitCode": 1,
61
- "reporters": ["console"],
62
- "minLines": 25,
63
- "ignore": [".claude/worktrees/**", "**/dist/**", "**/CHANGELOG.md"]
64
- }
65
- ```
42
+ [js-policy-lint_js_yml](./policy/lint_js_yml/lint_js_yml.mdc)
66
43
 
67
- Канон ключів `.jscpd.json` (`gitignore`, `exitCode`, `reporters`, `minLines`, `ignore` як subset-of): [.jscpd.json.snippet.json](./policy/jscpd/template/.jscpd.json.snippet.json)
68
-
69
- ```text title=".gitignore (фрагмент)"
70
- .claude/worktrees/
71
- ```
72
-
73
- ## Залежнісна політика (що не додавати)
74
-
75
- `@e18e/eslint-plugin` окремо не додавай — він уже в залежностях `@nitra/eslint-config` (з **3.8.0**), oxlint підвантажує його з `node_modules`. Пакети oxlint/eslint/jscpd/knip теж не додавай у `devDependencies` без потреби монорепо — `bunx` тягне їх ad-hoc.
76
-
77
- ## knip
78
-
79
- Залежнісний аналіз (knip — невикористані залежності/експорти, `knip.json` канон) крос-файловий; запускається автоматично у `lint --full` разом з jscpd.
80
-
81
- У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/rules/js/js/tooling/knip-canonical.json`](./js/tooling/knip-canonical.json). Він покриває типові false-positives для наших правил: `entry` зі CLI-конфігами (eslint, stylelint, oxlint, jscpd, markdownlint-cli2, `commitlint`), `project` для `**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}`, `ignore` для `**/__fixtures__/**`, `ignoreDependencies` для пакетів, посилання на які є лише в не-JS-конфігах (`@nitra/cspell-dict`, `/@cspell\/dict-.+/`, `graphql`), і `ignoreBinaries` для CLI, які канон вимагає викликати через `npx`/`bunx` і яких заборонено додавати в `devDependencies` (`actionlint`, `cspell`, `eslint`, `git-ai`, `jscpd`, `markdownlint-cli2`, `oxfmt`, `oxlint`, `shellcheck`, `uvx`, `v8r`, `zizmor`).
82
-
83
- Якщо `knip.json` відсутній — `npx @nitra/cursor fix js` копіює канон у корінь проєкту (side effect). Після створення модифікуй файл під свій проєкт як завгодно: перевіряємо лише наявність, зміст подальших змін не валідується.
84
-
85
- Пакет `knip` окремо в `devDependencies` не додавай — `bunx knip` тягне його ad-hoc, як oxlint/eslint/jscpd.
86
-
87
- ## Заборона `@nitra/as-integrations-fastify`
88
-
89
- Пакет **`@nitra/as-integrations-fastify`** заборонений у **`dependencies`**, **`peerDependencies`** та в import-specifier-ах. Це чистий републіш upstream, застряглий на peer `@apollo/server: "^4.0.0"`, тож на Apollo 5 `bun install` дає `warn: incorrect peer dependency`. Заміна — upstream **`@as-integrations/fastify`** (**`^3.1.0`**): peer `@apollo/server: "^4.0.0 || ^5.0.0"` + `fastify: "^5.3.0"`.
90
-
91
- Міграція — лише specifier у трьох місцях: залежність у `package.json`, `import`, та `vi.mock(...)` / `await import(...)` у тестах. **Код не міняється**, експорти ті самі: `default` → `fastifyApollo`, named → `fastifyApolloDrainPlugin`.
92
-
93
- ```javascript title="❌ до"
94
- import fastifyApollo, { fastifyApolloDrainPlugin } from '@nitra/as-integrations-fastify'
95
- ```
96
-
97
- ```javascript title="✅ після"
98
- import fastifyApollo, { fastifyApolloDrainPlugin } from '@as-integrations/fastify'
99
- ```
100
-
101
- ## jscpd: рефакторинг і структура
102
-
103
- Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
104
-
105
- - **Рефакторинг:** винеси спільні фрагменти в функції, модулі, утиліти, composables/hooks, спільні компоненти або базові типи — залежно від контексту.
106
- - **Структура:** якщо одна й та сама логіка розмазана між файлами чи пакетами, запропонуй зміну структури (наприклад, спільний модуль, `shared/`, внутрішній пакет у monorepo), щоб була **одна канонічна реалізація** і повторні місця лише викликали її.
107
-
108
- Так ти уникаєш **хибних обходів** перевірки: розширення `ignore` чи завищення `minLines` лише щоб прибрати звіт — не заміна рефакторингу для справжніх клонів. Якщо збіг **семантично випадковий** (генерований код, формальні шаблони без спільної логіки), після оцінки допустимо точковий `ignore` або зміна порогу — з коротким обґрунтуванням.
109
-
110
- Але обов'язково перед рефакторингом перевір чи є тести на блоки які підлягають зміні, а саме Bun.test для js та playwright для vue. Якщо є, то перевір чи вони покривають блоки які підлягають зміні. Якщо не покривають або тестів немає — спочатку створи їх, перевір що вони покривають і відпрацьовують коректно, а потім роби рефакторинг і ще раз запускай тести, але якщо тести не відпрацьовують коректно після рефакторингу, то не роби рефакторинг.
111
-
112
- ## Структура спільних модулів: `utils/` vs `lib/`
113
-
114
- Коли спільні методи виносяться з кількох JS-файлів в окремий каталог — назву каталога обирай за призначенням модулів усередині.
115
-
116
- - **`utils/`** — низькорівневі, чисті, generic helpers без бізнес-логіки і без залежностей від домену проєкту. Те, що могло б жити в окремому npm-пакеті. Приклади: `formatDate()`, `chunk(array, size)`, `retry(fn, opts)`, `deepMerge()`, `parseDuration('5m')`, `sleep(ms)`.
117
-
118
- - **`lib/`** — внутрішні модулі / підсистеми проєкту, які знають про домен. Більші за `utils/`, часто з власним state, конфігом або side effects. Приклади: `lib/gmail-client.js`, `lib/circuit-breaker.js`, `lib/skill-loader.js`, `lib/safety/dry-run.js`.
119
-
120
- Швидкий тест: якщо файл завтра можна опублікувати окремим npm-пакетом без переписування — це `utils/`; якщо він тримає domain-state, читає конфіг проєкту або викликає зовнішні сервіси/файли — це `lib/`. Не плутай із чужими каталогами на кшталт `shared/` чи `common/`: канонічні назви — лише `utils/` і `lib/`.
121
-
122
- Автоматично гарантується: `npx @nitra/cursor fix js` (концерн `utils_imports`) обходить кожен `utils/`-каталог у воркспейсах і падає, якщо знаходить relative-імпорт з `..` (вихід за межі каталогу) у будь-якому не-тестовому файлі. Це механічне втілення правила «utils не знає про домен»: тільки same-dir (`./X`), bare-пакети та `node:*` дозволені; cross-rule, конфіги проєкту чи sibling-utils — fail. Якщо потрібна domain-залежність — перенеси файл у `lib/`.
123
-
124
- Додай workflow:
125
-
126
- ```yaml title=".github/workflows/lint-js.yml"
127
- name: Lint JS
128
-
129
- on:
130
- push:
131
- branches:
132
- - dev
133
- - main
134
- paths:
135
- - '**/*.js'
136
- - '**/*.mjs'
137
- - '**/*.cjs'
138
- - '**/*.jsx'
139
- - '**/*.ts'
140
- - '**/*.tsx'
141
- - '**/*.vue'
142
- - '**/eslint.config.*'
143
-
144
- pull_request:
145
- branches:
146
- - dev
147
- - main
148
-
149
- concurrency:
150
- group: ${{ github.ref }}-${{ github.workflow }}
151
- cancel-in-progress: true
152
-
153
- jobs:
154
- eslint:
155
- runs-on: ubuntu-latest
156
- permissions:
157
- contents: read
158
- steps:
159
- - uses: actions/checkout@v6
160
- with:
161
- persist-credentials: false
162
-
163
- - uses: ./.github/actions/setup-bun-deps
164
-
165
- - name: Eslint
166
- run: |
167
- bunx oxlint
168
- bunx eslint .
169
- bunx jscpd .
170
- bunx knip --no-config-hints
171
- ```
172
-
173
- Канон workflow `.github/workflows/lint-js.yml`: [lint-js.yml.snippet.yml](./policy/lint_js_yml/template/lint-js.yml.snippet.yml)
174
-
175
- Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
176
-
177
- Один workflow на лінт JS; зайвий `lint.yml` з тими самими кроками — прибери.
178
-
179
- ```javascript title="eslint.config.js"
180
- import { getConfig } from '@nitra/eslint-config'
181
-
182
- export default [
183
- {
184
- ignores: ['**/auto-imports.d.ts']
185
- },
186
- ...getConfig({
187
- node: ['npm']
188
- })
189
- ]
190
- ```
191
-
192
- У монорепо пакети з Vite (frontend) вкажи в секції `vue`, решту — у секції `node` у виклику `getConfig`.
193
-
194
- ## Додаткові js правила
195
-
196
- Завжди додавай до package.json що підтримується 24+ версія node і Bun 1.3+:
197
-
198
- ```json title="package.json"
199
- "engines": {
200
- "node": ">=24",
201
- "bun": ">=1.3"
202
- }
203
- ```
204
-
205
- ## `for...in` заборонено — рефакторити на `for...of`
206
-
207
- Конструкція `for (const k in obj)` обходить успадковані ключі прототипу, тому майже завжди тягне `Object.hasOwn(obj, k)`-guard. Заборонена у `@nitra/eslint-config` через `no-restricted-syntax` для `ForInStatement` (з версії **3.8.0**). У каноні oxlint лишається `guard-for-in` як часткова страховка (oxlint не підтримує `no-restricted-syntax`). Замість цього обходь масив напряму, а обʼєкт — через `Object.entries` / `Object.keys` / `Object.values`. У такому коді guard за `Object.hasOwn` стає непотрібним і має зникнути разом із `for...in`.
208
-
209
- ```javascript title="❌ до"
210
- for (const k in obj) {
211
- if (!Object.hasOwn(obj, k)) continue
212
- use(k, obj[k])
213
- }
214
-
215
- for (const i in arr) {
216
- use(arr[i])
217
- }
218
- ```
219
-
220
- ```javascript title="✅ після"
221
- for (const [k, v] of Object.entries(obj)) {
222
- use(k, v)
223
- }
224
-
225
- for (const item of arr) {
226
- use(item)
227
- }
228
- ```
229
-
230
- ## Тести
231
-
232
- Проєкт має бути покритий unit-тестами (**Bun test**). Код: синтаксис Node **24+**, **top level await** (узгоджено з `engines.node` у `package.json`).
233
-
234
- ## Покриття + мутаційне тестування JS
235
-
236
- Покриття + мутаційне тестування JS постачаються через `n-cursor coverage` (правило `test.mdc`). Реалізація провайдера — у `npm/rules/js/coverage/coverage.mjs`: `bun test --coverage --coverage-reporter=lcov` + `bunx stryker run`. Stryker конфігурується в `stryker.config.mjs` у JS-корені (single-package або `workspaces[0]`).
44
+ [js-policy-vscode_extensions](./policy/vscode_extensions/vscode_extensions.mdc)
@@ -0,0 +1,14 @@
1
+ ## Rego-gate `.jscpd.json`
2
+
3
+ Rego-пакет: `js_lint.jscpd`
4
+
5
+ Цільовий файл: `.jscpd.json` у корені проєкту.
6
+
7
+ Що перевіряється:
8
+
9
+ - `gitignore` — точна відповідність (`true`)
10
+ - `exitCode` — точна відповідність (`1`)
11
+ - `reporters` — subset-of: масив має містити всі елементи з канону (`["console"]`)
12
+ - `minLines` — число, значення >= канонічного порогу (`25`); більше дозволено
13
+
14
+ Канон-snippet: [.jscpd.json.snippet.json](./template/.jscpd.json.snippet.json)
@@ -0,0 +1,14 @@
1
+ ## Rego-gate `.github/workflows/lint-js.yml`
2
+
3
+ Rego-пакет: `js_lint.lint_js_yml`
4
+
5
+ Цільовий файл: `.github/workflows/lint-js.yml`.
6
+
7
+ Що перевіряється:
8
+
9
+ - Наявність усіх `uses:`-кроків з канону (subset-of по `jobs.eslint.steps`)
10
+ - Наявність усіх рядків `run:` з канону (substring-match по всіх кроках)
11
+ - `actions/checkout@v6` має мати `with.persist-credentials: false` (inverse-check)
12
+ - Заборона `--fix` у CI: `bunx oxlint ... --fix` або `eslint --fix` у будь-якому `run:` → deny
13
+
14
+ Канон-snippet: [lint-js.yml.snippet.yml](./template/lint-js.yml.snippet.yml)
@@ -0,0 +1,15 @@
1
+ ## Rego-gate `package.json`
2
+
3
+ Rego-пакет: `js_lint.package_json`
4
+
5
+ Цільовий файл: `package.json` (корінь і workspace-пакети).
6
+
7
+ Що перевіряється:
8
+
9
+ - `"type"` — точна відповідність `"module"` (з канон-snippet)
10
+ - `scripts.lint-js` — нормалізована точна відповідність (collapse whitespace)
11
+ - `engines.node` — semver-парсинг: major >= 24 (inverse, без template)
12
+ - `engines.bun` — semver-парсинг: >= 1.3 (inverse, без template)
13
+ - `devDependencies["@nitra/eslint-config"]` — наявність ключа + semver >= порогу зі snippet (`^3.10.0`); `workspace:*` завжди проходить
14
+
15
+ Канон-snippet: [package.json.snippet.json](./template/package.json.snippet.json)
@@ -0,0 +1,11 @@
1
+ ## Rego-gate `.vscode/extensions.json`
2
+
3
+ Rego-пакет: `js_lint.vscode_extensions`
4
+
5
+ Цільовий файл: `.vscode/extensions.json`.
6
+
7
+ Що перевіряється:
8
+
9
+ - `recommendations` — subset-of: масив має містити всі три канонічні розширення (`dbaeumer.vscode-eslint`, `github.vscode-github-actions`, `oxc.oxc-vscode`)
10
+
11
+ Канон-snippet: [extensions.json.snippet.json](./template/extensions.json.snippet.json)
@@ -0,0 +1,15 @@
1
+ ## Перехід з `pg` / `mysql2` на Bun native SQL
2
+
3
+ PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом, підключаємось як `mysql://`).
4
+
5
+ Якщо в проєкті використовуються бібліотеки `pg`, `pg-format` або `mysql2`, їх потрібно замінити на Bun native SQL: <https://bun.com/docs/runtime/sql>.
6
+
7
+ - Видалити з `dependencies`: `pg-pool`, `pg-native`, `pg-format`, `mysql`, `mysql2`.
8
+ - Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
9
+ - Замінити на `import { sql, SQL } from 'bun'` — Bun має вбудований клієнт із пулом, prepared statements та tagged templates.
10
+
11
+ Канон заборонених `dependencies` (`pg-format`, `mysql2`): [package.json.deny.json](./policy/package_json/template/package.json.deny.json). Сам `pg` із денилисту прибрано — він має одне легітимне виключення (LISTEN/NOTIFY), яке зважує AST-сканер; деталі — у `js/pg-listen-notify.mdc`.
12
+
13
+ `pg-format` (unscoped) — це ручне форматування SQL через escape (`format('... %L ...', value)`); такі рядки легко поламати неправильним типом, locale-залежним escape або забутим `%L`. Tagged template Bun SQL параметризує значення нативно (`sql\`... ${value} ...\``) і не лишає простору для injection — окремий «форматер» **для значень** не потрібен.
14
+
15
+ Виключення є **лише** для **динамічних identifiers** (назви схем / таблиць / колонок / індексів / ролей / БД) і whitelist-фрагментів типу `ASC`/`DESC`: Bun SQL їх параметризувати не вміє, тож тут дозволено окремий пакет **`@scaleleap/pg-format`** (scoped форк, не unscoped `pg-format`) — деталі й приклади у `js/pg-format-identifiers.mdc`.
@@ -0,0 +1,42 @@
1
+ ## Підключення (singleton + env)
2
+
3
+ Дефолтний експорт `sql` з `'bun'` сам читає змінні середовища (`DATABASE_URL`, `POSTGRES_URL`, `MYSQL_URL`, `PGHOST`/`PGUSER`/... та `MYSQL_HOST`/`MYSQL_USER`/...) і керує пулом — окремий `Pool` як у `pg` створювати не треба.
4
+
5
+ Для явного конфігу — `new SQL(...)` як **singleton** на рівні модуля, а не на кожен запит. Файл кладеться у `src/conn/db.mjs` і експортує іменовані константи `pgWrite` (основний запис) та `pgRead` (read-only replica), щоб glob `**/src/conn/**` у правилах покривав ці файли:
6
+
7
+ ```javascript
8
+ // src/conn/db.mjs
9
+ import { SQL } from 'bun'
10
+
11
+ export const pgWrite = new SQL({
12
+ url: process.env.DATABASE_URL,
13
+ max: 20,
14
+ idleTimeout: 30,
15
+ connectionTimeout: 10
16
+ })
17
+
18
+ export const pgRead = new SQL({
19
+ url: process.env.PG_CONN_READ ?? process.env.DATABASE_URL,
20
+ max: 10,
21
+ idleTimeout: 30,
22
+ connectionTimeout: 10
23
+ })
24
+ ```
25
+
26
+ Connection string обирає адаптер автоматично:
27
+
28
+ - `postgres://...` / `postgresql://...` → PostgreSQL
29
+ - `mysql://...` / `mysql2://...` → MySQL/MariaDB
30
+ - `sqlite://...` / `file://...` / `:memory:` → SQLite
31
+
32
+ ### Не створювати підключення на кожен запит
33
+
34
+ ```javascript
35
+ // ❌ нове підключення/інстанс на кожен виклик
36
+ function getUser(id) {
37
+ const pgWrite = new SQL(process.env.DATABASE_URL)
38
+ return pgWrite`SELECT * FROM users WHERE id = ${id}`
39
+ }
40
+ ```
41
+
42
+ `new SQL(...)` має створюватись **один раз** на рівні модуля. Bun сам тримає пул (`max`, `idleTimeout`, `maxLifetime`) — окремих `Pool`/`Client` як у `pg` не потрібно.
@@ -0,0 +1,102 @@
1
+ ## Динамічна SQL-структура: `@scaleleap/pg-format` для identifiers
2
+
3
+ Bun SQL **не вміє** параметризувати назви схем, таблиць, колонок, індексів, ролей, БД — а `sql\`SELECT * FROM ${table}\`` забіндив би це як значення і зламав би синтаксис. Для **динамічних identifiers** дозволено окремий пакет:
4
+
5
+ ```bash
6
+ bun add @scaleleap/pg-format
7
+ ```
8
+
9
+ ⚠️ Це **scoped `@scaleleap/pg-format`**, а не unscoped `pg-format` (той у [deny-списку](./policy/package_json/template/package.json.deny.json)). Беремо форк `@scaleleap` **тільки** заради `%I` / `%s`-можливостей; значення все одно проходять через Bun parameters, **не** через `%L`.
10
+
11
+ ### Дозволений патерн
12
+
13
+ - **`%I`** — escape SQL identifier (schema / table / column / index / role / database).
14
+ - **`%s`** — raw fragment, **тільки** для whitelist-значень (`ASC` / `DESC`, тип JOIN'у тощо).
15
+ - Значення — позиційні параметри `$1, $2, …`, які передаються другим аргументом у `sql.unsafe(query, [bindParams])`.
16
+ - На рядку виклику `sql.unsafe(...)` обов'язковий маркер `// allow-unsafe: <причина>` (div. `js/unsafe.mdc`).
17
+
18
+ ```javascript
19
+ import format from '@scaleleap/pg-format'
20
+ import { sql } from 'bun'
21
+
22
+ const allowedColumns = new Set(['created_at', 'email', 'name'])
23
+ if (!allowedColumns.has(sortBy)) throw new Error('Invalid sort column')
24
+
25
+ const direction = sortDir === 'asc' ? 'ASC' : 'DESC'
26
+
27
+ const query = format(
28
+ 'SELECT * FROM %I.%I ORDER BY %I %s LIMIT $1',
29
+ schemaName,
30
+ tableName,
31
+ sortBy,
32
+ direction
33
+ )
34
+ // allow-unsafe: динамічні schema/table/column; значення біндяться через $N
35
+ const rows = await sql.unsafe(query, [limit])
36
+ ```
37
+
38
+ Multi-row `INSERT` через `VALUES %L` теж типовий легітимний кейс, але передавай значення колонок як паралельні масиви через `unnest(...)` Bun SQL — `format('VALUES %L', rows)` лишай тільки коли альтернатива з `unnest` неможлива:
39
+
40
+ ```javascript
41
+ const query = format(
42
+ /* sql */ `
43
+ INSERT INTO "order".delivery_status (order_id, status, changed_at)
44
+ SELECT v.order_id::uuid, v.status, v.changed_at::timestamptz
45
+ FROM (VALUES %L) AS v(order_id, status, changed_at)
46
+ `,
47
+ values
48
+ )
49
+ // allow-unsafe: multi-row VALUES для бекфілу; values формуються з валідованого input
50
+ await sql.unsafe(query)
51
+ ```
52
+
53
+ ### Заборонено й після підключення `@scaleleap/pg-format`
54
+
55
+ - **`%L` для user input** — це повернення `pg-format`-стилю. Завжди bind через Bun (`sql\`... = ${value}\``) або позиційний параметр `$N` + `sql.unsafe(query, [params])`.
56
+ - Збирати весь `WHERE` через `format(...)` з `%L` — користуйся whitelist полів і ручним складанням `$N`-placeholder'ів (приклад нижче).
57
+ - Власні функції `format` / `pgFormat` / `sqlFormat` / `pgFmt` з тілом, що містить `%L` / `%I` / `%s`, — `fail` сканера (це шим, а не імпорт з бібліотеки).
58
+ - Експортовані `quoteLiteral` / `quoteIdent` / `escapeLiteral` / `escapeIdent` — `fail` сканера (pg-format-специфічні API замість Bun parameters).
59
+
60
+ ### Dynamic `WHERE` — без `format(...)`, через whitelist + `$N`
61
+
62
+ ```javascript
63
+ const conditions = []
64
+ const values = []
65
+
66
+ if (email) {
67
+ values.push(email)
68
+ conditions.push(`email = $${values.length}`)
69
+ }
70
+ if (status) {
71
+ values.push(status)
72
+ conditions.push(`status = $${values.length}`)
73
+ }
74
+
75
+ const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''
76
+ const query = `SELECT * FROM users ${where}`
77
+ // allow-unsafe: динамічний WHERE з whitelist-полів; значення біндяться через $N
78
+ const rows = await sql.unsafe(query, values)
79
+ ```
80
+
81
+ ### Коротка таблиця рішень
82
+
83
+ | Сценарій | Що використовувати |
84
+ | --------------------------------- | ---------------------------------------------------- |
85
+ | `WHERE id = ${...}` | Bun SQL tagged template |
86
+ | `INSERT` одного рядка | Bun SQL tagged template |
87
+ | `INSERT` масиву (object/colset) | Bun SQL helper `sql(rows, 'a', 'b')` або `unnest` |
88
+ | `UPDATE field = ${value}` | Bun SQL tagged template |
89
+ | Динамічна назва schema / table | `@scaleleap/pg-format` `%I` + `sql.unsafe(q, [...])` |
90
+ | Динамічна назва колонки | `@scaleleap/pg-format` `%I` + bind |
91
+ | Динамічний `ORDER BY column` | whitelist + `%I` |
92
+ | `ASC` / `DESC`, тип JOIN'у | whitelist + `%s` |
93
+ | Динамічний `WHERE` (полів багато) | whitelist + ручні `$N` + `sql.unsafe(text, vals)` |
94
+ | Сирий migration / DDL | `sql.unsafe(text)` з `// allow-unsafe: <причина>` |
95
+ | User input як value | **тільки** Bun parameters / `$N` bind |
96
+ | Масив значень у `unnest(...)` | `sql.array(arr, type)` — обов'язково з типом |
97
+
98
+ Головне правило:
99
+
100
+ - **SQL values** → Bun SQL parameters (tagged template `${value}` або `$N` + `sql.unsafe(text, values)`).
101
+ - **SQL identifiers** → `@scaleleap/pg-format` `%I` (schema, table, column, index, role, database).
102
+ - **SQL fragments** (`ASC`/`DESC` тощо) → whitelist + `%s`.
@@ -0,0 +1,99 @@
1
+ ## `pg-format`: повне видалення, без шимів
2
+
3
+ Міграція з `pg-format` — це **зміна стилю запитів**, а не збереження API. У проєкті після переходу на Bun SQL **заборонено** залишати:
4
+
5
+ - функцію з іменем `format` (чи `pgFormat`, `sqlFormat`, `pgFmt`), що приймає шаблон з `%L` / `%I` / `%s` і значення;
6
+ - допоміжні `quoteLiteral`, `quoteIdent`, `escapeLiteral`, `escapeIdent` як публічні експорти модуля;
7
+ - обгортки `pgRead.query(text, params)` / `pgWrite.query(text, params)` / `db.query(text, params)`, які складають SQL-рядок (з або без `format`) і викликають `sql.unsafe(text, params)` — це повертає injection-поверхню, від якої ми йдемо, тільки під «зручним» іменем.
8
+
9
+ Замість цього всі точки використання потрібно перевести на tagged template `sql\`...\${value}...\``. Кожне `${value}` стає окремим параметром bind, без рядкового екранування.
10
+
11
+ ### Типові ідіоми `pg-format` → Bun SQL
12
+
13
+ | Було (`pg-format`) | Стало (Bun SQL) |
14
+ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
15
+ | `format('... WHERE id = %L', id)` | `sql\`... WHERE id = ${id}\`` |
16
+ | `format('... IN (%L)', ids)` | `sql\`... IN ${sql(ids)}\`` (з guard на пустоту перед запитом) |
17
+ | `format('INSERT ... VALUES %L', [row])` (1 рядок) | `sql\`INSERT ... VALUES (${a}, ${b}, ...)\`` |
18
+ | `format('INSERT ... VALUES %L', rows)` (N рядків) | `sql\`INSERT INTO t ${sql(rows, 'a', 'b')}\`` або `unnest($1::T[], $2::T[]) AS t(a, b)` |
19
+ | `format('MERGE ... USING (VALUES %L) AS d(...)')` | `sql\`MERGE ... USING (SELECT * FROM unnest(${arrA}::A[], ${arrB}::B[]) AS t(a, b)) AS d\`` |
20
+ | `format('... %I ...', tableName)` (whitelist) | `@scaleleap/pg-format`: `format('%I', name)` + `sql.unsafe(text, [params])` з маркером |
21
+
22
+ Для multi-row `VALUES` у `MERGE` / `INSERT` з конкретними типами — паралельні масиви по колонках і `unnest($1::TYPE[], $2::TYPE[], ...) AS t(col1, col2, ...)`. Кожна колонка передається одним параметром-масивом; типи задаються кастом масиву (`::uuid[]`, `::bigint[]`, `::numeric[]`, `::text[]`, …).
23
+
24
+ #### Приклад: MERGE з UNNEST і динамічними колонками
25
+
26
+ ```javascript
27
+ // ❌ format + pgWrite.unsafe — N×7 окремих значень, план змінюється при кожному batch.length
28
+ const valuesSql = batch
29
+ .map(row => format('(%L::int, %L::date, %L::jsonb)', row.id, row.date, JSON.stringify(row.data)))
30
+ .join(',')
31
+ const sql = format(`MERGE INTO t USING (VALUES %s) AS s(id, date, data) ON ...`, valuesSql)
32
+ await pgWrite.unsafe(sql)
33
+
34
+ // ✅ UNNEST — 3 параметри незалежно від розміру batch; план стабільний і може кешуватись
35
+ await pgWrite`
36
+ WITH s(id, date, data) AS (
37
+ SELECT * FROM unnest(
38
+ ${pgWrite.array(col(batch, 'id'), 'int4')},
39
+ ${pgWrite.array(col(batch, 'date'), 'date')},
40
+ ${pgWrite.array(col(batch, 'data'), 'jsonb')}
41
+ )
42
+ )
43
+ MERGE INTO my_table AS t
44
+ USING s ON t.id = s.id
45
+ WHEN MATCHED THEN
46
+ UPDATE SET date = s.date, data = s.data
47
+ WHEN NOT MATCHED THEN
48
+ INSERT (id, date, data) VALUES (s.id, s.date, s.data)
49
+ `
50
+ ```
51
+
52
+ Якщо частина колонок у SET/INSERT залежить від параметра (plan/fact, тип тощо) — динамічні імена колонок не можна параметризувати через `${value}`; використовуй умовні Bun SQL фрагменти:
53
+
54
+ ```javascript
55
+ // ✅ умовні фрагменти для динамічних ідентифікаторів колонок
56
+ const colFrag = isPlan ? pgWrite`plan_value` : pgWrite`fact_value`
57
+ const hashFrag = isPlan ? pgWrite`hash = s.hash,` : pgWrite``
58
+
59
+ await pgWrite`
60
+ ...
61
+ WHEN MATCHED THEN
62
+ UPDATE SET
63
+ ${colFrag} = s.value,
64
+ ${hashFrag}
65
+ updated_by = s.updated_by
66
+ WHEN NOT MATCHED THEN
67
+ INSERT (id, ${colFrag}, updated_by)
68
+ VALUES (s.id, s.value, s.updated_by)
69
+ `
70
+ ```
71
+
72
+ ### Заборонений «drop-in» шим
73
+
74
+ ```javascript
75
+ // ❌ pg-format-сумісний шим, що ховає `unsafe` під «безпечним» іменем
76
+ export function format(fmt, ...args) {
77
+ let i = 0
78
+ return fmt.replaceAll(/%[LIs]/g, () => quoteLiteral(args[i++]))
79
+ }
80
+
81
+ // ❌ і його типовий call-site — той самий injection-вектор, що і прямий sql.unsafe із конкатенацією
82
+ await sql.unsafe(format('... WHERE id = %L', userId))
83
+ ```
84
+
85
+ ```javascript
86
+ // ❌ pg-сумісна обгортка над Bun SQL — ще один прихований `unsafe`
87
+ export const pgWrite = {
88
+ query(text, params) {
89
+ return sql.unsafe(text, params)
90
+ }
91
+ }
92
+ ```
93
+
94
+ ```javascript
95
+ // ✅ напряму tagged template — параметризація через wire-protocol bind
96
+ await sql`... WHERE id = ${userId}`
97
+ ```
98
+
99
+ Виняток для шиму `format()` під час поетапної міграції допускається **тільки** в окремому commit'і з TODO-маркером і дедлайном; готовий код у main з таким шимом — `fail` правила (детектори: `pg-format shim`, `query wrapper over unsafe`).
@@ -0,0 +1,27 @@
1
+ ## Прибирати pg-leftover виклики (`.connect()`, `.end()`)
2
+
3
+ У файлах з Bun SQL (`import { sql, SQL } from 'bun'`) залишки від `pg` — `pool.connect()`, `client.end()`, `pool.end()` — мають бути видалені. Bun SQL пулом керує сам: на першому запиті підключається, idle/lifetime закриває за конфігом — окремий життєвий цикл вручну не потрібен.
4
+
5
+ ```javascript
6
+ // ❌ pg-leftover: ручний lifecycle, який Bun SQL робить за тебе
7
+ const client = await pool.connect()
8
+ try {
9
+ await client.query('...')
10
+ } finally {
11
+ await client.end()
12
+ }
13
+
14
+ // ✅ Bun SQL — без явних .connect()/.end()
15
+ await sql`...`
16
+ ```
17
+
18
+ Якщо виклик дійсно потрібен (наприклад, `sql.end()` у graceful shutdown або `.connect()` на сторонньому об'єкті, що випадково ділить імʼя методу), додай маркер `// allow-pg-leftover: <причина>` на тому ж рядку (trailing) або на рядку безпосередньо перед викликом:
19
+
20
+ ```javascript
21
+ // allow-pg-leftover: graceful shutdown — закриваємо пул перед exit
22
+ await sql.end()
23
+
24
+ ws.connect(url) // allow-pg-leftover: WebSocket, не pg
25
+ ```
26
+
27
+ Формат маркера: `allow-pg-leftover: <непорожня причина>` у line- або block-коментарі. Без маркера й без причини — **fail** перевірки.