@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,267 @@
1
+ # flow-resolve.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `flow-resolve.mjs` реалізує **cwd-незалежний резолвер активного flow** для команд `spec`, `plan`, `verify`, `review`, `gate`, `release` (беклог адаптації #1).
6
+
7
+ Призначення: знайти файл стану `*.flow.json` поточної задачі навіть тоді, коли команду запущено **не** з кореня worktree (наприклад, з кореня головного дерева репозиторію або з вкладеної підтеки worktree). Інакше функція `flowStatePath(cwd)` обчислила б хибний шлях і команда повідомила б «стану нема», хоча flow реально активний.
8
+
9
+ Порядок резолвингу (відповідно до spec `2026-06-01-flow-cwd-state-resolution`):
10
+
11
+ 1. Якщо передано явний `branch` — формуємо шлях `<repoRoot>/.worktrees/<sanitizeBranch(branch)>.flow.json` і перевіряємо існування теки worktree.
12
+ 2. Швидкий шлях: якщо `cwd` сам уже є текою worktree і поряд лежить файл стану — беремо його (без виклику git).
13
+ 3. Toplevel-резолвинг: `git rev-parse --show-toplevel` від `cwd`; якщо toplevel розташований **безпосередньо** під `<repoRoot>/.worktrees/` і для нього існує стан — повертаємо його. Якщо стану нема — це проблема саме цього worktree, чужий активний flow **не** підтягуємо.
14
+ 4. Скан: якщо `cwd` поза будь-яким worktree (наприклад, головне дерево) — перебираємо `<repoRoot>/.worktrees/*.flow.json`, шукаючи статуси `in_progress`. Якщо рівно один — авторезолв; якщо кілька — помилка зі списком; якщо нуль — «стану нема».
15
+
16
+ Резолвер **не пише** на диск і **не модифікує** стан. Усі залежності (`git`, FS-операції, `readState`) ін'єктуються через параметр `deps`, тож логіку можна тестувати без реального git-репозиторію.
17
+
18
+ ## Експорти / API
19
+
20
+ | Експорт | Тип | Призначення |
21
+ | ---------------------------------------- | --------------- | --------------------------------------------------------- |
22
+ | `resolveActiveFlowState(params?, deps?)` | named function | Основна функція-резолвер; повертає об'єкт `ResolvedFlow`. |
23
+ | `ResolvedFlow` | JSDoc-`typedef` | Тип-опис форми результату резолвингу. |
24
+
25
+ Внутрішні (не експортуються) допоміжні функції: `realGit`, `mainRepoRoot`, `currentToplevel`, `notFound`.
26
+
27
+ ### Тип `ResolvedFlow`
28
+
29
+ JSDoc-`typedef`, що описує форму результату:
30
+
31
+ | Поле | Тип | Семантика |
32
+ | -------------- | ---------------- | ------------------------------------------------------------------------------------------------------------- |
33
+ | `statePath` | `string \| null` | Абсолютний шлях до `.flow.json` або `null`, якщо стан не знайдено. |
34
+ | `worktreeDir` | `string \| null` | Тека worktree (ефективний `cwd` для гейтів) або `null`. |
35
+ | `label` | `string \| null` | Мітка flow (sanitized branch) або `null`. |
36
+ | `autoResolved` | `boolean` | `true`, якщо стан знайдено скануванням (тобто `cwd` був поза worktree, а активний flow знайшовся однозначно). |
37
+ | `error` | `string \| null` | Повідомлення для логу, якщо `statePath === null`; інакше `null`. |
38
+
39
+ ### Константа `FLOW_STATE_SUFFIX`
40
+
41
+ Внутрішня (не експортована) константа: `'.flow.json'`. Використовується для фільтрації імен файлів у теці `.worktrees/` під час скану та для обрізання суфікса при формуванні `label`.
42
+
43
+ ## Функції
44
+
45
+ ### `realGit(args, cwd)`
46
+
47
+ Сигнатура: `function realGit(args: string[], cwd: string): { status: number, stdout: string }`
48
+
49
+ Параметри:
50
+
51
+ - `args` — масив аргументів для бінарника `git` (наприклад, `['worktree', 'list', '--porcelain']`);
52
+ - `cwd` — робочий каталог, у якому запустити `git`.
53
+
54
+ Повертає об'єкт `{ status, stdout }`:
55
+
56
+ - `status` — exit-код процесу `git`; якщо `spawnSync` повернув `null` — підставляється `1`;
57
+ - `stdout` — захоплений stdout у кодуванні `utf8`; якщо `null` — порожній рядок.
58
+
59
+ Side effects: синхронний запуск дочірнього процесу `git` через `spawnSync`. Це **єдина** функція в модулі, що звертається до зовнішнього процесу; усе інше — чистий FS/обчислення. Використовується як **дефолтний** git-runner: при кожному виклику резолвера для конкретного `cwd` створюється стрілкова замикальна функція `args => realGit(args, cwd)`, якщо `deps.git` не передано.
60
+
61
+ ### `mainRepoRoot(git)`
62
+
63
+ Сигнатура: `function mainRepoRoot(git: (args: string[]) => { status: number, stdout: string }): string | null`
64
+
65
+ Параметри:
66
+
67
+ - `git` — git-runner (для тестів — мок; у проді — обгортка над `realGit`).
68
+
69
+ Повертає абсолютний шлях кореня **головного** worktree репозиторію або `null`, якщо неможливо встановити (git недоступний, не репо тощо).
70
+
71
+ Алгоритм:
72
+
73
+ 1. Виконати `git worktree list --porcelain`.
74
+ 2. Якщо `status !== 0` — повернути `null`.
75
+ 3. У stdout знайти **перший** рядок, що починається з `worktree ` (це і є головне дерево за конвенцією porcelain-формату).
76
+ 4. Зрізати префікс `worktree ` і пробіли; якщо рядок непорожній — повернути; інакше `null`.
77
+
78
+ Side effects: один git-виклик через переданий runner.
79
+
80
+ ### `currentToplevel(git)`
81
+
82
+ Сигнатура: `function currentToplevel(git: (args: string[]) => { status: number, stdout: string }): string | null`
83
+
84
+ Параметри:
85
+
86
+ - `git` — git-runner.
87
+
88
+ Повертає абсолютний шлях `toplevel`-теки для **поточного** worktree (`git rev-parse --show-toplevel`) або `null`, якщо команда впала або stdout порожній.
89
+
90
+ Side effects: один git-виклик через переданий runner.
91
+
92
+ ### `resolveActiveFlowState(params?, deps?)`
93
+
94
+ Сигнатура:
95
+
96
+ ```
97
+ export function resolveActiveFlowState(
98
+ params?: { cwd?: string, branch?: string },
99
+ deps?: {
100
+ git?: (args: string[]) => { status: number, stdout: string },
101
+ exists?: (p: string) => boolean,
102
+ readState?: (p: string) => object | null,
103
+ readdir?: (d: string) => string[],
104
+ repoRoot?: string,
105
+ },
106
+ ): ResolvedFlow
107
+ ```
108
+
109
+ Параметри:
110
+
111
+ - `params.cwd` — робочий каталог, від якого вести резолвинг. За замовчуванням — результат `process.cwd()` (через `cwd as processCwd` з `node:process`).
112
+ - `params.branch` — явна гілка, для якої треба знайти стан. Якщо задана, активний flow в інших worktree **ігнорується** — використовується «фіксований» режим.
113
+ - `deps.git` — кастомний git-runner; за замовчуванням — `args => realGit(args, cwd)` (замикає `cwd` із `params`).
114
+ - `deps.exists` — кастомна реалізація `existsSync`; за замовчуванням — `existsSync` з `node:fs`.
115
+ - `deps.readState` — кастомний читач стану; за замовчуванням — `readState`, реекспортований із `./state-store.mjs` як `defaultReadState`.
116
+ - `deps.readdir` — кастомне читання каталогу; за замовчуванням — функція, що повертає `readdirSync(d)` якщо тека існує, інакше `[]` (захист від ENOENT, коли `.worktrees/` ще не створено).
117
+ - `deps.repoRoot` — наперед обчислений корінь репозиторію; якщо передано — обхід git для пошуку кореня пропускається.
118
+
119
+ Повертає об'єкт `ResolvedFlow` (див. вище).
120
+
121
+ Алгоритм (по гілках):
122
+
123
+ 1. **Локальний `resolveRoot()`** — стрілкова функція, що повертає `deps.repoRoot` або викликає `mainRepoRoot(git)`. Викликається лазиво — лише там, де потрібен корінь.
124
+
125
+ 2. **Гілка 1: явний `branch`.**
126
+ - Отримати `repoRoot` через `resolveRoot()`. Якщо `null` — повернути `notFound('стану нема — спершу `flow init`')`.
127
+ - `label = sanitizeBranch(branch)` (нормалізація імені гілки для імені файлу стану).
128
+ - `worktreeDir = worktreePaths(repoRoot, branch).checkout` — фізична тека worktree.
129
+ - Якщо `worktreeDir` **не** існує (`!exists(worktreeDir)`) — повернути `notFound` із повідомленням, що worktree для цієї гілки не знайдено (з підказкою перевірити назву або виконати `flow init`). Це захищає від ENOENT при подальших гейтах.
130
+ - Інакше повернути `{ statePath: flowStatePath(worktreeDir), worktreeDir, label, autoResolved: false, error: null }`.
131
+
132
+ 3. **Гілка 2: швидкий шлях без git.**
133
+ - Обчислити `direct = flowStatePath(cwd)`.
134
+ - Якщо файл існує — повернути `{ statePath: direct, worktreeDir: cwd, label: basename(cwd), autoResolved: false, error: null }`. Це типовий випадок: користувач у корені свого worktree.
135
+
136
+ 4. **Потрібен `repoRoot` через git** (`resolveRoot()`). Якщо `null` — повернути `notFound('стану нема — спершу `flow init`')`. Це безпечна деградація: якщо git недоступний, не лазимо далі.
137
+
138
+ 5. Обчислити `worktreesDir = join(repoRoot, '.worktrees')`.
139
+
140
+ 6. **Гілка 3: toplevel-резолвинг.**
141
+ - `top = currentToplevel(git)`.
142
+ - Якщо `top` визначено **і** `dirname(top) === worktreesDir` (тобто toplevel лежить безпосередньо під `<repoRoot>/.worktrees/`) — ми всередині worktree (можливо, з вкладеної підтеки):
143
+ - `statePath = flowStatePath(top)`.
144
+ - Якщо `exists(statePath)` — повернути `{ statePath, worktreeDir: top, label: basename(top), autoResolved: false, error: null }`.
145
+ - Інакше — `notFound('стану нема — спершу `flow init`')`. **Чужий** активний flow тут **не** підтягуємо: це проблема саме цього worktree.
146
+
147
+ 7. **Гілка 4: скан активних flow** (виконується, якщо `top` неможливо отримати або `dirname(top) !== worktreesDir`, тобто `cwd` поза будь-яким worktree — наприклад, у головному дереві).
148
+ - `active = []`.
149
+ - Для кожного `name` із `readdir(worktreesDir)`:
150
+ - Якщо ім'я не закінчується на `FLOW_STATE_SUFFIX` (`.flow.json`) — пропустити.
151
+ - Сформувати `statePath = join(worktreesDir, name)`.
152
+ - Спробувати `state = readState(statePath)`; будь-яка помилка (`catch`) — пропустити елемент (пошкоджений стан не валить скан).
153
+ - Якщо `state?.status === 'in_progress'`:
154
+ - `label = name.slice(0, -FLOW_STATE_SUFFIX.length)` — обрізати суфікс `.flow.json`.
155
+ - `worktreeDir = join(worktreesDir, label)`.
156
+ - Додати `{ statePath, worktreeDir, label }` у `active`.
157
+ - Якщо `active.length === 1` — повернути `{ ...active[0], autoResolved: true, error: null }`.
158
+ - Якщо `active.length > 1` — повернути `notFound` із форматованим списком: «кілька активних flow — уточни `--branch <гілка>` або `cd` у потрібний worktree:\n - <label1>\n - <label2>...».
159
+ - Інакше — `notFound('стану нема — спершу `flow init`')`.
160
+
161
+ Side effects: тільки **читання** — `git`, `existsSync`, `readdirSync`, `readState`. Жодних записів на диск.
162
+
163
+ ### `notFound(error)`
164
+
165
+ Сигнатура: `function notFound(error: string): ResolvedFlow`
166
+
167
+ Параметри:
168
+
169
+ - `error` — текст повідомлення для логу.
170
+
171
+ Повертає об'єкт-«заглушку» з усіма полями стану, виставленими в `null`/`false`, і заданим `error`:
172
+
173
+ ```
174
+ { statePath: null, worktreeDir: null, label: null, autoResolved: false, error }
175
+ ```
176
+
177
+ Side effects: немає (чиста функція).
178
+
179
+ ## Залежності
180
+
181
+ ### Стандартна бібліотека Node.js
182
+
183
+ - `node:fs` — `existsSync`, `readdirSync` (дефолтні реалізації `exists`/`readdir`).
184
+ - `node:child_process` — `spawnSync` (запуск `git`).
185
+ - `node:path` — `basename`, `dirname`, `join` (формування та аналіз шляхів).
186
+ - `node:process` — `cwd as processCwd` (дефолт для `params.cwd`).
187
+
188
+ ### Внутрішні модулі
189
+
190
+ - `../../lib/worktree.mjs` — імпортуються:
191
+ - `sanitizeBranch(branch)` — нормалізація імені гілки → ім'я-мітка файлу стану;
192
+ - `worktreePaths(repoRoot, branch)` — повертає об'єкт із полем `checkout` (фізична тека worktree).
193
+ - `./state-store.mjs` — імпортуються:
194
+ - `flowStatePath(worktreeDir)` — формує шлях файлу стану для заданої теки worktree;
195
+ - `readState as defaultReadState` — функція читання + парсингу JSON-стану; під час скану викликається у `try/catch`, тож може кидати помилку.
196
+
197
+ ### Зовнішні процеси
198
+
199
+ - `git` (через `spawnSync`):
200
+ - `git worktree list --porcelain` — для знаходження кореня головного worktree;
201
+ - `git rev-parse --show-toplevel` — для визначення поточного worktree.
202
+
203
+ ## Потік виконання / Використання
204
+
205
+ ### Сценарій 1 — запуск із кореня worktree
206
+
207
+ ```
208
+ cwd = /repo/.worktrees/feat-foo
209
+ ```
210
+
211
+ 1. `branch` не передано → Гілка 1 пропускається.
212
+ 2. `direct = flowStatePath('/repo/.worktrees/feat-foo')` існує → **повертається одразу** без виклику git. Це найшвидший і найчастіший випадок.
213
+
214
+ ### Сценарій 2 — запуск із підтеки worktree
215
+
216
+ ```
217
+ cwd = /repo/.worktrees/feat-foo/src/lib
218
+ ```
219
+
220
+ 1. Гілка 1 — ні.
221
+ 2. Гілка 2 — `direct` не існує (бо `cwd` не корінь worktree).
222
+ 3. Через `mainRepoRoot(git)` отримуємо `/repo`; `worktreesDir = /repo/.worktrees`.
223
+ 4. `currentToplevel(git)` → `/repo/.worktrees/feat-foo`; `dirname === /repo/.worktrees` ✓ → повертаємо стан саме цього worktree.
224
+
225
+ ### Сценарій 3 — запуск із головного дерева (поза worktree)
226
+
227
+ ```
228
+ cwd = /repo
229
+ ```
230
+
231
+ 1. Гілка 1 — ні.
232
+ 2. Гілка 2 — `flowStatePath('/repo')` не існує.
233
+ 3. `mainRepoRoot` → `/repo`; `worktreesDir = /repo/.worktrees`.
234
+ 4. `currentToplevel` → `/repo`; `dirname('/repo') !== '/repo/.worktrees'` → переходимо до скану.
235
+ 5. Скан `.worktrees/*.flow.json`:
236
+ - 1 із `status: in_progress` → `autoResolved: true`, повертається.
237
+ - 2+ → помилка зі списком кандидатів та пропозицією `--branch` або `cd`.
238
+ - 0 → `notFound('стану нема — спершу `flow init`')`.
239
+
240
+ ### Сценарій 4 — явний `--branch`
241
+
242
+ ```
243
+ params = { branch: 'feat-foo' }
244
+ ```
245
+
246
+ 1. `repoRoot` отриманий → перевіряється існування фізичної теки worktree `feat-foo`.
247
+ 2. Якщо тека існує — повертається стан (навіть якщо файлу стану ще нема — це **очікуваний** шлях, користувач обере цю гілку).
248
+ 3. Якщо теки нема — `notFound` з підказкою перевірити назву / зробити `flow init`.
249
+
250
+ ### Сценарій 5 — git недоступний
251
+
252
+ - Якщо `mainRepoRoot` повертає `null` (наприклад, бінарника git нема або це не репо) і прямий шлях через `cwd` теж не спрацював — повертаємо `notFound('стану нема — спершу `flow init`')`. Жодних падінь.
253
+
254
+ ### Контракт виклику з боку команд
255
+
256
+ Команди `spec/plan/verify/review/gate/release` мають викликати `resolveActiveFlowState(...)` **на самому старті**, потім:
257
+
258
+ - Якщо `result.statePath === null` — вивести `result.error` і завершитися ненульовим кодом.
259
+ - Інакше використовувати:
260
+ - `result.statePath` — для читання/запису файлу стану через `state-store.mjs`;
261
+ - `result.worktreeDir` — як **ефективний** `cwd` для виконання гейтів (linter, tests, scripts) у потрібному worktree;
262
+ - `result.label` — для логів/повідомлень;
263
+ - `result.autoResolved` — щоб показати користувачу, що flow знайдено скануванням (корисно для UX: «авторезолв на гілку X»).
264
+
265
+ ### Тестування
266
+
267
+ Усі зовнішні залежності (`git`, `exists`, `readState`, `readdir`, `repoRoot`) ін'єктуються через параметр `deps`. Це дає змогу повністю покрити функцію unit-тестами без реального git-репозиторію та без створення файлів на диску — достатньо передати моки, що повертають фіктивні `status`/`stdout`/булеві значення.
@@ -0,0 +1,231 @@
1
+ # `gate.mjs` — реалізація команди `flow gate`
2
+
3
+ ## Огляд
4
+
5
+ Модуль реалізує підкоманду `flow gate` диспатчера — структурований вердикт релізної готовності у стилі BMAD `qa-gate`, адаптованого до внутрішнього флоу проєкту. Скрипт **синтезує** два джерела сигналів, що накопичуються у файлі стану флоу (`.flow.json`):
6
+
7
+ 1. Механічні гейти, які заповнює крок `verify` (`state.gates` — масив `{ name, ok }`).
8
+ 2. Adversarial-зауваження, які залишає крок `review` (`state.review.findings` — масив об'єктів із полем `severity`).
9
+
10
+ На виході формується єдиний вердикт `PASS | CONCERNS | FAIL`, числовий `score` у діапазоні `0..100` та список людиночитаних причин. Командa `gate` **не приймає рішень** за `verify` чи `review`, а лише агрегує їх — це забезпечує traceability «чому готово / не готово».
11
+
12
+ Архітектурно модуль складається з двох частин:
13
+
14
+ - `computeGate(state)` — **чиста** функція, повністю детермінована від вхідного стану, не торкається диска й часу. Призначена для unit-тестів без I/O-моків.
15
+ - `gate(_rest, deps)` — async-обгортка, що читає стан з диска, викликає `computeGate`, фіксує результат як подію в `events.jsonl` і повертає exit-код для CLI. Усі залежності IO (`cwd`, `log`, `now`) ін'єктуються через `deps`, що робить функцію тестопридатною.
16
+
17
+ ## Експорти / API
18
+
19
+ | Експорт | Тип | Призначення |
20
+ | ------------- | ---------------- | --------------------------------------- |
21
+ | `computeGate` | `function` | Чистий синтез вердикту з об'єкта стану. |
22
+ | `gate` | `async function` | CLI-handler підкоманди `flow gate`. |
23
+
24
+ Модуль не має default-експорту. Внутрішня константа `PENALTY` (штрафи score) **не експортується** і є приватною деталлю реалізації.
25
+
26
+ ### `computeGate(state)`
27
+
28
+ Сигнатура (JSDoc):
29
+
30
+ ```
31
+ @param {{ gates?: { name: string, ok: boolean }[],
32
+ review?: { findings?: { severity?: string }[] } }} state
33
+ @returns {{ verdict: 'PASS' | 'CONCERNS' | 'FAIL',
34
+ score: number,
35
+ reasons: string[] }}
36
+ ```
37
+
38
+ ### `gate(_rest, deps)`
39
+
40
+ Сигнатура (JSDoc):
41
+
42
+ ```
43
+ @param {string[]} _rest аргументи CLI (не використовуються)
44
+ @param {{ cwd?: string,
45
+ log?: (m: string) => void,
46
+ now?: () => number,
47
+ branch?: string }} [deps] ін'єкції
48
+ @returns {Promise<number>} exit-код (FAIL → 1; PASS/CONCERNS → 0)
49
+ ```
50
+
51
+ Параметр `_rest` зберігається в сигнатурі для уніфікації з рештою CLI-handlers диспатчера; підкреслення на початку позначає навмисне ігнорування. Крім задокументованих у JSDoc полів `deps`, реалізація читає також `deps.branch` — він передається в `resolveActiveFlowState`.
52
+
53
+ ## Функції
54
+
55
+ ### `computeGate(state)`
56
+
57
+ - **Сигнатура:** `computeGate(state) → { verdict, score, reasons }`.
58
+ - **Параметри:**
59
+ - `state.gates` (необов'язково) — масив об'єктів `{ name: string, ok: boolean }`. Якщо відсутній, береться `[]`.
60
+ - `state.review.findings` (необов'язково) — масив об'єктів із полем `severity` зі значенням `'high'` або `'med'` (інші значення ігноруються при підрахунку штрафів). Якщо відсутній — `[]`.
61
+ - **Повертає:** об'єкт `{ verdict, score, reasons }`.
62
+ - `verdict` — рядок `'PASS'`, `'CONCERNS'` або `'FAIL'`.
63
+ - `score` — ціле число `0..100`, обмежене через `Math.max(0, Math.min(100, …))`.
64
+ - `reasons` — масив рядкових пояснень (можливо порожній при `PASS`).
65
+ - **Side effects:** немає. Функція чиста, детермінована, без I/O і без використання глобального часу.
66
+
67
+ **Алгоритм синтезу:**
68
+
69
+ 1. Розбиває `gates` на `failedGates = gates.filter(g => !g.ok)`.
70
+ 2. Розбиває `findings` на дві групи за `severity`: `high` і `med`.
71
+ 3. Прапор `noVerify = gates.length === 0` — стан, коли `verify` ще не запускався.
72
+ 4. Формує `reasons`:
73
+ - для кожного провального гейта: `gate «<name>» провалено`;
74
+ - якщо `high.length > 0`: `<N> high-severity review finding(s)`;
75
+ - якщо `med.length > 0`: `<N> med-severity review finding(s)`;
76
+ - якщо `noVerify`: `verify ще не запускався`.
77
+ 5. Визначає `verdict` за пріоритетом:
78
+ - `FAIL` — якщо є хоча б один провальний гейт **або** хоча б один `high`-finding;
79
+ - інакше `CONCERNS` — якщо є `med`-findings **або** `noVerify`;
80
+ - інакше `PASS`.
81
+ 6. Обраховує `penalty` як суму штрафів `PENALTY`:
82
+ - `failedGate = 40` за кожен провальний гейт;
83
+ - `high = 25` за кожен `high`-finding;
84
+ - `med = 8` за кожен `med`-finding;
85
+ - `noVerify = 15` (одноразово, якщо `gates` порожній).
86
+ 7. `score = max(0, min(100, 100 - penalty))`.
87
+
88
+ **Інваріанти:**
89
+
90
+ - `FAIL` ⇒ `score < 100` (`penalty ≥ 25` гарантовано через high або failedGate ≥ 40).
91
+ - `PASS` ⇒ `penalty = 0` ⇒ `score = 100`.
92
+ - `CONCERNS` досяжний при `penalty > 0` без `failedGates` і без `high`.
93
+ - `score` ніколи не виходить за `[0, 100]`.
94
+
95
+ ### `gate(_rest, deps = {})`
96
+
97
+ - **Сигнатура:** `async gate(_rest, deps) → Promise<number>`.
98
+ - **Параметри:**
99
+ - `_rest` — масив рядків (CLI-аргументи), ігнорується.
100
+ - `deps.cwd` — поточна робоча директорія; default — `process.cwd()`.
101
+ - `deps.log` — функція логування; default — `console.error`.
102
+ - `deps.now` — провайдер часу `() => number`; default — `Date.now`.
103
+ - `deps.branch` — додаткова підказка для `resolveActiveFlowState` (необов'язково).
104
+ - **Повертає:** `Promise<number>` — exit-код процесу:
105
+ - `1` — якщо стан недоступний (немає активного флоу або `flow init` ще не виконувався) або вердикт `FAIL`;
106
+ - `0` — для `PASS` і `CONCERNS`.
107
+ - **Side effects:**
108
+ - Викликає `resolveActiveFlowState` (може читати worktree/гілку).
109
+ - Викликає `readState(statePath)` — синхронне читання `.flow.json`.
110
+ - Викликає `recordTransition(...)` — мутація `.flow.json` і дописування в `events.jsonl` (через `flowEventsPath(cwd)`).
111
+ - Логування в `deps.log` (за замовчуванням stderr).
112
+
113
+ **Покроковий потік:**
114
+
115
+ 1. Розв'язує дефолтні залежності: `cwd0`, `log`, `now`.
116
+ 2. `resolveActiveFlowState({ cwd: cwd0, branch })` визначає активний флоу:
117
+ - якщо `resolved.statePath` пустий — логує `gate: <error>` і повертає `1`;
118
+ - якщо `resolved.autoResolved` — логує повідомлення `flow: авторезолвлено активний flow «<label>» (cwd поза worktree)`.
119
+ 3. Підставляє `cwd = resolved.worktreeDir ?? cwd0` і `statePath = resolved.statePath`.
120
+ 4. Читає стан `readState(statePath)`; якщо `null/undefined` — логує `gate: стану нема — спершу `flow init``і повертає`1`.
121
+ 5. Викликає `computeGate(state)` → `result`.
122
+ 6. Викликає `recordTransition` з:
123
+ - шляхами `{ statePath, eventsPath: flowEventsPath(cwd) }`,
124
+ - подією `{ type: 'gate', verdict: result.verdict }`,
125
+ - редьюсером `s => ({ ...s, gate: { ...result, at: new Date(now()).toISOString() } })` — додає до стану секцію `gate` з `verdict`/`score`/`reasons`/`at` (ISO-таймстемп);
126
+ - провайдером часу `now`.
127
+ 7. Логує рядок `gate: <VERDICT> (score <N>)`, потім по ` · <reason>` для кожної причини.
128
+ 8. Повертає `1`, якщо `result.verdict === 'FAIL'`, інакше `0`.
129
+
130
+ ## Залежності
131
+
132
+ ### Зовнішні (Node.js core)
133
+
134
+ - `node:process` — імпорт `cwd as processCwd` для дефолтного значення `deps.cwd`.
135
+
136
+ ### Внутрішні модулі диспатчера
137
+
138
+ - `./events.mjs` — функція `flowEventsPath(cwd)` будує шлях до файлу подій `events.jsonl` поточного флоу.
139
+ - `./state-store.mjs`:
140
+ - `readState(statePath)` — синхронне читання `.flow.json` (повертає `null`, якщо нема);
141
+ - `recordTransition({ statePath, eventsPath }, event, reducer, now)` — атомарна мутація стану + лог подій.
142
+ - `./flow-resolve.mjs` — `resolveActiveFlowState({ cwd, branch }, deps)` визначає активний worktree/гілку/`statePath`; повертає поля `statePath`, `worktreeDir`, `autoResolved`, `label`, `error`.
143
+
144
+ ### Глобальні
145
+
146
+ - `console.error` — дефолтний логер (замінюється через `deps.log` у тестах).
147
+ - `Date.now`, `new Date(...).toISOString()` — джерело таймстемпів (час ін'єктується через `deps.now`, але `new Date(...).toISOString()` працює з результатом).
148
+
149
+ ## Потік виконання / Використання
150
+
151
+ ### Інтеграція в CLI
152
+
153
+ `gate` реєструється в основному диспатчері `flow` як handler підкоманди `gate`. Виклик відбувається у форматі:
154
+
155
+ ```
156
+ flow gate
157
+ ```
158
+
159
+ Додаткові позиційні аргументи не зчитуються (`_rest` ігнорується).
160
+
161
+ ### Типовий ланцюжок флоу
162
+
163
+ ```
164
+ flow init → створює .flow.json
165
+ flow verify → заповнює state.gates
166
+ flow review → заповнює state.review.findings
167
+ flow gate → агрегує → state.gate = { verdict, score, reasons, at }
168
+ ```
169
+
170
+ ### Сценарії exit-коду
171
+
172
+ | Сценарій | Лог | Exit |
173
+ | ------------------------- | --------------------------------------- | ---- |
174
+ | Активний флоу не знайдено | `gate: <error>` | `1` |
175
+ | `.flow.json` відсутній | `gate: стану нема — спершу `flow init`` | `1` |
176
+ | `verdict = FAIL` | `gate: FAIL (score …)` + причини | `1` |
177
+ | `verdict = CONCERNS` | `gate: CONCERNS (score …)` + причини | `0` |
178
+ | `verdict = PASS` | `gate: PASS (score 100)` | `0` |
179
+
180
+ ### Приклад чистого використання `computeGate`
181
+
182
+ ```js
183
+ import { computeGate } from './gate.mjs'
184
+
185
+ const state = {
186
+ gates: [
187
+ { name: 'lint', ok: true },
188
+ { name: 'test', ok: false }
189
+ ],
190
+ review: { findings: [{ severity: 'med' }] }
191
+ }
192
+
193
+ computeGate(state)
194
+ // → { verdict: 'FAIL', score: 52, reasons: [
195
+ // 'gate «test» провалено',
196
+ // '1 med-severity review finding(s)'
197
+ // ] }
198
+ ```
199
+
200
+ ### Приклад тестування `gate` через ін'єкції
201
+
202
+ ```js
203
+ import { gate } from './gate.mjs'
204
+
205
+ const logs = []
206
+ const code = await gate([], {
207
+ cwd: '/tmp/fixture',
208
+ log: m => logs.push(m),
209
+ now: () => 1_700_000_000_000
210
+ })
211
+
212
+ // code === 0 | 1
213
+ // logs містить рядки виду 'gate: PASS (score 100)' / ' · …'
214
+ ```
215
+
216
+ ### Записи в `.flow.json`
217
+
218
+ Після успішного виконання у стані з'являється секція:
219
+
220
+ ```json
221
+ {
222
+ "gate": {
223
+ "verdict": "CONCERNS",
224
+ "score": 92,
225
+ "reasons": ["1 med-severity review finding(s)"],
226
+ "at": "2024-01-01T00:00:00.000Z"
227
+ }
228
+ }
229
+ ```
230
+
231
+ Подія `{ type: 'gate', verdict }` дописується в `events.jsonl` через `recordTransition`.