@nitra/cursor 4.1.2 → 5.0.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 (72) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/docs/n-cursor.md +1 -9
  3. package/bin/n-cursor.js +3 -25
  4. package/package.json +1 -1
  5. package/rules/docker/lib/docs/docker-mirror.md +1 -1
  6. package/rules/docker/lib/docs/docker-native-addon.md +1 -1
  7. package/rules/npm-module/npm-module.mdc +1 -1
  8. package/rules/npm-module/policy/npm_publish_yml/template/npm-publish.yml.snippet.yml +1 -1
  9. package/rules/test/coverage/coverage.mjs +9 -19
  10. package/rules/test/test.mdc +1 -1
  11. package/scripts/dispatcher/trace.mjs +4 -16
  12. package/scripts/docs/build-agents-commands.md +1 -1
  13. package/scripts/docs/worktree-cli.md +1 -1
  14. package/scripts/lib/changed-files.mjs +19 -3
  15. package/scripts/lib/sync-gitignore-worktree.mjs +4 -5
  16. package/scripts/worktree-cli.mjs +1 -2
  17. package/skills/docgen/js/docgen-gen.mjs +7 -7
  18. package/docs/flow.MD +0 -1364
  19. package/scripts/dispatcher/docs/graph.md +0 -346
  20. package/scripts/dispatcher/docs/index.md +0 -236
  21. package/scripts/dispatcher/docs/trace.md +0 -296
  22. package/scripts/dispatcher/graph/lib/cmd-init.mjs +0 -112
  23. package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +0 -96
  24. package/scripts/dispatcher/graph/lib/cmd-kill.mjs +0 -141
  25. package/scripts/dispatcher/graph/lib/cmd-plan.mjs +0 -142
  26. package/scripts/dispatcher/graph/lib/cmd-run.mjs +0 -328
  27. package/scripts/dispatcher/graph/lib/cmd-scan.mjs +0 -115
  28. package/scripts/dispatcher/graph/lib/cmd-setup.mjs +0 -111
  29. package/scripts/dispatcher/graph/lib/cmd-signals.mjs +0 -328
  30. package/scripts/dispatcher/graph/lib/cmd-status.mjs +0 -131
  31. package/scripts/dispatcher/graph/lib/cmd-verify.mjs +0 -100
  32. package/scripts/dispatcher/graph/lib/cmd-watch.mjs +0 -128
  33. package/scripts/dispatcher/graph/lib/config.mjs +0 -103
  34. package/scripts/dispatcher/graph/lib/frontmatter.mjs +0 -224
  35. package/scripts/dispatcher/graph/lib/nnn.mjs +0 -127
  36. package/scripts/dispatcher/graph/lib/node-state.mjs +0 -157
  37. package/scripts/dispatcher/graph/lib/scanner.mjs +0 -235
  38. package/scripts/dispatcher/graph/lib/worktree-ops.mjs +0 -193
  39. package/scripts/dispatcher/graph-tasks.mjs +0 -92
  40. package/scripts/dispatcher/graph.mjs +0 -212
  41. package/scripts/dispatcher/index.mjs +0 -45
  42. package/scripts/dispatcher/lib/docs/active.md +0 -348
  43. package/scripts/dispatcher/lib/docs/artifact.md +0 -232
  44. package/scripts/dispatcher/lib/docs/budget.md +0 -167
  45. package/scripts/dispatcher/lib/docs/capability.md +0 -196
  46. package/scripts/dispatcher/lib/docs/commands.md +0 -210
  47. package/scripts/dispatcher/lib/docs/events.md +0 -183
  48. package/scripts/dispatcher/lib/docs/executor.md +0 -190
  49. package/scripts/dispatcher/lib/docs/gate.md +0 -231
  50. package/scripts/dispatcher/lib/docs/level.md +0 -335
  51. package/scripts/dispatcher/lib/docs/plan-panel.md +0 -181
  52. package/scripts/dispatcher/lib/docs/plan.md +0 -200
  53. package/scripts/dispatcher/lib/docs/planner.md +0 -269
  54. package/scripts/dispatcher/lib/docs/review.md +0 -255
  55. package/scripts/dispatcher/lib/docs/reviewer.md +0 -240
  56. package/scripts/dispatcher/lib/docs/snapshot.md +0 -247
  57. package/scripts/dispatcher/lib/docs/spec.md +0 -203
  58. package/scripts/dispatcher/lib/docs/state-store.md +0 -303
  59. package/scripts/dispatcher/lib/docs/subagent-runner.md +0 -173
  60. package/scripts/dispatcher/lib/events.mjs +0 -67
  61. package/scripts/dispatcher/lib/executor.mjs +0 -107
  62. package/scripts/dispatcher/lib/plan-panel.mjs +0 -76
  63. package/scripts/dispatcher/lib/state-store.mjs +0 -173
  64. package/scripts/dispatcher/lib/subagent-runner.mjs +0 -53
  65. package/scripts/graph/index.mjs +0 -115
  66. package/scripts/graph/lib/config.mjs +0 -62
  67. package/scripts/graph/lib/dag.mjs +0 -161
  68. package/scripts/graph/lib/frontmatter.mjs +0 -70
  69. package/scripts/graph/lib/nnn.mjs +0 -77
  70. package/scripts/graph/lib/state.mjs +0 -110
  71. package/scripts/graph/scan.mjs +0 -64
  72. package/scripts/graph/status.mjs +0 -86
