@nitra/cursor 3.22.0 → 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 (228) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/CHANGELOG.md +31 -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-ci/docs/fix.md +154 -0
  74. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  75. package/rules/js-mssql/docs/fix.md +128 -0
  76. package/rules/js-mssql/js/docs/deps.md +263 -0
  77. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  78. package/rules/js-run/docs/fix.md +144 -0
  79. package/rules/js-run/js/docs/runtime.md +388 -0
  80. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  81. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  82. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  83. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  84. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  85. package/rules/k8s/docs/fix.md +129 -0
  86. package/rules/k8s/js/docs/manifests.md +344 -0
  87. package/rules/k8s/js/manifests.mjs +6 -2
  88. package/rules/k8s/k8s.mdc +4 -2
  89. package/rules/k8s/lint/docs/lint.md +411 -0
  90. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  91. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  92. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  93. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  94. package/rules/npm-module/docs/fix.md +98 -0
  95. package/rules/npm-module/js/docs/package_structure.md +274 -0
  96. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  97. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  98. package/rules/php/docs/fix.md +107 -0
  99. package/rules/php/js/docs/tooling.md +152 -0
  100. package/rules/php/lint/docs/lint.md +215 -0
  101. package/rules/python/docs/fix.md +163 -0
  102. package/rules/python/js/docs/applies.md +108 -0
  103. package/rules/python/js/docs/tooling.md +153 -0
  104. package/rules/python/lint/docs/lint.md +322 -0
  105. package/rules/rego/docs/fix.md +121 -0
  106. package/rules/rego/js/docs/applies.md +174 -0
  107. package/rules/rego/js/docs/lint.md +118 -0
  108. package/rules/rego/lint/docs/lint.md +204 -0
  109. package/rules/release/docs/change.md +185 -0
  110. package/rules/release/docs/fix.md +119 -0
  111. package/rules/release/docs/release.md +222 -0
  112. package/rules/release/lib/docs/aggregate.md +246 -0
  113. package/rules/release/lib/docs/change-file.md +200 -0
  114. package/rules/release/lib/docs/fallback.md +203 -0
  115. package/rules/rust/docs/fix.md +129 -0
  116. package/rules/rust/js/docs/applies.md +140 -0
  117. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  118. package/rules/security/docs/fix.md +86 -0
  119. package/rules/security/js/docs/lint.md +171 -0
  120. package/rules/security/js/docs/sample_secret.md +190 -0
  121. package/rules/security/js/docs/trufflehog.md +137 -0
  122. package/rules/security/js/lint.mjs +9 -1
  123. package/rules/style-lint/docs/fix.md +155 -0
  124. package/rules/style-lint/js/docs/lint.md +184 -0
  125. package/rules/style-lint/js/docs/tooling.md +194 -0
  126. package/rules/tauri/docs/fix.md +158 -0
  127. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  128. package/rules/tauri/js/docs/tooling.md +228 -0
  129. package/rules/test/coverage/coverage.mjs +15 -3
  130. package/rules/test/docs/fix.md +132 -0
  131. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  132. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  134. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  135. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  136. package/rules/test/js/docs/location.md +136 -0
  137. package/rules/test/js/docs/no-process-chdir.md +160 -0
  138. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  139. package/rules/test/js/docs/stryker_config.md +152 -0
  140. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  141. package/rules/text/docs/fix.md +118 -0
  142. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  143. package/rules/text/js/docs/formatting.md +256 -0
  144. package/rules/text/js/docs/lint.md +122 -0
  145. package/rules/text/lint/docs/lint.md +220 -0
  146. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  147. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  148. package/rules/text/lint/docs/run-v8r.md +197 -0
  149. package/rules/vue/docs/fix.md +127 -0
  150. package/rules/vue/js/docs/packages.md +335 -0
  151. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  152. package/rules/worktree/docs/fix.md +161 -0
  153. package/schemas/rule-meta.json +5 -1
  154. package/scripts/auto-rules.mjs +7 -4
  155. package/scripts/coverage-classify/docs/apply.md +202 -0
  156. package/scripts/coverage-classify/docs/cache.md +203 -0
  157. package/scripts/coverage-classify/docs/index.md +218 -0
  158. package/scripts/coverage-classify/docs/prompt.md +132 -0
  159. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  160. package/scripts/coverage-fix-extract.mjs +122 -0
  161. package/scripts/coverage-fix.mjs +1 -1
  162. package/scripts/dispatcher/docs/graph.md +346 -0
  163. package/scripts/dispatcher/docs/index.md +236 -0
  164. package/scripts/dispatcher/docs/trace.md +296 -0
  165. package/scripts/dispatcher/index.mjs +1 -1
  166. package/scripts/dispatcher/lib/active.mjs +4 -8
  167. package/scripts/dispatcher/lib/commands.mjs +7 -11
  168. package/scripts/dispatcher/lib/docs/active.md +348 -0
  169. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  170. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  171. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  172. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  173. package/scripts/dispatcher/lib/docs/events.md +182 -0
  174. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  175. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  176. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  177. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  178. package/scripts/dispatcher/lib/docs/level.md +335 -0
  179. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  180. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  181. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  182. package/scripts/dispatcher/lib/docs/review.md +255 -0
  183. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  184. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  185. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  186. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  187. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  188. package/scripts/dispatcher/lib/executor.mjs +6 -1
  189. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  190. package/scripts/dispatcher/lib/level.mjs +29 -3
  191. package/scripts/dispatcher/lib/review.mjs +1 -1
  192. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  193. package/scripts/docs/auto-rules.md +376 -0
  194. package/scripts/docs/auto-skills.md +173 -0
  195. package/scripts/docs/build-agents-commands.md +183 -0
  196. package/scripts/docs/cli-entry.md +153 -0
  197. package/scripts/docs/coverage-fix.md +177 -0
  198. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  199. package/scripts/lib/changed-files.mjs +4 -1
  200. package/scripts/lib/docs/changed-files.md +149 -0
  201. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  202. package/scripts/lib/docs/check-reporter.md +175 -0
  203. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  204. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  205. package/scripts/lib/docs/ensure-tool.md +254 -0
  206. package/scripts/lib/docs/generated-markdown.md +275 -0
  207. package/scripts/lib/docs/gha-workflow.md +326 -0
  208. package/scripts/lib/docs/inline-template-links.md +303 -0
  209. package/scripts/lib/docs/list-rule-ids.md +156 -0
  210. package/scripts/lib/docs/load-cursor-config.md +147 -0
  211. package/scripts/lib/docs/mirror-parity.md +167 -0
  212. package/scripts/lib/worktree.mjs +26 -0
  213. package/scripts/worktree-cli.mjs +12 -2
  214. package/skills/coverage-fix/SKILL.md +34 -45
  215. package/skills/docgen/SKILL.md +44 -23
  216. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  217. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  218. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  219. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  220. package/skills/docgen/js/docgen-scan.mjs +37 -21
  221. package/skills/llm-patch/SKILL.md +23 -2
  222. package/skills/start-check/SKILL.md +26 -53
  223. package/skills/start-check/js/check.mjs +211 -0
  224. package/skills/taze/SKILL.md +9 -3
  225. package/skills/taze/js/diff.mjs +154 -0
  226. package/types/bin/n-cursor.d.ts +1 -1
  227. package/skills/fix-tests/SKILL.md +0 -119
  228. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,346 @@
