@nitra/cursor 3.22.0 → 3.23.1

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 (229) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/AGENTS.template.md +4 -0
  3. package/CHANGELOG.md +37 -3
  4. package/bin/docs/n-cursor.md +636 -0
  5. package/bin/docs/rename-yaml-extensions.md +207 -0
  6. package/bin/n-cursor.js +30 -3
  7. package/package.json +1 -1
  8. package/rules/abie/docs/fix.md +18 -0
  9. package/rules/abie/js/docs/applies.md +26 -0
  10. package/rules/abie/js/docs/env_dns.md +32 -0
  11. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  12. package/rules/abie/js/docs/hc_pairing.md +35 -0
  13. package/rules/abie/js/docs/ua_http_route.md +28 -0
  14. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  15. package/rules/abie/lib/docs/enabled.md +29 -0
  16. package/rules/abie/lib/docs/env-dns.md +35 -0
  17. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  18. package/rules/abie/lib/docs/http-route.md +44 -0
  19. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  20. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  21. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  22. package/rules/abie/lib/docs/yaml.md +29 -0
  23. package/rules/adr/docs/fix.md +148 -0
  24. package/rules/adr/js/docs/hooks.md +259 -0
  25. package/rules/bun/docs/fix.md +156 -0
  26. package/rules/bun/js/docs/layout.md +393 -0
  27. package/rules/capacitor/docs/fix.md +121 -0
  28. package/rules/capacitor/js/docs/platforms.md +295 -0
  29. package/rules/changelog/changelog.mdc +4 -2
  30. package/rules/changelog/docs/fix.md +174 -0
  31. package/rules/changelog/js/consistency.mjs +114 -13
  32. package/rules/changelog/js/docs/consistency.md +387 -0
  33. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  34. package/rules/ci4/docs/fix.md +179 -0
  35. package/rules/ci4/js/docs/marksman_config.md +128 -0
  36. package/rules/docker/docker.mdc +8 -3
  37. package/rules/docker/docs/fix.md +171 -0
  38. package/rules/docker/js/docs/lint.md +258 -0
  39. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  40. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  41. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  42. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  43. package/rules/docker/lint/docs/lint.md +193 -0
  44. package/rules/efes/docs/fix.md +203 -0
  45. package/rules/feedback/docs/fix.md +140 -0
  46. package/rules/flow/docs/fix.md +152 -0
  47. package/rules/ga/docs/fix.md +158 -0
  48. package/rules/ga/js/docs/lint.md +100 -0
  49. package/rules/ga/js/docs/workflows.md +217 -0
  50. package/rules/ga/lint/docs/lint.md +209 -0
  51. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  52. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  53. package/rules/graphql/docs/fix.md +126 -0
  54. package/rules/graphql/js/docs/tooling.md +264 -0
  55. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  56. package/rules/hasura/docs/fix.md +120 -0
  57. package/rules/hasura/hasura.mdc +14 -0
  58. package/rules/hasura/js/docs/internal_urls.md +326 -0
  59. package/rules/image-avif/docs/fix.md +132 -0
  60. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  61. package/rules/image-compress/docs/fix.md +150 -0
  62. package/rules/image-compress/js/docs/package_setup.md +191 -0
  63. package/rules/js-bun-db/docs/fix.md +148 -0
  64. package/rules/js-bun-db/js/docs/safety.md +231 -0
  65. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  66. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  67. package/rules/js-bun-redis/docs/fix.md +123 -0
  68. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  69. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  70. package/rules/js-lint/docs/fix.md +117 -0
  71. package/rules/js-lint/js/docs/lint.md +250 -0
  72. package/rules/js-lint/js/docs/tooling.md +348 -0
  73. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  74. package/rules/js-lint-ci/docs/fix.md +154 -0
  75. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  76. package/rules/js-mssql/docs/fix.md +128 -0
  77. package/rules/js-mssql/js/docs/deps.md +263 -0
  78. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  79. package/rules/js-run/docs/fix.md +144 -0
  80. package/rules/js-run/js/docs/runtime.md +388 -0
  81. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  82. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  83. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  84. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  85. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  86. package/rules/k8s/docs/fix.md +129 -0
  87. package/rules/k8s/js/docs/manifests.md +344 -0
  88. package/rules/k8s/js/manifests.mjs +6 -2
  89. package/rules/k8s/k8s.mdc +4 -2
  90. package/rules/k8s/lint/docs/lint.md +411 -0
  91. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  92. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  93. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  94. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  95. package/rules/npm-module/docs/fix.md +98 -0
  96. package/rules/npm-module/js/docs/package_structure.md +274 -0
  97. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  98. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  99. package/rules/php/docs/fix.md +107 -0
  100. package/rules/php/js/docs/tooling.md +152 -0
  101. package/rules/php/lint/docs/lint.md +215 -0
  102. package/rules/python/docs/fix.md +163 -0
  103. package/rules/python/js/docs/applies.md +108 -0
  104. package/rules/python/js/docs/tooling.md +153 -0
  105. package/rules/python/lint/docs/lint.md +322 -0
  106. package/rules/rego/docs/fix.md +121 -0
  107. package/rules/rego/js/docs/applies.md +174 -0
  108. package/rules/rego/js/docs/lint.md +118 -0
  109. package/rules/rego/lint/docs/lint.md +204 -0
  110. package/rules/release/docs/change.md +185 -0
  111. package/rules/release/docs/fix.md +119 -0
  112. package/rules/release/docs/release.md +222 -0
  113. package/rules/release/lib/docs/aggregate.md +246 -0
  114. package/rules/release/lib/docs/change-file.md +200 -0
  115. package/rules/release/lib/docs/fallback.md +203 -0
  116. package/rules/rust/docs/fix.md +129 -0
  117. package/rules/rust/js/docs/applies.md +140 -0
  118. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  119. package/rules/security/docs/fix.md +86 -0
  120. package/rules/security/js/docs/lint.md +171 -0
  121. package/rules/security/js/docs/sample_secret.md +190 -0
  122. package/rules/security/js/docs/trufflehog.md +137 -0
  123. package/rules/security/js/lint.mjs +9 -1
  124. package/rules/style-lint/docs/fix.md +155 -0
  125. package/rules/style-lint/js/docs/lint.md +184 -0
  126. package/rules/style-lint/js/docs/tooling.md +194 -0
  127. package/rules/tauri/docs/fix.md +158 -0
  128. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  129. package/rules/tauri/js/docs/tooling.md +228 -0
  130. package/rules/test/coverage/coverage.mjs +15 -3
  131. package/rules/test/docs/fix.md +132 -0
  132. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  135. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  136. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  137. package/rules/test/js/docs/location.md +136 -0
  138. package/rules/test/js/docs/no-process-chdir.md +160 -0
  139. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  140. package/rules/test/js/docs/stryker_config.md +152 -0
  141. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  142. package/rules/text/docs/fix.md +118 -0
  143. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  144. package/rules/text/js/docs/formatting.md +256 -0
  145. package/rules/text/js/docs/lint.md +122 -0
  146. package/rules/text/lint/docs/lint.md +220 -0
  147. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  148. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  149. package/rules/text/lint/docs/run-v8r.md +197 -0
  150. package/rules/vue/docs/fix.md +127 -0
  151. package/rules/vue/js/docs/packages.md +335 -0
  152. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  153. package/rules/worktree/docs/fix.md +161 -0
  154. package/schemas/rule-meta.json +5 -1
  155. package/scripts/auto-rules.mjs +7 -4
  156. package/scripts/coverage-classify/docs/apply.md +202 -0
  157. package/scripts/coverage-classify/docs/cache.md +203 -0
  158. package/scripts/coverage-classify/docs/index.md +218 -0
  159. package/scripts/coverage-classify/docs/prompt.md +132 -0
  160. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  161. package/scripts/coverage-fix-extract.mjs +122 -0
  162. package/scripts/coverage-fix.mjs +1 -1
  163. package/scripts/dispatcher/docs/graph.md +346 -0
  164. package/scripts/dispatcher/docs/index.md +236 -0
  165. package/scripts/dispatcher/docs/trace.md +296 -0
  166. package/scripts/dispatcher/index.mjs +1 -1
  167. package/scripts/dispatcher/lib/active.mjs +4 -8
  168. package/scripts/dispatcher/lib/commands.mjs +7 -11
  169. package/scripts/dispatcher/lib/docs/active.md +348 -0
  170. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  171. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  172. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  173. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  174. package/scripts/dispatcher/lib/docs/events.md +182 -0
  175. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  176. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  177. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  178. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  179. package/scripts/dispatcher/lib/docs/level.md +335 -0
  180. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  181. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  182. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  183. package/scripts/dispatcher/lib/docs/review.md +255 -0
  184. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  185. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  186. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  187. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  188. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  189. package/scripts/dispatcher/lib/executor.mjs +6 -1
  190. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  191. package/scripts/dispatcher/lib/level.mjs +29 -3
  192. package/scripts/dispatcher/lib/review.mjs +1 -1
  193. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  194. package/scripts/docs/auto-rules.md +376 -0
  195. package/scripts/docs/auto-skills.md +173 -0
  196. package/scripts/docs/build-agents-commands.md +183 -0
  197. package/scripts/docs/cli-entry.md +153 -0
  198. package/scripts/docs/coverage-fix.md +177 -0
  199. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  200. package/scripts/lib/changed-files.mjs +4 -1
  201. package/scripts/lib/docs/changed-files.md +149 -0
  202. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  203. package/scripts/lib/docs/check-reporter.md +175 -0
  204. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  205. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  206. package/scripts/lib/docs/ensure-tool.md +254 -0
  207. package/scripts/lib/docs/generated-markdown.md +275 -0
  208. package/scripts/lib/docs/gha-workflow.md +326 -0
  209. package/scripts/lib/docs/inline-template-links.md +303 -0
  210. package/scripts/lib/docs/list-rule-ids.md +156 -0
  211. package/scripts/lib/docs/load-cursor-config.md +147 -0
  212. package/scripts/lib/docs/mirror-parity.md +167 -0
  213. package/scripts/lib/worktree.mjs +26 -0
  214. package/scripts/worktree-cli.mjs +12 -2
  215. package/skills/coverage-fix/SKILL.md +34 -45
  216. package/skills/docgen/SKILL.md +44 -23
  217. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  218. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  219. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  220. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  221. package/skills/docgen/js/docgen-scan.mjs +37 -21
  222. package/skills/llm-patch/SKILL.md +23 -2
  223. package/skills/start-check/SKILL.md +26 -53
  224. package/skills/start-check/js/check.mjs +211 -0
  225. package/skills/taze/SKILL.md +9 -3
  226. package/skills/taze/js/diff.mjs +154 -0
  227. package/types/bin/n-cursor.d.ts +1 -1
  228. package/skills/fix-tests/SKILL.md +0 -119
  229. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,433 @@
