@nitra/cursor 12.8.5 → 12.8.7

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 (202) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/n-cursor.js +5 -5
  3. package/package.json +1 -1
  4. package/rules/abie/js/http_route_base.mdc +25 -0
  5. package/rules/abie/js/ua_http_route.mdc +1 -1
  6. package/rules/abie/main.mdc +12 -0
  7. package/rules/adr/js/hooks.mdc +32 -0
  8. package/rules/adr/js/madr_format.mdc +96 -0
  9. package/rules/adr/js/settings_policy.mdc +34 -0
  10. package/rules/adr/main.mdc +13 -95
  11. package/rules/bun/js/bunfig.mdc +12 -0
  12. package/rules/bun/js/layout.mdc +60 -0
  13. package/rules/bun/js/lint.mdc +9 -0
  14. package/rules/bun/js/package_json.mdc +19 -0
  15. package/rules/bun/main.mdc +9 -61
  16. package/rules/capacitor/js/ios_spm.mdc +69 -0
  17. package/rules/capacitor/js/version.mdc +29 -0
  18. package/rules/capacitor/main.mdc +8 -22
  19. package/rules/changelog/js/agent-workflow.mdc +15 -0
  20. package/rules/changelog/js/changelog-format.mdc +33 -0
  21. package/rules/changelog/js/comparison-models.mdc +40 -0
  22. package/rules/changelog/main.mdc +4 -98
  23. package/rules/ci4/js/marksman_config.mdc +31 -0
  24. package/rules/ci4/js/vscode_extensions.mdc +33 -0
  25. package/rules/ci4/main.mdc +14 -14
  26. package/rules/docker/js/compile.mdc +44 -0
  27. package/rules/docker/js/hadolint.mdc +50 -0
  28. package/rules/docker/js/mirror.mdc +13 -0
  29. package/rules/docker/js/multistage.mdc +13 -0
  30. package/rules/docker/js/native-addon.mdc +43 -0
  31. package/rules/docker/js/nginx-tag.mdc +7 -0
  32. package/rules/docker/js/nginx-user.mdc +37 -0
  33. package/rules/docker/js/non-root.mdc +39 -0
  34. package/rules/docker/main.mdc +15 -196
  35. package/rules/ga/js/lint_toolchain.mdc +15 -0
  36. package/rules/ga/js/required_workflows.mdc +35 -0
  37. package/rules/ga/js/vscode.mdc +17 -0
  38. package/rules/ga/js/workflow_common.mdc +108 -0
  39. package/rules/ga/js/workflows.mdc +32 -0
  40. package/rules/ga/js/zizmor.mdc +7 -0
  41. package/rules/ga/main.mdc +17 -125
  42. package/rules/graphql/js/tooling.mdc +13 -0
  43. package/rules/graphql/js/vscode_extensions.mdc +13 -0
  44. package/rules/graphql/main.mdc +3 -22
  45. package/rules/hasura/js/internal_urls.mdc +27 -0
  46. package/rules/hasura/js/migrations.mdc +13 -0
  47. package/rules/hasura/js/svc_hl.mdc +17 -0
  48. package/rules/hasura/main.mdc +8 -30
  49. package/rules/image-avif/js/avif_generation.mdc +26 -0
  50. package/rules/image-avif/js/package_json_optout.mdc +21 -0
  51. package/rules/image-avif/main.mdc +7 -34
  52. package/rules/image-compress/js/package_json.mdc +7 -0
  53. package/rules/image-compress/js/package_setup.mdc +13 -0
  54. package/rules/image-compress/main.mdc +4 -12
  55. package/rules/js/docs/index.md +3 -3
  56. package/rules/js/js/dep-policy.mdc +17 -0
  57. package/rules/js/js/eslint-config.mdc +28 -0
  58. package/rules/js/js/extensions.mdc +8 -0
  59. package/rules/js/js/file-extensions.mdc +12 -0
  60. package/rules/js/js/for-in.mdc +26 -0
  61. package/rules/js/js/jscpd.mdc +42 -0
  62. package/rules/js/js/knip.mdc +15 -0
  63. package/rules/js/js/lint-js-workflow.mdc +58 -0
  64. package/rules/js/js/oxlintrc.mdc +20 -0
  65. package/rules/js/js/package-json.mdc +31 -0
  66. package/rules/js/js/tests.mdc +9 -0
  67. package/rules/js/js/utils-lib-structure.mdc +15 -0
  68. package/rules/js/main.mdc +21 -214
  69. package/rules/js-bun-db/js/bun-sql-migration.mdc +15 -0
  70. package/rules/js-bun-db/js/connection.mdc +42 -0
  71. package/rules/js-bun-db/js/pg-format-identifiers.mdc +102 -0
  72. package/rules/js-bun-db/js/pg-format-shim.mdc +99 -0
  73. package/rules/js-bun-db/js/pg-leftover.mdc +27 -0
  74. package/rules/js-bun-db/js/pg-listen-notify.mdc +51 -0
  75. package/rules/js-bun-db/js/query-safety.mdc +117 -0
  76. package/rules/js-bun-db/js/sql-array.mdc +88 -0
  77. package/rules/js-bun-db/js/unsafe.mdc +65 -0
  78. package/rules/js-bun-db/main.mdc +15 -605
  79. package/rules/js-bun-redis/js/imports.mdc +47 -0
  80. package/rules/js-bun-redis/js/package_json.mdc +44 -0
  81. package/rules/js-bun-redis/main.mdc +3 -11
  82. package/rules/js-mssql/js/mssql-in-list.mdc +38 -0
  83. package/rules/js-mssql/js/mssql-pool.mdc +56 -0
  84. package/rules/js-mssql/js/mssql-query-template.mdc +33 -0
  85. package/rules/js-mssql/js/mssql-tvp.mdc +75 -0
  86. package/rules/js-mssql/js/mssql-version.mdc +7 -0
  87. package/rules/js-mssql/main.mdc +10 -198
  88. package/rules/js-run/js/check-env.mdc +35 -0
  89. package/rules/js-run/js/conn-aliases.mdc +109 -0
  90. package/rules/js-run/js/jsconfig.mdc +20 -0
  91. package/rules/js-run/js/otel-configmap.mdc +6 -0
  92. package/rules/js-run/js/pino.mdc +6 -0
  93. package/rules/js-run/js/project-structure.mdc +11 -0
  94. package/rules/js-run/js/runtime.mdc +14 -0
  95. package/rules/js-run/js/scope.mdc +11 -0
  96. package/rules/js-run/js/settimeout.mdc +11 -0
  97. package/rules/js-run/js/temporal.mdc +5 -0
  98. package/rules/js-run/main.mdc +16 -218
  99. package/rules/k8s/js/configmap.mdc +41 -0
  100. package/rules/k8s/js/deployment_resources.mdc +49 -0
  101. package/rules/k8s/js/hasura_httproute.mdc +91 -0
  102. package/rules/k8s/js/hpa_apiversion.mdc +27 -0
  103. package/rules/k8s/js/ingress_gateway.mdc +16 -0
  104. package/rules/k8s/js/kustomize_structure.mdc +144 -0
  105. package/rules/k8s/js/lint_k8s.mdc +72 -0
  106. package/rules/k8s/js/multidoc_yaml.mdc +5 -0
  107. package/rules/k8s/js/network_policy.mdc +136 -0
  108. package/rules/k8s/js/schema_modeline.mdc +57 -0
  109. package/rules/k8s/js/service.mdc +44 -0
  110. package/rules/k8s/js/topology_hpa_pdb.mdc +181 -0
  111. package/rules/k8s/main.mdc +30 -843
  112. package/rules/nginx-default-tpl/js/dockerfile.mdc +36 -0
  113. package/rules/nginx-default-tpl/js/http-route.mdc +41 -0
  114. package/rules/nginx-default-tpl/js/ini-keys.mdc +21 -0
  115. package/rules/nginx-default-tpl/js/template-structure.mdc +86 -0
  116. package/rules/nginx-default-tpl/js/vscode.mdc +37 -0
  117. package/rules/nginx-default-tpl/main.mdc +6 -112
  118. package/rules/npm-module/js/docs/index.md +5 -5
  119. package/rules/npm-module/js/docs/rule_meta.md +6 -6
  120. package/rules/npm-module/js/docs/skill_meta.md +8 -8
  121. package/rules/npm-module/js/header_doc_pointer.mdc +18 -0
  122. package/rules/npm-module/js/package_structure.mdc +62 -0
  123. package/rules/npm-module/js/rule_meta.mdc +11 -0
  124. package/rules/npm-module/js/skill_meta.mdc +11 -0
  125. package/rules/npm-module/main.mdc +10 -55
  126. package/rules/php/js/lint_php_yml.mdc +12 -0
  127. package/rules/php/js/tooling.mdc +66 -0
  128. package/rules/php/main.mdc +7 -66
  129. package/rules/python/js/lint_python_yml.mdc +23 -0
  130. package/rules/python/js/pyproject_toml.mdc +32 -0
  131. package/rules/python/js/tooling.mdc +23 -0
  132. package/rules/python/main.mdc +9 -33
  133. package/rules/rego/js/rego-lint.mdc +31 -0
  134. package/rules/rego/js/vscode_extensions.mdc +11 -0
  135. package/rules/rego/js/vscode_settings.mdc +13 -0
  136. package/rules/rego/main.mdc +8 -24
  137. package/rules/rust/js/coverage.mdc +28 -0
  138. package/rules/rust/js/lint.mdc +22 -0
  139. package/rules/rust/js/tauri_composition.mdc +8 -0
  140. package/rules/rust/js/vscode_extensions.mdc +12 -0
  141. package/rules/rust/main.mdc +8 -38
  142. package/rules/security/js/rego_policies.mdc +15 -0
  143. package/rules/security/js/sample_secret.mdc +19 -0
  144. package/rules/security/js/trufflehog.mdc +21 -0
  145. package/rules/security/main.mdc +7 -35
  146. package/rules/style/js/admin-table.mdc +88 -0
  147. package/rules/style/js/colors.mdc +21 -0
  148. package/rules/style/js/gap.mdc +22 -0
  149. package/rules/style/js/quasar-fixes.mdc +32 -0
  150. package/rules/style/js/quasar.mdc +7 -0
  151. package/rules/style/js/tooling.mdc +85 -0
  152. package/rules/style/main.mdc +13 -253
  153. package/rules/tauri/js/cargo_mutants_config.mdc +39 -0
  154. package/rules/tauri/js/tool_surface.mdc +21 -0
  155. package/rules/tauri/js/tooling.mdc +25 -0
  156. package/rules/tauri/main.mdc +8 -78
  157. package/rules/test/js/cargo_mutants_config.mdc +18 -0
  158. package/rules/test/js/docs/index.md +7 -7
  159. package/rules/test/js/location.mdc +52 -0
  160. package/rules/test/js/no-console-store-restore.mdc +11 -0
  161. package/rules/test/js/no-process-chdir.mdc +15 -0
  162. package/rules/test/js/no-relative-fs-path.mdc +22 -0
  163. package/rules/test/js/sandbox-aware-test.mdc +28 -0
  164. package/rules/test/js/stryker_config.mdc +26 -0
  165. package/rules/test/js/vitest-config-pool-forks.mdc +33 -0
  166. package/rules/test/main.mdc +18 -184
  167. package/rules/text/js/ci-lint-text.mdc +15 -0
  168. package/rules/text/js/cspell.mdc +81 -0
  169. package/rules/text/js/dotenv-linter.mdc +16 -0
  170. package/rules/text/js/forbidden-prettier.mdc +13 -0
  171. package/rules/text/js/markdownlint.mdc +25 -0
  172. package/rules/text/js/oxfmt.mdc +35 -0
  173. package/rules/text/js/package-json.mdc +26 -0
  174. package/rules/text/js/shellcheck.mdc +18 -0
  175. package/rules/text/js/v8r.mdc +23 -0
  176. package/rules/text/js/vscode.mdc +86 -0
  177. package/rules/text/main.mdc +20 -237
  178. package/rules/vue/js/composition-api.mdc +82 -0
  179. package/rules/vue/js/nheader-layout.mdc +171 -0
  180. package/rules/vue/js/node-imports.mdc +25 -0
  181. package/rules/vue/js/quasar-ui.mdc +32 -0
  182. package/rules/vue/js/structure.mdc +101 -0
  183. package/rules/vue/js/testing.mdc +32 -0
  184. package/rules/vue/js/tfm-translations.mdc +26 -0
  185. package/rules/vue/js/vite-config.mdc +126 -0
  186. package/rules/vue/js/vite-env.mdc +55 -0
  187. package/rules/vue/js/vue-imports.mdc +25 -0
  188. package/rules/vue/main.mdc +16 -640
  189. package/scripts/auto-rules.mjs +6 -6
  190. package/scripts/auto-skills.mjs +3 -3
  191. package/scripts/docs/auto-rules.md +17 -31
  192. package/scripts/docs/auto-skills.md +18 -163
  193. package/scripts/docs/index.md +16 -16
  194. package/scripts/lib/docs/index.md +36 -36
  195. package/scripts/lib/docs/mirror-parity.md +7 -7
  196. package/scripts/lib/docs/rule-meta.md +12 -12
  197. package/scripts/lib/docs/skill-meta.md +9 -9
  198. package/scripts/lib/docs/worktree-notice.md +10 -8
  199. package/scripts/lib/rule-meta.mjs +6 -6
  200. package/scripts/lib/skill-meta.mjs +6 -6
  201. package/scripts/lib/worktree-notice.mjs +2 -2
  202. package/scripts/utils/docs/index.md +14 -14
