@nitra/cursor 1.13.52 → 1.13.57

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.57] - 2026-05-19
8
+
9
+ ### Changed
10
+
11
+ - `check js-bun-db`: новий **hard fail** на `sql.unsafe(template_literal_with_interpolation)` — будь-який виклик з template-літералом, що містить `${...}`-інтерполяцію, тепер падає **навіть з маркером** `// allow-unsafe`. Причина: шаблонна підстановка `${name}` у `sql.unsafe`-рядок не екранує identifier'ів (reserved words, спецсимволи, пробіли в імені) і не біндить значень; такий код виглядає звично через знайому tagged-template-форму, але насправді робить просту строкову конкатенацію без жодних гарантій. Канон — зібрати `text` окремо: identifiers через `@scaleleap/pg-format` `format('%I', name)`, values як позиційні `$N` + другий аргумент `sql.unsafe(text, [params])`. Раніше дозволений приклад `sql.unsafe(\\\`CREATE TABLE \\\${TABLE} (id int)\\\`)` з marker'ом тепер fail — переписати через `format('CREATE TABLE %I (id int)', TABLE)`. Не зачепило: `sql.unsafe('SELECT 1')` (статичний рядок), `sql.unsafe(\\\`SELECT 1\\\`)` (template без інтерполяції), `sql.unsafe(text, [params])` зі змінною `text`. Зачеплено: [bun-sql-scan.mjs](scripts/utils/bun-sql-scan.mjs) (новий експорт `findBunSqlUnsafeWithInterpolatedTemplateInText`, що флагає лише `<obj>.unsafe(TemplateLiteral)` з `expressions.length > 0`), [check.mjs](rules/js-bun-db/fix/safety/check.mjs) (новий лічильник `unsafeTemplateInterp` + окреме повідомлення з порадою на `@scaleleap/pg-format`), [check.test.mjs](rules/js-bun-db/fix/safety/check.test.mjs) (попередній DDL-тест переписано на безпечний `format('%I', ...)`-варіант, додано **негативний** тест на template-interp + marker і **позитивний** тест на статичний template без інтерполяції), [js-bun-db.mdc](rules/js-bun-db/js-bun-db.mdc) (нова підсекція «sql.unsafe з template-літералом і ${...}-інтерполяцією — заборонено навіть з маркером» зі зразками поганого/гарного коду; основний приклад DDL у секції unsafe-allowlist переписано на `format` + готовий `text`). Bump `js-bun-db.mdc` `1.10` → `1.11`.
12
+
13
+ ## [1.13.56] - 2026-05-19
14
+
15
+ ### Changed
16
+
17
+ - `check js-bun-db`: пакет **`pg`** більше не повністю заборонений — додано виключення для **PostgreSQL LISTEN/NOTIFY**, який Bun SQL поки не реалізує. Причина: dev-теми з notifications (черги нотифікацій, інвалідація кешу через `pg_notify`, бот-консьюмери на каналі) досі мають законну потребу у клієнті `pg`, а попереднє правило flat-out забороняло це навіть у файлах, що буквально нічого не роблять, окрім виклику `client.query` з рядком `LISTEN ...` плюс listener `client.on` на події `notification`. Тепер `dependencies.pg` дозволено, **якщо** AST-сканер знаходить у проєкті хоч один сигнал LISTEN/NOTIFY: метод `query` / `queryArray` / `queryStream` зі string- або template-літералом, що починається з `LISTEN`, `UNLISTEN` або `NOTIFY` (case-insensitive), або метод `on` із першим аргументом-рядком `notification`, або tagged template з тегом `sql` і першим quasi, що починається з тих самих ключових слів. Якщо жодного — `fail` з посиланням на нову секцію .mdc. Додатково — **per-file**: будь-який файл з `import 'pg'` (або `require('pg')`) повинен сам містити LISTEN/NOTIFY; звичайні `SELECT`/`INSERT`/`UPDATE` через `pg` лишаються забороненими (переписати на Bun SQL і лишити LISTEN/NOTIFY в окремому модулі). Заборона `pg-format` і `mysql2` не змінилася. Зачеплено: [bun-sql-scan.mjs](scripts/utils/bun-sql-scan.mjs) (нові експорти `textHasPgLibImport`, `findPgLibImportInText`, `findPgListenNotifyUsageInText` + AST-хелпери для розпізнавання pg-style LISTEN/NOTIFY-запитів і `notification`-listener'ів), [check.mjs](rules/js-bun-db/fix/safety/check.mjs) (нова функція `checkPgDependencyAndUsage`, що пробігає по всіх `package.json` і per-file pg-imports; перевірку `pg` повністю переведено з Rego в JS, бо Rego не бачить JS-коду), [package.json.deny.json](rules/js-bun-db/policy/package_json/template/package.json.deny.json) (прибрано `pg`, лишилися `pg-format`/`mysql2`), [package_json_test.rego](rules/js-bun-db/policy/package_json/package_json_test.rego) (`test_deny_pg` → `test_allow_pg_in_dependencies` + новий `test_deny_pg_format`), [check.test.mjs](rules/js-bun-db/fix/safety/check.test.mjs) (5 нових сценаріїв: успіх з LISTEN, успіх з notification-listener, помилка `pg` без LISTEN/NOTIFY, помилка змішаних файлів — один із LISTEN, інший зі звичайними запитами, успіх з `NOTIFY` як виправдання). `.mdc` отримало нову секцію «pg: виключення для LISTEN/NOTIFY» з прикладом окремого `pg-listen.ts`-модуля і явним переліком сигналів, які зважує сканер. Bump `js-bun-db.mdc` `1.9` → `1.10`.
18
+
19
+ ## [1.13.55] - 2026-05-19
20
+
21
+ ### Changed
22
+
23
+ - `check js-bun-db`: правило [js-bun-db.mdc](rules/js-bun-db/js-bun-db.mdc) **пом'якшено** для випадків, де Bun SQL принципово не може допомогти — **динамічних SQL identifiers** (назви schema/table/column/index/role/database) і whitelist-фрагментів типу `ASC`/`DESC`. Раніше для них рекомендувалось будувати рядок шаблонною підстановкою у `sql.unsafe`, але інтерполяція identifier'у в template literal не робить escape (reserved words, спецсимволи) — це слабкий захист. Тепер канон — окремий пакет **`@scaleleap/pg-format`** (scoped форк, не unscoped `pg-format`): виклик типу `format('SELECT * FROM %I', name)` повертає коректно екранований PostgreSQL identifier, далі рядок іде у `sql.unsafe(query, [bindParams])` з обов'язковим маркером `// allow-unsafe: <причина>`. Значення (user input, фільтри, INSERT/UPDATE) — **завжди** через Bun parameters (tagged template або `$N` + `sql.unsafe(text, values)`); `%L` для значень лишається забороненим, як і власні шими `format`/`pgFormat`/`quoteIdent` тощо. Unscoped `pg-format` лишається у [deny-списку](rules/js-bun-db/policy/package_json/template/package.json.deny.json) — виключення стосується **тільки** scoped `@scaleleap/pg-format`. Зачеплено: вступ секції «Заміна на Bun native SQL» (зафіксовано виключення), рядок таблиці ідіом для `%I` (тепер через `@scaleleap/pg-format`, не через `sql.unsafe` з шаблонним рядком), нова секція «Динамічна SQL-структура: @scaleleap/pg-format для identifiers» з прикладами (динамічний `ORDER BY` зі whitelist, multi-row `INSERT` через `VALUES %L`, dynamic `WHERE` через ручні `$N`) і коротка таблиця рішень. AST-сканер [bun-sql-scan.mjs](scripts/utils/bun-sql-scan.mjs) не зачеплений — він знаходить лише **визначення** функцій-шимів (`format` з `%L`/`%I`/`%s` у тілі), а імпорт `format` із `@scaleleap/pg-format` як зовнішня бібліотека не флагається. Bump `js-bun-db.mdc` `1.8` → `1.9`.
24
+
25
+ ## [1.13.54] - 2026-05-19
26
+
27
+ ### Changed
28
+
29
+ - `check k8s` / `lint-k8s`: правила під `npm/rules/k8s/` спрощено — за каноном `k8s.mdc` тримаємо лише `.yaml`, тож **rego-цілі** і **rego-вирази** очищено від `.yml`. Зачеплено: глоби `walkGlob` у `npm/rules/k8s/policy/{manifest,base_manifest,gateway,hpa_pdb}/target.json` — лише `**/*.yaml`; у [base_kustomization.rego](rules/k8s/policy/base_kustomization/base_kustomization.rego) `is_hpa_or_pdb_filename` більше не містить `hpa.yml` / `pdb.yml`; тест `test_deny_hpa_yml_in_subdir` → `test_deny_hpa_yaml_in_subdir`. **Safety-net** у [check.mjs](rules/k8s/fix/manifests/check.mjs) **збережено**: `findK8sYamlFiles` та `checkK8sYamlFile` все ще пропускають `.yml` далі, але одразу падають з повідомленням `розширення .yml — перейменуй на .yaml (див. k8s.mdc)` — щоб випадково створений `*.yml` під `k8s/` не залишився непоміченим (автоматичне перейменування — окрема ручна команда `npx @nitra/cursor rename-yaml-extensions`, яка з `check k8s` не викликається). Згадки про `.github/workflows/*.yml` у JSDoc лишилися (це чуже правило `ga.mdc`, де канон — `.yml`). Bump `k8s.mdc` `1.40` → `1.41`.
30
+
31
+ ## [1.13.53] - 2026-05-19
32
+
33
+ ### Changed
34
+
35
+ - `check k8s`: **NetworkPolicy переїхав з `components/` у `base/`**. Раніше канон вимагав `…/k8s/<pkg>/components/networkpolicy.yaml` (Kustomize Component, sibling до `base/`), а локальний `networkpolicy.yaml` у base був забороненим (file-existence error) — через що **dev-середовище** (рендер лише з base без overlay → без components) **не отримувало жодних мережевих обмежень** і pod'и були відкриті для будь-якого трафіку. Тепер NP лежить у `base/networkpolicy.yaml` поруч з workload-маніфестом і підключений через `base/kustomization.yaml` `resources:` — обмеження діють і на dev, і на всіх overlays через звичайний `resources: [- ../base]`. Канон `components/`: лише `hpa.yaml` + `pdb.yaml` (HPA/PDB лишаються env-залежними й підключаються тільки прод-overlays). У не-base overlays `networkpolicy.yaml` поруч з workload — опційний overlay-specific override. Зачеплено: [npm/rules/k8s/k8s.mdc](rules/k8s/k8s.mdc) (нова секція «NetworkPolicy у `base/`», оновлені приклади `components/kustomization.yaml` без NP і новий приклад `base/networkpolicy.yaml`), [npm/rules/k8s/fix/manifests/check.mjs](rules/k8s/fix/manifests/check.mjs) (видалено `failIfBaseLayerHasLocalNetworkPolicy` і `validateComponentsNetworkPolicyFile`; `validateNetworkPoliciesForK8sWorkloads` і `ensureNetworkPoliciesForWorkloadsInDir` тепер завжди шукають `networkpolicy.yaml` у `dir`, autofix додає його у `base/kustomization.yaml` `resources:`; `validateComponentsKustomizationManifest` більше не вимагає NP у resources), [npm/rules/k8s/policy/base_kustomization/base_kustomization.rego](rules/k8s/policy/base_kustomization/base_kustomization.rego) (deny прибирає `networkpolicy.yaml` зі списку заборонених у base resources — лишаються тільки HPA/PDB), [npm/rules/k8s/lint/lint.mjs](rules/k8s/lint/lint.mjs) (оновлено JSDoc про C-0260: NP тепер у base і kustomize-збірка нормалізує namespace природньо). Bump `k8s.mdc` `1.39` → `1.40`.
36
+
7
37
  ## [1.13.52] - 2026-05-19
8
38
 
9
39
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.52",
3
+ "version": "1.13.57",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -413,9 +413,7 @@ export async function check() {
413
413
  const ignorePaths = await loadCursorIgnorePaths(process.cwd())
414
414
 
415
415
  if (!(await hasAnyVueRasterReference(ignorePaths))) {
416
- pass(
417
- 'image-avif: у .vue/.html немає raster-посилань для переписування — AVIF-генерація і cleanup пропущені'
418
- )
416
+ pass('image-avif: у .vue/.html немає raster-посилань для переписування — AVIF-генерація і cleanup пропущені')
419
417
  return reporter.getExitCode()
420
418
  }
421
419
 
@@ -2,12 +2,20 @@
2
2
  * Перевіряє правило js-bun-db.mdc.
3
3
  *
4
4
  * 1) У жодному `package.json` (включно з workspace-пакетами) у `dependencies` не повинно
5
- * бути `pg`, `pg-format` чи `mysql2` — ці бібліотеки треба замінити на Bun native SQL
6
- * (`import { sql, SQL } from 'bun'`, https://bun.com/docs/runtime/sql).
7
- * `pg-format` — ручне форматування SQL через escape; tagged template Bun SQL
8
- * параметризує значення нативно і не лишає простору для injection.
5
+ * бути `pg-format` чи `mysql2` — їх треба замінити на Bun native SQL
6
+ * (`import { sql, SQL } from 'bun'`, https://bun.com/docs/runtime/sql). `pg-format` —
7
+ * ручне форматування SQL через escape; tagged template Bun SQL параметризує значення
8
+ * нативно і не лишає простору для injection. Перевірка цих двох — у Rego-полісі
9
+ * `npm/policy/js_bun_db/package_json/`.
9
10
  *
10
- * 2) Якщо в коді використовується Bun SQL (імпорт `sql`/`SQL` з `'bun'`), додатково
11
+ * 2) Для `pg` діє виключення: Bun SQL поки не реалізує LISTEN/NOTIFY, тож якщо у
12
+ * проекті знайдено реальне використання `LISTEN ...` / `NOTIFY ...` / `UNLISTEN ...`
13
+ * або listener'а `.on('notification', ...)`, dependency `pg` дозволено. Інакше
14
+ * `pg` лишається забороненим — fail з підказкою про виключення. Додатково — per-file:
15
+ * кожен файл з `import ... from 'pg'` повинен сам містити LISTEN/NOTIFY-патерн;
16
+ * звичайні SELECT/INSERT/UPDATE через `pg` (replace на Bun SQL!) не дозволені.
17
+ *
18
+ * 3) Якщо в коді використовується Bun SQL (імпорт `sql`/`SQL` з `'bun'`), додатково
11
19
  * перевіряє небезпечні патерни:
