@nitra/cursor 1.8.212 → 1.8.213

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,30 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.8.213] - 2026-05-09
8
+
9
+ ### Added
10
+
11
+ - Нове правило `js-bun-redis` (`npm/mdc/js-bun-redis.mdc`): заміна `ioredis` /
12
+ `node-redis` (включно з кореневим `redis` v4 і підпакетами `@redis/*`) на
13
+ Bun native Redis (`import { redis } from 'bun'`,
14
+ <https://bun.com/docs/runtime/redis>).
15
+ - AST-сканер `npm/scripts/utils/redis-imports.mjs` (`oxc-parser`) ловить
16
+ `import` / `require` / динамічний `import()` пакетів `ioredis`, `node-redis`,
17
+ `redis`, підшляхів `ioredis/...` / `redis/...` і `@redis/*`. Не зачіпає
18
+ сторонні `redis-*` (наприклад, `redis-mock`).
19
+ - `npm/scripts/check-js-bun-redis.mjs` запускає AST-скан по JS/TS-джерелах і
20
+ доступний як `npx @nitra/cursor check js-bun-redis`.
21
+ - Rego-полісі `npm/policy/js_bun_redis/package_json/` — заборона
22
+ `ioredis` / `node-redis` / `redis` / `@redis/*` у `dependencies` будь-якого
23
+ `package.json` у дереві; зареєстрована таргетом у
24
+ `npm/scripts/lint-conftest.mjs` (`bun run lint-conftest`).
25
+ - Авто-увімкнення правила в `.n-cursor.json`: `npm/scripts/auto-rules.mjs`
26
+ додає `js-bun-redis`, якщо в `dependencies` хоч одного `package.json` є
27
+ `ioredis` або `node-redis` (умова — у `npm/bin/auto-rules.md`).
28
+ - Тести: `npm/tests/redis-imports.test.mjs` (AST-сканер) і нові кейси у
29
+ `npm/tests/auto-rules.test.mjs` (детект `ioredis` / `node-redis`).
30
+
7
31
  ## [1.8.212] - 2026-05-08
8
32
 
9
33
  ### Changed
@@ -54,7 +78,7 @@
54
78
  `sqlFormat` / `pgFmt` з `%L`/`%I`/`%s` у тілі, плюс `quoteLiteral` /
55
79
  `quoteIdent` / `escapeLiteral` / `escapeIdent` без додаткової перевірки)