1
+ # graph.mjs — DAG-позиція вузлів графа (`n-cursor graph`)
2
+
3
+ ## Огляд
4
+
5
+ Модуль реалізує **read-only** підкоманду `n-cursor graph` — інспекцію поточного стану DAG-графа задач, описаного контрактом `docs/specs/2026-06-01-node-dag-state.md`. У цьому файлі реалізовано **перший зріз** — `status`: сканування каталогу `docs/graphs/<graph>/nodes/`, групування артефактів-файлів по вузлах, деривацію статусу кожного вузла та текстове відображення позиції графа.
6
+
7
+ Ключові архітектурні рішення:
8
+
9
+ - **State-on-FS**: ввесь стан DAG зберігається у markdown-файлах у `docs/graphs/<graph>/nodes/`. Модуль нічого не мутує — лише читає.
10
+ - **Pure derivation**: статус вузла обчислюється з набору файлів-артефактів та їх front-matter, без зовнішніх БД/сервісів.
11
+ - **DI for FS**: усі функції приймають інжектовані `readdir` / `readFile` через об'єкт `deps`, що робить логіку детермінованою та тестованою без доступу до файлової системи.
12
+ - **Stem + qid модель**: ім'я файлу-артефакта складається з `<stem><суфікс>.md`, де `<stem>` — `id-slug` вузла (наприклад, `B02-parser`), а суфікс позначає тип артефакту (`.plan`, `.claim`, `.fact`, `.ask-<qid>`, `.ans-<qid>`).
13
+ - **Roadmap**: подальші зрізи — `claim`, `tick`, `dispatch` — у цьому файлі ще не реалізовані; згадані лише як plan-маркери в JSDoc-заголовку.
14
+
15
+ Категорії статусів вузла (значення поля `status`): `done`, `failed`, `awaiting-human`, `in_progress`, `ready`, `blocked`.
16
+
17
+ ## Експорти / API
18
+
19
+ | Експорт | Тип | Призначення |
20
+ | ------------------------------- | -------------- | ------------------------------------------------------------------------------------ |
21
+ | `classifyArtifact(name)` | named function | Класифікує ім'я файлу-артефакту в `{ stem, kind, qid? }`. |
22
+ | `parseIdList(value)` | named function | Парсить inline-список `[A, B]` із front-matter у масив id. |
23
+ | `scanGraph(root, graph, deps?)` | named function | Сканує `docs/graphs/<graph>/nodes/`, групує артефакти, повертає сирий список вузлів. |
24
+ | `deriveStatus(node, doneSet)` | named function | Чисте обчислення статусу одного вузла на базі прапорців та `dependsOn`. |
25
+ | `deriveGraph(nodes)` | named function | Деривує статус для всіх вузлів графа (спочатку обчислюючи `doneSet`). |
26
+ | `renderGraph(graph, nodes)` | named function | Текстовий рендер графа в одну багаторядкову таблицю. |
27
+ | `runGraphCli(args, deps?)` | named function | CLI-точка входу для `n-cursor graph <sub> [graph]`. |
28
+
29
+ Внутрішня (не експортована):
30
+
31
+ - `listGraphs(root, readdir)` — перелік підкаталогів у `docs/graphs/`.
32
+
33
+ Константи (модульно-приватні):
34
+
35
+ - `PLAIN` — масив пар `[суфікс, kind]` для артефактів без `qid`:
36
+ - `.plan` → `plan`
37
+ - `.claim` → `claim`
38
+ - `.fact` → `fact`
39
+ - `QID` — масив пар `[префікс, kind]` для артефактів з `qid`:
40
+ - `.ask-` → `ask`
41
+ - `.ans-` → `ans`
42
+
43
+ ## Функції
44
+
45
+ ### `classifyArtifact(name)`
46
+
47
+ **Сигнатура:** `(name: string) => { stem: string, kind: string, qid?: string } | null`
48
+
49
+ **Параметри:**
50
+
51
+ - `name` — рядок з іменем файлу (очікується закінчення на `.md`).
52
+
53
+ **Повертає:** об'єкт класифікації або `null`, якщо файл не схожий на артефакт.
54
+
55
+ **Алгоритм:**
56
+
57
+ 1. Якщо ім'я не закінчується на `.md` — повертається `null`.
58
+ 2. Відрізається суфікс `.md` → `base`.
59
+ 3. Перебираються пари в `PLAIN`; перший суфікс, на який закінчується `base`, дає `{ stem: base без суфікса, kind }`.
60
+ 4. Якщо PLAIN не спрацював — перебираються пари в `QID`. Для кожної шукається **остання** входженість префікса (`lastIndexOf`), і якщо знайдено, повертається `{ stem: base до префікса, kind, qid: усе після префікса }`.
61
+ 5. Якщо нічого не підійшло — `null`.
62
+
63
+ **Side effects:** немає (чиста функція).
64
+
65
+ **Приклади:**
66
+
67
+ - `classifyArtifact('B02-parser.plan.md')` → `{ stem: 'B02-parser', kind: 'plan' }`
68
+ - `classifyArtifact('B02-parser.ask-q1.md')` → `{ stem: 'B02-parser', kind: 'ask', qid: 'q1' }`
69
+ - `classifyArtifact('README.md')` → `null`
70
+
71
+ ---
72
+
73
+ ### `parseIdList(value)`
74
+
75
+ **Сигнатура:** `(value: string | null | undefined) => string[]`
76
+
77
+ **Параметри:**
78
+
79
+ - `value` — значення поля front-matter, наприклад `"[A, B, C]"`.
80
+
81
+ **Повертає:** масив id; порожні елементи відкидаються; усі елементи `trim`-аються.
82
+
83
+ **Алгоритм:** якщо `value` не рядок — повертається `[]`; інакше прибираються перший `[` та перший `]`, рядок ділиться по комі, кожен елемент trim-ається, відфільтровуються falsy значення.
84
+
85
+ **Side effects:** немає.
86
+
87
+ **Зауваги:**
88
+
89
+ - `replace('[', '')` / `replace(']', '')` без `g`-флага: видаляється лише перше входження кожної дужки — інші лишаються в результаті.
90
+
91
+ ---
92
+
93
+ ### `scanGraph(root, graph, deps?)`
94
+
95
+ **Сигнатура:** `(root: string, graph: string, deps?: { readdir?, readFile? }) => Node[]`
96
+
97
+ **Параметри:**
98
+
99
+ - `root` — абсолютний шлях до кореня репо.
100
+ - `graph` — id графа (= ім'я каталогу під `docs/graphs/`).
101
+ - `deps.readdir` — функція `(dir: string) => string[]`; за замовчуванням обгортка над `fs.existsSync` + `fs.readdirSync` (повертає `[]`, якщо каталога немає).
102
+ - `deps.readFile` — функція `(file: string) => string`; за замовчуванням `fs.readFileSync(file, 'utf8')`.
103
+
104
+ **Повертає:** масив об'єктів-вузлів. Кожен вузол має поля:
105
+
106
+ | Поле | Тип | Значення |
107
+ | ------------ | ---------------- | --------------------------------------------------------------- |
108
+ | `stem` | `string` | `id-slug` стем артефакту |
109
+ | `id` | `string` | id вузла (із front-matter `plan` або `stem.split('-')[0]`) |
110
+ | `slug` | `string` | усе після першого `-` у `stem` |
111
+ | `dependsOn` | `string[]` | id-залежності з `plan.dependsOn` |
112
+ | `owner` | `string \| null` | власник з `plan.owner` |
113
+ | `hasClaim` | `boolean` | чи присутній `.claim`-артефакт |
114
+ | `hasFact` | `boolean` | чи присутній `.fact`-артефакт |
115
+ | `factStatus` | `string \| null` | значення `status` із front-matter `.fact` (за замовч. `'done'`) |
116
+ | `asks` | `string[]` | список `qid` з `.ask-<qid>.md` файлів |
117
+ | `answered` | `string[]` | список `qid` з `.ans-<qid>.md` файлів |
118
+
119
+ **Алгоритм:**
120
+
121
+ 1. Збирається шлях `dir = root/docs/graphs/<graph>/nodes`.
122
+ 2. Створюється `Map<stem, node>` та лінива функція `ensure(stem)`, яка ініціалізує запис при першому зверненні.
123
+ 3. Для кожного імені у `readdir(dir)`:
124
+ - класифікується через `classifyArtifact`; якщо `null` — пропускається;
125
+ - дістається запис вузла за `stem` через `ensure`;
126
+ - `kind` диспатчиться:
127
+ - `plan` — читається файл, парситься front-matter, переписуються `id`, `dependsOn`, `owner`;
128
+ - `claim` — встановлюється `hasClaim = true`;
129
+ - `fact` — `hasFact = true`, `factStatus` із front-matter або `'done'`;
130
+ - `ask` — `qid` додається в `asks`;
131
+ - `ans` — `qid` додається в `answered`.
132
+ 4. Повертається `[...byStem.values()]`.
133
+
134
+ **Side effects:**
135
+
136
+ - Дефолтні `readdir`/`readFile` читають з реальної ФС.
137
+ - При інжектованих `deps` функція повністю детермінована.
138
+
139
+ **Зауваги щодо стійкості:**
140
+
141
+ - `parseFrontMatter` може повернути `null` — fallback `?? {}` гарантує безпечний доступ до полів.
142
+ - Якщо в одному графі є кілька `.plan` файлів для одного `stem`, перемагає останній.
143
+
144
+ ---
145
+
146
+ ### `deriveStatus(node, doneSet)`
147
+
148
+ **Сигнатура:** `(node, doneSet: Set<string>) => 'done' | 'failed' | 'awaiting-human' | 'in_progress' | 'ready' | 'blocked'`
149
+
150
+ **Параметри:**
151
+
152
+ - `node` — об'єкт вузла з полями `hasFact`, `factStatus`, `hasClaim`, `asks`, `answered`, `dependsOn`.
153
+ - `doneSet` — множина `id` вузлів, які вже мають `fact` зі статусом ≠ `'failed'`.
154
+
155
+ **Повертає:** статус вузла. Логіка пріоритетів (зверху-вниз):
156
+
157
+ 1. `hasFact === true` → якщо `factStatus === 'failed'` → `'failed'`, інакше `'done'`.
158
+ 2. Інакше шукається відкрите питання: `openAsk = node.asks.some(q => !node.answered.includes(q))`.
159
+ 3. `hasClaim && openAsk` → `'awaiting-human'` (роботу взяли, але потрібна відповідь людини).
160
+ 4. `hasClaim` → `'in_progress'`.
161
+ 5. Усі `dependsOn` є в `doneSet` → `'ready'`.
162
+ 6. Інакше → `'blocked'`.
163
+
164
+ **Side effects:** немає (чиста функція).
165
+
166
+ ---
167
+
168
+ ### `deriveGraph(nodes)`
169
+
170
+ **Сигнатура:** `(nodes: Node[]) => (Node & { status })[]`
171
+
172
+ **Параметри:**
173
+
174
+ - `nodes` — масив вузлів від `scanGraph`.
175
+
176
+ **Повертає:** новий масив вузлів з доданим полем `status`.
177
+
178
+ **Алгоритм:**
179
+
180
+ 1. `doneSet` = множина `id` усіх вузлів, у яких `hasFact && factStatus !== 'failed'`.
181
+ 2. Кожен вузол мапиться в `{ ...n, status: deriveStatus(n, doneSet) }`.
182
+
183
+ **Side effects:** немає.
184
+
185
+ ---
186
+
187
+ ### `renderGraph(graph, nodes)`
188
+
189
+ **Сигнатура:** `(graph: string, nodes: Node[]) => string`
190
+
191
+ **Параметри:**
192
+
193
+ - `graph` — id графа (для заголовка).
194
+ - `nodes` — вузли з полем `status` (тобто результат `deriveGraph`).
195
+
196
+ **Повертає:** багаторядковий текстовий рендер.
197
+
198
+ **Формат виводу:**
199
+
200
+ ```
201
+ граф <graph> — <status1>:<n1> <status2>:<n2> ...
202
+ <id> · <slug> [<status>][ <owner>][ ←[dep1,dep2]]
203
+ ...
204
+ ```
205
+
206
+ - Якщо `nodes.length === 0` → `"граф <graph>: вузлів не знайдено"`.
207
+ - Заголовок міститиме лише ті статуси, для яких `count > 0`, в порядку `['in_progress', 'awaiting-human', 'ready', 'blocked', 'failed', 'done']`.
208
+ - Для кожного вузла: `id · slug [status]`, потім опційно ` <owner>` (якщо є), потім опційно ` ←[deps]` (якщо є залежності).
209
+
210
+ **Side effects:** немає.
211
+
212
+ ---
213
+
214
+ ### `listGraphs(root, readdir)` (internal)
215
+
216
+ **Сигнатура:** `(root: string, readdir: (dir: string) => string[]) => string[]`
217
+
218
+ Повертає список імен з `docs/graphs/`. Інжектована `readdir` дозволяє підставити dummy-FS. Експортовано не для зовнішнього вжитку — лише як локальний helper.
219
+
220
+ ---
221
+
222
+ ### `runGraphCli(args, deps?)`
223
+
224
+ **Сигнатура:** `(args: string[], deps?: { cwd?, readdir?, readFile?, log? }) => number`
225
+
226
+ **Параметри:**
227
+
228
+ - `args` — позиційні аргументи після слова `graph` у вихідному `argv` (тобто `[sub, graphArg]`).
229
+ - `deps.cwd` — root репо; за замовч. `process.cwd()`.
230
+ - `deps.readdir` — як у `scanGraph`/`listGraphs`.
231
+ - `deps.readFile` — як у `scanGraph`.
232
+ - `deps.log` — функція логування; за замовч. `console.log`.
233
+
234
+ **Повертає:** exit-код (`0` — все ok, `1` — невідома/відсутня підкоманда).
235
+
236
+ **Алгоритм:**
237
+
238
+ 1. `root = deps.cwd ?? process.cwd()`.
239
+ 2. `[sub, graphArg] = args`.
240
+ 3. Якщо `sub !== 'status'` → друкується usage-рядок `Usage: n-cursor graph status [<graph>]` і повертається `1`.
241
+ 4. Інакше:
242
+ - якщо `graphArg` заданий → `graphs = [graphArg]`, інакше — усі підкаталоги `docs/graphs/`.
243
+ - Якщо `graphs.length === 0` → `"graph: у docs/graphs/ немає графів"` і `0`.
244
+ - Інакше для кожного `g` друкується `renderGraph(g, deriveGraph(scanGraph(root, g, { readdir, readFile })))`.
245
+ 5. Повертається `0`.
246
+
247
+ **Side effects:**
248
+
249
+ - Викликає `log` (зазвичай `console.log`).
250
+ - Читає каталог `docs/graphs/` та файли вузлів через дефолтні `readdir`/`readFile`.
251
+ - Не пише в ФС.
252
+
253
+ ## Залежності
254
+
255
+ ### Імпорти з `node:` (стандартна бібліотека Node.js)
256
+
257
+ - `node:fs` — `existsSync`, `readdirSync`, `readFileSync`. Використовуються лише як дефолти для `deps.readdir`/`deps.readFile`.
258
+ - `node:path` — `join` для побудови шляхів `docs/graphs/<g>/nodes/<file>`.
259
+ - `node:process` — `cwd as processCwd` для дефолту `root`.
260
+
261
+ ### Імпорти з проєкту
262
+
263
+ - `./trace.mjs` — функція `parseFrontMatter(text)` для парсингу YAML-frontmatter у markdown-файлах артефактів.
264
+
265
+ ### Зовнішні залежності
266
+
267
+ Немає (npm-залежності не використовуються).
268
+
269
+ ### Контракт і пов'язані документи
270
+
271
+ - `docs/specs/2026-06-01-node-dag-state.md` — контракт стану DAG-вузлів (форма артефактів і деривація статусу).
272
+ - Структура каталогів: `docs/graphs/<graph>/nodes/<stem><suffix>.md`.
273
+
274
+ ## Потік виконання / Використання
275
+
276
+ ### CLI-сценарій
277
+
278
+ Файл імпортується диспатчером верхнього рівня (`n-cursor`) і викликається через `runGraphCli`. Очікувана форма виклику з shell:
279
+
280
+ ```
281
+ n-cursor graph status # усі графи в docs/graphs/
282
+ n-cursor graph status <graph> # лише один граф
283
+ n-cursor graph # друк usage, exit 1
284
+ n-cursor graph <other> # друк usage, exit 1
285
+ ```
286
+
287
+ ### Внутрішня pipeline за один граф
288
+
289
+ ```
290
+ readdir(docs/graphs/<g>/nodes)
291
+ → for each file: classifyArtifact + readFile (для plan/fact)
292
+ → scanGraph → Node[]
293
+ → deriveGraph → Node[] із полем status
294
+ → renderGraph → string
295
+ → log(string)
296
+ ```
297
+
298
+ ### Програмне використання (наприклад, у тестах)
299
+
300
+ ```js
301
+ import { runGraphCli, scanGraph, deriveGraph, renderGraph } from './graph.mjs'
302
+
303
+ const logs = []
304
+ const fakeReaddir = dir => {
305
+ if (dir.endsWith('docs/graphs')) return ['g1']
306
+ if (dir.endsWith('docs/graphs/g1/nodes'))
307
+ return ['B01-init.plan.md', 'B01-init.fact.md', 'B02-parser.plan.md', 'B02-parser.claim.md', 'B02-parser.ask-q1.md']
308
+ return []
309
+ }
310
+ const fakeReadFile = file => {
311
+ if (file.endsWith('B01-init.plan.md')) return '---\nid: B01\ndependsOn: []\nowner: alice\n---\n'
312
+ if (file.endsWith('B01-init.fact.md')) return '---\nstatus: done\n---\n'
313
+ if (file.endsWith('B02-parser.plan.md')) return '---\nid: B02\ndependsOn: [B01]\nowner: bob\n---\n'
314
+ return ''
315
+ }
316
+
317
+ const code = runGraphCli(['status'], {
318
+ cwd: '/repo',
319
+ readdir: fakeReaddir,
320
+ readFile: fakeReadFile,
321
+ log: m => logs.push(m)
322
+ })
323
+ // code === 0
324
+ // logs[0] починається з "граф g1 — awaiting-human:1 done:1"
325
+ ```
326
+
327
+ ### Розширення (планується контрактом)
328
+
329
+ Наступні зрізи DAG (поза цим файлом):
330
+
331
+ - `claim` — позначення взяття вузла в роботу.
332
+ - `tick` — інкрементальне оновлення стану.
333
+ - `dispatch` — диспатч готових (`ready`) вузлів виконавцям.
334
+
335
+ ### Тестованість
336
+
337
+ - Усі чисті функції (`classifyArtifact`, `parseIdList`, `deriveStatus`, `deriveGraph`, `renderGraph`) — без I/O, тестуються прямо.
338
+ - `scanGraph`, `runGraphCli`, `listGraphs` — отримують `readdir`/`readFile`/`log` через `deps`, що дозволяє повний unit-тест без диска.
339
+
340
+ ### Граничні випадки
341
+
342
+ - Каталог `docs/graphs/<g>/nodes` відсутній → дефолтний `readdir` повертає `[]` → `scanGraph` віддає порожній масив → `renderGraph` друкує `"граф <g>: вузлів не знайдено"`.
343
+ - Файл без розпізнаного суфікса → пропускається.
344
+ - `parseFrontMatter` повертає `null` → fallback `?? {}` запобігає винятку.
345
+ - Кілька `plan`/`fact` для одного `stem` → перемагає останній проскансований; `claim`/`ask`/`ans` — кумулятивні.
346
+ - `parseIdList` із вкладеними дужками (наприклад `"[[A], B]"`) — `replace` без флага `g` видалить лише першу пару, інші лишаться у значеннях.
@@ -0,0 +1,236 @@
1
+ # `npm/scripts/dispatcher/index.mjs`
2
+
3
+ ## Огляд
4
+
5
+ Файл реалізує **CLI-диспетчер підкоманди `n-cursor flow`** — точку входу другого рівня, до якої делегує гілка `case 'flow'` у `bin/n-cursor.js`. Реалізація відповідає специфікації §8 "Dual-Mode Dispatcher" і надає два фасади поверх єдиного джерела істини `.flow.json`:
6
+
7
+ - **Фасад A — "Пасивний Турнікет"** (`init`, `spec`, `plan`, `verify`, `review`, `gate`, `release`). Призначений для IDE-агентів (Cursor, Claude Code), які самі пишуть код; `n-cursor` лише виносить вердикт (judge).
8
+ - **Фасад B — "Активний Раннер"** (`run`, `resume`, `cancel`, `repair`). Повний 5-фазний polyfill-цикл для headless/CI-сценаріїв, де агент відсутній.
9
+
10
+ Модуль не містить ні I/O-побічних ефектів сам по собі (окрім `console.error` для usage), ні бізнес-логіки фаз. Він виключно:
11
+
12
+ 1. парсить `argv` (підкоманда + опційний прапорець `--branch <гілка>`);
13
+ 2. валідовує наявність handler-а;
14
+ 3. делегує виклик до конкретного handler-модуля з директорії `./lib/`;
15
+ 4. повертає `exit code` (число).
16
+
17
+ Така архітектура дає змогу повністю мокати `handlers` через DI в тестах, а сам диспатчер тримати тонким і детермінованим.
18
+
19
+ ## Експорти / API
20
+
21
+ Модуль ESM (`.mjs`). Експортує:
22
+
23
+ | Експорт | Тип | Призначення |
24
+ | ------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `DEFAULT_HANDLERS` | `Record<string, (rest: string[], deps: object) => Promise<number>>` | Стандартна мапа підкоманд → handler-функцій (`init`, `spec`, `plan`, `verify`, `review`, `gate`, `release`, `run`, `resume`, `cancel`, `repair`). |
26
+ | `extractBranchFlag` | `function(args: string[]) → { rest: string[], branch: string \| undefined }` | Чистий парсер опційного `--branch <гілка>` / `--branch=<гілка>`; повертає очищений масив аргументів і значення гілки (або `undefined`). |
27
+ | `runFlowCli` | `async function(args: string[], deps?: object) → Promise<number>` | Власне точка входу диспатчера; маршрутизує підкоманду на handler і повертає exit code. |
28
+
29
+ Default-експорту немає — імпорт іменований.
30
+
31
+ ## Функції
32
+
33
+ ### `extractBranchFlag(args)`
34
+
35
+ **Сигнатура.**
36
+
37
+ ```js
38
+ export function extractBranchFlag(args: string[]): { rest: string[], branch: string | undefined }
39
+ ```
40
+
41
+ **Параметри.**
42
+
43
+ - `args: string[]` — масив "сирих" аргументів, які залишилися після виокремлення підкоманди (тобто `argv` без префіксу `flow <sub>`).
44
+
45
+ **Повертає.**
46
+
47
+ Об'єкт із двома полями:
48
+
49
+ - `rest: string[]` — очищений масив аргументів, з якого вилучено всі форми `--branch …`.
50
+ - `branch: string | undefined` — значення гілки, якщо було вказано непорожньою формою; інакше `undefined`.
51
+
52
+ **Підтримувані форми прапорця.**
53
+
54
+ - Пробільна форма: `--branch <гілка>` — поглинає **наступний** аргумент **лише** якщо він існує і не починається з `-` (щоб не "з'їсти" сусідній прапорець); інакше `--branch` тихо стає no-op без помилки.
55
+ - Inline-форма: `--branch=<гілка>` — значення береться суфіксом після `=`. Порожній суфікс (`--branch=`) ігнорується (гілку не встановлює).
56
+
57
+ **Особливості / захисти.**
58
+
59
+ - Захист від тихого "пожирання" сусіднього прапорця: `--branch --other-flag` не призведе до того, що `--other-flag` стане значенням гілки.
60
+ - Може зустрічатися кілька разів — **остання** валідна форма перемагає (стандартна семантика циклу: змінна `branch` перезаписується).
61
+ - Чиста функція: не залежить від `process`, файлової системи чи мережі.
62
+
63
+ **Side effects.** Жодних.
64
+
65
+ ---
66
+
67
+ ### `runFlowCli(args, deps)`
68
+
69
+ **Сигнатура.**
70
+
71
+ ```js
72
+ export async function runFlowCli(
73
+ args: string[],
74
+ deps?: {
75
+ handlers?: Record<string, (rest: string[], deps: object) => Promise<number>>,
76
+ branch?: string,
77
+ // …довільні інші deps, які потрібні конкретним handler-ам (логер, fs-мок тощо)
78
+ }
79
+ ): Promise<number>
80
+ ```
81
+
82
+ **Параметри.**
83
+
84
+ - `args: string[]` — масив аргументів **після** слова `flow`. Перший елемент трактується як підкоманда (`sub`), решта — як її аргументи (`raw`).
85
+ - `deps` — опційний об'єкт ін'єкції залежностей:
86
+ - `deps.handlers` — кастомна мапа handler-ів (для тестів / альтернативних збірок). Якщо не передано, використовується `DEFAULT_HANDLERS`.
87
+ - `deps.branch` — попередньо встановлена гілка з вищого рівня; має пріоритет над тією, що витягнута з `args` (`deps.branch ?? branch`).
88
+ - Інші довільні поля проходять "прозоро" до handler-а через spread `{ ...deps, branch: … }`.
89
+
90
+ **Повертає.**
91
+
92
+ `Promise<number>` — exit code:
93
+
94
+ - `1` — підкоманда відсутня (`!sub`) або невідома (`Object.hasOwn(handlers, sub) === false`). У цьому разі додатково надсилається текст `USAGE` у `stderr` через `console.error`.
95
+ - Будь-яке інше число — результат, який повернув викликаний handler.
96
+
97
+ **Side effects.**
98
+
99
+ - При невалідному вводі — запис у `stderr` (`console.error(USAGE)`).
100
+ - Делегує **всі** інші побічні ефекти (читання `.flow.json`, мутації worktree, git-операції тощо) у handler-модулі — сам диспатчер їх не виконує.
101
+
102
+ **Алгоритм (по кроках).**
103
+
104
+ 1. Деструктурує `args` як `[sub, ...raw]`.
105
+ 2. Бере `handlers = deps.handlers ?? DEFAULT_HANDLERS`.
106
+ 3. Перевірка валідності: якщо `sub` фолсі або `Object.hasOwn(handlers, sub)` === `false` → `console.error(USAGE)` і `return 1`. `Object.hasOwn` ужито замість `in`/`hasOwnProperty`, щоб уникнути колізій із prototype-полями (безпечніше для ін'єкційного `handlers`).
107
+ 4. Викликає `extractBranchFlag(raw)` → отримує `{ rest, branch }`.
108
+ 5. Викликає `await handlers[sub](rest, { ...deps, branch: deps.branch ?? branch })` і повертає його результат.
109
+ 6. Передача `branch` далі в `deps` забезпечує cwd-незалежний резолв стану (беклог №1 — `.flow.json` можна знайти й виконати команду поза worktree).
110
+
111
+ ## Залежності
112
+
113
+ Усі залежності — **локальні** ESM-імпорти з директорії `./lib/`. Кожен handler — окремий модуль; диспатчер не знає їхньої внутрішньої логіки.
114
+
115
+ | Імпорт | З модуля | Призначення / Фасад |
116
+ | ----------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
117
+ | `cancel`, `repair`, `resume`, `run` | `./lib/active.mjs` | Фасад B — Активний Раннер: повний 5-фазний цикл (`run`), продовження з чекпойнта (`resume`), скасування з прибиранням стану (`cancel`), відновлення пошкодженого стану (`repair`). |
118
+ | `init`, `release`, `verify` | `./lib/commands.mjs` | Фасад A — Турнікет: створення worktree + `.flow.json` (`init`), Quality Gates (`verify`), фіксація `.changes` + completion snapshot (`release`). |
119
+ | `gate` | `./lib/gate.mjs` | Фасад A: фінальний вердикт `PASS / CONCERNS / FAIL` (комбінований `verify + review`). |
120
+ | `plan` | `./lib/plan.mjs` | Фасад A: фаза плану → `docs/plans/<…>` + оновлення стану. |
121
+ | `review` | `./lib/review.mjs` | Фасад A: adversarial diff-review (інтенсивність залежить від `level`). |
122
+ | `spec` | `./lib/spec.mjs` | Фасад A: фаза дизайну → `docs/specs/<…>`. |
123
+
124
+ **Зовнішніх npm-залежностей немає** — модуль використовує лише вбудоване `console` API та `Object.hasOwn` (ECMAScript 2022+).
125
+
126
+ ## Потік виконання / Використання
127
+
128
+ ### Як викликається
129
+
130
+ Модуль вмонтований у CLI як обробник підкоманди `flow` у `bin/n-cursor.js`. Скорочена картина:
131
+
132
+ ```js
133
+ // у bin/n-cursor.js
134
+ import { runFlowCli } from '../npm/scripts/dispatcher/index.mjs'
135
+
136
+ switch (cmd) {
137
+ case 'flow':
138
+ process.exit(await runFlowCli(rest))
139
+ break
140
+ // …інші команди
141
+ }
142
+ ```
143
+
144
+ ### Приклади з CLI
145
+
146
+ Усі приклади з блоку `USAGE`, який видається у `stderr` при невалідному виклику:
147
+
148
+ ```text
149
+ npx @nitra/cursor flow init "<опис>" # Фасад A: worktree + .flow.json (+ level)
150
+ npx @nitra/cursor flow spec [--panel] # Фасад A: фаза дизайну → docs/specs/<…>
151
+ npx @nitra/cursor flow plan [--panel] # Фасад A: фаза плану → docs/plans/<…> + state
152
+ npx @nitra/cursor flow verify # Фасад A: Quality Gates (pass/fail)
153
+ npx @nitra/cursor flow review # Фасад A: adversarial diff-review (за level)
154
+ npx @nitra/cursor flow gate # Фасад A: вердикт PASS/CONCERNS/FAIL (verify+review)
155
+ npx @nitra/cursor flow release # Фасад A: .changes + completion snapshot
156
+ npx @nitra/cursor flow run "<опис>" # Фасад B: повний 5-фазний цикл
157
+ npx @nitra/cursor flow resume # продовжити з чекпойнта
158
+ npx @nitra/cursor flow cancel # скасувати, прибрати стан
159
+ npx @nitra/cursor flow repair [--discard-step-work] # відновлення пошкодженого стану
160
+ ```
161
+
162
+ ### Опційний `--branch <гілка>`
163
+
164
+ Підтримується **для будь-якої** підкоманди. Допомагає, коли користувач запускає `n-cursor flow …` **поза** worktree цільової задачі — гілка вказує, який `.flow.json` (з якого worktree-резолва) брати.
165
+
166
+ Приклади:
167
+
168
+ ```bash
169
+ npx @nitra/cursor flow verify --branch feature/payments
170
+ npx @nitra/cursor flow verify --branch=feature/payments
171
+ ```
172
+
173
+ ### Програмний виклик (для тестів)
174
+
175
+ `runFlowCli` повністю замокується через DI:
176
+
177
+ ```js
178
+ import { runFlowCli } from './npm/scripts/dispatcher/index.mjs'
179
+
180
+ const calls = []
181
+ const fakeHandlers = {
182
+ verify: async (rest, deps) => {
183
+ calls.push({ rest, deps })
184
+ return 0
185
+ }
186
+ }
187
+
188
+ const code = await runFlowCli(['verify', '--branch', 'main'], { handlers: fakeHandlers })
189
+ // code === 0
190
+ // calls[0].deps.branch === 'main'
191
+ // calls[0].rest === []
192
+ ```
193
+
194
+ ### Послідовність кроків `runFlowCli`
195
+
196
+ 1. Деструктуризація `args` → `sub` + `raw`.
197
+ 2. Підбір мапи handler-ів (`deps.handlers ?? DEFAULT_HANDLERS`).
198
+ 3. Валідація `sub` через `Object.hasOwn(handlers, sub)`.
199
+ - Якщо невалідно → `console.error(USAGE)` + `return 1`.
200
+ 4. Парсинг `--branch` через `extractBranchFlag(raw)` → `{ rest, branch }`.
201
+ 5. Виклик `handlers[sub](rest, { ...deps, branch: deps.branch ?? branch })`.
202
+ 6. Повернення exit code від handler-а як результату Promise.
203
+
204
+ ### Коди завершення
205
+
206
+ | Код | Коли | Хто повертає |
207
+ | -------------- | ------------------------------------------------------------------------ | ------------------------------ |
208
+ | `1` | відсутня або невідома підкоманда (`!sub` чи `Object.hasOwn` === `false`) | сам диспатчер |
209
+ | будь-яке число | результат handler-а | відповідний модуль із `./lib/` |
210
+
211
+ ### Обмеження та інваріанти
212
+
213
+ - Диспатчер **не** інтерпретує жоден аргумент окрім `--branch`; усі інші опції-прапорці (`--panel`, `--discard-step-work` тощо) прокидаються в handler як частина `rest`.
214
+ - Усі handler-и повинні повертати `Promise<number>`; синхронний throw обірве промісі та "вибухне" вище — це навмисно (диспатчер не маскує внутрішні помилки фаз).
215
+ - Кеш стану й бізнес-логіка фаз — поза цим файлом; його роль виключно — маршрутизація.
216
+
217
+ ## Rebuild Test
218
+
219
+ Якщо реалізацію цього файлу повністю видалити, з опису вище має бути можливо відтворити її без читання оригіналу:
220
+
221
+ 1. ESM-модуль із трьома експортами: `DEFAULT_HANDLERS`, `extractBranchFlag`, `runFlowCli`.
222
+ 2. Імпортувати 11 handler-ів із 6 модулів у `./lib/` (`active.mjs` → 4 шт., `commands.mjs` → 3 шт., `gate.mjs`, `plan.mjs`, `review.mjs`, `spec.mjs` → по 1 шт.) і скласти в `DEFAULT_HANDLERS`.
223
+ 3. `extractBranchFlag(args)` — лінійний прохід `for`-циклом по `args`:
224
+ - якщо елемент `=== '--branch'` — спробувати взяти `args[i+1]` як значення, якщо `!== undefined && !startsWith('-')`, інакше залишити `branch` як є; обидва випадки пропускають елемент (не пушать у `rest`); якщо значення взято — `i++` додатково;
225
+ - інакше якщо елемент `startsWith('--branch=')` — взяти суфікс після `=`, якщо непорожній — присвоїти `branch`; не пушати в `rest`;
226
+ - інакше — `rest.push(args[i])`.
227
+ - Повернути `{ rest, branch }`.
228
+ 4. `runFlowCli(args, deps = {})`:
229
+ - `[sub, ...raw] = args`;
230
+ - `handlers = deps.handlers ?? DEFAULT_HANDLERS`;
231
+ - якщо `!sub || !Object.hasOwn(handlers, sub)` → `console.error(USAGE); return 1;`
232
+ - `{ rest, branch } = extractBranchFlag(raw)`;
233
+ - `return await handlers[sub](rest, { ...deps, branch: deps.branch ?? branch })`.
234
+ 5. `USAGE` — багаторядковий текстовий блок із прикладами використання всіх 11 підкоманд + рядок `--branch` у `repair` (опційний `[--discard-step-work]`).
235
+
236
+ Відтворена реалізація має пройти тести з `npm/scripts/dispatcher/tests/`, які перевіряють: маршрутизацію відомих підкоманд, повернення `1` для невідомих, прокидання `branch` із обох форм прапорця, відмову поглинати сусідній прапорець після `--branch` без значення, ігнорування порожньої форми `--branch=`, пріоритет `deps.branch` над парсингом і DI `handlers` для мок-тестів.