@@ -0,0 +1,42 @@
1
+ ## `.jscpd.json` — виявлення дублювання коду
2
+
3
+ У корені проєкту має бути `.jscpd.json`. Мінімум: увімкнути облік `.gitignore`, ненульовий код виходу при знаходженні клонів, консольний звіт.
4
+
5
+ ```json title=".jscpd.json"
6
+ {
7
+ "gitignore": true,
8
+ "exitCode": 1,
9
+ "reporters": ["console"],
10
+ "minLines": 25,
11
+ "ignore": [".claude/worktrees/**", "**/dist/**", "**/CHANGELOG.md"]
12
+ }
13
+ ```
14
+
15
+ Канон ключів `.jscpd.json` (`gitignore`, `exitCode`, `reporters`, `minLines`, `ignore` як subset-of): [.jscpd.json.snippet.json](./policy/jscpd/template/.jscpd.json.snippet.json)
16
+
17
+ ### Ігнорування `.claude/worktrees/`
18
+
19
+ Каталог `.claude/worktrees/` (робочі копії, які Claude Code створює через **superpowers:using-git-worktrees**) має ігноруватися:
20
+ - додай `.claude/worktrees/` у кореневий `.gitignore` (штатне місце для не-комітних робочих копій);
21
+ - у `.jscpd.json` додай `.claude/worktrees/**` у `ignore` як страховку на випадок запуску без `gitignore: true`.
22
+
23
+ Без цього jscpd сканує паралельну копію репо в worktree і фіксує самозбіги між дзеркальними файлами.
24
+
25
+ ```text title=".gitignore (фрагмент)"
26
+ .claude/worktrees/
27
+ ```
28
+
29
+ ### Ігнорування `**/CHANGELOG.md`
30
+
31
+ `**/CHANGELOG.md` у каноні `ignore`: release-журнали різних пакетів структурно повторюються (заголовки `## [x.y.z] - YYYY-MM-DD`, секції `### Added / Changed / Fixed` за Keep a Changelog), і `jscpd` за `minLines: 25` фіксує їх як клон — це false positive. Без цього в монорепо легко зловити критичний `bun run lint` на парі CHANGELOG-ів довжиною від ~25 рядків.
32
+
33
+ ### Рефакторинг vs конфіг
34
+
35
+ Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
36
+
37
+ - **Рефакторинг:** винеси спільні фрагменти в функції, модулі, утиліти, composables/hooks, спільні компоненти або базові типи — залежно від контексту.
38
+ - **Структура:** якщо одна й та сама логіка розмазана між файлами чи пакетами, запропонуй зміну структури (наприклад, спільний модуль, `shared/`, внутрішній пакет у monorepo), щоб була **одна канонічна реалізація** і повторні місця лише викликали її.
39
+
40
+ Розширення `ignore` чи завищення `minLines` лише щоб прибрати звіт — не заміна рефакторингу для справжніх клонів. Якщо збіг **семантично випадковий** (генерований код, формальні шаблони без спільної логіки), після оцінки допустимо точковий `ignore` або зміна порогу — з коротким обґрунтуванням.
41
+
42
+ Перед рефакторингом перевір чи є тести на блоки які підлягають зміні (Bun.test для js, playwright для vue). Якщо тестів немає або вони не покривають — спочатку створи їх, перевір що вони відпрацьовують коректно, потім роби рефакторинг і ще раз запускай тести.
@@ -0,0 +1,15 @@
1
+ ## `knip.json` — аналіз невикористаних залежностей та експортів
2
+
3
+ Залежнісний аналіз (knip — невикористані залежності/експорти, `knip.json` канон) крос-файловий; запускається автоматично у `lint --full` разом з jscpd.
4
+
5
+ У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/rules/js/js/tooling/knip-canonical.json`](./js/tooling/knip-canonical.json). Канон покриває типові false-positives для наших правил:
6
+
7
+ - `entry` зі CLI-конфігами (eslint, stylelint, oxlint, jscpd, markdownlint-cli2, `commitlint`);
8
+ - `project` для `**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}`;
9
+ - `ignore` для `**/__fixtures__/**`;
10
+ - `ignoreDependencies` для пакетів, посилання на які є лише в не-JS-конфігах (`@nitra/cspell-dict`, `/@cspell\/dict-.+/`, `graphql`);
11
+ - `ignoreBinaries` для CLI, які канон вимагає викликати через `npx`/`bunx` і яких заборонено додавати в `devDependencies` (`actionlint`, `cspell`, `eslint`, `git-ai`, `jscpd`, `markdownlint-cli2`, `oxfmt`, `oxlint`, `shellcheck`, `uvx`, `v8r`, `zizmor`).
12
+
13
+ Якщо `knip.json` відсутній — `npx @nitra/cursor fix js` копіює канон у корінь проєкту (side effect). Після створення модифікуй файл під свій проєкт як завгодно: перевіряємо лише наявність, зміст подальших змін не валідується.
14
+
15
+ Пакет `knip` окремо в `devDependencies` не додавай — `bunx knip` тягне його ad-hoc, як oxlint/eslint/jscpd.
@@ -0,0 +1,58 @@
1
+ ## GitHub Actions: `lint-js.yml` — CI-лінт JS
2
+
3
+ Додай workflow для лінту JS у CI:
4
+
5
+ ```yaml title=".github/workflows/lint-js.yml"
6
+ name: Lint JS
7
+
8
+ on:
9
+ push:
10
+ branches:
11
+ - dev
12
+ - main
13
+ paths:
14
+ - '**/*.js'
15
+ - '**/*.mjs'
16
+ - '**/*.cjs'
17
+ - '**/*.jsx'
18
+ - '**/*.ts'
19
+ - '**/*.tsx'
20
+ - '**/*.vue'
21
+ - '**/eslint.config.*'
22
+
23
+ pull_request:
24
+ branches:
25
+ - dev
26
+ - main
27
+
28
+ concurrency:
29
+ group: ${{ github.ref }}-${{ github.workflow }}
30
+ cancel-in-progress: true
31
+
32
+ jobs:
33
+ eslint:
34
+ runs-on: ubuntu-latest
35
+ permissions:
36
+ contents: read
37
+ steps:
38
+ - uses: actions/checkout@v6
39
+ with:
40
+ persist-credentials: false
41
+
42
+ - uses: ./.github/actions/setup-bun-deps
43
+
44
+ - name: Eslint
45
+ run: |
46
+ bunx oxlint
47
+ bunx eslint .
48
+ bunx jscpd .
49
+ bunx knip --no-config-hints
50
+ ```
51
+
52
+ Канон workflow `.github/workflows/lint-js.yml`: [lint-js.yml.snippet.yml](./policy/lint_js_yml/template/lint-js.yml.snippet.yml)
53
+
54
+ Перед `./.github/actions/setup-bun-deps` — `actions/checkout@v6` (з `persist-credentials: false`, див. `ga.mdc`). Composite action: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
55
+
56
+ Один workflow на лінт JS; зайвий `lint.yml` з тими самими кроками (`bunx oxlint`, `bunx eslint`, `jscpd`) — прибери.
57
+
58
+ У CI `--fix` для oxlint/eslint — **заборонено**: CI лише перевіряє, не виправляє.
@@ -0,0 +1,20 @@
1
+ ## `.oxlintrc.json` — канонічна конфігурація oxlint
2
+
3
+ У корені має бути `.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`).
4
+
5
+ Поле `ignorePatterns` працює як `rules`: канонічні патерни (наразі `**/schema.graphql`, `**/auto-imports.d.ts`) мають бути присутні; додаткові локальні glob-и дозволені.
6
+
7
+ Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
8
+
9
+ ```json title=".oxlintrc.json (фрагмент)"
10
+ {
11
+ "jsPlugins": ["@e18e/eslint-plugin"],
12
+ "rules": {
13
+ "e18e/prefer-includes": "error"
14
+ }
15
+ }
16
+ ```
17
+
18
+ Модуль `@e18e/eslint-plugin` не оголошуй окремо в `package.json` — він уже в залежностях `@nitra/eslint-config` (з **3.8.0**), oxlint підвантажує його з `node_modules`.
19
+
20
+ Канон `oxlint-canonical.json` — source-of-truth, редагується напряму; у споживачі оновлюється копіюванням файлу з репозиторію пакета.
@@ -0,0 +1,31 @@
1
+ ## `package.json` — модульна система та runtime-вимоги
2
+
3
+ ### `"type": "module"` у кожному `package.json`
4
+
5
+ У **кожному** `package.json` проєкту (корінь і всі workspace-пакети) має бути `"type": "module"` — весь код у ESM.
6
+
7
+ ### `engines` — мінімальні версії Node і Bun
8
+
9
+ ```json title="package.json"
10
+ {
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=24",
14
+ "bun": ">=1.3"
15
+ },
16
+ "devDependencies": {
17
+ "@nitra/eslint-config": "^3.10.0"
18
+ }
19
+ }
20
+ ```
21
+
22
+ Канон `type`, мінімальна `@nitra/eslint-config` (semver-поріг `devDependencies`) і `engines`: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json).
23
+
24
+ ### `@nitra/eslint-config` у `devDependencies`
25
+
26
+ У `devDependencies` кореневого `package.json` має бути `@nitra/eslint-config` — версія не нижче канонічного мінімуму зі snippet (semver-поріг, єдине джерело істини):
27
+ - з **3.8.0** — правило `no-restricted-syntax` забороняє `for...in`;
28
+ - з **3.9.2** — у `getConfig` вбудовано ignore для `**/adr/**`;
29
+ - транзитивно йде `@e18e/eslint-plugin` для oxlint.
30
+
31
+ Окремого `lint-js` скрипта немає — лінт через `n-cursor lint js` (CI — `--read-only`).
@@ -0,0 +1,9 @@
1
+ ## Тести та покриття
2
+
3
+ ### Unit-тести (Bun test)
4
+
5
+ Проєкт має бути покритий unit-тестами (**Bun test**). Код: синтаксис Node **24+**, **top level await** (узгоджено з `engines.node` у `package.json`).
6
+
7
+ ### Покриття + мутаційне тестування JS
8
+
9
+ Покриття + мутаційне тестування 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]`).
@@ -0,0 +1,15 @@
1
+ ## Структура спільних модулів: `utils/` vs `lib/`
2
+
3
+ Коли спільні методи виносяться з кількох JS-файлів в окремий каталог — назву каталога обирай за призначенням модулів усередині.
4
+
5
+ - **`utils/`** — низькорівневі, чисті, generic helpers без бізнес-логіки і без залежностей від домену проєкту. Те, що могло б жити в окремому npm-пакеті. Приклади: `formatDate()`, `chunk(array, size)`, `retry(fn, opts)`, `deepMerge()`, `parseDuration('5m')`, `sleep(ms)`.
6
+
7
+ - **`lib/`** — внутрішні модулі / підсистеми проєкту, які знають про домен. Більші за `utils/`, часто з власним state, конфігом або side effects. Приклади: `lib/gmail-client.js`, `lib/circuit-breaker.js`, `lib/skill-loader.js`, `lib/safety/dry-run.js`.
8
+
9
+ Швидкий тест: якщо файл завтра можна опублікувати окремим npm-пакетом без переписування — це `utils/`; якщо він тримає domain-state, читає конфіг проєкту або викликає зовнішні сервіси/файли — це `lib/`. Не плутай із чужими каталогами на кшталт `shared/` чи `common/`: канонічні назви — лише `utils/` і `lib/`.
10
+
11
+ ### Автоматична перевірка імпортів у `utils/`
12
+
13
+ `npx @nitra/cursor fix js` (концерн `utils_imports`) обходить кожен `utils/`-каталог у воркспейсах і падає, якщо знаходить relative-імпорт з `..` (вихід за межі каталогу) у будь-якому не-тестовому файлі. Це механічне втілення правила «utils не знає про домен»: тільки same-dir (`./X`), bare-пакети та `node:*` дозволені; cross-rule, конфіги проєкту чи sibling-utils — fail.
14
+
15
+ Якщо потрібна domain-залежність — перенеси файл у `lib/`.
package/rules/js/main.mdc CHANGED
@@ -5,232 +5,39 @@ 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 і фіксує самозбіги між дзеркальними файлами.
54
-
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 рядків.
56
-
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
- ```
66
-
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]`).
38
+ | Namespace | Що перевіряє |
39
+ |---|---|
40
+ | `js_lint.package_json` | `package.json`: `"type": "module"`, `engines.node >= 24`, `engines.bun >= 1.3`, `@nitra/eslint-config` >= semver-поріг |
41
+ | `js_lint.jscpd` | `.jscpd.json`: `gitignore`, `exitCode`, `reporters` (subset), `minLines >= канон` |
42
+ | `js_lint.lint_js_yml` | `.github/workflows/lint-js.yml`: required uses/run кроки, `persist-credentials: false`, заборона `--fix` у CI |
43
+ | `js_lint.vscode_extensions` | `.vscode/extensions.json`: наявність трьох канонічних recommendations |
@@ -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`.