56
80
  та `findPgFormatLikeQueryWrapperInText` (`{ query(text, params) { ...
57
- <obj>.unsafe(...) ... } }`). Скан запускається лише у файлах з
81
+ <obj>.unsafe(...) ... } }`). Скан запускається лише у файлах з
58
82
  `import { sql|SQL } from 'bun'`.
59
83
  - `npm/scripts/check-js-bun-db.mjs` рапортує `pgFormatShim` / `queryWrapper` —
60
84
  окремі лічильники й `pass`-рядки, без зміни існуючих перевірок.
@@ -105,10 +129,10 @@
105
129
  (prettier-залежність, `@nitra/eslint-config ≥ 3.9.2`),
106
130
  `checkPackageJsonTypeModule` для root, `checkEnginesNode/Bun` для root,
107
131
  канонічний `lint-js`-скрипт, валідація `lint-js.yml` (`verifyLintJsWorkflowStructure`
108
- + fallback). Лишилися — `.oxlintrc.json` canonical-snapshot, VSCode-розширення,
109
- workspace-ітерація для `type: "module"` і engines, дубль JS-кроків у `lint.yml`,
110
- `.jscpd.json`. Прибрано непотрібні імпорти `parseWorkflowYaml`,
111
- `verifyLintJsWorkflowStructure` і `OXLINT_FIX_RE`.
132
+ - fallback). Лишилися — `.oxlintrc.json` canonical-snapshot, VSCode-розширення,
133
+ workspace-ітерація для `type: "module"` і engines, дубль JS-кроків у `lint.yml`,
134
+ `.jscpd.json`. Прибрано непотрібні імпорти `parseWorkflowYaml`,
135
+ `verifyLintJsWorkflowStructure` і `OXLINT_FIX_RE`.
112
136
  - `npm/scripts/check-js-run.mjs` — без перевірок `bunyan` / `@nitra/bunyan` у
113
137
  залежностях, canonical `jsconfig.json` через `deepEqualJson`,
114
138
  `OTEL_RESOURCE_ATTRIBUTES` у `configmap.yaml`. Лишилися AST-скан коду
@@ -220,7 +244,7 @@
220
244
  - `check-k8s.mjs` (автоконверт `image-replace` patches → `images:`): тепер працює і для `patches[i].patch` із **кількома** ops, а не лише з одинокою image-replace op. Сканує всі ops у патчі, конвертує **кожну** `op: replace` на `/spec/template/spec/containers/<N>/image` (target `kind: Deployment`) у запис `images:`; якщо всі ops патча конвертовано — `patches[i]` видаляється повністю; інакше inline `patch:` переписується через `parseDocument` без конвертованих ops зі збереженням block-literal scalar (`|-`) і вихідного порядку решти ops. Реалізовано через нові функції `tryParseJson6902Array` (≥ 1 op, замість `tryParseSingleJson6902Array`) і `rewriteInlinePatchWithoutOps`; `imageReplaceDeploymentPatchInfo` повертає `{ deployName, totalOps, ops: [{ containerIndex, newImage, opIndex }] }` (раніше — одиничний `{ deployName, containerIndex, newImage }` лише за `length === 1`); `applyConversionsToDoc` групує конвертації по індексу патча й вирізає ops або сам патч за потреби. Сортування решти ops після видалення лишається поза цією зміною — за нього відповідає окрема перевірка `kustomizationInlinePatchOpsSortedViolation`.
221
245
  - `mdc/k8s.mdc` (v1.26 → v1.27): уточнено крок 1 авто-перевірки в розділі «Зміна image — через `images:`, не через `patches[]`» — тепер описує і випадок, коли в `patches[i].patch` лишаються не-image ops (їх зберігає, у вихідному порядку, без коментарів).
222
246
  - `check-js-lint.mjs` + `mdc/js-lint.mdc` (v1.16 → v1.17): мінімум `@nitra/eslint-config` піднято з `^3.8.0` до `^3.9.2`. Обґрунтування: з 3.9.2 у `getConfig` вбудовано ignore для `**/adr/**`, тож ADR-документи не валідуються ESLint, і консьюмерам не треба додавати цей glob у `eslint.config.js` локально. `nitraEslintConfigMeetsMinVersion` тепер повертає `false` для діапазонів `^3.8.x`–`^3.9.1`; `workspace:*` лишається ok без змін. Pass/fail-повідомлення `checkPackageJsonLintDeps` оновлено під новий мінімум; `for...in`-бан з 3.8.0 згадується як накопичена відмінність. Тести `nitraEslintConfigMeetsMinVersion` розширено: `^3.9.2`/`^3.9.10`/`^3.10.0`/`^4.0.0` — ok; `^3.9.1`/`^3.8.0`/`^3.6.12`/`^3.4.3` — ні.
223
- - `bin/n-cursor.js` (`reexecIfPackageVersionChanged` + `spawnSync`-виклик): `process.env.NITRA_CURSOR_REEXEC` і `...process.env` замінено на `env.NITRA_CURSOR_REEXEC` і `...env` з `node:process` (`import { cwd, env } from 'node:process'`). Підстава: правило `js-run.mdc` забороняє прямий `process.env.*` у Node-коді; `NITRA_CURSOR_REEXEC` — опційна змінна (виставляється лише при re-exec), тож імпорт `env` з `node:process` (а не з `@nitra/check-env`) — канонічна форма для опційних. Поведінка не змінена; раніше `npm/scripts/check-js-run.mjs` помилявся на `bin/n-cursor.js:1136` (правило `process-env`), тепер intergation-test `check-* на реальному репозиторії` проходить.
247
+ - `bin/n-cursor.js` (`reexecIfPackageVersionChanged` + `spawnSync`-виклик): `process.env.NITRA_CURSOR_REEXEC` і `...process.env` замінено на `env.NITRA_CURSOR_REEXEC` і `...env` з `node:process` (`import { cwd, env } from 'node:process'`). Підстава: правило `js-run.mdc` забороняє прямий `process.env.*` у Node-коді; `NITRA_CURSOR_REEXEC` — опційна змінна (виставляється лише при re-exec), тож імпорт `env` з `node:process` (а не з `@nitra/check-env`) — канонічна форма для опційних. Поведінка не змінена; раніше `npm/scripts/check-js-run.mjs` помилявся на `bin/n-cursor.js:1136` (правило `process-env`), тепер integration-test `check-* на реальному репозиторії` проходить.
224
248
 
225
249
  ### Added
226
250
 
package/bin/auto-rules.md CHANGED
@@ -34,6 +34,8 @@ js-mssql - якщо в хоч одному package.json в секції dependen
34
34
 
35
35
  js-bun-db - якщо в хоч одному package.json в секції dependencies присутній пакет pg, pg-format або mysql2 або є імпорт sql/SQL з Bun (приклад: import { sql } from "bun")
36
36
 
37
+ js-bun-redis - якщо в хоч одному package.json в секції dependencies присутній пакет ioredis або node-redis
38
+
37
39
  k8s - якщо присутня хоч одна директорія k8s
38
40
 
39
41
  nginx-default-tpl - якщо присутній хоч один файл з переліку - default.conf.template, default.conf, nginx.conf
package/mdc/js-bun-db.mdc CHANGED
@@ -26,7 +26,7 @@ PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом,
26
26
  - допоміжні `quoteLiteral`, `quoteIdent`, `escapeLiteral`, `escapeIdent` як публічні експорти модуля;
27
27
  - обгортки `pgRead.query(text, params)` / `pgWrite.query(text, params)` / `db.query(text, params)`, які складають SQL-рядок (з або без `format`) і викликають `sql.unsafe(text, params)` — це повертає injection-поверхню, від якої ми йдемо, тільки під «зручним» іменем.
28
28
 
29
- Замість цього всі точки використання потрібно перевести на tagged template `sql\`...\${value}...\``. Кожне `${value}` стає окремим параметром bind, без рядкового екранування.
29
+ Замість цього всі точки використання потрібно перевести на tagged template `sql\`...\${value}...\``. Кожне`${value}` стає окремим параметром bind, без рядкового екранування.
30
30
 
31
31
  ### Типові ідіоми `pg-format` → Bun SQL
32
32
 
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: Використання Redis/Valkey з Bun
3
+ alwaysApply: true
4
+ version: '1.0'
5
+ ---
6
+
7
+ ## Підтримувані версії redis
8
+
9
+ Redis 7.2+
10
+
11
+ ## Заміна на Bun native Redis
12
+
13
+ Якщо в проєкті використовуються бібліотеки `ioredis`, `node-redis`, їх потрібно замінити на Bun native Redis: <https://bun.com/docs/runtime/redis>.
14
+
15
+ - Видалити з `dependencies`: `ioredis`, `node-redis`.
16
+ - Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
17
+ - Замінити на `import { redis } from 'bun'`
18
+
19
+ ## Перевірка
20
+
21
+ `npx @nitra/cursor check js-bun-redis`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.212",
3
+ "version": "1.8.213",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,37 @@
1
+ # Перевірка `dependencies` для правила `js-bun-redis.mdc` — паралель до
2
+ # `npm/policy/js_bun_db/package_json/package_json.rego`.
3
+ #
4
+ # Запуск (локально, для будь-якого `package.json` у дереві):
5
+ # conftest test path/to/package.json -p npm/policy/js_bun_redis \
6
+ # --namespace js_bun_redis.package_json
7
+ #
8
+ # Перевіряє: у `dependencies` не повинно бути `ioredis`, `node-redis`,
9
+ # `redis` або жодного з підпакетів `@redis/*` — заміна на Bun native Redis
10
+ # (https://bun.com/docs/runtime/redis).
11
+ #
12
+ # AST-скан коду (`import` / `require` / dynamic `import()` тих самих пакетів)
13
+ # лишається у `npm/scripts/check-js-bun-redis.mjs` (потребує `oxc-parser`).
14
+ #
15
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
16
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
17
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
18
+ package js_bun_redis.package_json
19
+
20
+ import rego.v1
21
+
22
+ forbidden_dependencies := {
23
+ "ioredis",
24
+ "node-redis",
25
+ "redis",
26
+ "@redis/client",
27
+ "@redis/json",
28
+ "@redis/search",
29
+ "@redis/time-series",
30
+ "@redis/bloom",
31
+ }
32
+
33
+ deny contains msg if {
34
+ some pkg_name in forbidden_dependencies
35
+ pkg_name in object.keys(object.get(input, "dependencies", {}))
36
+ msg := sprintf("dependencies містить заборонений %q — заміни на Bun native Redis (js-bun-redis.mdc)", [pkg_name])
37
+ }
@@ -2,9 +2,9 @@
2
2
  * Автовизначення правил для `.n-cursor.json` за умовами з `npm/bin/auto-rules.md`.
3
3
  *
4
4
  * Модуль аналізує дерево проєкту (наявність файлів/директорій, `gql\`...\`` у source,
5
- * залежності `mssql` / `pg` / `pg-format` / `mysql2` у `package.json`, імпорт `sql`/`SQL` з `bun`, кореневий
6
- * `package.json`, `config.yaml` з рядком `metadata_directory: metadata` для hasura)
7
- * та повертає ідентифікатори правил, які потрібно автододати.
5
+ * залежності `mssql` / `pg` / `pg-format` / `mysql2` / `ioredis` / `node-redis` у `package.json`,
6
+ * імпорт `sql`/`SQL` з `bun`, кореневий `package.json`, `config.yaml` з рядком
7
+ * `metadata_directory: metadata` для hasura) та повертає ідентифікатори правил, які потрібно автододати.
8
8
  *
9
9
  * Враховує винятки `disable-rules`: елементи зі списку не додаються автоматично.
10
10
  *
@@ -39,6 +39,7 @@ export const AUTO_RULE_ORDER = Object.freeze([
39
39
  'js-lint',
40
40
  'js-mssql',
41
41
  'js-bun-db',
42
+ 'js-bun-redis',
42
43
  'js-run',
43
44
  'k8s',
44
45
  'nginx-default-tpl',
@@ -602,10 +603,18 @@ export async function detectAutoRules({
602
603
  : null
603
604
  )
604
605
  const isAbie = typeof repositoryUrl === 'string' && repositoryUrl.toLowerCase().includes(ABIE_REPOSITORY_URL_MARKER)
605
- const depHits = await collectDependencyKeysPresentInPackageJsonTree(root, ['mssql', 'pg', 'pg-format', 'mysql2'])
606
+ const depHits = await collectDependencyKeysPresentInPackageJsonTree(root, [
607
+ 'mssql',
608
+ 'pg',
609
+ 'pg-format',
610
+ 'mysql2',
611
+ 'ioredis',
612
+ 'node-redis'
613
+ ])
606
614
  const hasMssqlDependency = depHits.has('mssql')
607
615
  const hasJsBunDbSignal =
608
616
  depHits.has('pg') || depHits.has('pg-format') || depHits.has('mysql2') || facts.hasBunSqlImport
617
+ const hasJsBunRedisSignal = depHits.has('ioredis') || depHits.has('node-redis')
609
618
  const hasNestedNodePackage = await hasNestedPackageJsonWithoutViteDevDependency(root)
610
619
 
611
620
  /** @type {string[]} */
