@nitra/cursor 3.21.1 → 3.23.0

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 (231) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/CHANGELOG.md +37 -3
  3. package/bin/docs/n-cursor.md +636 -0
  4. package/bin/docs/rename-yaml-extensions.md +207 -0
  5. package/bin/n-cursor.js +30 -3
  6. package/package.json +1 -1
  7. package/rules/abie/docs/fix.md +18 -0
  8. package/rules/abie/js/docs/applies.md +26 -0
  9. package/rules/abie/js/docs/env_dns.md +32 -0
  10. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  11. package/rules/abie/js/docs/hc_pairing.md +35 -0
  12. package/rules/abie/js/docs/ua_http_route.md +28 -0
  13. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  14. package/rules/abie/lib/docs/enabled.md +29 -0
  15. package/rules/abie/lib/docs/env-dns.md +35 -0
  16. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  17. package/rules/abie/lib/docs/http-route.md +44 -0
  18. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  19. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  20. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  21. package/rules/abie/lib/docs/yaml.md +29 -0
  22. package/rules/adr/docs/fix.md +148 -0
  23. package/rules/adr/js/docs/hooks.md +259 -0
  24. package/rules/bun/docs/fix.md +156 -0
  25. package/rules/bun/js/docs/layout.md +393 -0
  26. package/rules/capacitor/docs/fix.md +121 -0
  27. package/rules/capacitor/js/docs/platforms.md +295 -0
  28. package/rules/changelog/changelog.mdc +2 -2
  29. package/rules/changelog/docs/fix.md +174 -0
  30. package/rules/changelog/js/consistency.mjs +114 -13
  31. package/rules/changelog/js/docs/consistency.md +387 -0
  32. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  33. package/rules/ci4/docs/fix.md +179 -0
  34. package/rules/ci4/js/docs/marksman_config.md +128 -0
  35. package/rules/docker/docker.mdc +8 -3
  36. package/rules/docker/docs/fix.md +171 -0
  37. package/rules/docker/js/docs/lint.md +258 -0
  38. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  39. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  40. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  41. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  42. package/rules/docker/lint/docs/lint.md +193 -0
  43. package/rules/efes/docs/fix.md +203 -0
  44. package/rules/feedback/docs/fix.md +140 -0
  45. package/rules/flow/docs/fix.md +152 -0
  46. package/rules/ga/docs/fix.md +158 -0
  47. package/rules/ga/js/docs/lint.md +100 -0
  48. package/rules/ga/js/docs/workflows.md +217 -0
  49. package/rules/ga/lint/docs/lint.md +209 -0
  50. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  51. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  52. package/rules/graphql/docs/fix.md +126 -0
  53. package/rules/graphql/js/docs/tooling.md +264 -0
  54. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  55. package/rules/hasura/docs/fix.md +120 -0
  56. package/rules/hasura/hasura.mdc +14 -0
  57. package/rules/hasura/js/docs/internal_urls.md +326 -0
  58. package/rules/image-avif/docs/fix.md +132 -0
  59. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  60. package/rules/image-compress/docs/fix.md +150 -0
  61. package/rules/image-compress/js/docs/package_setup.md +191 -0
  62. package/rules/js-bun-db/docs/fix.md +148 -0
  63. package/rules/js-bun-db/js/docs/safety.md +231 -0
  64. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  65. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  66. package/rules/js-bun-redis/docs/fix.md +123 -0
  67. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  68. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  69. package/rules/js-lint/docs/fix.md +117 -0
  70. package/rules/js-lint/js/docs/lint.md +250 -0
  71. package/rules/js-lint/js/docs/tooling.md +348 -0
  72. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  73. package/rules/js-lint/js/lint-findings.mjs +110 -0
  74. package/rules/js-lint/js/lint.mjs +86 -15
  75. package/rules/js-lint-ci/docs/fix.md +154 -0
  76. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  77. package/rules/js-mssql/docs/fix.md +128 -0
  78. package/rules/js-mssql/js/docs/deps.md +263 -0
  79. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  80. package/rules/js-run/docs/fix.md +144 -0
  81. package/rules/js-run/js/docs/runtime.md +388 -0
  82. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  83. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  84. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  85. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  86. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  87. package/rules/k8s/docs/fix.md +129 -0
  88. package/rules/k8s/js/docs/manifests.md +344 -0
  89. package/rules/k8s/js/manifests.mjs +6 -2
  90. package/rules/k8s/k8s.mdc +4 -2
  91. package/rules/k8s/lint/docs/lint.md +411 -0
  92. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  93. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  94. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  95. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  96. package/rules/npm-module/docs/fix.md +98 -0
  97. package/rules/npm-module/js/docs/package_structure.md +274 -0
  98. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  99. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  100. package/rules/php/docs/fix.md +107 -0
  101. package/rules/php/js/docs/tooling.md +152 -0
  102. package/rules/php/lint/docs/lint.md +215 -0
  103. package/rules/python/docs/fix.md +163 -0
  104. package/rules/python/js/docs/applies.md +108 -0
  105. package/rules/python/js/docs/tooling.md +153 -0
  106. package/rules/python/lint/docs/lint.md +322 -0
  107. package/rules/rego/docs/fix.md +121 -0
  108. package/rules/rego/js/docs/applies.md +174 -0
  109. package/rules/rego/js/docs/lint.md +118 -0
  110. package/rules/rego/lint/docs/lint.md +204 -0
  111. package/rules/release/docs/change.md +185 -0
  112. package/rules/release/docs/fix.md +119 -0
  113. package/rules/release/docs/release.md +222 -0
  114. package/rules/release/lib/docs/aggregate.md +246 -0
  115. package/rules/release/lib/docs/change-file.md +200 -0
  116. package/rules/release/lib/docs/fallback.md +203 -0
  117. package/rules/rust/docs/fix.md +129 -0
  118. package/rules/rust/js/docs/applies.md +140 -0
  119. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  120. package/rules/security/docs/fix.md +86 -0
  121. package/rules/security/js/docs/lint.md +171 -0
  122. package/rules/security/js/docs/sample_secret.md +190 -0
  123. package/rules/security/js/docs/trufflehog.md +137 -0
  124. package/rules/security/js/lint.mjs +9 -1
  125. package/rules/style-lint/docs/fix.md +155 -0
  126. package/rules/style-lint/js/docs/lint.md +184 -0
  127. package/rules/style-lint/js/docs/tooling.md +194 -0
  128. package/rules/tauri/docs/fix.md +158 -0
  129. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  130. package/rules/tauri/js/docs/tooling.md +228 -0
  131. package/rules/test/coverage/coverage.mjs +15 -3
  132. package/rules/test/docs/fix.md +132 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  135. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  136. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  137. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  138. package/rules/test/js/docs/location.md +136 -0
  139. package/rules/test/js/docs/no-process-chdir.md +160 -0
  140. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  141. package/rules/test/js/docs/stryker_config.md +152 -0
  142. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  143. package/rules/text/docs/fix.md +118 -0
  144. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  145. package/rules/text/js/docs/formatting.md +256 -0
  146. package/rules/text/js/docs/lint.md +122 -0
  147. package/rules/text/lint/docs/lint.md +220 -0
  148. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  149. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  150. package/rules/text/lint/docs/run-v8r.md +197 -0
  151. package/rules/vue/docs/fix.md +127 -0
  152. package/rules/vue/js/docs/packages.md +335 -0
  153. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  154. package/rules/worktree/docs/fix.md +161 -0
  155. package/schemas/rule-meta.json +5 -1
  156. package/scripts/auto-rules.mjs +7 -4
  157. package/scripts/coverage-classify/docs/apply.md +202 -0
  158. package/scripts/coverage-classify/docs/cache.md +203 -0
  159. package/scripts/coverage-classify/docs/index.md +218 -0
  160. package/scripts/coverage-classify/docs/prompt.md +132 -0
  161. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  162. package/scripts/coverage-fix-extract.mjs +122 -0
  163. package/scripts/coverage-fix.mjs +1 -1
  164. package/scripts/dispatcher/docs/graph.md +346 -0
  165. package/scripts/dispatcher/docs/index.md +236 -0
  166. package/scripts/dispatcher/docs/trace.md +296 -0
  167. package/scripts/dispatcher/index.mjs +1 -1
  168. package/scripts/dispatcher/lib/active.mjs +4 -8
  169. package/scripts/dispatcher/lib/commands.mjs +7 -11
  170. package/scripts/dispatcher/lib/docs/active.md +348 -0
  171. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  172. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  173. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  174. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  175. package/scripts/dispatcher/lib/docs/events.md +182 -0
  176. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  177. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  178. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  179. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  180. package/scripts/dispatcher/lib/docs/level.md +335 -0
  181. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  182. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  183. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  184. package/scripts/dispatcher/lib/docs/review.md +255 -0
  185. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  186. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  187. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  188. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  189. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  190. package/scripts/dispatcher/lib/executor.mjs +6 -1
  191. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  192. package/scripts/dispatcher/lib/level.mjs +29 -3
  193. package/scripts/dispatcher/lib/review.mjs +1 -1
  194. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  195. package/scripts/docs/auto-rules.md +376 -0
  196. package/scripts/docs/auto-skills.md +173 -0
  197. package/scripts/docs/build-agents-commands.md +183 -0
  198. package/scripts/docs/cli-entry.md +153 -0
  199. package/scripts/docs/coverage-fix.md +177 -0
  200. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  201. package/scripts/lib/changed-files.mjs +4 -1
  202. package/scripts/lib/diff-added-lines.mjs +85 -0
  203. package/scripts/lib/docs/changed-files.md +149 -0
  204. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  205. package/scripts/lib/docs/check-reporter.md +175 -0
  206. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  207. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  208. package/scripts/lib/docs/ensure-tool.md +254 -0
  209. package/scripts/lib/docs/generated-markdown.md +275 -0
  210. package/scripts/lib/docs/gha-workflow.md +326 -0
  211. package/scripts/lib/docs/inline-template-links.md +303 -0
  212. package/scripts/lib/docs/list-rule-ids.md +156 -0
  213. package/scripts/lib/docs/load-cursor-config.md +147 -0
  214. package/scripts/lib/docs/mirror-parity.md +167 -0
  215. package/scripts/lib/worktree.mjs +26 -0
  216. package/scripts/worktree-cli.mjs +12 -2
  217. package/skills/coverage-fix/SKILL.md +34 -45
  218. package/skills/docgen/SKILL.md +44 -23
  219. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  220. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  221. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  222. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  223. package/skills/docgen/js/docgen-scan.mjs +37 -21
  224. package/skills/llm-patch/SKILL.md +23 -2
  225. package/skills/start-check/SKILL.md +26 -53
  226. package/skills/start-check/js/check.mjs +211 -0
  227. package/skills/taze/SKILL.md +9 -3
  228. package/skills/taze/js/diff.mjs +154 -0
  229. package/types/bin/n-cursor.d.ts +1 -1
  230. package/skills/fix-tests/SKILL.md +0 -119
  231. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,347 @@