12
20
  * - `new SQL(...)` всередині функції (пул має бути singleton на рівні модуля).
13
21
  * - Будь-який `<obj>.unsafe(...)` без маркера-коментаря `// allow-unsafe: <reason>`
@@ -30,12 +38,16 @@ import {
30
38
  findBunSqlPerRequestConnectionInText,
31
39
  findBunSqlPgLeftoverCallInText,
32
40
  findBunSqlUnsafeUseWithoutAllowMarkerInText,
41
+ findBunSqlUnsafeWithInterpolatedTemplateInText,
33
42
  findPgFormatLikeQueryWrapperInText,
34
43
  findPgFormatShimDefinitionInText,
44
+ findPgLibImportInText,
45
+ findPgListenNotifyUsageInText,
35
46
  findUnsafeBunSqlDynamicSqlListInText,
36
47
  findUnsafeBunSqlInListMissingEmptyGuardInText,
37
48
  isBunSqlScanSourceFile,
38
- textHasBunSqlImport
49
+ textHasBunSqlImport,
50
+ textHasPgLibImport
39
51
  } from '../../../../scripts/utils/bun-sql-scan.mjs'
40
52
  import { findAllPackageJsonPaths } from '../../../../scripts/utils/find-package-json-paths.mjs'
41
53
  import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