@@ -634,6 +643,7 @@ export async function detectAutoRules({
634
643
  { enabled: facts.hasJsLikeSource, id: 'js-lint' },
635
644
  { enabled: hasMssqlDependency, id: 'js-mssql' },
636
645
  { enabled: hasJsBunDbSignal, id: 'js-bun-db' },
646
+ { enabled: hasJsBunRedisSignal, id: 'js-bun-redis' },
637
647
  { enabled: hasNestedNodePackage, id: 'js-run' },
638
648
  { enabled: facts.hasK8sDir, id: 'k8s' },
639
649
  { enabled: facts.hasNginxDefaultTplFile, id: 'nginx-default-tpl' },
@@ -12,13 +12,7 @@
12
12
  */
13
13
 
14
14
  /** Порядок автододавання skills відповідно до `auto-skills.md`. */
15
- export const AUTO_SKILL_ORDER = Object.freeze([
16
- 'abie-kustomize',
17
- 'fix',
18
- 'lint',
19
- 'publish-telegram',
20
- 'taze'
21
- ])
15
+ export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint', 'publish-telegram', 'taze'])
22
16
 
23
17
  /**
24
18
  * Залежність скілів від правил (`auto-skills.md` синтаксис `skill - [rules]`).
@@ -148,9 +148,8 @@ async function checkEnvFile(relPath, expected, reporter) {
148
148
  const value = m[1].trim()
149
149
  const parsed = parseInternalHasuraEndpoint(value)
150
150
  if (!parsed.ok) {
151
-
152
151
  const example =
153
- "https://<service>.<namespace>.svc.<cluster>.internal:<port> або http://<service>.<namespace>.svc.cluster.local:<port>"
152
+ 'https://<service>.<namespace>.svc.<cluster>.internal:<port> або http://<service>.<namespace>.svc.cluster.local:<port>'
154
153
  fail(
155
154
  `${relPath}: HASURA_GRAPHQL_ENDPOINT="${value}" — потрібен внутрішній кластерний URL виду ${example} (hasura.mdc)`
156
155
  )
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Перевіряє правило `js-bun-redis.mdc`.
3
+ *
4
+ * Заборонено в JS/TS-джерелах будь-який `import` / `require` / динамічний `import()` пакетів
5
+ * `ioredis`, `node-redis`, `redis` (та підпакетів `@redis/*`, підшляхів `ioredis/...` /
6
+ * `redis/...`). Замість них треба використовувати Bun native Redis:
7
+ * `import { redis } from 'bun'` (<https://bun.com/docs/runtime/redis>).
8
+ *
9
+ * Перевірку `dependencies` (заборона `ioredis` / `node-redis` / `redis` / `@redis/*` у будь-якому
10
+ * `package.json`) винесено в Rego-полісі `npm/policy/js_bun_redis/package_json/`; її запускає
11
+ * `bun run lint-conftest`. Тут лишився AST-скан коду через `oxc-parser`.
12
+ */
13
+ import { existsSync } from 'node:fs'
14
+ import { readFile } from 'node:fs/promises'
15
+ import { join, relative } from 'node:path'
16
+
17
+ import { createCheckReporter } from './utils/check-reporter.mjs'
18
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
19
+ import { findRedisImportsInText, isRedisScanSourceFile, shouldSkipFileForRedisScan } from './utils/redis-imports.mjs'
20
+ import { walkDir } from './utils/walkDir.mjs'
21
+
22
+ /**
23
+ * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану заборонених redis-імпортів.
24
+ * @param {string} repoRoot абсолютний шлях до кореня репозиторію
25
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
26
+ * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
27
+ */
28
+ async function findAllSourcePathsForRedisScan(repoRoot, ignorePaths) {
29
+ /** @type {string[]} */
30
+ const paths = []
31
+ await walkDir(
32
+ repoRoot,
33
+ absPath => {
34
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
35
+ if (isRedisScanSourceFile(rel) && !shouldSkipFileForRedisScan(rel)) {
36
+ paths.push(absPath)
37
+ }
38
+ },
39
+ ignorePaths
40
+ )
41
+ paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
42
+ return paths
43
+ }
44
+
45
+ /**
46
+ * Сканує JS/TS-джерела на заборонені імпорти/require пакетів `ioredis` / `node-redis` / `redis`.
47
+ * @param {string[]} sourcePaths абсолютні шляхи джерел
48
+ * @param {string} repoRoot абсолютний шлях до кореня
49
+ * @param {(msg: string) => void} fail callback при помилці
50
+ * @returns {Promise<number>} кількість знайдених порушень
51
+ */
52
+ async function scanSourcesForRedisImports(sourcePaths, repoRoot, fail) {
53
+ let violations = 0
54
+ for (const absPath of sourcePaths) {
55
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
56
+ const content = await readFile(absPath, 'utf8')
57
+ for (const v of findRedisImportsInText(content, rel)) {
58
+ violations++
59
+ fail(
60
+ `js-bun-redis: ${rel}:${v.line} — заміни '${v.module}' на Bun native Redis ` +
61
+ `(import { redis } from 'bun', https://bun.com/docs/runtime/redis): ${v.snippet}`
62
+ )
63
+ }
64
+ }
65
+ return violations
66
+ }
67
+
68
+ /**
69
+ * Перевіряє відповідність проєкту правилу `js-bun-redis.mdc`.
70
+ * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
71
+ */
72
+ export async function check() {
73
+ const reporter = createCheckReporter()
74
+ const { pass, fail } = reporter
75
+
76
+ const repoRoot = process.cwd()
77
+ if (!existsSync(join(repoRoot, 'package.json'))) {
78
+ pass('js-bun-redis: package.json у корені відсутній — перевірку пропущено')
79
+ return reporter.getExitCode()
80
+ }
81
+
82
+ const ignorePaths = await loadCursorIgnorePaths(repoRoot)
83
+ const sourcePaths = await findAllSourcePathsForRedisScan(repoRoot, ignorePaths)
84
+ if (sourcePaths.length === 0) {
85
+ pass('js-bun-redis: немає JS/TS файлів для скану імпортів ioredis / node-redis / redis')
86
+ return reporter.getExitCode()
87
+ }
88
+
89
+ const violations = await scanSourcesForRedisImports(sourcePaths, repoRoot, fail)
90
+ if (violations === 0) {
91
+ pass(
92
+ "js-bun-redis: немає імпортів 'ioredis' / 'node-redis' / 'redis' / '@redis/*' у джерелах " +
93
+ '(використовується Bun native Redis або redis взагалі не задіяно)'
94
+ )
95
+ }
96
+
97
+ return reporter.getExitCode()
98
+ }
@@ -479,7 +479,7 @@ async function validateKustomizationResourcesSortedAlphabetically(root, yamlFile
479
479
  }
480
480
 
481
481
  /**
482
- * Лексичне порівняння двох тuplіе рядків через `localeCompare('en', { sensitivity: 'base' })`.
482
+ * Лексичне порівняння двох tuple рядків через `localeCompare('en', { sensitivity: 'base' })`.
483
483
  * Менший за довжиною список доповнюється порожніми рядками.
484
484
  * @param {string[]} a перший tuple
485
485
  * @param {string[]} b другий tuple
@@ -594,7 +594,7 @@ function jsonPointerPathsAreDisjoint(paths) {
594
594
  }
595
595
 
596
596
  /**
597
- * Парсить рядок JSON6902-патчa в плоский масив операцій `{ op, path }` (без значень).
597
+ * Парсить рядок JSON6902-патча в плоский масив операцій `{ op, path }` (без значень).
598
598
  * Повертає `null`, якщо це не YAML-масив об'єктів з полями `op`/`path` як рядки.
599
599
  * @param {string} raw тіло inline `patch:` (literal block scalar)
600
600
  * @returns {{ op: string, path: string }[] | null} нормалізований список ops або `null` за невідповідного формату
@@ -183,6 +183,12 @@ const TARGETS = [
183
183
  rule: 'js-bun-db',
184
184
  walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
185
185
  },
186
+ {
187
+ namespace: 'js_bun_redis.package_json',
188
+ policyDir: 'js_bun_redis',
189
+ rule: 'js-bun-redis',
190
+ walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
191
+ },
186
192
  {
187
193
  namespace: 'js_run.package_json',
188
194
  policyDir: 'js_run',
@@ -257,7 +263,10 @@ function collectFiles(root, match) {
257
263
  continue
258
264
  }
259
265
  if (!e.isFile()) continue
260
- const rel = abs.slice(root.length + 1).split(sep).join('/')
266
+ const rel = abs
267
+ .slice(root.length + 1)
268
+ .split(sep)
269
+ .join('/')
261
270
  if (match(rel)) out.push(rel)
262
271
  }
263
272
  }
@@ -297,11 +306,10 @@ function runConftestForTarget(conftestBin, target, files) {
297
306
  return 0
298
307
  }
299
308
  console.log(`\n▶ conftest (${target.namespace} — ${files.length} файл(ів))`)
300
- const r = spawnSync(
301
- conftestBin,
302
- ['test', ...files, '-p', policyAbs, '--namespace', target.namespace, '--no-color'],
303
- { stdio: 'inherit', env: process.env }
304
- )
309
+ const r = spawnSync(conftestBin, ['test', ...files, '-p', policyAbs, '--namespace', target.namespace, '--no-color'], {
310
+ stdio: 'inherit',
311
+ env: process.env
312
+ })
305
313
  if (r.error) {
306
314
  console.error(`❌ Не вдалося запустити conftest: ${r.error.message}`)
307
315
  return 1
@@ -425,8 +425,7 @@ export function findPgFormatLikeQueryWrapperInText(content, virtualPath = 'scan.
425
425
  for (const prop of properties) {
426
426
  if (!prop || prop.type !== 'Property') continue
427
427
  const key = prop.key
428
- const keyName =
429
- key && key.type === 'Identifier' ? key.name : key && key.type === 'Literal' ? key.value : null
428
+ const keyName = key && key.type === 'Identifier' ? key.name : key && key.type === 'Literal' ? key.value : null
430
429
  if (keyName !== 'query') continue
431
430
  const value = prop.value
432
431
  if (!value || (value.type !== 'FunctionExpression' && value.type !== 'ArrowFunctionExpression')) continue
@@ -100,7 +100,7 @@ function collectNamedExportNames(program) {
100
100
  (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') &&
101
101
  decl.id &&
102
102
  typeof decl.id === 'object' &&
103
- typeof /** @type {Record<string, unknown>} */ (decl.id).name === 'string'
103
+ typeof (/** @type {Record<string, unknown>} */ (decl.id).name) === 'string'
104
104
  ) {
105
105
  out.push(/** @type {string} */ (/** @type {Record<string, unknown>} */ (decl.id).name))
106
106
  }
@@ -128,7 +128,11 @@ function hasDefaultExport(program) {
128
128
  const body = /** @type {Record<string, unknown>} */ (program).body
129
129
  if (!Array.isArray(body)) return false
130
130
  for (const node of body) {
131
- if (node && typeof node === 'object' && /** @type {Record<string, unknown>} */ (node).type === 'ExportDefaultDeclaration') {
131
+ if (
132
+ node &&
133
+ typeof node === 'object' &&
134
+ /** @type {Record<string, unknown>} */ (node).type === 'ExportDefaultDeclaration'
135
+ ) {
132
136
  return true
133
137
  }
134
138
  }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Знаходить імпорти з `ioredis` та `node-redis` (та підшляхів `redis/...`) у джерелах —
3
+ * їх треба замінити на Bun native Redis (`import { redis } from 'bun'`) згідно з
4
+ * `js-bun-redis.mdc` (<https://bun.com/docs/runtime/redis>).
5
+ *
6
+ * Семантика береться з **oxc-parser** (`module.staticImports`) — без regex по тілу файлу.
7
+ * Додатково по AST програми ловимо `require('ioredis')` і динамічний `import('ioredis')`,
8
+ * щоб правило працювало і у CommonJS, і при динамічному `import` у межах одного файлу.
9
+ *
10
+ * `node-redis` публікується під рядом імен:
11
+ * - кореневий пакет `redis` (саме так його імпортують у v4+);
12
+ * - історичний `node-redis` (рідше);
13
+ * - підпакети, які тягнуться разом: `@redis/client`, `@redis/json`, `@redis/search`,
14
+ * `@redis/time-series`, `@redis/bloom` — їх теж треба прибирати разом із основним
15
+ * клієнтом, щоб не лишилось «половини» інтеграції після переходу на Bun.
16
+ *
17
+ * Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається порожній
18
+ * результат — спочатку треба полагодити синтаксис, потім перезапустити перевірку.
19
+ */
20
+ import { parseSync } from 'oxc-parser'
21
+
22
+ import { langFromPath, normalizeSnippet, offsetToLine } from './ast-scan-utils.mjs'
23
+
24
+ const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
25
+ const FORBIDDEN_MODULE_NAMES = new Set([
26
+ 'ioredis',
27
+ 'node-redis',
28
+ 'redis',
29
+ '@redis/client',
30
+ '@redis/json',
31
+ '@redis/search',
32
+ '@redis/time-series',
33
+ '@redis/bloom'
34
+ ])
35
+
36
+ /**
37
+ * Чи є рядок-специфікатор імпорту забороненим (`ioredis`, `node-redis`, `redis`, `redis/...`,
38
+ * `ioredis/...`, `@redis/<sub>`).
39
+ *
40
+ * Використовуємо префікс-збіг для `ioredis/` та `redis/` — щоб ловити підшляхи
41
+ * (`ioredis/built/utils`, `redis/dist/...`), але не зачепити сторонні пакети
42
+ * на кшталт `redis-mock`, які треба валідувати окремо.
43
+ * @param {string} mod рядкове значення з `import '...'` / `require('...')`
44
+ * @returns {boolean} true, якщо такий specifier треба викинути на користь Bun native Redis
45
+ */
46
+ function isForbiddenRedisModule(mod) {
47
+ if (FORBIDDEN_MODULE_NAMES.has(mod)) return true
48
+ return mod.startsWith('ioredis/') || mod.startsWith('redis/') || mod.startsWith('@redis/')
49
+ }
50
+
51
+ /**
52
+ * Перевіряє, чи це виклик `require('<module>')` з рядковим аргументом.
53
+ * @param {Record<string, unknown> | null | undefined} node вузол AST
54
+ * @returns {string | null} ім'я модуля з аргументу, інакше `null`
55
+ */
56
+ function requireCallModule(node) {
57
+ if (!node || node.type !== 'CallExpression') return null
58
+ const callee = node.callee
59
+ if (!callee || callee.type !== 'Identifier' || callee.name !== 'require') return null
60
+ const arg = node.arguments?.[0]
61
+ if (!arg || arg.type !== 'Literal' || typeof arg.value !== 'string') return null
62
+ return arg.value
63
+ }
64
+
65
+ /**
66
+ * Перевіряє, чи це динамічний `import('<module>')` з рядковим аргументом.
67
+ * @param {Record<string, unknown> | null | undefined} node вузол AST
68
+ * @returns {string | null} ім'я модуля, інакше `null`
69
+ */
70
+ function dynamicImportModule(node) {
71
+ if (!node || node.type !== 'ImportExpression') return null
72
+ const src = node.source
73
+ if (!src || src.type !== 'Literal' || typeof src.value !== 'string') return null
74
+ return src.value
75
+ }
76
+
77
+ /**
78
+ * Простий рекурсивний обхід AST: заходимо в усі обʼєкти/масиви, щоб знайти require/import-вузли.
79
+ * @param {unknown} node корінь або під-вузол AST
80
+ * @param {(n: unknown) => void} visit виклик для кожного обʼєкта-вузла
81
+ * @returns {void}
82
+ */
83
+ function walkAst(node, visit) {
84
+ if (!node || typeof node !== 'object') return
85
+ if (Array.isArray(node)) {
86
+ for (const item of node) walkAst(item, visit)
87
+ return
88
+ }
89
+ if (typeof node.type === 'string') {
90
+ visit(node)
91
+ }
92
+ for (const key of Object.keys(node)) {
93
+ if (key !== 'parent') {
94
+ const v = node[key]
95
+ if (v && typeof v === 'object') walkAst(v, visit)
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Знаходить заборонені імпорти/require з `ioredis` / `node-redis` у тексті.
102
+ * @param {string} content вихідний код
103
+ * @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/foo.ts`)
104
+ * @returns {{ line: number, snippet: string, module: string }[]} список порушень
105
+ */
106
+ export function findRedisImportsInText(content, virtualPath = 'scan.ts') {
107
+ const pathForLang = virtualPath || 'scan.ts'
108
+ const lang = langFromPath(pathForLang)
109
+ let result
110
+ try {
111
+ result = parseSync(pathForLang, content, { lang, sourceType: 'module' })
112
+ } catch {
113
+ return []
114
+ }
115
+ if (result.errors?.length) {
116
+ return []
117
+ }
118
+
119
+ /** @type {{ line: number, snippet: string, module: string }[]} */
120
+ const out = []
121
+
122
+ for (const imp of result.module?.staticImports ?? []) {
123
+ const mod = imp.moduleRequest?.value
124
+ if (typeof mod === 'string' && isForbiddenRedisModule(mod)) {
125
+ out.push({
126
+ line: offsetToLine(content, imp.start),
127
+ snippet: normalizeSnippet(content.slice(imp.start, imp.end)),
128
+ module: mod
129
+ })
130
+ }
131
+ }
132
+
133
+ walkAst(result.program, node => {
134
+ const reqMod = requireCallModule(node)
135
+ if (reqMod && isForbiddenRedisModule(reqMod)) {
136
+ out.push({
137
+ line: offsetToLine(content, node.start),
138
+ snippet: normalizeSnippet(content.slice(node.start, node.end)),
139
+ module: reqMod
140
+ })
141
+ return
142
+ }
143
+ const dynMod = dynamicImportModule(node)
144
+ if (dynMod && isForbiddenRedisModule(dynMod)) {
145
+ out.push({
146
+ line: offsetToLine(content, node.start),
147
+ snippet: normalizeSnippet(content.slice(node.start, node.end)),
148
+ module: dynMod
149
+ })
150
+ }
151
+ })
152
+
153
+ return out
154
+ }
155
+
156
+ /**
157
+ * Чи сканувати цей файл за розширенням (JS/TS-сімʼя).
158
+ * @param {string} relativePath відносний шлях до файлу
159
+ * @returns {boolean} `true`, якщо розширення підходить для пошуку імпорту
160
+ */
161
+ export function isRedisScanSourceFile(relativePath) {
162
+ return SOURCE_FILE_RE.test(relativePath)
163
+ }
164
+
165
+ /**
166
+ * Чи слід пропустити файл під час обходу пакета (декларації типів — лише типи, не виконувані).
167
+ * @param {string} relativePosix шлях з posix-слешами
168
+ * @returns {boolean} `true`, якщо файл не сканувати
169
+ */
170
+ export function shouldSkipFileForRedisScan(relativePosix) {
171
+ return relativePosix.endsWith('.d.ts')
172
+ }
@@ -13,7 +13,7 @@ description: >-
13
13
 
14
14
  ## Передумови
15
15
 
16
- - Чисте робоче дерево (`git status` без незакоммічених змін у `package.json` / `bun.lock` / `node_modules`) — інакше різницю не відрізнити від оновлення.
16
+ - Чисте робоче дерево (`git status` без незакомічених змін у `package.json` / `bun.lock` / `node_modules`) — інакше різницю не відрізнити від оновлення.
17
17
  - Встановлений `bun` і доступний `bunx`.
18
18
  - Запуск з кореня проекту (де лежить `package.json` / `bun.lock`).
19
19
 
@@ -66,6 +66,7 @@ rg -n "<імпорт|функція|опція>" --type ts --type js --type vue
66
66
  ```
67
67
 
68
68
  Класифікувати:
69
+
69
70
  - **сумісно** — проект не використовує зачеплене API → нічого не робити.
70
71
  - **несумісно** — використання знайдено → перейти до п. 6.
71
72