1
+ # bun-sql-scan.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `bun-sql-scan.mjs` — це AST-сканер небезпечних патернів використання Bun SQL у JavaScript/TypeScript-коді. Він є частиною npm-правила `js-bun-db` (тека `npm/rules/js-bun-db/lib/`) і реалізує семантичний аналіз коду через парсер **oxc-parser** (без regex-сканування по сирому тексту, за винятком дешевих pre-filter-ів на наявність імпорту).
6
+
7
+ Призначення сканера — знаходити у файлах, що імпортують `sql`/`SQL` з пакета `bun`, ситуації, які можуть призвести до SQL-injection, антипатернів продуктивності або «недомігрованого» коду з PostgreSQL-клієнта `pg`. Кожна публічна функція пошуку повертає список знахідок із номером рядка та snippet'ом коду — для подальшого перетворення на `fail`-повідомлення у відповідному `.mdc`-правилі.
8
+
9
+ Загальні принципи:
10
+
11
+ - **Семантика через AST**, не regex. Regex використовуються лише для дешевих текстових pre-filter'ів (`textHasBunSqlImport`, `textHasPgLibImport`), маркерів-коментарів та коротких підрядків (LISTEN/NOTIFY, pg-format placeholders).
12
+ - **Контракт обробки помилок**: якщо файл не парситься (`parseProgramOrNull` повертає `null`), сканер повертає порожній масив. Це навмисний відмовостійкий шлях: спершу треба полагодити синтаксис, потім перезапускати перевірку.
13
+ - **Opt-in маркери-коментарі**: деякі небезпечні патерни можна свідомо дозволити, проставивши коментар `// allow-unsafe: <reason>` або `// allow-pg-leftover: <reason>` на тому ж рядку, що й виклик, або на рядку безпосередньо вище. Причина (`<reason>`) обов'язкова — інакше маркер не приймається.
14
+ - **Скоп файлу**: pg-format-шими, pg-leftover-виклики та інші «недомігровані» патерни шукаються лише у файлах, де **в цьому ж файлі** є `import { sql|SQL } from 'bun'` — щоб не плутати з невинними збігами імен у непов'язаному коді.
15
+
16
+ ## Експорти / API
17
+
18
+ Усі експорти — іменовані (named exports), default-експорту немає.
19
+
20
+ ### Експортовані функції-сканери (повертають список порушень)
21
+
22
+ | Функція | Сигнатура (скорочено) | Що шукає |
23
+ | ----------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `findBunSqlPerRequestConnectionInText(content, virtualPath?)` | `(string, string?) => { line, snippet }[]` | `new SQL(...)` усередині будь-якої функції (порушення singleton-пулу). |
25
+ | `findBunSqlUnsafeUseWithoutAllowMarkerInText(content, virtualPath?)` | `(string, string?) => { line, snippet }[]` | `<obj>.unsafe(...)` без `// allow-unsafe: <reason>`. |
26
+ | `findBunSqlUnsafeWithInterpolatedTemplateInText(content, virtualPath?)` | `(string, string?) => { line, snippet }[]` | `<obj>.unsafe(\`...${x}...\`)`— interpolated template literal як аргумент`unsafe` (injection-поверхня навіть із allow-маркером). |
27
+ | `findBunSqlPgLeftoverCallInText(content, virtualPath?)` | `(string, string?) => { line, snippet, methodName }[]` | `<obj>.connect(...)` / `<obj>.end(...)` у Bun SQL-файлах без `// allow-pg-leftover: <reason>`. |
28
+ | `findUnsafeBunSqlDynamicSqlListInText(content, virtualPath?)` | `(string, string?) => { line, snippet }[]` | `IN (...)` / `VALUES (...)` з `.join(',')` у template literal. |
29
+ | `findUnsafeBunSqlInListMissingEmptyGuardInText(content, virtualPath?)` | `(string, string?) => { line, snippet, reason, name? }[]` | `IN (${...})` без винесення у змінну або без guard `if (empty) throw` перед запитом. |
30
+ | `findPgFormatShimDefinitionInText(content, virtualPath?)` | `(string, string?) => { line, snippet, kind, name }[]` | Визначення pg-format-сумісних шимів (`format`, `pgFormat`, `quoteLiteral`, ...). |
31
+ | `findPgFormatLikeQueryWrapperInText(content, virtualPath?)` | `(string, string?) => { line, snippet }[]` | Об'єктні pg-сумісні `{ query(text, params) { ... unsafe ... } }`-обгортки. |
32
+ | `findPgLibImportInText(content, virtualPath?)` | `(string, string?) => { line, snippet }[]` | Імпорт/`require('pg')` (точно пакет `pg`, не `pg-format`/`pg-pool`). |
33
+ | `findPgListenNotifyUsageInText(content, virtualPath?)` | `(string, string?) => { line, snippet, kind }[]` | LISTEN/UNLISTEN/NOTIFY-запити та `.on('notification', ...)`-listener'и. |
34
+
35
+ ### Експортовані допоміжні предикати
36
+
37
+ | Функція | Сигнатура | Призначення |
38
+ | ------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
39
+ | `textHasBunSqlImport(content)` | `(string) => boolean` | Текстова перевірка: чи є у файлі `import { sql\|SQL } from 'bun'`. Дешевий pre-filter (без AST). |
40
+ | `textHasPgLibImport(content)` | `(string) => boolean` | Текстова перевірка: чи є у файлі `import ... from 'pg'` або `require('pg')`. Не матчить `pg-format`, `pg-pool`. |
41
+ | `isBunSqlScanSourceFile(relativePathPosix)` | `(string) => boolean` | Чи сканувати цей файл за розширенням: JS/TS-сімʼя (`.js`, `.cjs`, `.mjs`, `.jsx`, `.ts`, `.cts`, `.mts`, `.tsx`), без `.d.ts`. |
42
+
43
+ ### Не експортовані сутності (internal helpers)
44
+
45
+ Регулярки-константи:
46
+
47
+ - `SOURCE_FILE_RE` — розширення JS/TS-сімʼї.
48
+ - `BUN_SQL_IMPORT_RE` — `import { sql\|SQL } from 'bun'`.
49
+ - `PG_LIB_IMPORT_RE` — `import ... from 'pg'` або `require('pg')` (без `pg-format`/`pg-pool`).
50
+ - `PG_LISTEN_NOTIFY_SQL_RE` — рядок, що починається з `LISTEN\|UNLISTEN\|NOTIFY` (case-insensitive).
51
+ - `IN_PLACEHOLDER_END_RE` — quasi, що закінчується на `IN ` або `IN (`.
52
+ - `ALLOW_UNSAFE_MARKER_RE` — `allow-unsafe: <непорожня причина>`.
53
+ - `ALLOW_PG_LEFTOVER_MARKER_RE` — `allow-pg-leftover: <непорожня причина>`.
54
+ - `PG_FORMAT_PLACEHOLDER_RE` — `%L`, `%I`, `%s`.
55
+ - `PG_QUERY_FIRST_PARAM_RE` — `text\|sql\|query` (імена першого параметра pg-style query-обгортки).
56
+
57
+ Set-константи:
58
+
59
+ - `PG_LEFTOVER_METHOD_NAMES` — `{ 'connect', 'end' }`.
60
+ - `PG_FORMAT_SHIM_FUNC_NAMES` — `{ 'format', 'pgFormat', 'sqlFormat', 'pgFmt' }`.
61
+ - `QUOTE_HELPER_NAMES` — `{ 'quoteLiteral', 'quoteIdent', 'escapeLiteral', 'escapeIdent' }`.
62
+
63
+ Внутрішні функції-предикати/витягувачі:
64
+
65
+ - `isLengthMember`, `isZeroNumberLiteral`, `isSqlHelperIdentifier`, `isEmptyListTest`, `consequentHasThrow`, `hasEmptyGuardBefore`, `findEnclosingBlockAndStatementIndex`, `isNewSqlConstructor`, `isUnsafeCall`, `isUnsafeCallNode` (alias), `hasMarkerCommentNear`, `asPgLeftoverCall`, `propertyKeyName`, `nodeContainsPgFormatPlaceholder`, `asNamedFunctionDecl`, `asPgFormatLikeQueryProp`, `hasPgQuerySignature`, `nodeContainsUnsafeCall`, `extractInListVarNameFromExpr`, `collectInListGuardViolationsFromTemplate`, `getStringLiteralValue`, `isRequireOfModule`, `listenNotifyFromCallExpression`, `listenNotifyFromTaggedTemplate`, `sqlStringStartsWithListenNotify`, `kindFromListenNotifyMatch`.
66
+
67
+ ## Функції
68
+
69
+ ### Експортовані сканери
70
+
71
+ #### `findBunSqlPerRequestConnectionInText(content, virtualPath = 'scan.ts')`
72
+
73
+ - **Параметри**: `content` — вихідний код; `virtualPath` — шлях для вибору `lang` парсером.
74
+ - **Повертає**: `Array<{ line: number, snippet: string }>`.
75
+ - **Що робить**: парсить програму через `parseProgramOrNull`. Якщо парс невдалий — повертає `[]`. Інакше обходить AST через `walkAstWithAncestors`. Для кожного `NewExpression` із callee-Identifier `SQL` перевіряє, чи знаходиться вузол усередині функції (через `ancestors.some(isFunctionNode)`). Якщо так — додає `{ line, snippet }` у результат.
76
+ - **Side effects**: жодних (чиста функція, повертає новий масив).
77
+ - **Контракт парсера**: ця функція **не** перевіряє, чи є у файлі `import { sql\|SQL } from 'bun'` — її викликає правило, яке вже встановило цю передумову.
78
+
79
+ #### `findBunSqlUnsafeUseWithoutAllowMarkerInText(content, virtualPath = 'scan.ts')`
80
+
81
+ - **Параметри**: вихідний код і опційний `virtualPath`.
82
+ - **Повертає**: `Array<{ line: number, snippet: string }>`.
83
+ - **Що робить**: парсить програму **разом із коментарями** через `parseProgramAndCommentsOrNull`. Для кожного виклику `<obj>.unsafe(...)` (визначається `isUnsafeCall`) перевіряє наявність маркера `// allow-unsafe: <reason>` поруч (через `hasMarkerCommentNear` + `ALLOW_UNSAFE_MARKER_RE`). Якщо маркера немає — додає у результат.
84
+ - **Семантика**: `<obj>` — будь-який об'єкт (`sql.unsafe`, `tx.unsafe`, `db.unsafe` тощо). Розрізняти імена не треба, бо файл сканується лише при наявності Bun SQL-імпорту (це передумова правила; у самій функції не перевіряється).
85
+ - **Side effects**: немає.
86
+
87
+ #### `findBunSqlUnsafeWithInterpolatedTemplateInText(content, virtualPath = 'scan.ts')`
88
+
89
+ - **Параметри**: вихідний код, `virtualPath`.
90
+ - **Повертає**: `Array<{ line: number, snippet: string }>`.
91
+ - **Що робить**: знаходить виклики `<obj>.unsafe(...)`, де перший аргумент — `TemplateLiteral` із непорожнім масивом `expressions`. Тобто `sql.unsafe(\`... ${x} ...\`)`. Цей патерн прапорує **навіть з маркером `// allow-unsafe`**, бо template-interpolation у `unsafe` — структурна injection-поверхня (не екранує identifier'ів і не біндить значення).
92
+ - **Що НЕ прапорує**: статичні рядки (`Literal`, `StringLiteral`, `TemplateLiteral` без `${...}`), виклики з готовим текстом-змінною. Для них діє основна перевірка `findBunSqlUnsafeUseWithoutAllowMarkerInText`.
93
+ - **Side effects**: немає.
94
+
95
+ #### `findBunSqlPgLeftoverCallInText(content, virtualPath = 'scan.ts')`
96
+
97
+ - **Параметри**: вихідний код, `virtualPath`.
98
+ - **Повертає**: `Array<{ line: number, snippet: string, methodName: 'connect' \| 'end' }>`.
99
+ - **Що робить**: спочатку перевіряє через `textHasBunSqlImport(content)` — якщо у файлі **немає** імпорту Bun SQL, повертає `[]` (скоп навмисно вузький: метод-імена `connect`/`end` занадто загальні — є у WebSocket, Stream, інших API). Інакше парсить програму з коментарями. Для кожного `<obj>.connect(...)` / `<obj>.end(...)` (через `asPgLeftoverCall`) перевіряє маркер `// allow-pg-leftover: <reason>`. Якщо маркера немає — додає у результат із `methodName`.
100
+ - **Side effects**: немає.
101
+
102
+ #### `findUnsafeBunSqlDynamicSqlListInText(content, virtualPath = 'scan.ts')`
103
+
104
+ - **Параметри**: вихідний код, `virtualPath`.
105
+ - **Повертає**: `Array<{ line: number, snippet: string }>`.
106
+ - **Що робить**: знаходить `TemplateLiteral` (як standalone, так і всередині `TaggedTemplateExpression`), де контекст — `IN (...)` / `VALUES (...)` (`isSqlListContextTemplate`) і серед expressions є виклик `.join(...)` (`isJoinCall`). Це означає, що у запит потрапляє конкатенований SQL замість параметризованих значень — треба використати `sql([...])`-helper.
107
+ - **Side effects**: немає.
108
+
109
+ #### `findUnsafeBunSqlInListMissingEmptyGuardInText(content, virtualPath = 'scan.ts')`
110
+
111
+ - **Параметри**: вихідний код, `virtualPath`.
112
+ - **Повертає**: `Array<{ line: number, snippet: string, reason: 'not_var' \| 'sql_helper_not_var' \| 'missing_guard', name? }>`.
113
+ - **Що робить**: знаходить SQL-list контексти (`IN (`/`IN`/`VALUES`-quasi) і обробляє кожен такий template через `collectInListGuardViolationsFromTemplate`. Якщо expression у `${...}` після `IN ` / `IN (` — це не Identifier (наприклад, виклик або складний вираз) — повертає `reason: 'not_var'`. Якщо це `sql(<non-Identifier>)` — `reason: 'sql_helper_not_var'`. Якщо це Identifier, але перед запитом у поточному `BlockStatement` немає guard'а `if (!ids.length) throw ...` / `if (ids.length === 0) throw ...` — `reason: 'missing_guard'` із `name`.
114
+ - **Side effects**: немає.
115
+
116
+ #### `findPgFormatShimDefinitionInText(content, virtualPath = 'scan.ts')`
117
+
118
+ - **Параметри**: вихідний код, `virtualPath`.
119
+ - **Повертає**: `Array<{ line: number, snippet: string, kind: 'format_function' \| 'quote_helper', name: string }>`.
120
+ - **Що робить**: pre-filter: `textHasBunSqlImport(content)` — якщо у файлі немає Bun SQL, повертає `[]`. Інакше парсить програму та шукає визначення функцій верхнього рівня (`FunctionDeclaration` або `VariableDeclarator` з `ArrowFunctionExpression`/`FunctionExpression`). Для кожного:
121
+ - якщо ім'я ∈ `QUOTE_HELPER_NAMES` (`quoteLiteral`, `quoteIdent`, `escapeLiteral`, `escapeIdent`) — прапорує **незалежно від тіла** як `'quote_helper'`;
122
+ - якщо ім'я ∈ `PG_FORMAT_SHIM_FUNC_NAMES` (`format`, `pgFormat`, `sqlFormat`, `pgFmt`) **і** тіло містить літерал з `%L\|%I\|%s` (`nodeContainsPgFormatPlaceholder`) — прапорує як `'format_function'`.
123
+ - **Snippet**: обмежений до 240 символів від `node.start`.
124
+ - **Side effects**: немає.
125
+
126
+ #### `findPgFormatLikeQueryWrapperInText(content, virtualPath = 'scan.ts')`
127
+
128
+ - **Параметри**: вихідний код, `virtualPath`.
129
+ - **Повертає**: `Array<{ line: number, snippet: string }>`.
130
+ - **Що робить**: pre-filter `textHasBunSqlImport`. Далі обходить AST, шукає `ObjectExpression`, у яких є властивість `query` з функцією-значенням, що:
131
+ - має 1–2 параметри (`hasPgQuerySignature`);
132
+ - перший параметр — Identifier з типовим pg-іменем (`text`/`sql`/`query`);
133
+ - у тілі функції є виклик `<obj>.unsafe(...)` (`nodeContainsUnsafeCall`).
134
+ - **Призначення**: знаходить «маскування» `unsafe` під безпечним ім'ям `query(text, params)` у pg-сумісній обгортці.
135
+ - **Side effects**: немає.
136
+
137
+ #### `findPgLibImportInText(content, virtualPath = 'scan.ts')`
138
+
139
+ - **Параметри**: вихідний код, `virtualPath`.
140
+ - **Повертає**: `Array<{ line: number, snippet: string }>`.
141
+ - **Що робить**: парсить програму, шукає `ImportDeclaration` із `source.value === 'pg'` або `CallExpression` виду `require('pg')`. Точне співпадіння — `pg-format`/`pg-pool`/`@types/pg` не матчаться.
142
+ - **Side effects**: немає.
143
+
144
+ #### `findPgListenNotifyUsageInText(content, virtualPath = 'scan.ts')`
145
+
146
+ - **Параметри**: вихідний код, `virtualPath`.
147
+ - **Повертає**: `Array<{ line: number, snippet: string, kind: 'listen_sql' \| 'notify_sql' \| 'unlisten_sql' \| 'notification_listener' }>`.
148
+ - **Що робить**: обходить AST. Для кожного вузла пробує дві сигнатури:
149
+ - `listenNotifyFromCallExpression`: `<obj>.query(...)` / `.queryArray(...)` / `.queryStream(...)` з першим аргументом — string literal або template literal, що починається з `LISTEN`/`UNLISTEN`/`NOTIFY`; також `<obj>.on('notification', fn)`.
150
+ - `listenNotifyFromTaggedTemplate`: TaggedTemplateExpression, де перший quasi починається з `LISTEN`/`UNLISTEN`/`NOTIFY`.
151
+ - **Призначення**: легітимна потреба у клієнті `pg` — Bun SQL не має LISTEN/NOTIFY. Сигнал для правила «можна виключити pg із заборони у цьому файлі».
152
+ - **Side effects**: немає.
153
+
154
+ ### Експортовані предикати
155
+
156
+ #### `textHasBunSqlImport(content)`
157
+
158
+ - Сигнатура: `(string) => boolean`.
159
+ - Текстова перевірка `BUN_SQL_IMPORT_RE`. Без AST — для дешевого pre-filter'у при зборі ознак авто-детекту правил.
160
+
161
+ #### `textHasPgLibImport(content)`
162
+
163
+ - Сигнатура: `(string) => boolean`.
164
+ - Текстова перевірка `PG_LIB_IMPORT_RE`. Швидкий pre-filter; точне місце імпорту — через `findPgLibImportInText`.
165
+
166
+ #### `isBunSqlScanSourceFile(relativePathPosix)`
167
+
168
+ - Сигнатура: `(string) => boolean`.
169
+ - Перевіряє розширення за `SOURCE_FILE_RE` (`.js`, `.cjs`, `.mjs`, `.jsx`, `.ts`, `.cts`, `.mts`, `.tsx`) і відкидає `.d.ts`-файли.
170
+
171
+ ### Внутрішні допоміжні функції (не експортуються)
172
+
173
+ | Функція | Призначення |
174
+ | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
175
+ | `isLengthMember(node, name)` | `MemberExpression` виду `<name>.length`. |
176
+ | `isZeroNumberLiteral(node)` | Числовий літерал `0` (`NumericLiteral` або `Literal`). |
177
+ | `isSqlHelperIdentifier(node)` | Identifier з ім'ям `sql`. |
178
+ | `extractInListVarNameFromExpr(expr)` | З `${ids}` → `{ name: 'ids' }`; з `${sql(ids)}` → `{ name: 'ids' }`; інакше — `{ error: 'not_var' \| 'sql_helper_not_var' }`. |
179
+ | `isEmptyListTest(test, name)` | Чи це `!ids.length` / `ids.length === 0` / `<= 0` / `< 1` (і дзеркальні форми). |
180
+ | `consequentHasThrow(consequent)` | Чи `consequent` (statement або `BlockStatement`) містить `ThrowStatement`. |
181
+ | `hasEmptyGuardBefore(block, statementIndex, name)` | Чи у `block.body` до індексу `statementIndex` є `IfStatement` з `isEmptyListTest` + `consequentHasThrow`. |
182
+ | `findEnclosingBlockAndStatementIndex(ancestors)` | Шукає найближчий `BlockStatement` і індекс statement у ньому. |
183
+ | `isNewSqlConstructor(node)` | `NewExpression` з callee-Identifier `SQL`. |
184
+ | `isUnsafeCall(node)` / `isUnsafeCallNode` (alias) | `CallExpression` з `callee` = `MemberExpression` `<obj>.unsafe` (не computed). |
185
+ | `hasMarkerCommentNear(callNode, comments, content, markerRe)` | Чи є маркер-коментар, що матчиться на `markerRe`, на тому ж рядку, що `callNode.start`, або рядком вище. Block-коментарі: важливим є `endLine`. |
186
+ | `asPgLeftoverCall(node)` | Якщо це `<obj>.connect(...)` або `<obj>.end(...)` — повертає `{ name }`; інакше `null`. |
187
+ | `propertyKeyName(key)` | Витягає ім'я з `Property.key` (`Identifier.name` або `Literal.value` для string/number). |
188
+ | `nodeContainsPgFormatPlaceholder(root)` | Чи у піддереві є літерал/regex/template з `%L`/`%I`/`%s`. |
189
+ | `asNamedFunctionDecl(node)` | З `FunctionDeclaration` або `VariableDeclarator` з функцією-init — повертає `{ name, body }`. |
190
+ | `asPgFormatLikeQueryProp(prop)` | Чи це `{ query(text, params) { ... unsafe ... } }`-Property. |
191
+ | `hasPgQuerySignature(params)` | 1–2 параметри, перший — Identifier з ім'ям `text`/`sql`/`query`. |
192
+ | `nodeContainsUnsafeCall(root)` | Чи у піддереві є `<obj>.unsafe(...)`. |
193
+ | `collectInListGuardViolationsFromTemplate(template, ancestors, content, out)` | Збирає порушення для одного template'а у контексті `IN (...)`. **Side effect**: пушить у переданий буфер `out`. |
194
+ | `getStringLiteralValue(node)` | `Literal`/`StringLiteral`-значення (string) або `null`. |
195
+ | `isRequireOfModule(node, moduleName)` | Чи `CallExpression` — це `require('<moduleName>')` (точне співпадіння). |
196
+ | `listenNotifyFromCallExpression(node)` | Для `<obj>.query/.queryArray/.queryStream(...)` з LISTEN/NOTIFY-рядком — повертає kind; для `<obj>.on('notification', ...)` — `'notification_listener'`. |
197
+ | `listenNotifyFromTaggedTemplate(node)` | Для TaggedTemplateExpression, де перший quasi починається з LISTEN/UNLISTEN/NOTIFY — повертає kind. |
198
+ | `sqlStringStartsWithListenNotify(arg)` | Аналіз першого аргумента `.query(...)`: string literal або template literal → kind. |
199
+ | `kindFromListenNotifyMatch(text)` | Текст → `'listen_sql'`/`'notify_sql'`/`'unlisten_sql'` або `null` (UNLISTEN мапиться у `unlisten_sql`). |
200
+
201
+ ## Залежності
202
+
203
+ ### Внутрішні (з `../../../scripts/utils/ast-scan-utils.mjs`)
204
+
205
+ Усі named imports:
206
+
207
+ - `isFunctionNode(node)` — чи вузол — функція (FunctionDeclaration / FunctionExpression / ArrowFunctionExpression / MethodDefinition тощо).
208
+ - `isJoinCall(expr)` — чи вираз — виклик `<obj>.join(...)`.
209
+ - `isSqlListContextTemplate(template)` — чи `TemplateLiteral` має `IN (...)` / `VALUES (...)` контекст у quasi.
210
+ - `normalizeSnippet(s)` — нормалізує snippet (стискає пробіли тощо).
211
+ - `offsetToLine(content, offset)` — переводить байтовий offset у номер рядка (1-індекс).
212
+ - `parseProgramAndCommentsOrNull(content, virtualPath)` — парсить через oxc-parser, повертає `{ program, comments }` або `null` при помилці.
213
+ - `parseProgramOrNull(content, virtualPath)` — парсить програму без коментарів, повертає `program` або `null`.
214
+ - `templateQuasisText(template)` — обʼєднує текст quasis у один рядок (без expressions).
215
+ - `walkAstWithAncestors(root, ancestors, visitor)` — обхід AST із передачею стека ancestors у visitor.
216
+
217
+ ### Зовнішні
218
+
219
+ Прямих імпортів зовнішніх npm-пакетів немає. Опосередковано через `ast-scan-utils.mjs` використовується **oxc-parser**.
220
+
221
+ ### Глобальні / runtime
222
+
223
+ - Стандартні JS-API: `RegExp`, `Set`, `Array`, `String.prototype.slice`/`Math.min`/`Array.prototype.entries`/`Array.prototype.indexOf` тощо.
224
+
225
+ ## Потік виконання / Використання
226
+
227
+ ### Загальна модель
228
+
229
+ Файл — це **бібліотека функцій без побічних ефектів**. Сам по собі модуль не виконує сканування при імпорті: тільки експортує функції-фабрики результатів. Викликаються вони з check-функцій правил (`check-*.mjs`) у `npm/rules/js-bun-db/`.
230
+
231
+ ### Типовий потік виклику з правила
232
+
233
+ 1. Правило отримує список файлів проекту.
234
+ 2. Для кожного файлу фільтрує через `isBunSqlScanSourceFile(relativePathPosix)` — лише JS/TS-файли, без `.d.ts`.
235
+ 3. Для дешевого pre-filter'у читає вміст і викликає `textHasBunSqlImport(content)`. Якщо `false` — пропускає файл (для скан-функцій, що працюють лише у Bun SQL-файлах).
236
+ 4. Викликає одну з функцій-сканерів `find...InText(content, relativePath)`. Отримує масив порушень із `line`/`snippet`/інколи додатковими полями (`reason`, `name`, `kind`, `methodName`).
237
+ 5. Перетворює порушення на `fail`-повідомлення у форматі правила.
238
+
239
+ ### Загальний контракт парсера
240
+
241
+ - Якщо `parseProgramOrNull` / `parseProgramAndCommentsOrNull` повертає `null` (синтаксична помилка) — сканер повертає `[]`. **Це не еквівалентно «жодних порушень»**: правило має іншим механізмом (інші лінтери) переконатися, що файл взагалі парситься.
242
+ - `virtualPath` (за замовчуванням `'scan.ts'`) передається парсеру для вибору мови (`lang` — `ts`/`tsx`/`js`/`jsx`/`cjs`/`mjs` тощо за розширенням). За замовчуванням `'scan.ts'` — щоб TypeScript-конструкції не ламали парс при не-TS-файлах із TS-подібним кодом.
243
+
244
+ ### Опт-ін маркери коментарів (формат і правила позиціювання)
245
+
246
+ Дозволені маркери:
247
+
248
+ - `// allow-unsafe: <reason>` — для `<obj>.unsafe(...)`.
249
+ - `// allow-pg-leftover: <reason>` — для `<obj>.connect(...)` / `<obj>.end(...)`.
250
+
251
+ Правила позиціювання (через `hasMarkerCommentNear`):
252
+
253
+ - Маркер дійсний, якщо коментар закінчується на тому ж рядку, що й виклик (`trailing comment`), **або** на рядку, що безпосередньо передує виклику.
254
+ - Між коментарем і викликом **не** допускається порожній рядок (відірваний коментар не зараховується).
255
+ - `<reason>` має бути непорожнім (хоча б один не-пробільний символ після `:`) — інакше маркер не приймається.
256
+ - Допускається як `Line`-коментар (`// ...`), так і `Block`-коментар (`/* ... */`); для блокового важлива саме `endLine` (бо block може займати кілька рядків).
257
+
258
+ ### Послідовність ухвалення рішення для деяких сканерів
259
+
260
+ **`findBunSqlUnsafeWithInterpolatedTemplateInText`** працює **незалежно** від `findBunSqlUnsafeUseWithoutAllowMarkerInText` — interpolated template у `unsafe` прапорується навіть із `// allow-unsafe`. Це навмисно: маркер дозволяє статичний `unsafe`-рядок, але не виправдовує template-interpolation.
261
+
262
+ **`findUnsafeBunSqlInListMissingEmptyGuardInText`** і **`findUnsafeBunSqlDynamicSqlListInText`** покривають дотичні, але різні випадки `IN (...)`:
263
+
264
+ - `join`-конкатенація у будь-якому контексті списку (`IN`/`VALUES`) → `findUnsafeBunSqlDynamicSqlListInText`.
265
+ - `IN (${vars})` без guard'у → `findUnsafeBunSqlInListMissingEmptyGuardInText`.
266
+
267
+ ### Приклади для розуміння (концептуально)
268
+
269
+ 1. **`new SQL(...)` per-request** — прапорує:
270
+
271
+ ```js
272
+ import { SQL } from 'bun'
273
+ export async function handler(req) {
274
+ const sql = new SQL(process.env.DATABASE_URL) // <-- порушення
275
+ return sql`select 1`
276
+ }
277
+ ```
278
+
279
+ 2. **`unsafe` без маркера** — прапорує:
280
+
281
+ ```js
282
+ import { sql } from 'bun'
283
+ const text = `select * from ${tableName}`
284
+ sql.unsafe(text) // <-- порушення: немає // allow-unsafe: <reason>
285
+ ```
286
+
287
+ 3. **`unsafe` з interpolated template** — прапорує навіть з маркером:
288
+
289
+ ```js
290
+ sql.unsafe(`select * from ${tableName}`) // allow-unsafe: dynamic-table
291
+ // ^-- прапорується сканером findBunSqlUnsafeWithInterpolatedTemplateInText
292
+ ```
293
+
294
+ 4. **`IN (${ids})` без guard'у** — прапорує:
295
+
296
+ ```js
297
+ const ids = req.body.ids
298
+ await sql`select * from t where id in (${ids})` // <-- reason: 'missing_guard'
299
+ ```
300
+
301
+ Виправлення — додати guard:
302
+
303
+ ```js
304
+ if (!ids.length) throw new Error('empty ids')
305
+ await sql`select * from t where id in (${sql(ids)})`
306
+ ```
307
+
308
+ 5. **pg-leftover у Bun SQL-файлі** — прапорує:
309
+
310
+ ```js
311
+ import { sql } from 'bun'
312
+ await sql.end() // <-- порушення: немає // allow-pg-leftover: <reason>
313
+ ```
314
+
315
+ ### Інваріанти
316
+
317
+ - Усі експортовані функції **детерміновані**: для одного й того ж `content` і `virtualPath` повертають однаковий результат.
318
+ - Функції **не модифікують глобальний стан** і не мають кешу — кешування (якщо потрібне) робиться на рівні викликача (правила).
319
+ - Знахідки повертаються у порядку обходу AST (від `walkAstWithAncestors`); `line` — 1-індексований.
320
+ - Snippet у `findPgFormatShimDefinitionInText` обмежений до 240 символів; у решті — це повний текст знайденого вузла.
321
+
322
+ ## Rebuild Test
323
+
324
+ Контрольний перелік, що дає змогу відновити інваріанти та логіку модуля «з нуля» лише з документації:
325
+
326
+ 1. **Список експортів** (10 функцій):
327
+ - сканери: `findBunSqlPerRequestConnectionInText`, `findBunSqlUnsafeUseWithoutAllowMarkerInText`, `findBunSqlUnsafeWithInterpolatedTemplateInText`, `findBunSqlPgLeftoverCallInText`, `findUnsafeBunSqlDynamicSqlListInText`, `findUnsafeBunSqlInListMissingEmptyGuardInText`, `findPgFormatShimDefinitionInText`, `findPgFormatLikeQueryWrapperInText`, `findPgLibImportInText`, `findPgListenNotifyUsageInText`;
328
+ - предикати: `textHasBunSqlImport`, `textHasPgLibImport`, `isBunSqlScanSourceFile`.
329
+ 2. **Контракт парс-помилок**: будь-який сканер, що отримав `null` від `parseProgramOrNull`/`parseProgramAndCommentsOrNull`, повертає `[]`.
330
+ 3. **Опт-ін маркери**: `// allow-unsafe: <reason>` (для `unsafe`), `// allow-pg-leftover: <reason>` (для `connect`/`end`). Дозволені позиції — той самий рядок (trailing) або безпосередньо попередній рядок. Причина обов'язкова.
331
+ 4. **Скоп Bun SQL-залежних сканерів**: `findBunSqlPgLeftoverCallInText`, `findPgFormatShimDefinitionInText`, `findPgFormatLikeQueryWrapperInText` спочатку перевіряють `textHasBunSqlImport(content)` і повертають `[]`, якщо `false`.
332
+ 5. **Структура повернень**: усі сканери повертають масиви об'єктів із як мінімум `{ line: number, snippet: string }`; деякі додають `reason` / `name` / `kind` / `methodName`.
333
+ 6. **AST-семантика, не regex**: усі сканери використовують `walkAstWithAncestors` і AST-предикати; regex використовуються лише для текстових pre-filter'ів і коротких рядкових патернів.
334
+ 7. **Розширення для скану**: `.js`, `.cjs`, `.mjs`, `.jsx`, `.ts`, `.cts`, `.mts`, `.tsx`; `.d.ts` — виключено.
335
+ 8. **`IN (...)`/`VALUES (...)` guard-логіка**:
336
+ - `not_var` — у `${...}` стоїть не Identifier;
337
+ - `sql_helper_not_var` — у `${sql(<x>)}` `x` — не Identifier;
338
+ - `missing_guard` — Identifier є, але перед запитом у тому ж `BlockStatement` немає `if (!ids.length) throw` / `if (ids.length === 0/<=0/<1) throw` (включно з дзеркальними `0 === ids.length` для `==`/`===`).
339
+ 9. **pg-format-шими**: іменам `quoteLiteral`/`quoteIdent`/`escapeLiteral`/`escapeIdent` достатньо самого імені; іменам `format`/`pgFormat`/`sqlFormat`/`pgFmt` — додатково потрібен `%L`/`%I`/`%s` десь у тілі (літерал/regex/template).
340
+ 10. **pg-сумісний query-wrapper**: `Property` з `key === 'query'` у `ObjectExpression`, з функцією-значенням, що має 1–2 параметри (перший — Identifier `text`/`sql`/`query`) і викликає `<obj>.unsafe(...)` десь у тілі.
341
+ 11. **LISTEN/NOTIFY-сигнали**: `<obj>.query/.queryArray/.queryStream(...)` з першим аргументом — string/template literal, що починається з `LISTEN`/`UNLISTEN`/`NOTIFY` (case-insensitive); `<obj>.on('notification', ...)`; TaggedTemplateExpression з тим самим початком у першому quasi.
342
+ 12. **pg-імпорт**: `ImportDeclaration` з `source.value === 'pg'` або `CallExpression` `require('pg')` — точне співпадіння; `pg-format`/`pg-pool` не матчаться.
343
+ 13. **`new SQL(...)` per-request**: `NewExpression` із callee-Identifier `SQL` і ancestors, серед яких є функція.
344
+ 14. **Snippet limit у `findPgFormatShimDefinitionInText`**: 240 символів від `node.start` (через `Math.min(node.end, node.start + 240)`); решта сканерів використовують `content.slice(start, end)`.
345
+ 15. **Determinism і відсутність side effects**: усі експортовані функції — чисті; локальний `collectInListGuardViolationsFromTemplate` має side effect лише на переданий буфер `out`.
346
+
347
+ Якщо всі ці пункти відтворені в коді — сканер семантично еквівалентний оригіналу.
@@ -0,0 +1,123 @@
1
+ # fix.mjs — правило `js-bun-redis`
2
+
3
+ ## Огляд
4
+
5
+ Файл `npm/rules/js-bun-redis/fix.mjs` — це тонкий **entry-point** правила `js-bun-redis` у системі `@nitra/cursor`. Він не містить власної бізнес-логіки перевірки/виправлення: уся робота (resolve `applies`, JS-concerns, policy, mdc-refs) делегується утиліті `runStandardRule`, а CLI-обгортка — `runRuleCli`.
6
+
7
+ Файл реалізує **дві ролі одночасно**:
8
+
9
+ 1. **Library mode** — інші модулі імпортують функцію `run(ctx)` і запускають правило в межах загального оркестрування (наприклад, `npx @nitra/cursor fix` обходить набір правил, передаючи спільний `walkCache` через `ctx`).
10
+ 2. **Standalone mode** — пряме виконання через `bun npm/rules/js-bun-redis/fix.mjs`. У цьому випадку модуль самостійно завантажує конфіг, застосовує whitelist і друкує summary, повертаючи коректний exit-code для CI/IDE.
11
+
12
+ Правило `js-bun-redis` належить до родини "standard rules" і параметризується тим, що `runStandardRule` отримує `import.meta.dirname` поточної теки — далі допоміжна бібліотека сама читає `meta.json`, `applies.mjs`, `check-*.mjs` тощо, які знаходяться поруч у каталозі `npm/rules/js-bun-redis/`.
13
+
14
+ ## Експорти / API
15
+
16
+ | Експорт | Тип | Опис |
17
+ | ------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
18
+ | `run` | `function (ctx?) => Promise<number>` | Іменований експорт. Запускає правило в library-режимі. Сумісний із загальним runner-ом, який очікує сигнатуру `run(ctx)`. |
19
+
20
+ Default-експорту немає. Side-effect-ний блок наприкінці файлу не експортується — він активний лише при прямому виконанні модуля як CLI.
21
+
22
+ ## Функції
23
+
24
+ ### `run(ctx)`
25
+
26
+ ```js
27
+ export function run(ctx)
28
+ ```
29
+
30
+ - **Сигнатура:** `run(ctx?: RuleContext): Promise<number>`
31
+ - **Параметри:**
32
+ - `ctx` (необовʼязковий) — обʼєкт контексту прогону правила (`RuleContext`, тип реекспортовано з `../../scripts/lib/run-standard-rule.mjs`). Типово містить кешовані результати обходу файлової системи (`walkCache`) та інші спільні структури, які передаються між правилами під час оркестрованого прогону. Якщо викликати без аргументу — `runStandardRule` створить локальний контекст самостійно.
33
+ - **Повертає:** `Promise<number>` — exit-код:
34
+ - `0` — правило пройшло без порушень,
35
+ - `1` — знайдено порушення (`runStandardRule` сам формує summary з деталями знайдених проблем у stdout/stderr).
36
+ - **Side effects:**
37
+ - Читання файлів проєкту, які матчаться `applies.mjs` цього правила.
38
+ - Логування результатів (summary) у stdout залежить від внутрішньої поведінки `runStandardRule`.
39
+ - Безпосередньо у `fix.mjs` ніяких глобальних мутацій, мережевих викликів чи запису у ФС немає — все інкапсульовано в делегованих бібліотеках.
40
+
41
+ ### CLI-блок (top-level, без імені)
42
+
43
+ ```js
44
+ if (isRunAsCli(import.meta.url)) {
45
+ process.exit(await runRuleCli(import.meta.dirname))
46
+ }
47
+ ```
48
+
49
+ - **Умова:** `isRunAsCli(import.meta.url)` повертає `true`, коли цей файл є entry-point поточного Node/Bun-процесу (а не імпортується з іншого модуля).
50
+ - **Дія:** виконує `await runRuleCli(import.meta.dirname)` — повний еквівалент `npx @nitra/cursor fix <id>`, включно з:
51
+ - завантаженням конфігурації,
52
+ - застосуванням whitelist,
53
+ - друком підсумкового summary.
54
+ - **Завершення процесу:** результат (числовий exit-code) передається у `process.exit(...)`, що зупиняє процес із потрібним статусом для CI / IDE-інтеграції.
55
+ - **ESLint-винятки:**
56
+ - `n/no-process-exit` та `unicorn/no-process-exit` локально відключено через те, що standalone-точка входу зобовʼязана повертати числовий exit-code (інакше CI не побачить статус правила).
57
+
58
+ ## Залежності
59
+
60
+ ### Внутрішні модулі
61
+
62
+ - `../../scripts/lib/run-rule-cli.mjs` — реекспортує:
63
+ - `isRunAsCli(metaUrl)` — детектор того, чи виконується модуль як CLI entry-point.
64
+ - `runRuleCli(dirname)` — повна CLI-обгортка для одного правила.
65
+ - `../../scripts/lib/run-standard-rule.mjs` — реекспортує:
66
+ - `runStandardRule(dirname, ctx?)` — стандартний пайплайн для "звичайного" правила: `applies` → JS-concerns → policy → mdc-refs.
67
+ - JSDoc-тип `RuleContext` (використовується лише в анотації параметра `ctx`).
68
+
69
+ ### Сусідні артефакти правила (читаються опосередковано через `runStandardRule` / `runRuleCli`)
70
+
71
+ - `npm/rules/js-bun-redis/meta.json` — метаінформація правила (id, severity, applies-патерни, посилання на `.mdc` тощо).
72
+ - `npm/rules/js-bun-redis/applies.mjs` — фільтр файлів, до яких застосовується правило.
73
+ - `npm/rules/js-bun-redis/check-*.mjs` — конкретні перевірки JS-concerns.
74
+ - `npm/rules/js-bun-redis/policy.mjs` (за наявності) — policy-шар.
75
+ - Звʼязаний `.mdc` у каталозі `mdc/` — людинозрозумілий опис правила (`mdc-refs`-перевірка переконується, що посилання збігаються).
76
+
77
+ ### Зовнішні залежності
78
+
79
+ Прямих імпортів npm-пакетів у файлі немає. Усі сторонні залежності (наприклад, `fs`, `path` тощо) приходять транзитивно через `runStandardRule` / `runRuleCli`.
80
+
81
+ ### Глобалі та середовище виконання
82
+
83
+ - `import.meta.dirname` — абсолютний шлях до теки `npm/rules/js-bun-redis/`. Використовується як ідентифікатор правила (`runStandardRule` із `dirname` зчитує `meta.json` та інші файли поруч).
84
+ - `import.meta.url` — URL поточного модуля. Передається в `isRunAsCli` для детекту режиму запуску.
85
+ - `process.exit(code)` — глобальний Node/Bun API. Використовується **виключно** у standalone-режимі.
86
+ - Підтримка top-level `await` — обовʼязкова; код розрахований на ESM-runtime (Node ≥ 14.8 з ESM або Bun).
87
+
88
+ ## Потік виконання / Використання
89
+
90
+ ### Сценарій 1. Library mode (виклик з оркестратора)
91
+
92
+ 1. Зовнішній runner (`npx @nitra/cursor fix`) імпортує `run` з цього файлу.
93
+ 2. Передає підготовлений `ctx` (зазвичай зі спільним `walkCache`, аби не повторювати обхід файлової системи між правилами).
94
+ 3. `run(ctx)` повертає `runStandardRule(import.meta.dirname, ctx)`.
95
+ 4. `runStandardRule`:
96
+ - читає `meta.json` поточної теки правила,
97
+ - виконує `applies` → JS-concerns → policy → mdc-refs (стандартний пайплайн "стандартного" правила),
98
+ - повертає `0`/`1`.
99
+ 5. Runner агрегує exit-коди всіх правил і вирішує загальний статус.
100
+
101
+ ### Сценарій 2. Standalone mode
102
+
103
+ 1. Користувач/CI запускає:
104
+ ```bash
105
+ bun npm/rules/js-bun-redis/fix.mjs
106
+ ```
107
+ 2. Module-evaluation сягає блоку `if (isRunAsCli(import.meta.url))` — умова `true`.
108
+ 3. Виконується `await runRuleCli(import.meta.dirname)`:
109
+ - завантаження конфігу,
110
+ - whitelist,
111
+ - повний CLI-пайплайн (включно з summary),
112
+ - повертає числовий exit-code.
113
+ 4. `process.exit(<code>)` завершує процес із цим кодом — CI/IDE бачить fail (`1`) або success (`0`).
114
+
115
+ ### Чому два режими в одному файлі
116
+
117
+ Це уніфікована конвенція системи правил `@nitra/cursor`: кожен `fix.mjs` є **і** імпортованим модулем (для оркестрованого прогону), **і** виконуваним скриптом (для локальної відладки конкретного правила). Завдяки `isRunAsCli` обидва шляхи безпечно співіснують: при імпорті side-effect-блок не спрацьовує.
118
+
119
+ ### Типові команди
120
+
121
+ - Прогнати лише це правило локально: `bun npm/rules/js-bun-redis/fix.mjs`.
122
+ - Прогнати в межах усього набору правил: `npx @nitra/cursor fix` (runner викличе саме `run(ctx)`).
123
+ - Прогнати з конкретним id через CLI: `npx @nitra/cursor fix js-bun-redis` (виконує по суті те саме, що й standalone-режим цього файлу).