1
+ # `check-env-scan.mjs` — AST-сканер правила «process.env / CheckEnv»
2
+
3
+ ## Огляд
4
+
5
+ Модуль `npm/rules/js-run/lib/check-env-scan.mjs` — це **статичний AST-сканер**,
6
+ що реалізує перевірку правила `js-run.mdc` для двох контрактів роботи зі
7
+ змінними оточення в JavaScript/TypeScript-коді:
8
+
9
+ 1. **Заборона прямого `process.env.X`.** Будь-який доступ до `process.env.X`
10
+ (через MemberExpression, computed-access чи деструктуризацію) завжди
11
+ реєструється як порушення з підказкою замінити його:
12
+ - на `env` із пакета `@nitra/check-env` (для обов'язкових змінних із
13
+ явним викликом `checkEnv([...])`);
14
+ - на `env` із `node:process` (для опційних).
15
+ 2. **Обов'язкове «закриття» `env.X` викликом `checkEnv(['X', ...])`.** Якщо
16
+ у файл імпортовано саме `env` з `@nitra/check-env`, то кожне використання
17
+ `env.X` (як MemberExpression або через деструктуризацію `const { X } = env`)
18
+ має бути зареєстроване хоча б одним літеральним викликом `checkEnv([...])`
19
+ у тому ж файлі. Порядок викликів і доступів не важливий — всі імена з
20
+ масивів `checkEnv(['A', 'B'])` зливаються в один спільний набір.
21
+
22
+ Точкове приглушення обох контрактів — коментар-маркер на рядку
23
+ **безпосередньо перед** порушенням:
24
+
25
+ ```
26
+ // @nitra/cursor ignore-next-line checkEnv
27
+ ```
28
+
29
+ Сканер працює тільки через AST (`parseProgramOrNull` з `oxc-parser` через
30
+ утиліту `ast-scan-utils.mjs`); по тілу файлу ніяких regex не виконується —
31
+ regex-перевірці підлягає лише сирий рядок із потенційним ignore-коментарем.
32
+
33
+ Якщо файл не парситься (синтаксична помилка), сканер повертає порожній
34
+ результат — спочатку треба полагодити синтаксис, лише потім запускати
35
+ правило.
36
+
37
+ ### Які форми доступу покриті
38
+
39
+ - `process.env.X` — MemberExpression із object=Identifier `process`,
40
+ property=Identifier `env`, потім parent MemberExpression із property=`X`;
41
+ - `process.env['X']` — те саме, але parent MemberExpression із
42
+ `computed: true` і Literal-string-ключем;
43
+ - `const { X, Y } = process.env` — VariableDeclarator, де init це
44
+ `process.env`, а id це ObjectPattern; ім'я береться **з ключа**
45
+ (`property.key`), а не з alias-локального ідентифікатора;
46
+ - `env.X` / `env['X']` / `const { X } = env` — аналогічно, але вузол це
47
+ Identifier `env`, і **тільки** якщо у файлі є
48
+ `import { env } from '@nitra/check-env'`.
49
+
50
+ ### Що ігнорується
51
+
52
+ - Обчислювані ключі (`process.env[varName]`, `env[varName]`): за статичним
53
+ AST неможливо встановити фактичне ім'я ENV, тому такі вирази проходять
54
+ тихо без помилки.
55
+ - `env` з інших джерел (локальна змінна, `node:process` import тощо):
56
+ `hasCheckEnvImport` повертає `false`, і AST-обхід просто не дивиться на
57
+ `env.X` для другого контракту. Перший контракт (`process.env`) при цьому
58
+ все ще діє.
59
+ - Aliased-імпорти `import { env as someName }`: свідомо **не** підтримуються —
60
+ правило вимагає канонічного імені `env`.
61
+ - Не-літеральні елементи всередині `checkEnv([...])` (Identifier, SpreadElement,
62
+ TemplateLiteral): просто пропускаються при збиранні `checkedNames` —
63
+ перевірка «ліберальна» і ловить лише явно неперевірені змінні.
64
+
65
+ ## Експорти / API
66
+
67
+ Модуль експортує дві named-функції:
68
+
69
+ | Експорт | Тип | Призначення |
70
+ | ------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------- |
71
+ | `findUncheckedProcessEnvInText` | `(content: string, virtualPath?: string) => EnvViolation[]` | Сканує текст файлу й повертає список порушень |
72
+ | `isCheckEnvScanSourceFile` | `(relativePathPosix: string) => boolean` | Фільтр придатних до сканування файлів за розширенням |
73
+
74
+ Тип `EnvViolation` (внутрішній, описаний через JSDoc `@typedef`):
75
+
76
+ ```js
77
+ /** @typedef {{
78
+ * line: number,
79
+ * name: string,
80
+ * kind: 'process-env' | 'check-env-missing-checkEnv'
81
+ * }} EnvViolation
82
+ */
83
+ ```
84
+
85
+ - `kind` — тип порушення:
86
+ - `'process-env'` — прямий доступ до `process.env.X`;
87
+ - `'check-env-missing-checkEnv'` — використання `env.X` без літерального
88
+ `checkEnv(['X', ...])` у файлі.
89
+ - `name` — ім'я ENV-змінної (наприклад, `DB_HOST`).
90
+ - `line` — 1-based номер рядка з порушенням (отриманий через `offsetToLine`).
91
+
92
+ Список порушень повертається в порядку обходу AST. У межах одного виклику
93
+ застосовується дедуплікація за ключем `kind|name|line` — повторні зустрічі
94
+ ідентичного «kind+name+line» в результат не потрапляють.
95
+
96
+ ## Функції
97
+
98
+ Нижче — повний перелік функцій модуля (у тому порядку, як вони визначені у файлі).
99
+
100
+ ### `isProcessEnvAccess(node) → boolean`
101
+
102
+ Перевіряє, чи вузол AST є виразом `process.env`.
103
+
104
+ - **Параметри:** `node: unknown` — AST-вузол.
105
+ - **Повертає:** `true`, якщо це не-computed `MemberExpression`, де `object` —
106
+ Identifier `process`, а `property` — Identifier `env`. Інакше `false`.
107
+ - **Side effects:** немає (чиста функція).
108
+
109
+ Зокрема, навмисно повертає `false` для `process['env']` (computed=true) —
110
+ це нестандартна форма, у нашому коді не зустрічається й окремо не
111
+ реєструється.
112
+
113
+ ### `envNameFromMember(node) → string | null`
114
+
115
+ Витягує статичне ім'я ENV з MemberExpression `obj.X` / `obj['X']`.
116
+
117
+ - **Параметри:** `node: Record<string, unknown>` — MemberExpression, у якому
118
+ `object` — це або `process.env` (тоді сам node — parent member), або
119
+ Identifier `env`.
120
+ - **Повертає:**
121
+ - Якщо `!node.computed && property.type === 'Identifier'` — `property.name`;
122
+ - Якщо `node.computed && property.type === 'Literal' && typeof value === 'string'`
123
+ — `property.value`;
124
+ - Інакше `null` (обчислювані ключі, не-рядкові літерали тощо).
125
+ - **Side effects:** немає.
126
+
127
+ ### `collectCheckedEnvNames(programNode) → Set<string>`
128
+
129
+ Збирає всі рядкові імена з літеральних викликів `checkEnv([...])` у файлі.
130
+
131
+ - **Параметри:** `programNode: unknown` — корінь AST (Program).
132
+ - **Повертає:** `Set<string>` — імена ENV, які явно перераховані як string-літерали
133
+ у першому аргументі (`ArrayExpression`) будь-якого виклику `checkEnv(...)`.
134
+ - **Поведінка:**
135
+ - Якщо `callee.type !== 'Identifier'` або `callee.name !== 'checkEnv'` —
136
+ вузол пропускається;
137
+ - Якщо `arguments` порожні або перший аргумент не `ArrayExpression` —
138
+ пропускається;
139
+ - Лише `Literal`-елементи з `typeof value === 'string'` додаються в set;
140
+ - Identifier, SpreadElement, TemplateLiteral, обчислювані вирази —
141
+ пропускаються (це робить перевірку «ліберальною»).
142
+ - **Side effects:** немає (обхід AST через `walkAstWithAncestors` —
143
+ read-only).
144
+
145
+ ### `hasCheckEnvImport(programNode) → boolean`
146
+
147
+ Перевіряє, чи у файлі є саме `import { env } from '@nitra/check-env'`.
148
+
149
+ - **Параметри:** `programNode: unknown` — корінь AST.
150
+ - **Повертає:** `true`, якщо знайдено `ImportDeclaration` з
151
+ `source.value === '@nitra/check-env'` і серед `specifiers` є хоч один
152
+ `ImportSpecifier` із `imported.name === 'env'` та `local.name === 'env'`.
153
+ Інакше `false`.
154
+ - **Side effects:** немає. Внутрішньо використовує закриту змінну `found`
155
+ для дострокового виходу — обхід AST все одно завершується, але
156
+ внутрішні гілки рано повертаються після знахідки.
157
+
158
+ Aliased-варіанти (наприклад, `{ env as x }` де `local.name !== 'env'`)
159
+ свідомо ігноруються.
160
+
161
+ ### `hasIgnoreDirective(lines, oneBasedLine) → boolean`
162
+
163
+ Перевіряє, чи попередній рядок містить маркер
164
+ `// @nitra/cursor ignore-next-line checkEnv`.
165
+
166
+ - **Параметри:**
167
+ - `lines: string[]` — рядки файлу (split за `\n`, без trailing `\r`);
168
+ - `oneBasedLine: number` — 1-based номер рядка з порушенням.
169
+ - **Повертає:** `true`, якщо `lines[oneBasedLine - 2]` (тобто рядок безпосередньо
170
+ над порушенням) матчиться regex `IGNORE_DIRECTIVE_RE`. Якщо `oneBasedLine <= 1`
171
+ (порушення в першому рядку — попереднього рядка не існує) — повертає `false`.
172
+ - **Side effects:** немає.
173
+
174
+ ### `isEnvIdentifierMember(node) → boolean`
175
+
176
+ Чи вузол — MemberExpression виду `env.<...>` (де `env` — Identifier).
177
+
178
+ - **Параметри:** `node: unknown` — AST-вузол.
179
+ - **Повертає:** `true`, якщо `node.type === 'MemberExpression'` і
180
+ `object.type === 'Identifier' && object.name === 'env'`. Інакше `false`.
181
+ - **Side effects:** немає.
182
+
183
+ Фільтр **джерела** імпорту `env` робиться окремо через `hasCheckEnvImport` —
184
+ ця функція лише розпізнає форму AST.
185
+
186
+ ### `isParentEnvMember(parent, node) → boolean`
187
+
188
+ Чи `parent` — це MemberExpression, у якому `node` (тобто `process.env`)
189
+ виступає як `object`.
190
+
191
+ - **Параметри:**
192
+ - `parent: unknown` — найближчий ancestor вузла;
193
+ - `node: unknown` — сам вузол `process.env`.
194
+ - **Повертає:** `true` для конструкцій `process.env.X` та `process.env['X']`.
195
+ - **Side effects:** немає.
196
+
197
+ ### `isParentObjectPatternDeclarator(parent, node) → boolean`
198
+
199
+ Чи `parent` — це `VariableDeclarator` виду `const { ... } = <node>`.
200
+
201
+ - **Параметри:**
202
+ - `parent: unknown` — ancestor;
203
+ - `node: unknown` — `process.env` (або інший init-вираз).
204
+ - **Повертає:** `true`, якщо `parent.type === 'VariableDeclarator'`,
205
+ `parent.init === node`, `parent.id.type === 'ObjectPattern'` і
206
+ `parent.id.properties` — масив. Інакше `false`.
207
+ - **Side effects:** немає.
208
+
209
+ ### `isEnvObjectPatternDeclarator(node) → boolean`
210
+
211
+ Чи `node` сам — це `VariableDeclarator` виду `const { ... } = env`, де
212
+ `env` — Identifier.
213
+
214
+ - **Параметри:** `node: Record<string, unknown>` — AST-вузол.
215
+ - **Повертає:** `true`, якщо `node.type === 'VariableDeclarator'`,
216
+ `init.type === 'Identifier' && init.name === 'env'` і
217
+ `id.type === 'ObjectPattern'`.
218
+ - **Side effects:** немає.
219
+
220
+ Різниця з `isParentObjectPatternDeclarator` у тому, що ця функція дивиться
221
+ **на сам поточний вузол**, бо `Identifier 'env'` сам по собі обходить
222
+ небагато інформації — простіше реагувати на VariableDeclarator як корінь
223
+ паттерна.
224
+
225
+ ### `staticPropertyName(property) → string | null`
226
+
227
+ Витягує статичне ім'я з вузла `Property` у `ObjectPattern`.
228
+
229
+ - **Параметри:** `property: unknown` — елемент
230
+ `ObjectPattern.properties[i]`.
231
+ - **Повертає:**
232
+ - `null`, якщо `type !== 'Property'` або `computed === true`;
233
+ - `key.name` для `key.type === 'Identifier'`;
234
+ - `key.value` для `key.type === 'Literal'` із `typeof value === 'string'`;
235
+ - інакше `null`.
236
+ - **Side effects:** немає.
237
+
238
+ Ім'я береться **з ключа** (`property.key`), а не з `value` (alias-локального
239
+ ідентифікатора), бо саме ключ відповідає реальному імені ENV-змінної.
240
+
241
+ ### `collectViolations(program, content, lines, checkedNames, envFromCheckEnv) → EnvViolation[]`
242
+
243
+ Серцева функція модуля: один прохід по AST, що реєструє всі порушення.
244
+
245
+ - **Параметри:**
246
+ - `program: unknown` — корінь AST;
247
+ - `content: string` — вихідний код (потрібен для `offsetToLine`);
248
+ - `lines: string[]` — split-рядки `content` (без CR), потрібні для
249
+ `hasIgnoreDirective`;
250
+ - `checkedNames: Set<string>` — імена, закриті літеральним `checkEnv([...])`;
251
+ - `envFromCheckEnv: boolean` — чи імпортовано `env` саме з
252
+ `@nitra/check-env`.
253
+ - **Повертає:** `EnvViolation[]` — список порушень у порядку обходу AST,
254
+ з дедуплікацією за ключем `kind|name|line`.
255
+ - **Side effects:** немає за межами повернутого масиву. Внутрішньо тримає
256
+ замикання-стан (`out`, `reported`), які живуть лише в межах виклику.
257
+
258
+ Внутрішня структура:
259
+
260
+ - `report(kind, name, line)` — додає порушення з урахуванням
261
+ `hasIgnoreDirective` і дедуплікації за `kind|name|line`;
262
+ - `reportObjectPatternKeys(declarator, kind, skipName)` — для VariableDeclarator
263
+ з ObjectPattern обходить усі properties; для кожного статичного імені
264
+ (`staticPropertyName`) перевіряє `skipName(name)` і реєструє порушення.
265
+ Якщо `p.start` відсутній — використовує `declarator.start` як
266
+ fallback-offset;
267
+ - `handleProcessEnv(node, ancestors)` — обробляє `process.env`-доступ:
268
+ читає `ancestors.at(-1)`, для MemberExpression-parent реєструє ім'я через
269
+ `envNameFromMember(parent)`, для VariableDeclarator-parent — через
270
+ `reportObjectPatternKeys(parent, 'process-env', () => false)` (для
271
+ `process.env` skipName завжди false — закрити прямий `process.env` через
272
+ `checkEnv` не можна);
273
+ - `handleCheckEnvAccess(node)` — обробляє `env.X` і `const { X } = env`;
274
+ для перших — реєструє лише якщо `!checkedNames.has(envName)`; для
275
+ деструктуризації — пропускає імена, що вже у `checkedNames`.
276
+
277
+ Сам обхід:
278
+
279
+ ```
280
+ walkAstWithAncestors(program, [], (node, ancestors) => {
281
+ if (isProcessEnvAccess(node)) { handleProcessEnv(node, ancestors); return }
282
+ if (envFromCheckEnv) handleCheckEnvAccess(node)
283
+ })
284
+ ```
285
+
286
+ Тобто `process.env` має пріоритет над `env`-перевіркою: якщо вузол — це
287
+ `process.env`, ми реєструємо порушення і виходимо, не дивлячись на нього як
288
+ на потенційний `env.X`.
289
+
290
+ ### `findUncheckedProcessEnvInText(content, virtualPath = 'scan.ts') → EnvViolation[]` _(export)_
291
+
292
+ Публічна точка входу: знаходить порушення правила в одному файлі.
293
+
294
+ - **Параметри:**
295
+ - `content: string` — вихідний код файлу;
296
+ - `virtualPath?: string` — шлях, який передається в `parseProgramOrNull`
297
+ для вибору `lang` парсера (`.ts`, `.tsx`, `.js`, `.mjs`, …). За
298
+ замовчуванням `'scan.ts'` — це означає, що content за замовчуванням
299
+ парситься як TypeScript.
300
+ - **Повертає:** `EnvViolation[]`. Порожній масив, якщо:
301
+ - файл не парситься (`parseProgramOrNull` повернув `null`);
302
+ - порушень не знайдено;
303
+ - усі порушення приглушено `ignore-next-line`-маркером.
304
+ - **Side effects:** немає — функція **чиста**, не читає файлів з диска і
305
+ нічого не пише.
306
+
307
+ Порядок дій усередині:
308
+
309
+ 1. `parseProgramOrNull(content, virtualPath)` — отримати AST; якщо `null`,
310
+ повертаємо `[]`.
311
+ 2. `collectCheckedEnvNames(program)` — зібрати імена з `checkEnv([...])`.
312
+ 3. `hasCheckEnvImport(program)` — визначити, чи активний другий контракт.
313
+ 4. Розбити `content` на `lines` (split за `\n`, без trailing `\r` —
314
+ нормалізація CRLF/LF).
315
+ 5. `collectViolations(...)` — отримати фінальний список і повернути.
316
+
317
+ ### `isCheckEnvScanSourceFile(relativePathPosix) → boolean` _(export)_
318
+
319
+ Фільтр придатних до сканування файлів за розширенням.
320
+
321
+ - **Параметри:** `relativePathPosix: string` — відносний шлях у posix-стилі
322
+ (з `/`-розділювачами).
323
+ - **Повертає:** `true`, якщо:
324
+ - `SOURCE_FILE_RE.test(relativePathPosix)` — розширення у множині
325
+ `{.js, .mjs, .cjs, .ts, .mts, .cts, .jsx, .tsx}` (regex
326
+ `/\.([cm]?[jt]sx?)$/u`);
327
+ - **і** шлях **не** закінчується на `.d.ts` (declaration-файли
328
+ декларують лише типи й не виконують коду — їх скан не цікавить).
329
+ - **Side effects:** немає.
330
+
331
+ ## Залежності
332
+
333
+ Модуль має **одну** локальну залежність (зовнішніх npm-пакетів напряму не
334
+ імпортує):
335
+
336
+ - `../../../scripts/utils/ast-scan-utils.mjs` — спільні AST-утиліти:
337
+ - `parseProgramOrNull(content, virtualPath)` — обгортка над `oxc-parser`,
338
+ повертає корінь AST або `null` при синтаксичних помилках;
339
+ - `walkAstWithAncestors(root, ancestors, visitor)` — обхід дерева з
340
+ підтримкою стеку предків;
341
+ - `offsetToLine(content, offset)` — конвертація байтового зміщення в
342
+ 1-based номер рядка.
343
+
344
+ Внутрішні константи модуля:
345
+
346
+ - `SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u` — regex розширень для
347
+ `isCheckEnvScanSourceFile`;
348
+ - `IGNORE_DIRECTIVE_RE = /\/\/\s*@nitra\/cursor\s+ignore-next-line\s+checkEnv\b/u`
349
+ — regex маркера приглушення;
350
+ - `CHECK_ENV_PACKAGE = '@nitra/check-env'` — спеціальне ім'я пакета, з
351
+ якого має імпортуватися `env`.
352
+
353
+ Опосередковано модуль покладається на формат AST, що його повертає
354
+ `oxc-parser`: типи вузлів і поля (`type`, `object`, `property`, `computed`,
355
+ `source.value`, `specifiers[].imported.name`, `id.type`, `id.properties[]`,
356
+ `init`, `start` і т.д.) — близькі до ESTree, але з нюансами oxc.
357
+
358
+ ## Потік виконання / Використання
359
+
360
+ ### Типове використання (caller — runner правила `js-run`)
361
+
362
+ ```js
363
+ import { readFileSync } from 'node:fs'
364
+ import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './check-env-scan.mjs'
365
+
366
+ const relativePath = 'src/db/connect.ts' // posix
367
+ if (isCheckEnvScanSourceFile(relativePath)) {
368
+ const content = readFileSync(relativePath, 'utf8')
369
+ const violations = findUncheckedProcessEnvInText(content, relativePath)
370
+ for (const v of violations) {
371
+ console.log(`${relativePath}:${v.line} [${v.kind}] ${v.name}`)
372
+ }
373
+ }
374
+ ```
375
+
376
+ ### Приклади порушень
377
+
378
+ **Контракт 1 — `process-env`:**
379
+
380
+ ```ts
381
+ // src/db/connect.ts
382
+ const host = process.env.DB_HOST // kind: 'process-env', name: 'DB_HOST'
383
+ const { DB_PORT } = process.env // kind: 'process-env', name: 'DB_PORT'
384
+ ```
385
+
386
+ **Контракт 2 — `check-env-missing-checkEnv`** (тільки якщо є відповідний
387
+ імпорт):
388
+
389
+ ```ts
390
+ import { env, checkEnv } from '@nitra/check-env'
391
+
392
+ checkEnv(['DB_HOST']) // закриває DB_HOST
393
+ const host = env.DB_HOST // OK
394
+ const port = env.DB_PORT // kind: 'check-env-missing-checkEnv', name: 'DB_PORT'
395
+
396
+ const { DB_USER } = env // kind: 'check-env-missing-checkEnv', name: 'DB_USER'
397
+ ```
398
+
399
+ **Приглушення:**
400
+
401
+ ```ts
402
+ // @nitra/cursor ignore-next-line checkEnv
403
+ const host = process.env.DB_HOST // НЕ реєструється
404
+ ```
405
+
406
+ ### Внутрішній порядок виконання `findUncheckedProcessEnvInText`
407
+
408
+ 1. `parseProgramOrNull(content, virtualPath)` → AST (або `null` → `[]`).
409
+ 2. `collectCheckedEnvNames(program)` → `Set<string> checkedNames`.
410
+ 3. `hasCheckEnvImport(program)` → `boolean envFromCheckEnv`.
411
+ 4. `content.split('\n').map(...)` → `string[] lines` (CRLF-нормалізація).
412
+ 5. `collectViolations(program, content, lines, checkedNames, envFromCheckEnv)`:
413
+ - один прохід `walkAstWithAncestors`;
414
+ - для кожного `process.env` — `handleProcessEnv` (member-access або
415
+ ObjectPattern);
416
+ - для кожного `env.X` / `const { X } = env` (тільки коли
417
+ `envFromCheckEnv === true`) — `handleCheckEnvAccess` з урахуванням
418
+ `checkedNames`;
419
+ - `report(...)` накладає `hasIgnoreDirective` і дедуплікацію.
420
+ 6. Повернути масив порушень.
421
+
422
+ ### Інтеграція з правилом `js-run.mdc`
423
+
424
+ Це частина «фікс/лінт»-шару правил `npm/rules/js-run/...`: runner правила
425
+ викликає `isCheckEnvScanSourceFile` як фільтр, потім —
426
+ `findUncheckedProcessEnvInText` на відфільтрованих файлах, і за вмістом
427
+ повернутого масиву звітує діагностику в стандартному форматі
428
+ правила (з `kind`, `name` і `line` будуються повідомлення-підказки про
429
+ заміну `process.env.X` на `env`/`checkEnv`).
430
+
431
+ Жодних інших побічних ефектів модуль не має: не пише на диск, не змінює
432
+ `process.env`, не виконує парс-коду інакше, ніж через `oxc-parser` усередині
433
+ `parseProgramOrNull`.