@@ -65,19 +77,32 @@ async function findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths) {
65
77
  }
66
78
 
67
79
  /**
68
- * Сканує JS/TS-джерела на небезпечні патерни Bun SQL.
80
+ * Сканує JS/TS-джерела на небезпечні патерни Bun SQL і збирає метадані про
81
+ * використання `pg`/LISTEN-NOTIFY (для виключення dependency `pg`).
69
82
  * @param {string[]} sourcePaths абсолютні шляхи джерел
70
83
  * @param {string} repoRoot абсолютний шлях до кореня
71
84
  * @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter колбеки pass і fail з перевірки
72
- * @returns {Promise<{ hasBunSqlImport: boolean, perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number, pgLeftover: number, pgFormatShim: number, queryWrapper: number }>}
73
- * `hasBunSqlImport` — чи знайдено хоч один `import { sql|SQL } from 'bun'` у джерелах;
74
- * решта — кількість порушень кожного типу.
85
+ * @returns {Promise<{
86
+ * hasBunSqlImport: boolean,
87
+ * perRequest: number,
88
+ * unsafeCall: number,
89
+ * dynamicList: number,
90
+ * inListGuard: number,
91
+ * pgLeftover: number,
92
+ * pgFormatShim: number,
93
+ * queryWrapper: number,
94
+ * pgUsage: { rel: string, imports: { line: number, snippet: string }[], listenNotify: { line: number, snippet: string, kind: string }[] }[]
95
+ * }>}
96
+ * `hasBunSqlImport` — чи є хоч один `import { sql|SQL } from 'bun'`;
97
+ * `pgUsage` — список файлів, що або імпортують `'pg'`, або містять LISTEN/NOTIFY-патерн
98
+ * (інші — пропущено, щоб не тримати в пам'яті метадані про всі файли).
75
99
  */
76
100
  async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
77
101
  const { fail } = reporter
78
102
  const counts = {
79
103
  perRequest: 0,
80
104
  unsafeCall: 0,
105
+ unsafeTemplateInterp: 0,
81
106
  dynamicList: 0,
82
107
  inListGuard: 0,
83
108
  pgLeftover: 0,
@@ -85,6 +110,8 @@ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
85
110
  queryWrapper: 0
86
111
  }
87
112
  let hasBunSqlImport = false
113
+ /** @type {{ rel: string, imports: { line: number, snippet: string }[], listenNotify: { line: number, snippet: string, kind: string }[] }[]} */
114
+ const pgUsage = []
88
115
 
