@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,326 @@
1
+ # promise-settimeout-scan.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `promise-settimeout-scan.mjs` — це AST-сканер, який виявляє у вихідному коді JavaScript/TypeScript антипаттерн «обгортка `setTimeout` у `new Promise`» виду:
6
+
7
+ ```js
8
+ new Promise(resolve => setTimeout(resolve, ms))
9
+ // або
10
+ await new Promise(resolve => setTimeout(resolve, ms))
11
+ ```
12
+
13
+ Згідно з правилом `js-run.mdc` (секція «Паузи через `setTimeout`») такий код потрібно замінити на ідіоматичний імпорт із `node:timers/promises`:
14
+
15
+ ```js
16
+ import { setTimeout as sleep } from 'node:timers/promises'
17
+ await sleep(ms)
18
+ ```
19
+
20
+ Сканер працює **структурно** (по AST), без regex-у по тілу: шукає `NewExpression`, у якого callee — Identifier `Promise`, а єдиний аргумент — функція з одним параметром-resolve, тіло якої — єдиний виклик `setTimeout(<resolve>, ms)`. Перший аргумент `setTimeout` мусить бути «голим» resolve — або сам ідентифікатор, або тривіальна безпараметрична обгортка `() => resolve()` / `function () { resolve() }` без жодних переданих аргументів. Якщо у виклик `resolve` передається значення — це вже не «чиста пауза», і паттерн **не** вмикається.
21
+
22
+ Сканер **толерантний до синтаксичних помилок**: якщо файл не парситься, повертається порожній список (узгоджено з рештою AST-сканерів проєкту — спочатку треба полагодити синтаксис, а вже потім ловити структурні порушення).
23
+
24
+ ## Експорти / API
25
+
26
+ | Експорт | Тип | Призначення |
27
+ | ----------------------------------- | ---------- | ---------------------------------------------------------------------------------- |
28
+ | `findPromiseSetTimeoutInText` | `function` | Знаходить усі входження антипаттерну в тексті, повертає масив `{ line, snippet }`. |
29
+ | `isPromiseSetTimeoutScanSourceFile` | `function` | Фільтр по відносному шляху: чи варто взагалі сканувати цей файл (за розширенням). |
30
+
31
+ Внутрішні (не експортовані) хелпери, які формують ядро аналізу:
32
+
33
+ - `isBareResolveCallback(arg, paramName)` — перевірка «чистоти» першого аргументу `setTimeout`.
34
+ - `extractSingleCallExpression(body)` — витягнення єдиного `CallExpression` з тіла функції.
35
+ - `isPromiseSetTimeoutDelay(node)` — головний предикат паттерну на рівні `NewExpression`.
36
+ - `walkAst(node, visit)` — простий рекурсивний обхід AST.
37
+
38
+ Константа модульного рівня:
39
+
40
+ - `SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/` — regex розширень JS/TS-сім'ї (`.js`, `.mjs`, `.cjs`, `.ts`, `.mts`, `.cts`, `.jsx`, `.tsx`).
41
+
42
+ ## Функції
43
+
44
+ ### `findPromiseSetTimeoutInText(content, virtualPath = 'scan.ts')`
45
+
46
+ **Призначення.** Публічна точка входу: парсить вихідний код, обходить AST і збирає всі позиції, де знайдено антипаттерн «`new Promise(... setTimeout ...)`».
47
+
48
+ **Сигнатура.**
49
+
50
+ ```js
51
+ export function findPromiseSetTimeoutInText(
52
+ content: string,
53
+ virtualPath?: string
54
+ ): { line: number, snippet: string }[]
55
+ ```
56
+
57
+ **Параметри.**
58
+
59
+ - `content: string` — повний текст файлу для сканування.
60
+ - `virtualPath: string` (за замовчуванням `'scan.ts'`) — віртуальний шлях, який передається у `parseProgramOrNull` для вибору мови парсера (TS/JSX тощо). Не використовується для read/write-у на диск; впливає лише на режим парсингу.
61
+
62
+ **Повертає.** Масив об'єктів `{ line: number, snippet: string }` — по одному запису на кожне виявлене порушення:
63
+
64
+ - `line` — 1-based номер рядка, де починається `NewExpression` (через `offsetToLine`).
65
+ - `snippet` — нормалізований текст самого `new Promise(...)`-виразу (через `normalizeSnippet`).
66
+
67
+ Якщо AST не побудувався (синтаксична помилка), повертає порожній масив `[]`.
68
+
69
+ **Side effects.** Чиста функція — жодного I/O, читання env, мутацій глобального стану. Усі дані повертаються через return value.
70
+
71
+ ---
72
+
73
+ ### `isPromiseSetTimeoutScanSourceFile(relativePath)`
74
+
75
+ **Призначення.** Фільтр для зовнішнього раннера/обхідника файлів: чи варто взагалі парсити цей файл.
76
+
77
+ **Сигнатура.**
78
+
79
+ ```js
80
+ export function isPromiseSetTimeoutScanSourceFile(
81
+ relativePath: string
82
+ ): boolean
83
+ ```
84
+
85
+ **Параметри.**
86
+
87
+ - `relativePath: string` — відносний шлях до файлу (наприклад, `src/utils/sleep.ts`).
88
+
89
+ **Повертає.** `true`, якщо:
90
+
91
+ 1. Розширення збігається з `SOURCE_FILE_RE` (JS/TS-сім'я), **та**
92
+ 2. Шлях **не** закінчується на `.d.ts` (декларації типів пропускаються).
93
+
94
+ Інакше — `false`.
95
+
96
+ **Side effects.** Немає.
97
+
98
+ ---
99
+
100
+ ### `isBareResolveCallback(arg, paramName)` (внутрішня)
101
+
102
+ **Призначення.** Перевіряє, що перший аргумент `setTimeout` — це або сам ідентифікатор `resolve`, або тривіальна безпараметрична обгортка, яка викликає `resolve()` без значення.
103
+
104
+ **Сигнатура.**
105
+
106
+ ```js
107
+ function isBareResolveCallback(
108
+ arg: Record<string, unknown> | null | undefined,
109
+ paramName: string
110
+ ): boolean
111
+ ```
112
+
113
+ **Параметри.**
114
+
115
+ - `arg` — AST-вузол першого аргументу виклику `setTimeout`.
116
+ - `paramName` — ім'я параметра-resolve у тіла-функції `Promise` (зазвичай `'resolve'`, але приймається будь-яке ім'я-Identifier).
117
+
118
+ **Повертає.** `true`, якщо аргумент — це:
119
+
120
+ - `Identifier` з іменем `paramName` (наприклад, `setTimeout(resolve, ms)`); **або**
121
+ - `ArrowFunctionExpression` / `FunctionExpression` без параметрів, тіло якого — рівно один `CallExpression`, callee — `Identifier paramName`, а список аргументів виклику — порожній (наприклад, `() => resolve()` або `function () { resolve() }`).
122
+
123
+ В інших випадках — `false`. Зокрема, якщо у виклик `resolve(x)` передається значення — це **не** «чиста пауза» (бо результат `await` був би `x`, а не `undefined`), і антипаттерн **не** фіксується.
124
+
125
+ **Side effects.** Немає.
126
+
127
+ ---
128
+
129
+ ### `extractSingleCallExpression(body)` (внутрішня)
130
+
131
+ **Призначення.** Витягує єдиний `CallExpression` з тіла функції — як у концизній стрілковій формі (`() => foo()`), так і в блоковій з рівно одним стейтментом (`() => { foo() }`).
132
+
133
+ **Сигнатура.**
134
+
135
+ ```js
136
+ function extractSingleCallExpression(
137
+ body: unknown
138
+ ): Record<string, unknown> | null
139
+ ```
140
+
141
+ **Параметри.**
142
+
143
+ - `body` — AST-вузол тіла функції (може бути `CallExpression`, `BlockStatement` або щось інше).
144
+
145
+ **Повертає.** AST-вузол `CallExpression`, якщо:
146
+
147
+ - `body.type === 'CallExpression'` — повертає сам body; **або**
148
+ - `body.type === 'BlockStatement'`, масив `body.body` містить рівно один елемент, цей елемент — `ExpressionStatement`, а його `expression.type === 'CallExpression'` — повертає цей `expression`.
149
+
150
+ Інакше — `null`.
151
+
152
+ **Side effects.** Немає.
153
+
154
+ ---
155
+
156
+ ### `isPromiseSetTimeoutDelay(node)` (внутрішня)
157
+
158
+ **Призначення.** Головний предикат паттерну: чи це `NewExpression` виду `new Promise(<resolve> => setTimeout(<resolve>, ms))`.
159
+
160
+ **Сигнатура.**
161
+
162
+ ```js
163
+ function isPromiseSetTimeoutDelay(
164
+ node: Record<string, unknown> | null | undefined
165
+ ): boolean
166
+ ```
167
+
168
+ **Параметри.**
169
+
170
+ - `node` — довільний AST-вузол (під час обходу через `walkAst`).
171
+
172
+ **Повертає.** `true`, якщо всі наступні умови виконані:
173
+
174
+ 1. `node.type === 'NewExpression'`;
175
+ 2. `node.callee` — `Identifier` з іменем `'Promise'` (саме глобальний/локальний ідентифікатор, без member-доступу `foo.Promise`);
176
+ 3. `node.arguments.length === 1`;
177
+ 4. Цей єдиний аргумент — `ArrowFunctionExpression` або `FunctionExpression` з мінімум одним параметром;
178
+ 5. Перший параметр — `Identifier` (запам'ятовується його `name` як ім'я resolve);
179
+ 6. Тіло функції містить рівно один `CallExpression` (через `extractSingleCallExpression`);
180
+ 7. Callee цього виклику — `Identifier` з іменем `'setTimeout'` (джерело — глобальне, з `node:timers`, з `globalThis` — для сканера не важливо);
181
+ 8. У виклику `setTimeout` хоча б один аргумент;
182
+ 9. Перший аргумент `setTimeout` — «голий» resolve (через `isBareResolveCallback`).
183
+
184
+ Якщо будь-яка умова порушена — повертає `false`.
185
+
186
+ **Side effects.** Немає.
187
+
188
+ ---
189
+
190
+ ### `walkAst(node, visit)` (внутрішня)
191
+
192
+ **Призначення.** Простий generic-обхід AST: рекурсивно спускається по всіх властивостях і елементах масивів, викликаючи `visit` для кожного об'єкта-вузла, який має поле `type` (тобто справжнього AST-вузла, а не службового мета-обʼєкта).
193
+
194
+ **Сигнатура.**
195
+
196
+ ```js
197
+ function walkAst(
198
+ node: unknown,
199
+ visit: (n: Record<string, unknown>) => void
200
+ ): void
201
+ ```
202
+
203
+ **Параметри.**
204
+
205
+ - `node` — корінь або під-вузол (Program, окремий вузол, масив, скаляр — все підтримується).
206
+ - `visit` — колбек, який отримує кожен AST-вузол із полем `type`. Викликається **до** спуску в дочірні поля (pre-order).
207
+
208
+ **Поведінка.**
209
+
210
+ - `null`/примітиви — пропускаються;
211
+ - Масиви — обходяться поелементно;
212
+ - Об'єкти з `typeof node.type === 'string'` — спершу передаються у `visit`, потім обходяться їхні поля;
213
+ - Поле з ключем `'parent'` пропускається (захист від циклів у деяких AST-моделях, де є зворотні посилання);
214
+ - Інші поля-об'єкти — рекурсивно обходяться.
215
+
216
+ **Повертає.** `undefined`.
217
+
218
+ **Side effects.** Викликає `visit` зовнішнього коду — single side effect виноситься назовні через колбек.
219
+
220
+ ## Залежності
221
+
222
+ ### Зовнішні (relative imports)
223
+
224
+ Із `../../../scripts/utils/ast-scan-utils.mjs`:
225
+
226
+ - `parseProgramOrNull(content, virtualPath)` — парсить вихідний код у Program-вузол AST з урахуванням мови (вибір TS/JS/JSX за розширенням у `virtualPath`); повертає `null` при синтаксичних помилках.
227
+ - `offsetToLine(content, offset)` — перетворює byte/char offset у 1-based номер рядка.
228
+ - `normalizeSnippet(text)` — нормалізує текст сніпета (стискання пробілів/переносів) для зручного виводу у звітах.
229
+
230
+ ### Глобальні
231
+
232
+ - `RegExp` (`SOURCE_FILE_RE`), `Array.isArray`, `Object.keys`, `String.prototype.endsWith`, `String.prototype.slice`, `Array.prototype.push` — стандартні API ES.
233
+
234
+ ### Без рантайм-залежностей
235
+
236
+ Жодних `node:`-вбудованих модулів, жодних npm-пакетів, жодних звернень до файлової системи, env або мережі.
237
+
238
+ ## Потік виконання / Використання
239
+
240
+ ### Типовий сценарій (з раннера/перевіряча)
241
+
242
+ ```js
243
+ import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from './promise-settimeout-scan.mjs'
244
+ import { readFile } from 'node:fs/promises'
245
+
246
+ async function scanRepo(files) {
247
+ const violations = []
248
+ for (const relPath of files) {
249
+ if (!isPromiseSetTimeoutScanSourceFile(relPath)) continue
250
+ const content = await readFile(relPath, 'utf8')
251
+ const hits = findPromiseSetTimeoutInText(content, relPath)
252
+ for (const hit of hits) {
253
+ violations.push({ file: relPath, line: hit.line, snippet: hit.snippet })
254
+ }
255
+ }
256
+ return violations
257
+ }
258
+ ```
259
+
260
+ ### Алгоритм всередині `findPromiseSetTimeoutInText`
261
+
262
+ 1. **Парсинг.** `parseProgramOrNull(content, virtualPath)` → якщо `null` (синтаксична помилка), функція одразу повертає `[]`.
263
+ 2. **Ініціалізація.** Створюється порожній масив `out` для накопичення знахідок.
264
+ 3. **Обхід AST.** `walkAst(program, visit)` рекурсивно проходить весь Program-вузол.
265
+ 4. **Перевірка паттерну.** Для кожного AST-вузла з полем `type` викликається `visit`, який:
266
+ - Перевіряє `isPromiseSetTimeoutDelay(node)`;
267
+ - Якщо `true` — пушить у `out` об'єкт `{ line, snippet }`, де `line` обчислюється з `node.start` через `offsetToLine`, а `snippet` — з `content.slice(node.start, node.end)` через `normalizeSnippet`.
268
+ 5. **Повернення.** Масив `out` повертається як результат (може бути порожнім, якщо порушень немає).
269
+
270
+ ### Що сканер ловить (приклади позитивних спрацювань)
271
+
272
+ ```js
273
+ new Promise(resolve => setTimeout(resolve, 1000))
274
+ await new Promise(r => setTimeout(r, ms))
275
+ new Promise(resolve => setTimeout(() => resolve(), 500))
276
+ new Promise(function (resolve) {
277
+ setTimeout(resolve, 200)
278
+ })
279
+ new Promise(resolve => {
280
+ setTimeout(function () {
281
+ resolve()
282
+ }, 100)
283
+ })
284
+ ```
285
+
286
+ ### Що сканер свідомо **не** ловить (негативні приклади)
287
+
288
+ - `new Promise(resolve => setTimeout(() => resolve(value), ms))` — у `resolve` передається значення; це не «чиста пауза».
289
+ - `new Promise((resolve, reject) => setTimeout(() => doStuff().then(resolve, reject), ms))` — у тілі більше ніж один call або інший callee.
290
+ - `new MyPromise(resolve => setTimeout(resolve, ms))` — callee — не `Promise`.
291
+ - `globalThis.Promise` як callee (MemberExpression) — не Identifier.
292
+ - Файли з синтаксичними помилками — `parseProgramOrNull` повертає `null`, сканер віддає `[]`.
293
+ - Файли з розширенням `.d.ts` — фільтруються `isPromiseSetTimeoutScanSourceFile`.
294
+
295
+ ### Інтеграція з правилом `js-run.mdc`
296
+
297
+ Цей сканер — частина перевірок правила js-run, секція «Паузи через `setTimeout`». Він використовується check-скриптом правила для збору списку порушень, які потім виводяться користувачу зі вказівкою замінити обгортку на `setTimeout` із `node:timers/promises`. Сам файл `promise-settimeout-scan.mjs` не виконує жодних дій з виправлення — лише детектує.
298
+
299
+ ## Rebuild Test
300
+
301
+ Перевірка контрактів модуля (умоглядно, без запуску):
302
+
303
+ 1. **`isPromiseSetTimeoutScanSourceFile`:**
304
+ - `'src/a.ts'` → `true`; `'src/a.tsx'` → `true`; `'src/a.mjs'` → `true`; `'src/a.cjs'` → `true`.
305
+ - `'src/a.d.ts'` → `false` (декларації пропускаються).
306
+ - `'src/a.md'` / `'src/a.json'` → `false` (інше розширення).
307
+
308
+ 2. **`findPromiseSetTimeoutInText` — позитивний кейс:**
309
+ - Вхід: `const s = new Promise(r => setTimeout(r, 100))`.
310
+ - Очікувано: масив з одним записом, `line === 1`, `snippet` містить нормалізований текст `new Promise(r => setTimeout(r, 100))`.
311
+
312
+ 3. **`findPromiseSetTimeoutInText` — негативний кейс (передача значення):**
313
+ - Вхід: `new Promise(r => setTimeout(() => r(42), 10))`.
314
+ - Очікувано: `[]` (бо `isBareResolveCallback` відсіває виклики з аргументами).
315
+
316
+ 4. **`findPromiseSetTimeoutInText` — синтаксична помилка:**
317
+ - Вхід: `const x = (` (незакритий вираз).
318
+ - Очікувано: `[]` (бо `parseProgramOrNull` повертає `null`).
319
+
320
+ 5. **`findPromiseSetTimeoutInText` — інший callee:**
321
+ - Вхід: `new MyPromise(r => setTimeout(r, 1))`.
322
+ - Очікувано: `[]` (бо `node.callee.name !== 'Promise'`).
323
+
324
+ 6. **`isPromiseSetTimeoutDelay` — більше одного стейтменту в блоці:**
325
+ - Тіло Promise-функції: `{ doSomething(); setTimeout(resolve, 10) }`.
326
+ - Очікувано: `false` (бо `extractSingleCallExpression` вимагає рівно один стейтмент).
@@ -0,0 +1,129 @@
1
+ # fix.mjs — точка входу правила `k8s`
2
+
3
+ ## Огляд
4
+
5
+ Файл `npm/rules/k8s/fix.mjs` — це **точка входу правила `k8s`** у системі `@nitra/cursor`. Він виконує дві ролі одночасно:
6
+
7
+ 1. **Library mode** — експортує функцію `run(ctx)`, яку імпортує верхньорівневий CLI-оркестратор (`@nitra/cursor`) під час прогону групи правил (`fix`/`lint`-команди). У цьому режимі правило отримує спільний контекст прогону (`RuleContext`), що містить, серед іншого, кеш обходу файлової системи (`walkCache`), і має повертати числовий exit-code без виклику `process.exit`.
8
+ 2. **Standalone mode** — якщо файл запущено напряму як CLI (`bun rules/k8s/fix.mjs`), він самостійно завантажує конфіг, застосовує whitelist, друкує summary та завершує процес із відповідним exit-code, тобто є повним еквівалентом команди `npx @nitra/cursor fix k8s`.
9
+
10
+ Сама логіка правила (які саме файли обходити, які `applies`/`JS-concerns`/`policy`/`mdc-refs` перевіряти) у цьому файлі **не описана** — вона делегується універсальному рушію `runStandardRule`, який знаходить артефакти правила за каталогом, переданим через `import.meta.dirname`.
11
+
12
+ Файл є тонкою адаптерною обгорткою — типовий шаблон `fix.mjs`, що повторюється для всіх правил у `npm/rules/<id>/`.
13
+
14
+ ## Експорти / API
15
+
16
+ | Експорт | Тип | Призначення |
17
+ | ------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------ |
18
+ | `run` | `(ctx?: RuleContext) => Promise<number>` | Запуск правила в **library mode**. Викликається верхньорівневим CLI під час оркестрації набору правил. |
19
+
20
+ Файл також має **side-effect блок верхнього рівня** (умовний `process.exit(await runRuleCli(...))`), який спрацьовує **лише** коли модуль є entry-point поточного Node/Bun-процесу. Цей блок не є експортом, але є частиною публічної поведінки файлу як standalone-скрипта.
21
+
22
+ Інших іменованих чи default-експортів файл не має.
23
+
24
+ ## Функції
25
+
26
+ ### `run(ctx)`
27
+
28
+ ```js
29
+ export function run(ctx) {
30
+ return runStandardRule(import.meta.dirname, ctx)
31
+ }
32
+ ```
33
+
34
+ **Сигнатура:**
35
+
36
+ ```ts
37
+ function run(ctx?: RuleContext): Promise<number>
38
+ ```
39
+
40
+ **Параметри:**
41
+
42
+ - `ctx` _(опційно)_ — об'єкт контексту прогону типу `RuleContext` (тип реекспортується з `../../scripts/lib/run-standard-rule.mjs`). У контексті, як зазначено в JSDoc, може передаватися, зокрема, `walkCache` — спільний кеш обходу файлової системи між правилами, щоб не сканувати дерево заново для кожного правила в одній сесії. Якщо параметр не передано, `runStandardRule` працює без зовнішнього кешу.
43
+
44
+ **Повертає:**
45
+
46
+ - `Promise<number>` — exit-code результату прогону правила:
47
+ - `0` — OK (порушень немає);
48
+ - `1` — знайдено порушення (`runStandardRule` повертає `1`, коли хоча б одна перевірка дала falsy/негативний результат).
49
+
50
+ **Side effects:**
51
+
52
+ - Делегує всю роботу `runStandardRule`, який, своєю чергою:
53
+ - обходить файлову систему за `applies`-патернами правила;
54
+ - запускає підмодулі правила (`applies`, `JS-concerns`, `policy`, `mdc-refs`) — у послідовності, визначеній стандартним рушієм;
55
+ - пише діагностику/виводить порушення у stdout/stderr (точна форма залежить від рушія, не цього файлу).
56
+ - Сама функція `run` не торкається `process.exit`, `process.cwd` чи глобального стану — увесь побічний ефект інкапсульовано всередині `runStandardRule`.
57
+
58
+ **Чому передається `import.meta.dirname`:** це абсолютний шлях до каталогу `npm/rules/k8s/`, в якому лежить даний `fix.mjs`. `runStandardRule` за цим шляхом знаходить решту артефактів правила (наприклад, `k8s.mdc`, `applies.mjs`, `check-*.mjs`, `policy`-файли тощо), не вимагаючи від `fix.mjs` явно перераховувати, які саме перевірки треба виконати.
59
+
60
+ ### Top-level standalone-блок
61
+
62
+ ```js
63
+ if (isRunAsCli(import.meta.url)) {
64
+ process.exit(await runRuleCli(import.meta.dirname))
65
+ }
66
+ ```
67
+
68
+ **Призначення:** дозволити запускати правило окремо — `bun npm/rules/k8s/fix.mjs` — і отримати такий самий результат, як від `npx @nitra/cursor fix k8s` (із завантаженням конфігу, застосуванням whitelist та друком summary).
69
+
70
+ **Поведінка:**
71
+
72
+ - `isRunAsCli(import.meta.url)` повертає `true`, якщо поточний файл є entry-point процесу (а не імпортований модуль). Якщо це бібліотечний імпорт — блок не виконується, побічних ефектів немає.
73
+ - `runRuleCli(import.meta.dirname)` — повний CLI-цикл одного правила: завантаження конфігу проєкту, фільтр whitelist, виклик `run`-логіки, друк підсумкового summary; повертає `Promise<number>` — exit-code.
74
+ - `process.exit(...)` примусово завершує процес із цим exit-code, щоб CI/IDE могли коректно проінтерпретувати результат прогону.
75
+
76
+ **Top-level `await`:** доступний у файлі завдяки тому, що `.mjs` є ES-модулем — це обов'язкова умова для коректної роботи `await runRuleCli(...)` поза `async`-функцією.
77
+
78
+ **ESLint-директива:** коментар `// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit` свідомо вимикає правила, що забороняють `process.exit`, тому що standalone entry-point **повинен** повертати exit-code (це і пояснює коментар-`-- ...` після назв правил).
79
+
80
+ ## Залежності
81
+
82
+ ### Внутрішні
83
+
84
+ - `../../scripts/lib/run-rule-cli.mjs` (`npm/scripts/lib/run-rule-cli.mjs`) — імпортуються два named-експорти:
85
+ - `isRunAsCli(metaUrl)` — детектор того, чи запущений файл як CLI-entry, а не як імпортований модуль (стандартний ESM-патерн «main module detection» через `import.meta.url`);
86
+ - `runRuleCli(ruleDir)` — повний цикл standalone-прогону одного правила (config-loading + whitelist + виклик `run` + summary).
87
+ - `../../scripts/lib/run-standard-rule.mjs` (`npm/scripts/lib/run-standard-rule.mjs`) — імпортується:
88
+ - `runStandardRule(ruleDir, ctx?)` — універсальний рушій «стандартного» правила, що знаходить за `ruleDir` усі артефакти (`applies`, `JS-concerns`, `policy`, `mdc-refs`) і запускає їх у правильному порядку;
89
+ - реекспортує тип `RuleContext` (використовується у JSDoc `@param`).
90
+
91
+ ### Зовнішні
92
+
93
+ - Глобальні Node-runtime API: `process.exit` (умовний виклик), `import.meta.dirname`, `import.meta.url`. Окремих npm-залежностей цей файл не має.
94
+
95
+ ## Потік виконання / Використання
96
+
97
+ ### Сценарій 1. Library mode (типове використання)
98
+
99
+ 1. Верхньорівневий CLI-оркестратор `@nitra/cursor` (наприклад, команда `fix`) збирає список активних правил.
100
+ 2. Для правила `k8s` він робить динамічний імпорт цього файлу (`import('npm/rules/k8s/fix.mjs')`).
101
+ 3. Оркестратор формує спільний `ctx: RuleContext` (зокрема `walkCache`, який буде поділено між правилами однієї сесії) і викликає `await mod.run(ctx)`.
102
+ 4. `run` делегує виклик `runStandardRule(import.meta.dirname, ctx)`, який:
103
+ - читає артефакти правила з каталогу `npm/rules/k8s/`;
104
+ - обходить FS відповідно до `applies`-патернів (використовуючи `ctx.walkCache`, якщо переданий);
105
+ - послідовно виконує перевірки `applies → JS-concerns → policy → mdc-refs`;
106
+ - повертає `0` (OK) або `1` (порушення).
107
+ 5. Оркестратор агрегує exit-коди з усіх правил у фінальний exit-code сесії.
108
+
109
+ ### Сценарій 2. Standalone mode (запуск одного правила вручну)
110
+
111
+ 1. Користувач запускає `bun npm/rules/k8s/fix.mjs` (або еквівалент через інший Node-runtime з підтримкою `import.meta.dirname`).
112
+ 2. ESM-завантажувач виконує файл як entry-point: визначення `run` потрапляє у scope модуля, але оркестратор ззовні його не викликає.
113
+ 3. Виконується top-level `if (isRunAsCli(import.meta.url))` — умова `true`, оскільки `import.meta.url` збігається з entry-point процесу.
114
+ 4. Викликається `await runRuleCli(import.meta.dirname)`:
115
+ - підвантажується конфіг проєкту;
116
+ - застосовується whitelist (якщо налаштований);
117
+ - всередині `runRuleCli` запускається та сама логіка правила (через ту саму експортовану функцію `run` / `runStandardRule`), що й у library mode;
118
+ - друкується summary з кількістю перевірених файлів і знайдених порушень.
119
+ 5. Отриманий `exit-code` передається в `process.exit(...)` — процес завершується із цим кодом, що дозволяє CI/IDE коректно реагувати на результат.
120
+
121
+ ### Сценарій 3. Імпорт як модуля без виклику `run`
122
+
123
+ Якщо файл імпортується суто для інтроспекції (`import * as fixK8s from '.../k8s/fix.mjs'`), `isRunAsCli(import.meta.url)` поверне `false` (`import.meta.url` модуля не співпаде з URL entry-point процесу), і standalone-блок не виконається. Жодних побічних ефектів імпорт не має — це **безпечно** для тулзів типу docgen чи статичного аналізу.
124
+
125
+ ### Замітки про шаблон
126
+
127
+ - Файл є **взаємозамінним** із аналогічними `fix.mjs` у сусідніх каталогах `npm/rules/<id>/` — відрізняється лише розташуванням (а отже — `import.meta.dirname`), і завдяки цьому той самий рушій `runStandardRule` обслуговує всі правила без копіювання логіки.
128
+ - Жодна k8s-специфічна логіка тут **не** жорстко закодована — вона лежить у сусідніх файлах правила (`k8s.mdc`, `applies.mjs`, `check-*.mjs`, `policy`-артефакти), які знаходить `runStandardRule` за каталогом.
129
+ - Для додавання нового правила достатньо створити каталог `npm/rules/<new-id>/` і покласти туди такий самий `fix.mjs` — без жодних модифікацій (за умови, що структура артефактів стандартна).