@@ -1,203 +0,0 @@
1
- # spec.mjs
2
-
3
- ## Огляд
4
-
5
- Модуль реалізує CLI-підкоманду `flow spec [--panel] [<spec.md>]` — фазу дизайну в lifecycle Пасивного Турнікета (§3 правила `flow.mdc`). Її призначення — зафіксувати у файлі стану воркфлоу (`flow-state`) шлях до spec-документа (`docs/specs/<date>-<slug>.md`), отриманого внаслідок brainstorm-сесії, та виконати read-only верифікацію ланцюга трасування (`trace`) між артефактами ADR → spec → plan.
6
-
7
- Ключові властивості:
8
-
9
- - **Код не пишеться:** модуль лише оновлює state-store та логує події, лінки front-matter (`adr/spec/plan`) у самому документі формує агент за контрактом `flow.mdc`.
10
- - **Brainstorm two-track:** human↔agent відбувається у звичайному IDE-діалозі; agent↔agent — через прапор `--panel`, який запускає синтез персон та суддю через `runPanel`.
11
- - **Risk-driven review:** значення `risk` із front-matter spec-документа (`low | med | high`) перетікає у стан, керуючи глибиною подальшої фази `flow review`.
12
- - **Worktree-aware:** якщо `cwd` поза worktree, активний flow автоматично резолвиться через `resolveActiveFlowState`.
13
-
14
- Модуль є чистою функцією верхнього рівня з контрольованими ін'єкціями залежностей (`deps`), що робить його придатним для unit-тестування без файлової системи реального проєкту.
15
-
16
- ## Експорти / API
17
-
18
- | Експорт | Тип | Призначення |
19
- | ------------------- | ---------------- | ---------------------------------------------------------------------------------------- |
20
- | `spec(rest, deps?)` | `async function` | Точка входу CLI-підкоманди `flow spec`. Повертає exit code (`0` — успіх, `1` — помилка). |
21
-
22
- Внутрішні (не експортуються):
23
-
24
- | Ідентифікатор | Тип | Призначення |
25
- | ---------------------------- | ------------- | ------------------------------------------------------------------------- |
26
- | `RISKS` | `Set<string>` | Допустимі рівні ризику: `low`, `med`, `high`. |
27
- | `riskFromSpec(doc, current)` | `function` | Зчитує валідний `risk` зі spec-frontmatter або повертає поточний у стані. |
28
-
29
- ## Функції
30
-
31
- ### `RISKS`
32
-
33
- ```text
34
- const RISKS = new Set(['low', 'med', 'high'])
35
- ```
36
-
37
- Замкнений перелік допустимих значень поля `risk` у front-matter spec-документа. Будь-яке інше значення (включно з `undefined`, опискою, або відсутністю фронт-матеру) ігнорується — у такому разі ризик у стані не змінюється.
38
-
39
- ### `riskFromSpec(doc, current)`
40
-
41
- **Сигнатура:**
42
-
43
- ```js
44
- function riskFromSpec(doc: string, current: string | undefined): string | undefined
45
- ```
46
-
47
- **Параметри:**
48
-
49
- - `doc` (`string`) — абсолютний або відносний шлях до spec-документа (`*.md`).
50
- - `current` (`string | undefined`) — поточне значення `risk` із state-store; використовується як fallback, коли в документі не вказано валідного ризику.
51
-
52
- **Повертає:** `string | undefined` — рівень ризику (`low`, `med`, `high`) або поточне значення.
53
-
54
- **Семантика:**
55
-
56
- 1. Зчитує вміст файлу `doc` як UTF-8 рядок.
57
- 2. Парсить front-matter через `parseFrontMatter`.
58
- 3. Якщо у фронт-матері є поле `risk` і воно є членом `RISKS` — повертає його.
59
- 4. Інакше повертає `current` (без модифікації).
60
-
61
- **Side effects:**
62
-
63
- - Виконує синхронний read файлової системи (`readFileSync`).
64
- - Будь-який виняток (відсутній файл, помилка парсингу, прав доступу) поглинається `try/catch` і функція повертає `current` — це гарантує, що відсутність front-matter не блокує транзицію стану.
65
-
66
- ### `spec(rest, deps)`
67
-
68
- **Сигнатура:**
69
-
70
- ```js
71
- async function spec(
72
- rest: string[],
73
- deps?: {
74
- cwd?: string,
75
- branch?: string,
76
- log?: (m: string) => void,
77
- runner?: object,
78
- trace?: (cwd: string) => number,
79
- now?: () => number
80
- }
81
- ): Promise<number>
82
- ```
83
-
84
- **Параметри:**
85
-
86
- - `rest` (`string[]`) — позиційні аргументи CLI після підкоманди `spec`. Розпізнаються:
87
- - `--panel` — флаг увімкнення agent-panel brainstorm.
88
- - Будь-який аргумент, що закінчується на `.md` — явний шлях до spec-документа (інакше використовується `resolveArtifact`).
89
- - `deps` (`object`, опційно) — ін'єкції для тестування та advanced use cases:
90
- - `cwd` — стартовий робочий каталог (default: `process.cwd()`).
91
- - `branch` — гілка для резолву активного flow (передається у `resolveActiveFlowState`).
92
- - `log` — функція логування (default: `console.error`).
93
- - `runner` — попередньо створений subagent runner (інакше викликається `createRunner(deps)`).
94
- - `trace` — функція трасування для `verifyTrace`.
95
- - `now` — джерело часу для `recordTransition` (default: `Date.now`).
96
-
97
- **Повертає:** `Promise<number>` — exit code:
98
-
99
- - `0` — транзиція стану успішно записана.
100
- - `1` — помилка резолву стану / відсутній state / помилка створення runner / відсутній spec-документ.
101
-
102
- **Потік виконання:**
103
-
104
- 1. **Резолв активного flow.** Викликає `resolveActiveFlowState({ cwd, branch }, deps)`. Якщо `statePath` не визначено — логує помилку й повертає `1`. Якщо flow авторезолвлено (cwd поза worktree) — логує інформаційне повідомлення з міткою.
105
- 2. **Читання стану.** `readState(statePath)` повертає об'єкт стану або `null`. У разі `null` — логує підказку `flow init` і повертає `1`.
106
- 3. **Опційний panel-brainstorm.** Якщо `rest` містить `--panel`:
107
- - Створює runner через `createRunner(deps)` (якщо не передано в `deps.runner`); виняток → лог + `1`.
108
- - Викликає `runPanel({ task: state.branch, cwd, runner, log, mode: 'spec' })`.
109
- - Якщо повернувся синтез — логує його (з підказкою зберегти в `docs/specs/` і повторити `flow spec`). Об'єктний синтез серіалізується через `JSON.stringify`.
110
- - **Важливо:** після `--panel` функція не виходить — продовжує спробу резолву документа (наступний крок). Це дозволяє за один виклик і синтезувати, і зафіксувати.
111
- 4. **Резолв документа.** Шукає в `rest` перший аргумент, що закінчується на `.md`; інакше — `resolveArtifact(cwd, 'specs', state.branch)`. Якщо документ не знайдено або файл не існує — логує підказку про brainstorm і повертає `1`.
112
- 5. **Trace-верифікація.** `verifyTrace(cwd, deps.trace)` — read-only перевірка ланцюга front-matter (adr/spec/plan). Якщо ланцюг розірвано — лише warning у лог (не фатально).
113
- 6. **Обчислення risk.** `risk = riskFromSpec(doc, state.risk)` — front-matter spec має пріоритет над поточним.
114
- 7. **Запис транзиції.** `recordTransition` із параметрами:
115
- - `paths`: `{ statePath, eventsPath: flowEventsPath(cwd) }`.
116
- - `event`: `{ type: 'spec' }`.
117
- - `mutator`: `s => ({ ...s, spec_doc: doc, risk, status: 'spec' })`.
118
- - `clock`: `deps.now ?? Date.now`.
119
- 8. **Завершення.** Лог `spec: зафіксовано <doc> → status: spec (risk <risk|—>)` і повернення `0`.
120
-
121
- **Side effects:**
122
-
123
- - Логування у stderr (`console.error` або кастомний `log`).
124
- - Read-only доступ до файлової системи (`existsSync`, `readFileSync` у `riskFromSpec`).
125
- - Запис у state-store та events-log через `recordTransition`.
126
- - Можливий запуск subagent-runner (panel mode), який сам має побічні ефекти (мережа/IPC).
127
-
128
- ## Залежності
129
-
130
- ### Стандартна бібліотека Node.js
131
-
132
- | Імпорт | Звідки | Використання |
133
- | ------------------- | -------------- | --------------------------------------------------------------------- |
134
- | `existsSync` | `node:fs` | Перевірка існування spec-документа перед записом транзиції. |
135
- | `readFileSync` | `node:fs` | Зчитування spec-документа для парсингу front-matter у `riskFromSpec`. |
136
- | `cwd as processCwd` | `node:process` | Default-значення для `deps.cwd`. |
137
-
138
- ### Внутрішні модулі
139
-
140
- | Імпорт | Шлях | Призначення |
141
- | -------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------- |
142
- | `resolveArtifact`, `verifyTrace` | `./artifact.mjs` | Резолв шляху артефакту за конвенцією `docs/<kind>/<date>-<slug>.md` та верифікація trace-ланцюга front-matter. |
143
- | `flowEventsPath` | `./events.mjs` | Шлях до файлу подій воркфлоу (для аудиту транзицій). |
144
- | `runPanel` | `./plan-panel.mjs` | Запуск agent-panel brainstorm у режимі `mode: 'spec'`. |
145
- | `createRunner` | `./subagent-runner.mjs` | Фабрика subagent runner для panel mode. |
146
- | `readState`, `recordTransition` | `./state-store.mjs` | Читання поточного стану та атомарний запис транзиції з мутатором. |
147
- | `resolveActiveFlowState` | `./flow-resolve.mjs` | Авторезолв активного flow (worktree-awareness, branch fallback). |
148
- | `parseFrontMatter` | `../trace.mjs` | Парсинг YAML-подібного front-matter Markdown-документа. |
149
-
150
- ## Потік виконання / Використання
151
-
152
- ### CLI-сценарій
153
-
154
- ```bash
155
- # 1. Простий запис: spec-документ резолвиться за конвенцією branch → docs/specs/
156
- flow spec
157
-
158
- # 2. Явний шлях до документа
159
- flow spec docs/specs/2026-01-15-new-feature.md
160
-
161
- # 3. З agent-panel brainstorm (синтез персон, потім збереження документа вручну)
162
- flow spec --panel
163
-
164
- # 4. Комбіноване: panel + явний документ
165
- flow spec --panel docs/specs/2026-01-15-new-feature.md
166
- ```
167
-
168
- ### Граф станів
169
-
170
- ```
171
- flow init → status: init
172
-
173
- (brainstorm в IDE або через --panel)
174
-
175
- flow spec → status: spec, spec_doc=<path>, risk=<low|med|high>
176
-
177
- flow plan / flow review / ...
178
- ```
179
-
180
- ### Інваріанти
181
-
182
- - Перед `flow spec` обов'язково має бути виконано `flow init` (інакше — exit 1).
183
- - Spec-документ має існувати на ФС (`existsSync`).
184
- - Trace-розрив **не** блокує транзицію — лише warning (бо лінки пише агент і може не встигнути на момент запуску).
185
- - Panel-mode не перериває основний flow: навіть якщо синтез повернувся, функція продовжує спробу резолвити документ. Якщо документу ще нема — exit 1 з підказкою зберегти результат.
186
-
187
- ### Контракт із `flow.mdc`
188
-
189
- - Контракт front-matter (поля `adr`, `spec`, `plan`, `risk`) — відповідальність агента, що генерує/редагує Markdown-документ.
190
- - `risk` із spec має пріоритет над попереднім `state.risk` — це механізм downstream-керування глибиною review.
191
- - Подія `{ type: 'spec' }` фіксується у `events.jsonl` для подальшого аудиту lifecycle.
192
-
193
- ### Тестованість
194
-
195
- Усі зовнішні точки контакту проброшено через `deps`:
196
-
197
- - `cwd`, `branch` — детермінований резолв flow.
198
- - `log` — capture логів у тестах.
199
- - `runner` — мок subagent без реальних викликів LLM.
200
- - `trace` — мок trace-перевірки.
201
- - `now` — детерміновані timestamps у events.
202
-
203
- Це дозволяє покрити модуль unit-тестами без файлової системи (тільки `readFileSync`/`existsSync` потребують фейкових файлів або моків `node:fs`).
@@ -1,303 +0,0 @@
1
- # state-store.mjs
2
-
3
- ## Огляд
4
-
5
- Модуль `state-store.mjs` реалізує **crash-safe сховище runtime-стану `flow`** (відповідно до spec §4 та §4.1 правила `n-flow`). Він зберігає поточний стан виконання worktree-флоу у вигляді JSON-файла, гарантує атомарність запису та fail-closed поведінку при пошкодженні даних.
6
-
7
- Ключові архітектурні рішення:
8
-
9
- - **Sibling-файл, а не файл усередині worktree.** Файл стану розміщується **поруч** із checkout-директорією worktree, а не всередині неї. Для checkout-директорії `…/.worktrees/feat-x` файл стану — `…/.worktrees/feat-x.flow.json`. Причина: файл усередині worktree був би `untracked` у feature-гілці й міг би випадково потрапити у `git add -A`.
10
- - **Атомарний запис** — через temp-файл на тому самому файловому системному рівні, `fsync` даних і атомарний `rename` (POSIX-гарантія: операція або повністю успішна, або не відбулась).
11
- - **Fail-closed на corruption** — будь-яка некоректність (невалідний JSON, несумісний `schema_version`) призводить до `throw`, а не до тихого скидання стану. Принцип: краще зупинити flow, ніж стартувати новий поверх зіпсованого стану.
12
- - **WAL-перехід** — пара `appendEvent` (журнал) → `updateState` (snapshot). Журнал — джерело істини для reconcile при `resume`, snapshot — швидкий зріз поточного стану.
13
- - **Усі шляхи абсолютні** — вимога правила `no-relative-fs-path`. Кожна публічна функція робить `isAbsolute()`-перевірку й кидає, якщо їй передали відносний шлях.
14
-
15
- ## Експорти / API
16
-
17
- | Експорт | Тип | Призначення |
18
- | ------------------------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------- |
19
- | `SCHEMA_VERSION` | `const number` (= `1`) | Поточна версія схеми JSON-стану. Несумісність → fail-closed read. |
20
- | `flowStatePath(worktreeDir)` | function | Дериватор шляху sibling-файла стану з абсолютного шляху worktree-checkout. |
21
- | `writeState(statePath, state)` | function | Атомарний запис стану з автоматичним проставленням `schema_version`. |
22
- | `readState(statePath)` | function | Читання стану з валідацією `schema_version`; `null`, якщо файлу нема. |
23
- | `updateState(statePath, fn)` | function | Read-modify-write: читає, прогонить через трансформер `fn`, атомарно пише. |
24
- | `removeState(statePath)` | function | Ідемпотентне видалення sibling-файла стану. |
25
- | `recordTransition({ statePath, eventsPath }, event, stateFn, now?)` | function | WAL-перехід: спершу `appendEvent`, потім `updateState`. |
26
- | `cleanupFlowSiblings(worktreeDir)` | function | Видалення всіх runtime-sibling-ів worktree (`.flow.json`, `.events.jsonl`, лок-каталог). |
27
-
28
- ## Функції
29
-
30
- ### `flowStatePath(worktreeDir)`
31
-
32
- **Сигнатура:** `(worktreeDir: string) => string`
33
-
34
- **Параметри:**
35
-
36
- - `worktreeDir` — абсолютний шлях checkout-директорії worktree (наприклад, `…/.worktrees/feat-x`).
37
-
38
- **Повертає:** Абсолютний шлях sibling-файла стану виду `…/.worktrees/feat-x.flow.json`.
39
-
40
- **Логіка:** Бере `dirname(worktreeDir)` (батьківську теку, як правило `.worktrees/`) і конкатенує з `basename(worktreeDir) + '.flow.json'`.
41
-
42
- **Помилки:** `Error('flowStatePath: очікується абсолютний шлях …')` — якщо `worktreeDir` не абсолютний.
43
-
44
- **Side effects:** Немає (чиста функція над шляхами).
45
-
46
- ---
47
-
48
- ### `fsyncPath(path)` (внутрішня)
49
-
50
- **Сигнатура:** `(path: string) => void`
51
-
52
- **Параметри:**
53
-
54
- - `path` — абсолютний шлях до файла або каталогу, який треба `fsync`-нути.
55
-
56
- **Повертає:** `void`.
57
-
58
- **Логіка:** Відкриває файл/каталог у режимі читання (`openSync(path, 'r')`), викликає `fsyncSync(fd)`, у `finally` закриває дескриптор через `closeSync`.
59
-
60
- **Side effects:** Системний виклик `fsync` — гарантує, що дані файла записані на фізичний носій (необхідно перед `rename`, щоб уникнути ситуації, коли rename видно, а вміст ще в буфері).
61
-
62
- **Примітка:** Функція приватна (не експортується), використовується лише `writeState`.
63
-
64
- ---
65
-
66
- ### `writeState(statePath, state)`
67
-
68
- **Сигнатура:** `(statePath: string, state: object) => object`
69
-
70
- **Параметри:**
71
-
72
- - `statePath` — абсолютний шлях кінцевого файла стану (`.flow.json`).
73
- - `state` — об'єкт стану **без** поля `schema_version` (воно проставляється автоматично).
74
-
75
- **Повертає:** Фактично записаний об'єкт виду `{ schema_version: SCHEMA_VERSION, ...state }`.
76
-
77
- **Алгоритм (атомарний запис):**
78
-
79
- 1. Перевірка абсолютності шляху → `throw`, якщо відносний.
80
- 2. `mkdirSync(dir, { recursive: true })` — гарантуємо існування батьківської теки.
81
- 3. Збираємо `payload = { schema_version: SCHEMA_VERSION, ...state }`.
82
- 4. Генеруємо унікальне ім'я temp-файла: `.${basename(statePath)}.${pid}.${randomHex6}.tmp` у тій самій теці (важливо — той самий FS, щоб `rename` був атомарним).
83
- 5. `writeFileSync(tmp, JSON.stringify(payload, null, 2) + '\n', 'utf8')`.
84
- 6. `fsyncPath(tmp)` — flush даних temp-файла на диск.
85
- 7. `renameSync(tmp, statePath)` — атомарна заміна.
86
- 8. Best-effort `fsyncPath(dir)` — fsync батьківського каталогу для durability rename. На Windows може кинути `EISDIR`/`EPERM` — помилка проковтується (некритично).
87
-
88
- **Помилки:** `Error('writeState: очікується абсолютний шлях …')` — якщо `statePath` не абсолютний. Інші помилки I/O (нема прав, диск повний тощо) пробрасуються нагору.
89
-
90
- **Side effects:**
91
-
92
- - Створення батьківської теки (якщо її нема).
93
- - Створення й видалення temp-файла з PID та випадковим суфіксом.
94
- - Перейменування `tmp → statePath`.
95
- - `fsync` файла та (best-effort) каталогу.
96
-
97
- ---
98
-
99
- ### `readState(statePath)`
100
-
101
- **Сигнатура:** `(statePath: string) => object | null`
102
-
103
- **Параметри:**
104
-
105
- - `statePath` — абсолютний шлях `.flow.json`.
106
-
107
- **Повертає:**
108
-
109
- - `null`, якщо файлу не існує (нормальна ситуація: flow ще не починався).
110
- - Розпарсений об'єкт стану — за успішного читання й валідації.
111
-
112
- **Алгоритм:**
113
-
114
- 1. Перевірка абсолютності шляху.
115
- 2. `existsSync(statePath)` → `false` → повертаємо `null`.
116
- 3. `readFileSync(statePath, 'utf8')`.
117
- 4. Спроба `JSON.parse(raw)`; якщо `SyntaxError` → `throw` з повідомленням `пошкоджений стан (невалідний JSON) … fail-closed`.
118
- 5. Валідація типу й `schema_version`: якщо не об'єкт, `null`, або `schema_version !== SCHEMA_VERSION` → `throw` `несумісний або пошкоджений schema_version … fail-closed`.
119
- 6. Повертаємо розпарсений об'єкт.
120
-
121
- **Помилки:**
122
-
123
- - `Error('readState: очікується абсолютний шлях …')`.
124
- - `Error('readState: пошкоджений стан (невалідний JSON) … fail-closed')` (§4.1.6).
125
- - `Error('readState: несумісний або пошкоджений schema_version … fail-closed')` (§4.1.6).
126
-
127
- **Side effects:** Тільки читання файла (без модифікацій).
128
-
129
- ---
130
-
131
- ### `updateState(statePath, fn)`
132
-
133
- **Сигнатура:** `(statePath: string, fn: (state: object) => object) => object`
134
-
135
- **Параметри:**
136
-
137
- - `statePath` — абсолютний шлях `.flow.json`.
138
- - `fn` — функція-трансформер: приймає поточний стан (або `{}`, якщо файла нема) і повертає новий стан.
139
-
140
- **Повертає:** Записаний об'єкт (результат `writeState`).
141
-
142
- **Алгоритм:**
143
-
144
- 1. `current = readState(statePath)`.
145
- 2. `next = fn(current ?? {})` — якщо файла не існує, `fn` отримує порожній об'єкт.
146
- 3. `return writeState(statePath, next)`.
147
-
148
- **Помилки:** Будь-яке пробивання з `readState` чи `writeState`. Окрім того, помилки всередині `fn` пробросяться як є.
149
-
150
- **Side effects:** Read-modify-write — повний цикл (читання → трансформ → атомарний запис).
151
-
152
- ---
153
-
154
- ### `removeState(statePath)`
155
-
156
- **Сигнатура:** `(statePath: string) => void`
157
-
158
- **Параметри:**
159
-
160
- - `statePath` — абсолютний шлях `.flow.json`.
161
-
162
- **Повертає:** `void`.
163
-
164
- **Алгоритм:** `rmSync(statePath, { force: true })` — ідемпотентно (відсутній файл не помилка).
165
-
166
- **Помилки:** `Error('removeState: очікується абсолютний шлях …')`.
167
-
168
- **Side effects:** Видалення sibling-файла стану.
169
-
170
- **Контекст використання:** Cleanup при `worktree remove` або `flow cancel`.
171
-
172
- ---
173
-
174
- ### `recordTransition({ statePath, eventsPath }, event, stateFn, now?)`
175
-
176
- **Сигнатура:** `({ statePath: string, eventsPath: string }, event: object, stateFn: (state: object) => object, now?: () => number) => object`
177
-
178
- **Параметри:**
179
-
180
- - `paths.statePath` — абсолютний шлях `.flow.json`.
181
- - `paths.eventsPath` — абсолютний шлях журналу подій `.events.jsonl`.
182
- - `event` — об'єкт події переходу (формат визначається `appendEvent`).
183
- - `stateFn` — трансформер стану (як у `updateState`).
184
- - `now` — фабрика поточного часу в ms (за замовчуванням `Date.now`); використовується для тестів.
185
-
186
- **Повертає:** Записаний стан (результат `updateState`).
187
-
188
- **Алгоритм (WAL-перехід, §4.1.2):**
189
-
190
- 1. `appendEvent(eventsPath, event, now)` — спершу **журнал** (durable WAL-запис події).
191
- 2. `updateState(statePath, stateFn)` — потім snapshot.
192
-
193
- **Гарантія:** Якщо крок 2 впаде, подія в журналі вже durable; на `resume` reconcile-логіка зможе відновити стан зі snapshot + хвоста журналу.
194
-
195
- **Side effects:** Дописування в `.events.jsonl` та атомарний read-modify-write `.flow.json`.
196
-
197
- ---
198
-
199
- ### `cleanupFlowSiblings(worktreeDir)`
200
-
201
- **Сигнатура:** `(worktreeDir: string) => void`
202
-
203
- **Параметри:**
204
-
205
- - `worktreeDir` — абсолютний шлях checkout-директорії worktree (`…/.worktrees/feat-x`).
206
-
207
- **Повертає:** `void`.
208
-
209
- **Алгоритм:** Для `base = basename(worktreeDir)` та `dir = dirname(worktreeDir)` видаляє:
210
-
211
- 1. `<dir>/<base>.flow.json` — sibling-snapshot стану.
212
- 2. `<dir>/<base>.events.jsonl` — sibling-журнал подій.
213
- 3. `<dir>/.flow-lock-<base>/` — лок-каталог (з `recursive: true`).
214
-
215
- Усі `rmSync` з `force: true` — ідемпотентні.
216
-
217
- **Помилки:** `Error('cleanupFlowSiblings: очікується абсолютний шлях …')`.
218
-
219
- **Side effects:** Видалення трьох sibling-артефактів. Викликається з `flow cancel` та `worktree remove`. Інакше sibling-и осиротіють — git їх не чистить (бо вони поза worktree).
220
-
221
- ## Залежності
222
-
223
- ### Node.js stdlib
224
-
225
- - `node:fs` — `closeSync`, `existsSync`, `fsyncSync`, `mkdirSync`, `openSync`, `readFileSync`, `renameSync`, `rmSync`, `writeFileSync`.
226
- - `node:path` — `basename`, `dirname`, `isAbsolute`, `join`.
227
- - `node:crypto` — `randomBytes` (унікальне ім'я temp-файла).
228
- - `node:process` — `pid` (також для унікальності temp-імені, особливо при паралельних процесах).
229
-
230
- ### Внутрішні залежності модуля
231
-
232
- - `./events.mjs` — функція `appendEvent(eventsPath, event, now)`. Використовується лише в `recordTransition`.
233
-
234
- ### Логічні залежності (через дизайн, не через `import`)
235
-
236
- - Spec `n-flow` §4, §4.1, §4.1.2, §4.1.6 — формальні вимоги до crash-safety та fail-closed.
237
- - Конвенція розміщення worktree: усі worktree під `.worktrees/<branch>/`; sibling-файли — на одному рівні з checkout.
238
-
239
- ## Потік виконання / Використання
240
-
241
- ### Типовий життєвий цикл стану flow
242
-
243
- ```
244
- 1. Старт flow:
245
- const statePath = flowStatePath('/abs/path/.worktrees/feat-x')
246
- // → '/abs/path/.worktrees/feat-x.flow.json'
247
-
248
- 2. Перший запис (initial state):
249
- writeState(statePath, { stage: 'init', step: 0 })
250
- // → файл містить { schema_version: 1, stage: 'init', step: 0 }
251
-
252
- 3. Перехід стану з журналюванням (WAL):
253
- recordTransition(
254
- { statePath, eventsPath: statePath.replace('.flow.json', '.events.jsonl') },
255
- { type: 'stage_advanced', from: 'init', to: 'apply' },
256
- (s) => ({ ...s, stage: 'apply', step: s.step + 1 })
257
- )
258
-
259
- 4. Read-modify-write без події (рідко):
260
- updateState(statePath, (s) => ({ ...s, last_seen_at: Date.now() }))
261
-
262
- 5. Читання при resume:
263
- const state = readState(statePath)
264
- if (state === null) { /* новий flow */ }
265
- else { /* відновлення з state */ }
266
-
267
- 6. Cleanup на завершення / cancel:
268
- cleanupFlowSiblings('/abs/path/.worktrees/feat-x')
269
- // → видалить feat-x.flow.json, feat-x.events.jsonl, .flow-lock-feat-x/
270
- ```
271
-
272
- ### Гарантії crash-safety
273
-
274
- - **Crash під час `writeFileSync(tmp, …)`** — кінцевий файл `statePath` залишається попередньою версією; temp-файл — orphan (буде перезаписаний при наступному запуску завдяки `pid + randomBytes`).
275
- - **Crash між `writeFileSync` і `fsyncPath(tmp)`** — temp-файл може бути порожнім/частковим, але `rename` ще не виконано — `statePath` цілий.
276
- - **Crash між `fsyncPath(tmp)` і `renameSync`** — temp-файл цілий і durable, але не на місці; `statePath` цілий.
277
- - **Crash під час `renameSync`** — POSIX гарантує атомарність: або `statePath` — старий вміст, або новий, **ніколи частковий**.
278
- - **Crash після `renameSync` до fsync каталогу** — на більшості FS rename вже durable; fsync каталогу — best-effort страховка.
279
-
280
- ### Fail-closed reading
281
-
282
- При `readState`:
283
-
284
- - **Файл порожній / не JSON** → `throw 'пошкоджений стан (невалідний JSON) … fail-closed'`. Адмін має вручну інспектувати журнал подій та прийняти рішення (replay або видалення).
285
- - **`schema_version` відсутня / інша** → `throw 'несумісний або пошкоджений schema_version … fail-closed'`. Захищає від запуску нової версії dispatcher над станом старого формату й навпаки.
286
-
287
- ### Контекст у dispatcher
288
-
289
- Модуль є інфраструктурною частиною `npm/scripts/dispatcher/`. Виклики йдуть із вищих шарів (стейт-машини flow, CLI-команди `flow start/resume/cancel`, обробники сигналів). Модуль сам не керує життєвим циклом — він лише надає примітиви read/write/update/remove/transition та шляхові деривації.
290
-
291
- ## Rebuild Test
292
-
293
- Документація містить достатньо інформації, щоб відновити функціональність модуля з нуля:
294
-
295
- - Точні імпорти з Node stdlib та локального `./events.mjs`.
296
- - Константу `SCHEMA_VERSION = 1`.
297
- - Сім публічних функцій з повними сигнатурами, валідацією аргументів, алгоритмами та повідомленнями про помилки.
298
- - Внутрішню helper-функцію `fsyncPath` зі сценарієм використання.
299
- - Точну схему атомарного запису (temp у тій самій теці → write → fsync → rename → best-effort fsync каталогу).
300
- - Точну схему fail-closed reading (`null` для відсутнього файла, throw для пошкодженого/несумісного).
301
- - WAL-послідовність `appendEvent` → `updateState` у `recordTransition`.
302
- - Перелік усіх трьох sibling-артефактів для `cleanupFlowSiblings` (`.flow.json`, `.events.jsonl`, `.flow-lock-<base>/`).
303
- - Дериватор `flowStatePath`: `join(dirname(worktreeDir), basename(worktreeDir) + '.flow.json')`.