89
116
  for (const absPath of sourcePaths) {
90
117
  const rel = relative(repoRoot, absPath).split('\\').join('/')
@@ -93,9 +120,30 @@ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
93
120
  hasBunSqlImport = true
94
121
  }
95
122
  scanFileForBunSqlPatterns(content, rel, fail, counts)
123
+ collectPgUsageForFile(content, rel, pgUsage)
96
124
  }
97
125
 
98
- return { hasBunSqlImport, ...counts }
126
+ return { hasBunSqlImport, pgUsage, ...counts }
127
+ }
128
+
129
+ /**
130
+ * Якщо у файлі є імпорт `'pg'` АБО LISTEN/NOTIFY-патерн — додає запис у `pgUsage`.
131
+ * Файли без жодного сигналу не зберігаються, щоб уникнути зайвої пам'яті.
132
+ * @param {string} content вміст файлу
133
+ * @param {string} rel posix-шлях відносно кореня
134
+ * @param {{ rel: string, imports: { line: number, snippet: string }[], listenNotify: { line: number, snippet: string, kind: string }[] }[]} pgUsage акумулятор
135
+ * @returns {void}
136
+ */
137
+ function collectPgUsageForFile(content, rel, pgUsage) {
138
+ // Дешевий pre-filter за текстом: AST-парсинг тільки коли файл містить
139
+ // або імпорт `'pg'`, або хоча б одне зі слів LISTEN / NOTIFY / UNLISTEN /
140
+ // 'notification' — інакше LISTEN/NOTIFY у ньому точно немає.
141
+ const mayHaveListenNotify = /\b(LISTEN|UNLISTEN|NOTIFY)\b/iu.test(content) || /['"`]notification['"`]/u.test(content)
142
+ if (!textHasPgLibImport(content) && !mayHaveListenNotify) return
143
+ const imports = findPgLibImportInText(content, rel)
144
+ const listenNotify = findPgListenNotifyUsageInText(content, rel)
145
+ if (imports.length === 0 && listenNotify.length === 0) return
146
+ pgUsage.push({ rel, imports, listenNotify })
99
147
  }
100
148
 
101
149
  /**
@@ -124,6 +172,16 @@ function scanFileForBunSqlPatterns(content, rel, fail, counts) {
124
172
  `(js-bun-db.mdc): ${v.snippet}`
125
173
  )
126
174
  }
175
+ for (const v of findBunSqlUnsafeWithInterpolatedTemplateInText(content, rel)) {
176
+ counts.unsafeTemplateInterp++
177
+ fail(
178
+ `js-bun-db: ${rel}:${v.line} — sql.unsafe(\`...\${x}...\`) з template-літералом і \${...}-інтерполяцією ` +
179
+ `заборонено навіть з allow-unsafe маркером: шаблонна підстановка identifier'у не екранує (reserved words, ` +
180
+ `спецсимволи), а значення не біндяться. Збери text через @scaleleap/pg-format format('%I', name) для ` +
181
+ `identifiers або позиційні $N для values, потім sql.unsafe(text, [params]). Деталі — секція ` +
182
+ `«Динамічна SQL-структура» в js-bun-db.mdc: ${v.snippet}`
183
+ )
184
+ }
127
185
  for (const v of findBunSqlPgLeftoverCallInText(content, rel)) {
128
186
  counts.pgLeftover++
129
187
  fail(
@@ -170,6 +228,71 @@ function scanFileForBunSqlPatterns(content, rel, fail, counts) {
170
228
  }
171
229
  }
172
230
 
231
+ /**
232
+ * Перевіряє виключення `pg` для LISTEN/NOTIFY: по кожному `package.json` з
233
+ * `dependencies.pg` — чи є у проекті хоч одне використання LISTEN/NOTIFY-патерну;
234
+ * додатково — кожен файл з `import 'pg'` повинен сам містити LISTEN/NOTIFY (інакше
235
+ * звичайні SELECT/INSERT/UPDATE через `pg` ховаються за легітимним dependency).
236
+ * @param {string[]} pkgJsonPaths абсолютні шляхи до всіх package.json
237
+ * @param {string} repoRoot абсолютний шлях до кореня
238
+ * @param {{ rel: string, imports: { line: number, snippet: string }[], listenNotify: { line: number, snippet: string, kind: string }[] }[]} pgUsage метадані з scanSourcesForBunSqlPatterns
239
+ * @param {{ fail: (m: string) => void }} reporter колбек fail для повідомлень
240
+ * @returns {Promise<{ pgDepFails: number, pgImportFails: number, pgDepsFound: number, hasAnyListenNotify: boolean, listenNotifyEvidence: string | null }>}
241
+ * counters і метадані для підсумкового `pass`-повідомлення (де саме знайдено перший LISTEN/NOTIFY).
242
+ */
243
+ async function checkPgDependencyAndUsage(pkgJsonPaths, repoRoot, pgUsage, reporter) {
244
+ const { fail } = reporter
245
+ let pgDepFails = 0
246
+ let pgImportFails = 0
247
+ let pgDepsFound = 0
248
+
249
+ const firstWithListenNotify = pgUsage.find(u => u.listenNotify.length > 0)
250
+ const hasAnyListenNotify = !!firstWithListenNotify
251
+ const listenNotifyEvidence = firstWithListenNotify
252
+ ? `${firstWithListenNotify.rel}:${firstWithListenNotify.listenNotify[0].line}`
253
+ : null
254
+
255
+ for (const absPkgPath of pkgJsonPaths) {
256
+ const relPkg = relative(repoRoot, absPkgPath).split('\\').join('/')
257
+ let pkg
258
+ try {
259
+ pkg = JSON.parse(await readFile(absPkgPath, 'utf8'))
260
+ } catch {
261
+ // невалідний JSON у package.json — це проблема інших правил, тут пропускаємо
262
+ continue
263
+ }
264
+ if (!pkg || typeof pkg !== 'object') continue
265
+ const deps = pkg.dependencies
266
+ if (!deps || typeof deps !== 'object' || !Object.prototype.hasOwnProperty.call(deps, 'pg')) continue
267
+ pgDepsFound++
268
+ if (!hasAnyListenNotify) {
269
+ pgDepFails++
270
+ fail(
271
+ `js-bun-db: ${relPkg}: dependencies.pg заборонено — у проекті не знайдено LISTEN / NOTIFY / UNLISTEN ` +
272
+ `(або listener'а .on('notification', ...)). Bun SQL покриває звичайні запити; ` +
273
+ `\`pg\` дозволений лише як виняток для LISTEN/NOTIFY (js-bun-db.mdc, ` +
274
+ `секція «pg для LISTEN/NOTIFY»)`
275
+ )
276
+ }
277
+ }
278
+
279
+ for (const f of pgUsage) {
280
+ if (f.imports.length === 0) continue
281
+ if (f.listenNotify.length > 0) continue
282
+ for (const imp of f.imports) {
283
+ pgImportFails++
284
+ fail(
285
+ `js-bun-db: ${f.rel}:${imp.line} — import 'pg' дозволено лише у файлах з LISTEN / NOTIFY / UNLISTEN ` +
286
+ `або .on('notification', ...). Перенеси звичайні запити на Bun SQL ` +
287
+ `(import { sql } from 'bun'), а LISTEN/NOTIFY-логіку лиши в окремому модулі ` +
288
+ `(js-bun-db.mdc): ${imp.snippet}`
289
+ )
290
+ }
291
+ }
292
+
293
+ return { pgDepFails, pgImportFails, pgDepsFound, hasAnyListenNotify, listenNotifyEvidence }
294
+ }
295
+
173
296
  /**
174
297
  * Будує повідомлення `fail` для порушення `findUnsafeBunSqlInListMissingEmptyGuardInText`
175
298
  * залежно від `reason` (різні діагностики однакового сімейства).
@@ -218,9 +341,9 @@ export async function check() {
218
341
  return reporter.getExitCode()
219
342
  }
220
343
 
221
- // Перевірку `dependencies` (заборона `pg` / `pg-format` / `mysql2`) перенесено
222
- // в Rego-полісі `npm/policy/js_bun_db/package_json/`; `npx @nitra/cursor check`
223
- // запускає її по всіх workspace-`package.json`. Тут лишився лише AST-скан коду.
344
+ // Заборону `pg-format` / `mysql2` у `dependencies` тримає Rego-поліс
345
+ // `npm/policy/js_bun_db/package_json/`. `pg` оброблено тут — як виняток для
346
+ // LISTEN/NOTIFY (Rego не бачить JS-коду, тож не може зважити сигнал).
224
347
 
225
348
  const sourcePaths = await findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths)
226
349
  if (sourcePaths.length === 0) {
@@ -228,8 +351,38 @@ export async function check() {
228
351
  return reporter.getExitCode()
229
352
  }
230
353
 
231
- const { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard, pgLeftover, pgFormatShim, queryWrapper } =
232
- await scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter)
354
+ const {
355
+ hasBunSqlImport,
356
+ pgUsage,
357
+ perRequest,
358
+ unsafeCall,
359
+ unsafeTemplateInterp,
360
+ dynamicList,
361
+ inListGuard,
362
+ pgLeftover,
363
+ pgFormatShim,
364
+ queryWrapper
365
+ } = await scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter)
366
+
367
+ const { pgDepFails, pgImportFails, pgDepsFound, listenNotifyEvidence } = await checkPgDependencyAndUsage(
368
+ pkgJsonPaths,
369
+ repoRoot,
370
+ pgUsage,
371
+ reporter
372
+ )
373
+ if (pgDepFails === 0) {
374
+ if (pgDepsFound === 0) {
375
+ pass('js-bun-db: dependencies.pg відсутнє у жодному package.json')
376
+ } else {
377
+ pass(
378
+ `js-bun-db: dependencies.pg виправдано LISTEN/NOTIFY у коді (виключення з js-bun-db.mdc; ` +
379
+ `доказ: ${listenNotifyEvidence})`
380
+ )
381
+ }
382
+ }
383
+ if (pgImportFails === 0) {
384
+ pass("js-bun-db: усі `import 'pg'` або відсутні, або у файлах з LISTEN/NOTIFY")
385
+ }
233
386
 
234
387
  if (!hasBunSqlImport) {
235
388
  pass("js-bun-db: Bun SQL не використовується в коді (немає import { sql|SQL } from 'bun')")
@@ -242,6 +395,12 @@ export async function check() {
242
395
  if (unsafeCall === 0) {
243
396
  pass('js-bun-db: усі sql.unsafe(...) або відсутні, або супроводжуються маркером "// allow-unsafe: <причина>"')
244
397
  }
398
+ if (unsafeTemplateInterp === 0) {
399
+ pass(
400
+ 'js-bun-db: немає sql.unsafe(`...${x}...`) з template-інтерполяцією ' +
401
+ '(identifiers через @scaleleap/pg-format %I, values — позиційні $N)'
402
+ )
403
+ }
245
404
  if (pgLeftover === 0) {
246
405
  pass(
247
406
  'js-bun-db: немає pg-leftover викликів .connect()/.end() у файлах з Bun SQL ' +
@@ -2,7 +2,7 @@
2
2
  description: Використання pg / mysql2 / Bun SQL у Node.js та Bun
3
3
  globs: "**/package.json,**/src/conn/**"
4
4
  alwaysApply: false
5
- version: '1.8'
5
+ version: '1.11'
6
6
  ---
7
7
 
8
8
  ## Підтримувані версії баз даних
@@ -13,13 +13,15 @@ PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом,
13
13
 
14
14
  Якщо в проєкті використовуються бібліотеки `pg`, `pg-format` або `mysql2`, їх потрібно замінити на Bun native SQL: <https://bun.com/docs/runtime/sql>.
15
15
 
16
- - Видалити з `dependencies`: `pg`, `pg-pool`, `pg-native`, `pg-format`, `mysql`, `mysql2`.
16
+ - Видалити з `dependencies`: `pg-pool`, `pg-native`, `pg-format`, `mysql`, `mysql2`.
17
17
  - Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
18
18
  - Замінити на `import { sql, SQL } from 'bun'` — Bun має вбудований клієнт із пулом, prepared statements та tagged templates.
19
19
 
20
- Канон заборонених `dependencies` (`pg`, `pg-format`, `mysql2`): [package.json.deny.json](./policy/package_json/template/package.json.deny.json).
20
+ Канон заборонених `dependencies` (`pg-format`, `mysql2`): [package.json.deny.json](./policy/package_json/template/package.json.deny.json). Сам `pg` із денилисту прибрано — він має одне легітимне виключення (LISTEN/NOTIFY), яке зважує AST-сканер; деталі — `## pg: виключення для LISTEN/NOTIFY`.
21
21
 
22
- `pg-format` — це ручне форматування SQL через escape (`format('... %L ...', value)`); такі рядки легко поламати неправильним типом, locale-залежним escape або забутим `%L`. Tagged template Bun SQL параметризує значення нативно (`sql\`... ${value} ...\``) і не лишає простору для injection — окремий «форматер» не потрібен.
22
+ `pg-format` (unscoped) — це ручне форматування SQL через escape (`format('... %L ...', value)`); такі рядки легко поламати неправильним типом, locale-залежним escape або забутим `%L`. Tagged template Bun SQL параметризує значення нативно (`sql\`... ${value} ...\``) і не лишає простору для injection — окремий «форматер» **для значень** не потрібен.
23
+
24
+ Виключення є **лише** для **динамічних identifiers** (назви схем / таблиць / колонок / індексів / ролей / БД) і whitelist-фрагментів типу `ASC`/`DESC`: Bun SQL їх параметризувати не вміє, тож тут дозволено окремий пакет **`@scaleleap/pg-format`** (scoped форк, не unscoped `pg-format`) — деталі й приклади у `## Динамічна SQL-структура: @scaleleap/pg-format для identifiers`.
23
25
 
24
26
  ## `pg-format`: повне видалення, без шимів
25
27
 
@@ -40,7 +42,7 @@ PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом,
40
42
  | `format('INSERT ... VALUES %L', [row])` (1 рядок) | `sql\`INSERT ... VALUES (${a}, ${b}, ...)\`` |
41
43
  | `format('INSERT ... VALUES %L', rows)` (N рядків) | `sql\`INSERT INTO t ${sql(rows, 'a', 'b')}\`` або `unnest($1::T[], $2::T[]) AS t(a, b)` |
42
44
  | `format('MERGE ... USING (VALUES %L) AS d(...)')` | `sql\`MERGE ... USING (SELECT * FROM unnest(${arrA}::A[], ${arrB}::B[]) AS t(a, b)) AS d\`` |
43
- | `format('... %I ...', tableName)` (whitelist) | `sql.unsafe(\`... \${tableName} ...\`)` з маркером `// allow-unsafe: <причина>` і whitelist'ом |
45
+ | `format('... %I ...', tableName)` (whitelist) | `@scaleleap/pg-format`: `format('%I', name)` + `sql.unsafe(text, [params])` з маркером |
44
46
 
45
47
  Для multi-row `VALUES` у `MERGE` / `INSERT` з конкретними типами — паралельні масиви по колонках і `unnest($1::TYPE[], $2::TYPE[], ...) AS t(col1, col2, ...)`. Кожна колонка передається одним параметром-масивом; типи задаються кастом масиву (`::uuid[]`, `::bigint[]`, `::numeric[]`, `::text[]`, …).
46
48
 
@@ -73,6 +75,160 @@ await sql`... WHERE id = ${userId}`
73
75
 
74
76
  Виняток для шиму `format()` під час поетапної міграції допускається **тільки** в окремому commit'і з TODO-маркером і дедлайном; готовий код у main з таким шимом — `fail` правила (детектори: `pg-format shim`, `query wrapper over unsafe`).
75
77
 
78
+ ## `pg`: виключення для LISTEN/NOTIFY
79
+
80
+ Bun SQL **поки не реалізує PostgreSQL LISTEN/NOTIFY** (асинхронні нотифікації через `pg_notify` / `LISTEN <channel>`). Тому якщо проєкт справді користується LISTEN/NOTIFY, npm-пакет `pg` дозволено тримати в `dependencies` **виключно** для LISTEN/NOTIFY-клієнта. Усі інші запити (SELECT/INSERT/UPDATE/DELETE/migration) — далі через Bun SQL.
81
+
82
+ Перевірка `pg` зважує цей сигнал автоматично (тому `pg` прибрано з [denylist](./policy/package_json/template/package.json.deny.json) — Rego не бачить JS-коду, тож зважування LISTEN/NOTIFY перенесено в `check-js-bun-db`).
83
+
84
+ ### Як перевірка визначає, що LISTEN/NOTIFY у проєкті є
85
+
86
+ AST-сканер шукає будь-який із сигналів:
87
+
88
+ - `client.query('LISTEN <channel>')` / `client.query('UNLISTEN *')` / `client.query('NOTIFY <channel>, ...')` — string- або template-literal-аргумент, що починається з `LISTEN` / `UNLISTEN` / `NOTIFY` (case-insensitive, leading whitespace допускається). Також покриті `queryArray` / `queryStream`.
89
+ - `client.on('notification', handler)` — listener на pg-події `notification`.
90
+ - TaggedTemplateExpression `<tag>\`LISTEN ...\`` — на випадок, якщо хтось загорнув LISTEN у власний tagged template.
91
+
92
+ Якщо хоч один сигнал є — `dependencies.pg` зважено як виправдане; інакше — `fail` із посиланням на цю секцію.
93
+
94
+ ### Правила для файлів з `import 'pg'`
95
+
96
+ Кожен файл, який імпортує `'pg'`, повинен **сам** містити один із LISTEN/NOTIFY-сигналів. Сценарій «один файл слухає, інший виконує `SELECT * FROM users`» — теж `fail`: звичайні запити через `pg` треба переписати на Bun SQL, а LISTEN/NOTIFY-логіку лишити в окремому модулі.
97
+
98
+ ### Приклад — окремий модуль для LISTEN
99
+
100
+ ```javascript
101
+ // src/db/pg-listen.ts — єдине місце, де живе import 'pg'
102
+ import { Client } from 'pg'
103
+
104
+ const listener = new Client({ connectionString: process.env.DATABASE_URL })
105
+
106
+ // allow-pg-leftover: pg LISTEN-клієнт не керується Bun SQL пулом
107
+ await listener.connect()
108
+ await listener.query('LISTEN orders_channel')
109
+ listener.on('notification', msg => {
110
+ // обробка нотифікації
111
+ })
112
+ ```
113
+
114
+ ```javascript
115
+ // src/db/users.ts — звичайні запити, через Bun SQL
116
+ import { sql } from 'bun'
117
+
118
+ export const getUser = id => sql`SELECT * FROM users WHERE id = ${id}`
119
+ ```
120
+
121
+ `pg-listen.ts` буде дозволений завдяки `LISTEN orders_channel` і `.on('notification', ...)`; `users.ts` не має імпорту `'pg'`, тож вільно живе з Bun SQL. `client.connect()` у файлі з Bun SQL потребував би маркер `// allow-pg-leftover: ...`; у файлі, де **Bun SQL не імпортовано**, pg-leftover-сканер не спрацьовує (див. `## Прибирати pg-leftover виклики`), але маркер як коментар-причина — корисний для рев'ю.
122
+
123
+ ### Що лишається забороненим
124
+
125
+ - `import 'pg'` у файлі без LISTEN/NOTIFY — `fail` з повідомленням «перенеси на Bun SQL, лиши LISTEN в окремому модулі».
126
+ - `dependencies.pg` без жодного LISTEN/NOTIFY-сигналу у проєкті — `fail` навіть якщо `pg` нібито «потрібен історично».
127
+ - `pg-format` (unscoped) — лишається у [denylist](./policy/package_json/template/package.json.deny.json); виключення для LISTEN/NOTIFY стосується **тільки** самого `pg`.
128
+ - `pg-pool`, `pg-native`, `mysql`, `mysql2` — виключень немає, видаляти повністю.
129
+
130
+ ## Динамічна SQL-структура: `@scaleleap/pg-format` для identifiers
131
+
132
+ Bun SQL **не вміє** параметризувати назви схем, таблиць, колонок, індексів, ролей, БД — а `sql\`SELECT * FROM ${table}\`` забіндив би це як значення і зламав би синтаксис. Для **динамічних identifiers** дозволено окремий пакет:
133
+
134
+ ```bash
135
+ bun add @scaleleap/pg-format
136
+ ```
137
+
138
+ ⚠️ Це **scoped `@scaleleap/pg-format`**, а не unscoped `pg-format` (той у [deny-списку](./policy/package_json/template/package.json.deny.json)). Беремо форк `@scaleleap` **тільки** заради `%I` / `%s`-можливостей; значення все одно проходять через Bun parameters, **не** через `%L`.
139
+
140
+ ### Дозволений патерн
141
+
142
+ - **`%I`** — escape SQL identifier (schema / table / column / index / role / database).
143
+ - **`%s`** — raw fragment, **тільки** для whitelist-значень (`ASC` / `DESC`, тип JOIN'у тощо).
144
+ - Значення — позиційні параметри `$1, $2, …`, які передаються другим аргументом у `sql.unsafe(query, [bindParams])`.
145
+ - На рядку виклику `sql.unsafe(...)` обов'язковий маркер `// allow-unsafe: <причина>` (див. `## sql.unsafe(...) за замовчуванням заборонено`).
146
+
147
+ ```javascript
148
+ import format from '@scaleleap/pg-format'
149
+ import { sql } from 'bun'
150
+
151
+ const allowedColumns = new Set(['created_at', 'email', 'name'])
152
+ if (!allowedColumns.has(sortBy)) throw new Error('Invalid sort column')
153
+
154
+ const direction = sortDir === 'asc' ? 'ASC' : 'DESC'
155
+
156
+ const query = format(
157
+ 'SELECT * FROM %I.%I ORDER BY %I %s LIMIT $1',
158
+ schemaName,
159
+ tableName,
160
+ sortBy,
161
+ direction
162
+ )
163
+ // allow-unsafe: динамічні schema/table/column; значення біндяться через $N
164
+ const rows = await sql.unsafe(query, [limit])
165
+ ```
166
+
167
+ Multi-row `INSERT` через `VALUES %L` теж типовий легітимний кейс, але передавай значення колонок як паралельні масиви через `unnest(...)` Bun SQL — `format('VALUES %L', rows)` лишай тільки коли альтернатива з `unnest` неможлива:
168
+
169
+ ```javascript
170
+ const query = format(
171
+ /* sql */ `
172
+ INSERT INTO "order".delivery_status (order_id, status, changed_at)
173
+ SELECT v.order_id::uuid, v.status, v.changed_at::timestamptz
174
+ FROM (VALUES %L) AS v(order_id, status, changed_at)
175
+ `,
176
+ values
177
+ )
178
+ // allow-unsafe: multi-row VALUES для бекфілу; values формуються з валідованого input
179
+ await sql.unsafe(query)
180
+ ```
181
+
182
+ ### Заборонено й після підключення `@scaleleap/pg-format`
183
+
184
+ - **`%L` для user input** — це повернення `pg-format`-стилю. Завжди bind через Bun (`sql\`... = ${value}\``) або позиційний параметр `$N` + `sql.unsafe(query, [params])`.
185
+ - Збирати весь `WHERE` через `format(...)` з `%L` — користуйся whitelist полів і ручним складанням `$N`-placeholder'ів (приклад нижче).
186
+ - Власні функції `format` / `pgFormat` / `sqlFormat` / `pgFmt` з тілом, що містить `%L` / `%I` / `%s`, — `fail` сканера (це шим, а не імпорт з бібліотеки).
187
+ - Експортовані `quoteLiteral` / `quoteIdent` / `escapeLiteral` / `escapeIdent` — `fail` сканера (pg-format-специфічні API замість Bun parameters).
188
+
189
+ ### Dynamic `WHERE` — без `format(...)`, через whitelist + `$N`
190
+
191
+ ```javascript
192
+ const conditions = []
193
+ const values = []
194
+
195
+ if (email) {
196
+ values.push(email)
197
+ conditions.push(`email = $${values.length}`)
198
+ }
199
+ if (status) {
200
+ values.push(status)
201
+ conditions.push(`status = $${values.length}`)
202
+ }
203
+
204
+ const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''
205
+ const query = `SELECT * FROM users ${where}`
206
+ // allow-unsafe: динамічний WHERE з whitelist-полів; значення біндяться через $N
207
+ const rows = await sql.unsafe(query, values)
208
+ ```
209
+
210
+ ### Коротка таблиця рішень
211
+
212
+ | Сценарій | Що використовувати |
213
+ | --------------------------------- | ---------------------------------------------------- |
214
+ | `WHERE id = ${...}` | Bun SQL tagged template |
215
+ | `INSERT` одного рядка | Bun SQL tagged template |
216
+ | `INSERT` масиву (object/colset) | Bun SQL helper `sql(rows, 'a', 'b')` або `unnest` |
217
+ | `UPDATE field = ${value}` | Bun SQL tagged template |
218
+ | Динамічна назва schema / table | `@scaleleap/pg-format` `%I` + `sql.unsafe(q, [...])` |
219
+ | Динамічна назва колонки | `@scaleleap/pg-format` `%I` + bind |
220
+ | Динамічний `ORDER BY column` | whitelist + `%I` |
221
+ | `ASC` / `DESC`, тип JOIN'у | whitelist + `%s` |
222
+ | Динамічний `WHERE` (полів багато) | whitelist + ручні `$N` + `sql.unsafe(text, vals)` |
223
+ | Сирий migration / DDL | `sql.unsafe(text)` з `// allow-unsafe: <причина>` |
224
+ | User input як value | **тільки** Bun parameters / `$N` bind |
225
+
226
+ Головне правило:
227
+
228
+ - **SQL values** → Bun SQL parameters (tagged template `${value}` або `$N` + `sql.unsafe(text, values)`).
229
+ - **SQL identifiers** → `@scaleleap/pg-format` `%I` (schema, table, column, index, role, database).
230
+ - **SQL fragments** (`ASC`/`DESC` тощо) → whitelist + `%s`.
231
+
76
232
  ## Підключення (singleton + env)
