@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,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** перевірки.
@@ -0,0 +1,51 @@
1
+ ## `pg`: виключення для LISTEN/NOTIFY
2
+
3
+ Bun SQL **поки не реалізує PostgreSQL LISTEN/NOTIFY** (асинхронні нотифікації через `pg_notify` / `LISTEN <channel>`). Тому якщо проєкт справді користується LISTEN/NOTIFY, npm-пакет `pg` дозволено тримати в `dependencies` **виключно** для LISTEN/NOTIFY-клієнта. Усі інші запити (SELECT/INSERT/UPDATE/DELETE/migration) — далі через Bun SQL.
4
+
5
+ Перевірка `pg` зважує цей сигнал автоматично (тому `pg` прибрано з [denylist](./policy/package_json/template/package.json.deny.json) — Rego не бачить JS-коду, тож зважування LISTEN/NOTIFY перенесено в `check-js-bun-db`).
6
+
7
+ ### Як перевірка визначає, що LISTEN/NOTIFY у проєкті є
8
+
9
+ AST-сканер шукає будь-який із сигналів:
10
+
11
+ - `client.query('LISTEN <channel>')` / `client.query('UNLISTEN *')` / `client.query('NOTIFY <channel>, ...')` — string- або template-literal-аргумент, що починається з `LISTEN` / `UNLISTEN` / `NOTIFY` (case-insensitive, leading whitespace допускається). Також покриті `queryArray` / `queryStream`.
12
+ - `client.on('notification', handler)` — listener на pg-події `notification`.
13
+ - TaggedTemplateExpression `<tag>\`LISTEN ...\`` — на випадок, якщо хтось загорнув LISTEN у власний tagged template.
14
+
15
+ Якщо хоч один сигнал є — `dependencies.pg` зважено як виправдане; інакше — `fail` із посиланням на цю секцію.
16
+
17
+ ### Правила для файлів з `import 'pg'`
18
+
19
+ Кожен файл, який імпортує `'pg'`, повинен **сам** містити один із LISTEN/NOTIFY-сигналів. Сценарій «один файл слухає, інший виконує `SELECT * FROM users`» — теж `fail`: звичайні запити через `pg` треба переписати на Bun SQL, а LISTEN/NOTIFY-логіку лишити в окремому модулі.
20
+
21
+ ### Приклад — окремий модуль для LISTEN
22
+
23
+ ```javascript
24
+ // src/db/pg-listen.ts — єдине місце, де живе import 'pg'
25
+ import { Client } from 'pg'
26
+
27
+ const listener = new Client({ connectionString: process.env.DATABASE_URL })
28
+
29
+ // allow-pg-leftover: pg LISTEN-клієнт не керується Bun SQL пулом
30
+ await listener.connect()
31
+ await listener.query('LISTEN orders_channel')
32
+ listener.on('notification', msg => {
33
+ // обробка нотифікації
34
+ })
35
+ ```
36
+
37
+ ```javascript
38
+ // src/db/users.ts — звичайні запити, через Bun SQL
39
+ import { sql } from 'bun'
40
+
41
+ export const getUser = id => sql`SELECT * FROM users WHERE id = ${id}`
42
+ ```
43
+
44
+ `pg-listen.ts` буде дозволений завдяки `LISTEN orders_channel` і `.on('notification', ...)`; `users.ts` не має імпорту `'pg'`, тож вільно живе з Bun SQL. `client.connect()` у файлі з Bun SQL потребував би маркер `// allow-pg-leftover: ...`; у файлі, де **Bun SQL не імпортовано**, pg-leftover-сканер не спрацьовує, але маркер як коментар-причина — корисний для рев'ю.
45
+
46
+ ### Що лишається забороненим
47
+
48
+ - `import 'pg'` у файлі без LISTEN/NOTIFY — `fail` з повідомленням «перенеси на Bun SQL, лиши LISTEN в окремому модулі».
49
+ - `dependencies.pg` без жодного LISTEN/NOTIFY-сигналу у проєкті — `fail` навіть якщо `pg` нібито «потрібен історично».
50
+ - `pg-format` (unscoped) — лишається у [denylist](./policy/package_json/template/package.json.deny.json); виключення для LISTEN/NOTIFY стосується **тільки** самого `pg`.
51
+ - `pg-pool`, `pg-native`, `mysql`, `mysql2` — виключень немає, видаляти повністю.
@@ -0,0 +1,117 @@
1
+ ## Безпечне виконання запитів
2
+
3
+ Тільки **tagged template** з `${...}` — Bun сам біндить позиційні параметри й захищає від SQL injection:
4
+
5
+ ```javascript
6
+ import { sql } from 'bun'
7
+
8
+ const userId = 42
9
+ const status = 'active'
10
+
11
+ const users = await sql`
12
+ SELECT * FROM users
13
+ WHERE id = ${userId} AND status = ${status}
14
+ `
15
+ ```
16
+
17
+ Об'єктний INSERT/UPDATE та `IN (...)` — через helper `sql(...)`:
18
+
19
+ ```javascript
20
+ const user = { name: 'Alice', email: 'a@example.com' }
21
+
22
+ const [created] = await sql`
23
+ INSERT INTO users ${sql(user)}
24
+ RETURNING *
25
+ `
26
+
27
+ await sql`UPDATE users SET ${sql(user, 'name', 'email')} WHERE id = ${created.id}`
28
+
29
+ const ids = [1, 2, 3]
30
+ await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
31
+ ```
32
+
33
+ Multi-row INSERT з масиву об'єктів — `sql(rows)` генерує column list і VALUES автоматично:
34
+
35
+ ```javascript
36
+ // ❌ format + pgWrite.unsafe — ручне склеювання рядків, injection-вектор
37
+ const insertWfQry = `insert into approval.workflow (request_id, job_title_id, name, status)
38
+ values ${approverJobs.map(job => `('${request.id}', ${job.id}, '${job.short_name}', 'pending')`).join(', ')}`
39
+ await pgWrite.unsafe(insertWfQry)
40
+
41
+ // ✅ sql(rows) — один параметр-масив, bind через wire-protocol
42
+ const wfRows = approverJobs.map(job => ({
43
+ request_id: request.id,
44
+ job_title_id: job.id,
45
+ name: job.short_name,
46
+ status: job.id === nextJobId ? 'current' : 'pending'
47
+ }))
48
+ await sql`INSERT INTO approval.workflow ${sql(wfRows)}`
49
+ ```
50
+
51
+ Коли потрібен стабільний план для великих batch'ів (N > 20) або строгі типи колонок — використовуй `unnest` (деталі у `js/pg-format-shim.mdc`, приклад MERGE з UNNEST). Для невеликих INSERT'ів де колонки відомі — `sql(rows)` коротший і зрозуміліший.
52
+
53
+ ## `IN (...)`: значення з template literal — тільки через змінну + guard на пустоту
54
+
55
+ Якщо список для `IN (...)` підставляється через `${...}` у template literal, його **потрібно**:
56
+
57
+ - винести в **окрему змінну** (не підставляти вираз напряму в `${...}`);
58
+ - **перевірити на пустоту** перед запитом і **throw** (щоб не виконувати некоректний SQL або запит з неочікуваною семантикою).
59
+
60
+ Приклад:
61
+
62
+ ```javascript
63
+ const ids = inputIds.map(Number).filter(n => Number.isFinite(n))
64
+ if (!ids.length) throw new Error('ids is empty')
65
+
66
+ await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
67
+ ```
68
+
69
+ Транзакції — через `sql.begin` (auto-commit/rollback), вкладені — через `tx.savepoint`:
70
+
71
+ ```javascript
72
+ await sql.begin(async tx => {
73
+ await tx`INSERT INTO users ${sql(user)}`
74
+ await tx`UPDATE accounts SET balance = balance - ${100} WHERE user_id = ${user.id}`
75
+ })
76
+ ```
77
+
78
+ ## JSONB-параметри: без `JSON.stringify`
79
+
80
+ Bun SQL серіалізує JS-об'єкти й масиви у JSON автоматично — викликати `JSON.stringify` перед передачею в `::jsonb` / `::jsonb[]` **заборонено**.
81
+
82
+ ```javascript
83
+ // ❌ зайвий JSON.stringify — подвійна серіалізація або зайвий рядок
84
+ await sql`INSERT INTO events (details) VALUES (${JSON.stringify(detailsForEvent)}::jsonb)`
85
+
86
+ await sql`SELECT * FROM unnest(${sql.array(batch.map(r => JSON.stringify(r.data)), 'jsonb')})`
87
+
88
+ // ✅ об'єкт/масив передається напряму
89
+ await sql`INSERT INTO events (details) VALUES (${detailsForEvent}::jsonb)`
90
+
91
+ await sql`SELECT * FROM unnest(${sql.array(col(batch, 'data'), 'jsonb')})`
92
+ ```
93
+
94
+ `UNION ALL`-цикл замість `unnest` підходить для малих динамічних запитів (2–5 рядків), де кожна гілка семантично різна. Для bulk upsert — завжди `unnest`.
95
+
96
+ ## Коментар під час виправлення SQL injection
97
+
98
+ Коли виправляєш місце з потенційним **SQL injection** (наприклад, заміна конкатенації/`.join(',')` на `sql(ids)` або перехід з `sql.unsafe(...)` на tagged template), **додай поруч короткий коментар** з описом причини.
99
+
100
+ Вимоги до коментаря:
101
+
102
+ - пояснити **що саме було небезпечно** (конкатенація, підмішування user input, динамічний `IN (...)`, тощо);
103
+ - пояснити **чому новий варіант безпечний** (параметризація через tagged template / `sql(...)`);
104
+ - без "романів": 1–2 рядки, достатньо для ревʼю.
105
+
106
+ Приклад:
107
+
108
+ ```javascript
109
+ // SQLi fix: не конкатенуємо значення в `IN (...)`; Bun parameterize через `sql(ids)`.
110
+ await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
111
+ ```
112
+
113
+ ## Що НЕ робити з бібліотеками
114
+
115
+ Якщо в коді з'явився `import { sql } from 'bun'`, то `pg`, `pg-format` та `mysql2` мають бути прибрані і з `dependencies`, і з імпортів — щоб не лишалось двох паралельних шляхів до БД та ручного форматування поряд із параметризованими template literal.
116
+
117
+ Те саме стосується **локальних шимів**: будь-який модуль, що експортує `format`, `pgRead`, `pgWrite`, `query(text, params)`, `quoteLiteral`, `quoteIdent` як обгортку над `sql.unsafe(...)`, потрібно переписати — всі call-site на tagged template, сам шим видалити (деталі у `js/pg-format-shim.mdc`).
@@ -0,0 +1,88 @@
1
+ ## `sql.array(arr, type)` для передачі масивів
2
+
3
+ Коли JS-масив передається як параметр у Bun SQL template literal всередині `unnest(...)` або іншого контексту, де PostgreSQL очікує типізований масив (`int4[]`, `uuid[]` тощо), — обов'язково використовувати `sql.array(arr, type)` (або `pgWrite.array` / `pgRead.array` — вони є екземплярами `SQL`). Другий аргумент (тип елементів) — обов'язковий.
4
+
5
+ ### Заборонені патерни
6
+
7
+ ```javascript
8
+ // ❌ пряма підстановка масиву — Bun серіалізує як рядок, не як pg-масив
9
+ ${ids}
10
+
11
+ // ❌ cast-синтаксис без .array() — працює в деяких версіях, але не гарантований
12
+ ${ids}::int8[]
13
+
14
+ // ❌ відсутній тип — Bun не може вивести тип pg, можливий mismatch
15
+ sql.array(ids)
16
+ ```
17
+
18
+ ### Дозволені патерни
19
+
20
+ ```javascript
21
+ // ✅ pgWrite.array з явним типом
22
+ ${pgWrite.array(ids, 'int8')}
23
+ ${pgWrite.array(uuids, 'uuid')}
24
+ ${pgWrite.array(flags, 'bool')}
25
+ ${pgWrite.array(amounts, 'numeric')}
26
+ ${pgWrite.array(names, 'text')}
27
+ ${pgWrite.array(dates, 'date')}
28
+ ${pgWrite.array(timestamps, 'timestamptz')}
29
+
30
+ // ✅ pgRead.array — те саме правило
31
+ ${pgRead.array(ids, 'int4')}
32
+ ```
33
+
34
+ ### Таблиця типів
35
+
36
+ | JS-тип | PostgreSQL тип | Аргумент |
37
+ | ------------- | -------------- | --------------- |
38
+ | number (int) | int4 | `'int4'` |
39
+ | bigint / id | int8 | `'int8'` |
40
+ | UUID string | uuid | `'uuid'` |
41
+ | boolean | bool | `'bool'` |
42
+ | decimal/float | numeric | `'numeric'` |
43
+ | string | text | `'text'` |
44
+ | date string | date | `'date'` |
45
+ | ISO datetime | timestamptz | `'timestamptz'` |
46
+
47
+ ### `col(arr, key)` — хелпер для unnest-колонок
48
+
49
+ OXC formatter (oxfmt ≥ 0.49) примусово розгортає будь-який `CallExpression`, де перший аргумент є `CallExpression` з callback, у багаторядковий блок — незалежно від `printWidth`. Тому `pgWrite.array(arr.map(r => r.field), 'type')` всередині tagged template literal завжди стає 4-рядковим блоком. `col(arr, 'field')` (перший аргумент — identifier, другий — string literal) цей тригер не зачіпає і лишається однорядковим.
50
+
51
+ Канонічне місце хелпера — `src/utils/col.mjs` (або `src/conn/col.mjs` залежно від структури проєкту):
52
+
53
+ ```javascript
54
+ // src/utils/col.mjs
55
+ export const col = (arr, key) => arr.map(r => r[key])
56
+ ```
57
+
58
+ ```javascript
59
+ import { pgWrite } from '#src/conn/db.mjs'
60
+ import { col } from '#src/utils/col.mjs'
61
+
62
+ // ❌ oxfmt розгортає на 4+ рядки незалежно від printWidth
63
+ ${pgWrite.array(rows.map(r => r.id), 'int4')}
64
+
65
+ // ✅ col(arr, key) — перший аргумент не є callback; oxfmt лишає однорядковим
66
+ ${pgWrite.array(col(rows, 'id'), 'int4')}
67
+ ```
68
+
69
+ ### Повний приклад (UNNEST + MERGE)
70
+
71
+ ```javascript
72
+ await pgWrite`
73
+ MERGE INTO "order".product p
74
+ USING (
75
+ SELECT * FROM unnest(
76
+ ${pgWrite.array(col(rows, 'order_id'), 'uuid')},
77
+ ${pgWrite.array(col(rows, 'product_id'), 'int4')},
78
+ ${pgWrite.array(col(rows, 'qty'), 'numeric')},
79
+ ${pgWrite.array(col(rows, 'is_refund'), 'bool')}
80
+ ) AS s(order_id, product_id, qty, is_refund)
81
+ ) AS s ON p.order_id = s.order_id AND p.product_id = s.product_id
82
+ WHEN MATCHED THEN
83
+ UPDATE SET qty = s.qty
84
+ WHEN NOT MATCHED THEN
85
+ INSERT (order_id, product_id, qty, is_refund)
86
+ VALUES (s.order_id, s.product_id, s.qty, s.is_refund)
87
+ `
88
+ ```
@@ -0,0 +1,65 @@
1
+ ## `sql.unsafe(...)` за замовчуванням заборонено
2
+
3
+ Будь-який виклик `sql.unsafe(...)` (так само `tx.unsafe(...)` всередині `sql.begin`) **заборонено**, окрім випадків, коли **обидві** умови виконані:
4
+
5
+ 1. значення підставляється з **коду** — константа, конфіг, whitelist; **не з user input**;
6
+ 2. треба підставити те, що **не можна параметризувати** через tagged template:
7
+ - назву **таблиці**,
8
+ - назву **колонки**,
9
+ - **dynamic SQL / DDL** (`CREATE`, `ALTER`, `DROP`, multi-statement migration, серверні `SET`/`SHOW` і подібне).
10
+
11
+ В усіх інших випадках — переробити на звичайний tagged template `sql\`...\${value}...\``: значення біндяться як параметри й injection не лишається.
12
+
13
+ Кожен легітимний `sql.unsafe(...)` має супроводжуватись **маркером-коментарем** з причиною — на тому ж рядку (trailing) або на рядку безпосередньо перед викликом. Маркер — opt-in для перевірки `js-bun-db` і слід для ревʼюера:
14
+
15
+ ```javascript
16
+ import format from '@scaleleap/pg-format'
17
+
18
+ const query = format('CREATE TABLE %I (id int)', tableName)
19
+ // allow-unsafe: DDL — назву таблиці параметризувати не можна; ідентифікатор екранує pg-format
20
+ await sql.unsafe(query)
21
+
22
+ await sql.unsafe('SELECT pg_advisory_lock($1)', [lockId]) // allow-unsafe: pg_advisory_lock — окремий шлях, без tagged template
23
+ ```
24
+
25
+ Формат маркера: `allow-unsafe: <непорожня причина>` у line- або block-коментарі. Без причини (`// allow-unsafe:`) і без маркера взагалі — **fail** перевірки.
26
+
27
+ ### `sql.unsafe` з template-літералом і `${...}`-інтерполяцією — заборонено навіть з маркером
28
+
29
+ `sql.unsafe(\`...\${x}...\`)` — окремий **hard fail**, який не знімається маркером `// allow-unsafe`. Шаблонна підстановка `${x}` у `sql.unsafe`-рядок:
30
+
31
+ - **не екранує** identifier'ів (reserved words, спецсимволи, пробіли в імені);
32
+ - **не біндить** значень (вони потрапляють у запит сирим текстом, як injection-вектор);
33
+ - виглядає «безпечно» через знайому tagged-template-форму, але не має жодних гарантій Bun SQL.
34
+
35
+ Канон — побудувати `text` окремо, потім передати в `sql.unsafe(text, [params])`:
36
+
37
+ - для **identifiers** — `@scaleleap/pg-format` `format('%I', name)` (екранує спецсимволи, reserved words);
38
+ - для **values** — позиційні `$1`, `$2`, … як placeholder'и в тексті + масив значень другим аргументом;
39
+ - для **fragments** з whitelist (`ASC`/`DESC`) — `format('%s', whitelistedValue)`.
40
+
41
+ ```javascript
42
+ // ❌ template-літерал з ${...} — fail навіть з allow-unsafe
43
+ // allow-unsafe: DDL
44
+ await sql.unsafe(`CREATE TABLE ${tableName} (id int)`)
45
+
46
+ // ✅ format('%I', ...) екранує identifier, sql.unsafe приймає готовий text
47
+ import format from '@scaleleap/pg-format'
48
+ const query = format('CREATE TABLE %I (id int)', tableName)
49
+ // allow-unsafe: DDL — назву таблиці параметризувати не можна
50
+ await sql.unsafe(query)
51
+ ```
52
+
53
+ Статичні `sql.unsafe(\`SELECT 1\`)` (без `${...}`) і `sql.unsafe(text, [params])` зі змінною `text`, зібраною заздалегідь, — допустимі (за наявності `// allow-unsafe`-маркера).
54
+
55
+ Заборонені кейси (треба переробити на tagged template):
56
+
57
+ ```javascript
58
+ // ❌ дані від користувача — параметризуй через tagged template
59
+ await sql.unsafe(`SELECT * FROM users WHERE id = ${userId}`)
60
+
61
+ // ❌ навіть у tagged template — динамічний список через .join(',')
62
+ await sql`SELECT * FROM users WHERE id IN (${ids.join(',')})`
63
+ ```
64
+
65
+ Для динамічних списків — `sql([...])` або `sql(rows, 'colA', 'colB')`, **не** `.join(',')`.