77
233
 
78
234
  Дефолтний експорт `sql` з `'bun'` сам читає змінні середовища (`DATABASE_URL`, `POSTGRES_URL`, `MYSQL_URL`, `PGHOST`/`PGUSER`/... та `MYSQL_HOST`/`MYSQL_USER`/...) і керує пулом — окремий `Pool` як у `pg` створювати не треба.
@@ -186,14 +342,45 @@ await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
186
342
  Кожен легітимний `sql.unsafe(...)` має супроводжуватись **маркером-коментарем** з причиною — на тому ж рядку (trailing) або на рядку безпосередньо перед викликом. Маркер — opt-in для перевірки `js-bun-db` і слід для ревʼюера:
187
343
 
188
344
  ```javascript
189
- // allow-unsafe: DDL — назву таблиці параметризувати не можна
190
- await sql.unsafe(`CREATE TABLE ${tableName} (id int)`)
345
+ import format from '@scaleleap/pg-format'
346
+
347
+ const query = format('CREATE TABLE %I (id int)', tableName)
348
+ // allow-unsafe: DDL — назву таблиці параметризувати не можна; ідентифікатор екранує pg-format
349
+ await sql.unsafe(query)
191
350
 
192
351
  await sql.unsafe('SELECT pg_advisory_lock($1)', [lockId]) // allow-unsafe: pg_advisory_lock — окремий шлях, без tagged template
193
352
  ```
194
353
 
195
354
  Формат маркера: `allow-unsafe: <непорожня причина>` у line- або block-коментарі. Без причини (`// allow-unsafe:`) і без маркера взагалі — **fail** перевірки.
196
355
 
356
+ ### `sql.unsafe` з template-літералом і `${...}`-інтерполяцією — заборонено навіть з маркером
357
+
358
+ `sql.unsafe(\`...\${x}...\`)` — окремий **hard fail**, який не знімається маркером `// allow-unsafe`. Шаблонна підстановка `${x}` у `sql.unsafe`-рядок:
359
+
360
+ - **не екранує** identifier'ів (reserved words, спецсимволи, пробіли в імені);
361
+ - **не біндить** значень (вони потрапляють у запит сирим текстом, як injection-вектор);
362
+ - виглядає «безпечно» через знайому tagged-template-форму, але не має жодних гарантій Bun SQL.
363
+
364
+ Канон — побудувати `text` окремо, потім передати в `sql.unsafe(text, [params])`:
365
+
366
+ - для **identifiers** — `@scaleleap/pg-format` `format('%I', name)` (екранує спецсимволи, reserved words);
367
+ - для **values** — позиційні `$1`, `$2`, … як placeholder'и в тексті + масив значень другим аргументом;
368
+ - для **fragments** з whitelist (`ASC`/`DESC`) — `format('%s', whitelistedValue)`.
369
+
370
+ ```javascript
371
+ // ❌ template-літерал з ${...} — fail навіть з allow-unsafe
372
+ // allow-unsafe: DDL
373
+ await sql.unsafe(`CREATE TABLE ${tableName} (id int)`)
374
+
375
+ // ✅ format('%I', ...) екранує identifier, sql.unsafe приймає готовий text
376
+ import format from '@scaleleap/pg-format'
377
+ const query = format('CREATE TABLE %I (id int)', tableName)
378
+ // allow-unsafe: DDL — назву таблиці параметризувати не можна
379
+ await sql.unsafe(query)
380
+ ```
381
+
382
+ Статичні `sql.unsafe(\`SELECT 1\`)` (без `${...}`) і `sql.unsafe(text, [params])` зі змінною `text`, зібраною заздалегідь, — допустимі (за наявності `// allow-unsafe`-маркера).
383
+
197
384
  ❌ Заборонені кейси (треба переробити на tagged template):
198
385
 
199
386
  ```javascript
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "dependencies": {
3
- "pg": "заміни на Bun native SQL (js-bun-db.mdc)",
4
3
  "pg-format": "заміни на Bun native SQL — без ручного форматування (js-bun-db.mdc)",
5
4
  "mysql2": "заміни на Bun native SQL (js-bun-db.mdc)"
6
5
  }