@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.
- package/CHANGELOG.md +22 -0
- package/bin/docs/n-cursor.md +1 -9
- package/bin/n-cursor.js +3 -25
- package/package.json +1 -1
- package/rules/docker/lib/docs/docker-mirror.md +1 -1
- package/rules/docker/lib/docs/docker-native-addon.md +1 -1
- package/rules/npm-module/npm-module.mdc +1 -1
- package/rules/npm-module/policy/npm_publish_yml/template/npm-publish.yml.snippet.yml +1 -1
- package/rules/test/coverage/coverage.mjs +9 -19
- package/rules/test/test.mdc +1 -1
- package/scripts/dispatcher/trace.mjs +4 -16
- package/scripts/docs/build-agents-commands.md +1 -1
- package/scripts/docs/worktree-cli.md +1 -1
- package/scripts/lib/changed-files.mjs +19 -3
- package/scripts/lib/sync-gitignore-worktree.mjs +4 -5
- package/scripts/worktree-cli.mjs +1 -2
- package/skills/docgen/js/docgen-gen.mjs +7 -7
- package/docs/flow.MD +0 -1364
- package/scripts/dispatcher/docs/graph.md +0 -346
- package/scripts/dispatcher/docs/index.md +0 -236
- package/scripts/dispatcher/docs/trace.md +0 -296
- package/scripts/dispatcher/graph/lib/cmd-init.mjs +0 -112
- package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +0 -96
- package/scripts/dispatcher/graph/lib/cmd-kill.mjs +0 -141
- package/scripts/dispatcher/graph/lib/cmd-plan.mjs +0 -142
- package/scripts/dispatcher/graph/lib/cmd-run.mjs +0 -328
- package/scripts/dispatcher/graph/lib/cmd-scan.mjs +0 -115
- package/scripts/dispatcher/graph/lib/cmd-setup.mjs +0 -111
- package/scripts/dispatcher/graph/lib/cmd-signals.mjs +0 -328
- package/scripts/dispatcher/graph/lib/cmd-status.mjs +0 -131
- package/scripts/dispatcher/graph/lib/cmd-verify.mjs +0 -100
- package/scripts/dispatcher/graph/lib/cmd-watch.mjs +0 -128
- package/scripts/dispatcher/graph/lib/config.mjs +0 -103
- package/scripts/dispatcher/graph/lib/frontmatter.mjs +0 -224
- package/scripts/dispatcher/graph/lib/nnn.mjs +0 -127
- package/scripts/dispatcher/graph/lib/node-state.mjs +0 -157
- package/scripts/dispatcher/graph/lib/scanner.mjs +0 -235
- package/scripts/dispatcher/graph/lib/worktree-ops.mjs +0 -193
- package/scripts/dispatcher/graph-tasks.mjs +0 -92
- package/scripts/dispatcher/graph.mjs +0 -212
- package/scripts/dispatcher/index.mjs +0 -45
- package/scripts/dispatcher/lib/docs/active.md +0 -348
- package/scripts/dispatcher/lib/docs/artifact.md +0 -232
- package/scripts/dispatcher/lib/docs/budget.md +0 -167
- package/scripts/dispatcher/lib/docs/capability.md +0 -196
- package/scripts/dispatcher/lib/docs/commands.md +0 -210
- package/scripts/dispatcher/lib/docs/events.md +0 -183
- package/scripts/dispatcher/lib/docs/executor.md +0 -190
- package/scripts/dispatcher/lib/docs/gate.md +0 -231
- package/scripts/dispatcher/lib/docs/level.md +0 -335
- package/scripts/dispatcher/lib/docs/plan-panel.md +0 -181
- package/scripts/dispatcher/lib/docs/plan.md +0 -200
- package/scripts/dispatcher/lib/docs/planner.md +0 -269
- package/scripts/dispatcher/lib/docs/review.md +0 -255
- package/scripts/dispatcher/lib/docs/reviewer.md +0 -240
- package/scripts/dispatcher/lib/docs/snapshot.md +0 -247
- package/scripts/dispatcher/lib/docs/spec.md +0 -203
- package/scripts/dispatcher/lib/docs/state-store.md +0 -303
- package/scripts/dispatcher/lib/docs/subagent-runner.md +0 -173
- package/scripts/dispatcher/lib/events.mjs +0 -67
- package/scripts/dispatcher/lib/executor.mjs +0 -107
- package/scripts/dispatcher/lib/plan-panel.mjs +0 -76
- package/scripts/dispatcher/lib/state-store.mjs +0 -173
- package/scripts/dispatcher/lib/subagent-runner.mjs +0 -53
- package/scripts/graph/index.mjs +0 -115
- package/scripts/graph/lib/config.mjs +0 -62
- package/scripts/graph/lib/dag.mjs +0 -161
- package/scripts/graph/lib/frontmatter.mjs +0 -70
- package/scripts/graph/lib/nnn.mjs +0 -77
- package/scripts/graph/lib/state.mjs +0 -110
- package/scripts/graph/scan.mjs +0 -64
- package/scripts/graph/status.mjs +0 -86
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
# reviewer.mjs
|
|
2
|
-
|
|
3
|
-
## Огляд
|
|
4
|
-
|
|
5
|
-
Модуль `reviewer.mjs` реалізує **Level-1 «Суддю»** з §8.4 специфікації Dispatcher-а: чистий, детермінований раннер **Quality Gates** (§5), що повертає структурований verdict про якість поточного робочого дерева.
|
|
6
|
-
|
|
7
|
-
Призначення:
|
|
8
|
-
|
|
9
|
-
- Запустити послідовність gate-перевірок (за замовчуванням — `lint` і `coverage --changed`) через **ін'єктований** runner-процесів.
|
|
10
|
-
- Зупинитись на першому проваленому gate (`fail-fast`) і повернути захоплений `stdout/stderr`.
|
|
11
|
-
- За умови **повного** проходження зняти `worktree-fingerprint` (відбиток дерева), щоб пізніше можна було відрізнити «свіжий» verdict від «протухлого» (stale): якщо файли змінилися після зняття fingerprint-а — попередній verdict більше нерелевантний.
|
|
12
|
-
|
|
13
|
-
Архітектурні принципи:
|
|
14
|
-
|
|
15
|
-
- Модуль **не знає** про LLM, API-ключі, мережу чи Anthropic SDK. Це **чистий FS/Git/процеси**-рівень.
|
|
16
|
-
- Всі побічні ефекти (виклик дочірніх процесів, обчислення fingerprint-а) **ін'єктуються через параметри** — це робить функцію тестопридатною без `child_process`-моків і без реальних запусків ESLint/Vitest/Stryker.
|
|
17
|
-
- Один і той самий `runReview` обслуговує два сценарії:
|
|
18
|
-
1. **Пасивний Турнікет** — команда `flow verify` (фінальна перевірка перед merge/commit).
|
|
19
|
-
2. **Активний Раннер** — per-step Ф4 (фаза 4 з flow-циклу, що оцінює крок ітерації).
|
|
20
|
-
- Gate-и за замовчуванням **scoped до змінених файлів**: `lint` — quick-режим через `changed-files.mjs`; `coverage --changed` — vitest `--changed` плюс Stryker `--mutate` по diff від base-гілки. Турнікет та per-step перевіряють лише змінене; повний прогін coverage — окрема операція (`bun run coverage`, скіл `/n-coverage-fix`).
|
|
21
|
-
|
|
22
|
-
## Експорти / API
|
|
23
|
-
|
|
24
|
-
Модуль експортує дві сутності:
|
|
25
|
-
|
|
26
|
-
| Експорт | Тип | Призначення |
|
|
27
|
-
| --------------- | ------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
28
|
-
| `DEFAULT_GATES` | `Array<{ name: string, cmd: string[] }>` (named export) | Канонічний список gate-ів за замовчуванням: `lint` і `coverage --changed`. |
|
|
29
|
-
| `runReview` | `function` (named export) | Виконує послідовність gate-ів і повертає `verdict`-об'єкт. |
|
|
30
|
-
|
|
31
|
-
Імпорт:
|
|
32
|
-
|
|
33
|
-
```js
|
|
34
|
-
import { runReview, DEFAULT_GATES } from './reviewer.mjs'
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### `DEFAULT_GATES`
|
|
38
|
-
|
|
39
|
-
Сталий масив із двома gate-ами, у фіксованому порядку:
|
|
40
|
-
|
|
41
|
-
```js
|
|
42
|
-
export const DEFAULT_GATES = [
|
|
43
|
-
{ name: 'lint', cmd: ['npx', '@nitra/cursor', 'lint'] },
|
|
44
|
-
{ name: 'coverage', cmd: ['npx', '@nitra/cursor', 'coverage', '--changed'] }
|
|
45
|
-
]
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Семантика полів:
|
|
49
|
-
|
|
50
|
-
- `name` — людиночитна назва gate-у; повертається у `verdict.gates[i].name`.
|
|
51
|
-
- `cmd` — масив `[executable, ...args]`. Перший елемент передається в `run` як виконуваний файл, решта — як аргументи.
|
|
52
|
-
|
|
53
|
-
Послідовність визначає **порядок** виконання та fail-fast-поведінку: `lint` запускається першим, бо він дешевший і ловить більшість регресій; `coverage` — другим, бо триваліший (включає тести + мутаційне тестування Stryker).
|
|
54
|
-
|
|
55
|
-
## Функції
|
|
56
|
-
|
|
57
|
-
### `runReview({ run, cwd, gates, fingerprint })`
|
|
58
|
-
|
|
59
|
-
Проганяє gate-и послідовно, повертає structured verdict.
|
|
60
|
-
|
|
61
|
-
**Сигнатура:**
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
runReview(input: {
|
|
65
|
-
run: (cmd: string, args: string[], opts: { cwd: string })
|
|
66
|
-
=> { status: number, stdout?: string, stderr?: string },
|
|
67
|
-
cwd: string,
|
|
68
|
-
gates?: Array<{ name: string, cmd: string[] }>,
|
|
69
|
-
fingerprint?: () => string | null
|
|
70
|
-
}): {
|
|
71
|
-
pass: boolean,
|
|
72
|
-
gates: Array<{ name: string, ok: boolean }>,
|
|
73
|
-
failedOutput: string | null,
|
|
74
|
-
fingerprint: string | null
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
**Параметри (всі — поля об'єктного аргументу):**
|
|
79
|
-
|
|
80
|
-
- `run` — **обов'язковий**. Синхронна (або синхронно-сумісна за shape) функція запуску дочірнього процесу. Має сигнатуру `(cmd, args, opts)` й мусить повертати об'єкт із принаймні `status: number`; опційно `stdout: string` і `stderr: string`. Передбачається, що це обгортка над `Bun.spawnSync` / `node:child_process.spawnSync`, але реалізація лишається за викликачем. Це **ін'єкція** — модуль сам процесів не породжує.
|
|
81
|
-
- `cwd` — **обов'язковий**. Робоча директорія, в якій виконувати кожен gate. Передається у виклик `run(..., { cwd })`. Має бути коренем worktree, з якого gate-команди (`npx @nitra/cursor lint` тощо) бачать правильну монорепу.
|
|
82
|
-
- `gates` — опційний. Перевизначає список gate-ів. За замовчуванням — `DEFAULT_GATES`. Дозволяє тестам/скриптам прогнати кастомний набір (наприклад, лише `lint`, або додати власний gate `typecheck`).
|
|
83
|
-
- `fingerprint` — опційний. Функція без аргументів, що повертає `string | null` — відбиток поточного стану worktree. За замовчуванням — `() => worktreeFingerprint()` (див. `../../utils/worktree-fingerprint.mjs`). Викликається **лише** у випадку повного pass.
|
|
84
|
-
|
|
85
|
-
**Повертає** verdict-об'єкт:
|
|
86
|
-
|
|
87
|
-
- `pass: boolean` — `true` лише якщо **всі** gate-и завершились зі статусом `0`. Якщо `gates` порожній (`[]`), результат `pass: true` (вакуумна істина: `results.length === gates.length === 0` і `every` на порожньому масиві — `true`).
|
|
88
|
-
- `gates: Array<{ name, ok }>` — звіт за кожним фактично виконаним gate-ом. У разі fail-fast масив містить **усі попередні** `{ ok: true }` плюс **один** `{ ok: false }`; gate-и після провалу до масиву **не потрапляють**.
|
|
89
|
-
- `failedOutput: string | null` — конкатенація `stdout` і `stderr` першого проваленого gate-у, обрізана `trim()`-ом. Якщо обидва стріми порожні після trim — `null`. У разі pass — `null`.
|
|
90
|
-
- `fingerprint: string | null` — результат `fingerprint()`, **тільки** якщо `pass === true`; інакше `null`. Це навмисно: stale-перевірка має сенс лише для свіжого позитивного verdict-у.
|
|
91
|
-
|
|
92
|
-
**Семантика `ok`:**
|
|
93
|
-
|
|
94
|
-
```js
|
|
95
|
-
const ok = (r?.status ?? 1) === 0
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
- `r === undefined`/`null` або `r.status === undefined` → трактується як **fail** (`status` defaultиться у `1`).
|
|
99
|
-
- `r.status === 0` → **ok**.
|
|
100
|
-
- Будь-який інший числовий код → **fail**.
|
|
101
|
-
|
|
102
|
-
**Логіка fail-fast:**
|
|
103
|
-
|
|
104
|
-
```js
|
|
105
|
-
for (const g of gates) {
|
|
106
|
-
const r = run(g.cmd[0], g.cmd.slice(1), { cwd })
|
|
107
|
-
const ok = (r?.status ?? 1) === 0
|
|
108
|
-
results.push({ name: g.name, ok })
|
|
109
|
-
if (!ok) {
|
|
110
|
-
failedOutput = `${r?.stdout ?? ''}\n${r?.stderr ?? ''}`.trim() || null
|
|
111
|
-
break
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
- Розбиття `cmd` на `[head, ...rest]` через `g.cmd[0]` і `g.cmd.slice(1)`.
|
|
117
|
-
- Якщо `stdout`/`stderr` відсутні — підставляється `''`, склейка через `\n`, далі `trim()`. Якщо після trim рядок порожній — повертається `null`, а не `""` (короткозамкнення `|| null`).
|
|
118
|
-
|
|
119
|
-
**Логіка fingerprint:**
|
|
120
|
-
|
|
121
|
-
```js
|
|
122
|
-
const pass = results.length === gates.length && results.every(x => x.ok)
|
|
123
|
-
return { pass, gates: results, failedOutput, fingerprint: pass ? fingerprint() : null }
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
- `pass` істинний лише якщо **жодного break-у** не сталось (довжини масивів збігаються) **і** всі результати `ok`.
|
|
127
|
-
- Виклик `fingerprint()` — **ліниво**, лише на pass-гілці; на fail зайвої роботи не робимо.
|
|
128
|
-
|
|
129
|
-
**Side effects:**
|
|
130
|
-
|
|
131
|
-
Сам `runReview` побічних ефектів **не має**: ані до файлової системи, ані до мережі, ані до stdout/stderr процесу-хоста. Усі побічні ефекти інкапсульовано в **ін'єкціях** `run` (запуск процесу, читання вихідних потоків) і `fingerprint` (за замовчуванням — обхід worktree для побудови відбитка). Це робить функцію **детермінованою при фіксованих ін'єкціях** і повністю unit-тестопридатною без stubs на `child_process`/`fs`.
|
|
132
|
-
|
|
133
|
-
**Особливості й edge-cases:**
|
|
134
|
-
|
|
135
|
-
- **Порожній `gates`**: `pass === true`, `gates: []`, `failedOutput: null`, `fingerprint: fingerprint()`. У такому разі fingerprint **усе одно** буде знятий — повний pass із 0 перевірок формально успіх.
|
|
136
|
-
- **`run` кидає виняток**: не перехоплюється всередині `runReview`. Помилка проб'ється у викликача — це навмисно (баг у runner-обгортці не маскується під «провалений gate»).
|
|
137
|
-
- **`fingerprint()` повертає `null`**: легітимний випадок (наприклад, worktree не git-репо); поле `fingerprint` у verdict-і просто буде `null`.
|
|
138
|
-
- **`fingerprint()` кидає виняток на pass-гілці**: проб'ється у викликача; не обгортається в try/catch.
|
|
139
|
-
|
|
140
|
-
## Залежності
|
|
141
|
-
|
|
142
|
-
### Імпорти
|
|
143
|
-
|
|
144
|
-
```js
|
|
145
|
-
import { worktreeFingerprint } from '../../utils/worktree-fingerprint.mjs'
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Єдина зовнішня залежність модуля — функція `worktreeFingerprint` з `npm/scripts/utils/worktree-fingerprint.mjs`. Використовується лише як **значення за замовчуванням** для параметра `fingerprint` (через стрілку `() => worktreeFingerprint()`). Якщо викликач передає власну `fingerprint`-функцію, `worktreeFingerprint` не викликається.
|
|
149
|
-
|
|
150
|
-
### Невидимі залежності (через ін'єкції)
|
|
151
|
-
|
|
152
|
-
- `run` (передається ззовні) — фактичний запуск дочірніх процесів (`npx @nitra/cursor lint`, `npx @nitra/cursor coverage --changed`).
|
|
153
|
-
- CLI `@nitra/cursor` — має бути доступний у `PATH` worktree-кореня (через `npx`).
|
|
154
|
-
- Сабкоманди `lint` і `coverage` пакету `@nitra/cursor`, що внутрішньо тягнуть ESLint, Vitest, Stryker та логіку «changed files» з `changed-files.mjs`.
|
|
155
|
-
|
|
156
|
-
### Зовнішні правила-документи
|
|
157
|
-
|
|
158
|
-
- §5 spec — Quality Gates.
|
|
159
|
-
- §8.4 spec — Level-1 «Суддя».
|
|
160
|
-
- `flow verify` (`n-flow.mdc`) — пасивний турнікет.
|
|
161
|
-
- `/n-coverage-fix`, `bun run coverage` — повний прогін coverage окремо.
|
|
162
|
-
|
|
163
|
-
## Потік виконання / Використання
|
|
164
|
-
|
|
165
|
-
### Типовий сценарій 1 — `flow verify` (пасивний турнікет)
|
|
166
|
-
|
|
167
|
-
1. Користувач/CI запускає `flow verify` у worktree.
|
|
168
|
-
2. Команда збирає ін'єкції:
|
|
169
|
-
- `run` — обгортка над `Bun.spawnSync` (синхронний запуск, capture stdout/stderr).
|
|
170
|
-
- `cwd` — корінь worktree.
|
|
171
|
-
- `gates` — `DEFAULT_GATES` (не перевизначається).
|
|
172
|
-
- `fingerprint` — default (`worktreeFingerprint`).
|
|
173
|
-
3. Виклик `runReview({...})`.
|
|
174
|
-
4. Аналіз verdict-у:
|
|
175
|
-
- `pass: true` → merge/commit дозволений; verdict разом із `fingerprint` зберігається у стан, щоб наступного разу детектити stale.
|
|
176
|
-
- `pass: false` → виводиться `failedOutput`, операція переривається.
|
|
177
|
-
|
|
178
|
-
### Типовий сценарій 2 — per-step Ф4 (активний раннер)
|
|
179
|
-
|
|
180
|
-
1. Dispatcher на фазі 4 циклу (після кожного кроку) викликає `runReview` для оцінки якості після зміни.
|
|
181
|
-
2. Та ж сигнатура, ті ж `DEFAULT_GATES`.
|
|
182
|
-
3. Verdict використовується для:
|
|
183
|
-
- прийняття рішення «крок успішний / відкотити»;
|
|
184
|
-
- формування feedback-у наступному LLM-кроку (через `failedOutput`).
|
|
185
|
-
|
|
186
|
-
### Псевдокод інтеграції
|
|
187
|
-
|
|
188
|
-
```js
|
|
189
|
-
import { runReview, DEFAULT_GATES } from './reviewer.mjs'
|
|
190
|
-
import { spawnSync } from 'node:child_process'
|
|
191
|
-
|
|
192
|
-
const run = (cmd, args, opts) => {
|
|
193
|
-
const r = spawnSync(cmd, args, { ...opts, encoding: 'utf8' })
|
|
194
|
-
return { status: r.status ?? 1, stdout: r.stdout, stderr: r.stderr }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const verdict = runReview({ run, cwd: process.cwd() })
|
|
198
|
-
|
|
199
|
-
if (verdict.pass) {
|
|
200
|
-
saveState({ fingerprint: verdict.fingerprint, verdict })
|
|
201
|
-
} else {
|
|
202
|
-
console.error(verdict.failedOutput)
|
|
203
|
-
process.exit(1)
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Послідовність виконання (внутрішня)
|
|
208
|
-
|
|
209
|
-
1. Ініціалізувати `results = []`, `failedOutput = null`.
|
|
210
|
-
2. Для кожного `g ∈ gates` у порядку оголошення:
|
|
211
|
-
- 2.1. Викликати `run(g.cmd[0], g.cmd.slice(1), { cwd })`.
|
|
212
|
-
- 2.2. Обчислити `ok = (r?.status ?? 1) === 0`.
|
|
213
|
-
- 2.3. Додати `{ name: g.name, ok }` у `results`.
|
|
214
|
-
- 2.4. Якщо `!ok` — записати `failedOutput` із `stdout + '\n' + stderr` (trim → `null` якщо пусто) і **вийти з циклу**.
|
|
215
|
-
3. Обчислити `pass = results.length === gates.length && results.every(x => x.ok)`.
|
|
216
|
-
4. Якщо `pass` — викликати `fingerprint()`, інакше — `null`.
|
|
217
|
-
5. Повернути `{ pass, gates: results, failedOutput, fingerprint }`.
|
|
218
|
-
|
|
219
|
-
### Stale-семантика (роль fingerprint у часі)
|
|
220
|
-
|
|
221
|
-
- Verdict із `fingerprint: 'abc123'` зберігається у стан Dispatcher-а.
|
|
222
|
-
- Перед повторним використанням verdict-у викликач знову обчислює `worktreeFingerprint()` і порівнює.
|
|
223
|
-
- Якщо відбитки збігаються → verdict ще валідний.
|
|
224
|
-
- Якщо ні → файли у дереві змінились після pass-у; verdict **stale**, треба перепрогнати gate-и.
|
|
225
|
-
- Цей механізм визначений §5 і реалізується **поза** `runReview` — модуль лише **постачає** fingerprint у момент успіху.
|
|
226
|
-
|
|
227
|
-
## Rebuild Test
|
|
228
|
-
|
|
229
|
-
Перевірка, що документація достатня для відтворення модуля з нуля:
|
|
230
|
-
|
|
231
|
-
1. **Експорти й сигнатура** — `DEFAULT_GATES` (named, масив із двох об'єктів `{ name, cmd }` у фіксованому порядку lint→coverage), `runReview(input)` (named) із чотирма іменованими полями: `run`, `cwd`, `gates?`, `fingerprint?`.
|
|
232
|
-
2. **Імпорт** — `worktreeFingerprint` з `../../utils/worktree-fingerprint.mjs`; використовується лише як значення за замовчуванням.
|
|
233
|
-
3. **Канонічні команди gate-ів** — `['npx', '@nitra/cursor', 'lint']` та `['npx', '@nitra/cursor', 'coverage', '--changed']` (саме в такому порядку аргументів).
|
|
234
|
-
4. **Алгоритм** — послідовний цикл for-of по `gates`, fail-fast із `break` на першому `!ok`, обчислення `ok` як `(r?.status ?? 1) === 0`.
|
|
235
|
-
5. **`failedOutput`** — конкатенація `stdout` і `stderr` (defaultяться у `''`), розділювач `\n`, далі `trim()` та fallback на `null` через `|| null`.
|
|
236
|
-
6. **`pass`** — комбінація двох умов: `results.length === gates.length` **і** `results.every(x => x.ok)` (друга умова критична для коректності, бо перша істинна вже на старті при порожньому `gates`).
|
|
237
|
-
7. **`fingerprint` у verdict-і** — тернар `pass ? fingerprint() : null`; виклик ліниво.
|
|
238
|
-
8. **Side-effect-free** — `runReview` сам не торкає FS/network/процеси; усе через ін'єкції.
|
|
239
|
-
9. **Дефолти параметрів** — `gates = DEFAULT_GATES`, `fingerprint = () => worktreeFingerprint()`. Жодних дефолтів для `run` і `cwd` — обидва обов'язкові, відсутність призведе до runtime-помилки в тілі функції.
|
|
240
|
-
10. **Структура verdict-у** — рівно чотири поля: `pass`, `gates`, `failedOutput`, `fingerprint`; жодних додаткових.
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
# snapshot.mjs
|
|
2
|
-
|
|
3
|
-
## Огляд
|
|
4
|
-
|
|
5
|
-
Модуль `snapshot.mjs` — частина шару `lib/` диспетчера flow-задач (`npm/scripts/dispatcher/lib/`). Його єдине призначення — створення та збереження **completion snapshot** (підсумкового слугу про виконання задачі) у durable-сховище, ще до того, як буде видалено transient-файл `.flow.json`.
|
|
6
|
-
|
|
7
|
-
Контекст і причина існування модуля (зі специфікації §3 Ф5, §7):
|
|
8
|
-
|
|
9
|
-
- Стан виконання flow-задачі тимчасово живе у файлі `.flow.json` всередині гілки/робочої теки.
|
|
10
|
-
- На етапі завершення (cleanup) цей transient-стан видаляється.
|
|
11
|
-
- Але потрібно, щоб **слід** про виконану задачу пережив cleanup: для аудиту, історії, ретроспективи.
|
|
12
|
-
- Тому перед видаленням ми будуємо стислий JSON-snapshot і вписуємо його в **task record** — markdown-файл `docs/tasks/<id>.md` між двома HTML-маркерами. Запис **ідемпотентний**: повторний прогін перезаписує блок Summary, а не дублює його.
|
|
13
|
-
|
|
14
|
-
Модуль експонує три pure/IO-функції з чітко розділеними рівнями (чиста трансформація → чистий upsert у тексті → IO у файлову систему), що робить логіку легко тестованою без mock-ів FS на більшості шляхів.
|
|
15
|
-
|
|
16
|
-
## Експорти / API
|
|
17
|
-
|
|
18
|
-
Модуль експортує три іменовані функції (`export function ...`):
|
|
19
|
-
|
|
20
|
-
| Експорт | Тип | Призначення |
|
|
21
|
-
| ---------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------- |
|
|
22
|
-
| `buildCompletionSnapshot(state, now?)` | pure-функція | Складає JSON-об'єкт snapshot зі стану `.flow.json`. |
|
|
23
|
-
| `upsertSummaryBlock(content, snapshot)` | pure-функція | Вставляє або оновлює блок Summary у наданому markdown-тексті між маркерами. |
|
|
24
|
-
| `writeSummaryToTaskRecord(taskPath, snapshot)` | IO-функція | Читає файл task record (якщо існує), застосовує `upsertSummaryBlock` і пише результат назад. |
|
|
25
|
-
|
|
26
|
-
Внутрішні (не експортовані) константи:
|
|
27
|
-
|
|
28
|
-
- `SUMMARY_START = '<!-- flow:summary:start -->'` — відкриваючий HTML-коментар-маркер.
|
|
29
|
-
- `SUMMARY_END = '<!-- flow:summary:end -->'` — закриваючий HTML-коментар-маркер.
|
|
30
|
-
|
|
31
|
-
Ці маркери — публічний контракт формату task record: будь-який інший інструмент може шукати ці ж літерали, щоб витягнути або переписати блок.
|
|
32
|
-
|
|
33
|
-
## Функції
|
|
34
|
-
|
|
35
|
-
### `buildCompletionSnapshot(state, now = Date.now)`
|
|
36
|
-
|
|
37
|
-
**Сигнатура:**
|
|
38
|
-
|
|
39
|
-
```js
|
|
40
|
-
buildCompletionSnapshot(state: object, now?: () => number): object
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**Параметри:**
|
|
44
|
-
|
|
45
|
-
- `state` — об'єкт стану задачі (типово розпарсений `.flow.json`). Очікувані поля (всі опційні, мають дефолти):
|
|
46
|
-
- `state.status` — рядковий статус, дефолт `'done'`.
|
|
47
|
-
- `state.branch` — назва гілки, дефолт `null`.
|
|
48
|
-
- `state.metadata?.base_commit` — base commit гілки; якщо відсутній, fallback `state.base_commit`; дефолт `null`.
|
|
49
|
-
- `state.gates` — масив об'єктів `{ name, ok }`; кожен gate перетворюється у пару `[name, ok ? 'ok' : 'fail']`.
|
|
50
|
-
- `state.change` — інформація про change-файл (n-changelog), дефолт `null`.
|
|
51
|
-
- `state.notified` — статус нотифікації, дефолт `null`.
|
|
52
|
-
- `now` — фабрика часу: функція, що повертає мілісекунди (`number`). За замовчуванням — `Date.now`. Передається явно, щоб **тести могли інжектувати детерміністичний час**.
|
|
53
|
-
|
|
54
|
-
**Повертає:**
|
|
55
|
-
|
|
56
|
-
Об'єкт `snapshot` із полями:
|
|
57
|
-
|
|
58
|
-
```text
|
|
59
|
-
{
|
|
60
|
-
status: string, // state.status ?? 'done'
|
|
61
|
-
branch: string|null,
|
|
62
|
-
base_commit: string|null,
|
|
63
|
-
gates: Record<string,'ok'|'fail'>, // плоска мапа name → status
|
|
64
|
-
change: any|null,
|
|
65
|
-
notified: any|null,
|
|
66
|
-
finished_at: string // ISO-8601, отриманий через new Date(now()).toISOString()
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
**Side effects:** жодних. Це чиста функція (за умови чистоти переданої `now`).
|
|
71
|
-
|
|
72
|
-
**Особливості реалізації:**
|
|
73
|
-
|
|
74
|
-
- Поле `base_commit` має **двоступеневий fallback**: спочатку `state.metadata?.base_commit`, потім `state.base_commit`. Це покриває обидва формати state, що зустрічаються в практиці.
|
|
75
|
-
- `gates` нормалізуються до **рядкового** статусу `'ok'`/`'fail'` (а не булевого `ok`), щоб JSON у markdown був самодокументованим і людиночитним.
|
|
76
|
-
- `finished_at` обчислюється з результату виклику `now()`, тому час фіксується саме в момент побудови snapshot.
|
|
77
|
-
- Якщо `state.gates` відсутній — використовується порожній масив, і поле `gates` буде `{}`.
|
|
78
|
-
|
|
79
|
-
### `upsertSummaryBlock(content, snapshot)`
|
|
80
|
-
|
|
81
|
-
**Сигнатура:**
|
|
82
|
-
|
|
83
|
-
```js
|
|
84
|
-
upsertSummaryBlock(content: string, snapshot: object): string
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
**Параметри:**
|
|
88
|
-
|
|
89
|
-
- `content` — вихідний markdown-текст task record (може бути порожнім рядком).
|
|
90
|
-
- `snapshot` — об'єкт, що буде серіалізований у JSON (через `JSON.stringify(..., null, 2)`) усередину блоку.
|
|
91
|
-
|
|
92
|
-
**Повертає:** новий markdown-рядок зі вставленим або оновленим блоком Summary.
|
|
93
|
-
|
|
94
|
-
**Side effects:** жодних, чиста рядкова трансформація.
|
|
95
|
-
|
|
96
|
-
**Структура блоку, який будує функція:**
|
|
97
|
-
|
|
98
|
-
````text
|
|
99
|
-
<!-- flow:summary:start -->
|
|
100
|
-
## Summary
|
|
101
|
-
```json
|
|
102
|
-
{ ... pretty-printed snapshot ... }
|
|
103
|
-
````
|
|
104
|
-
|
|
105
|
-
<!-- flow:summary:end -->
|
|
106
|
-
|
|
107
|
-
````
|
|
108
|
-
|
|
109
|
-
(У реальному виводі трійні бектики справжні; тут показано схематично, бо ми всередині markdown.)
|
|
110
|
-
|
|
111
|
-
**Алгоритм (idempotent upsert):**
|
|
112
|
-
|
|
113
|
-
1. Сформувати рядок `block` із маркерів, заголовка `## Summary`, fenced JSON-блоку та закриваючого маркера.
|
|
114
|
-
2. Знайти індекси `i = content.indexOf(SUMMARY_START)` та `j = content.indexOf(SUMMARY_END)`.
|
|
115
|
-
3. Якщо обидва маркери знайдено і `j > i` — **замінити** діапазон `[i, j + len(SUMMARY_END))` на `block`. Це гарантує, що повторний виклик не дублює блок і не залишає старого вмісту.
|
|
116
|
-
4. Інакше (маркерів немає, або вони у неправильному порядку) — **дописати** блок у кінець: `content.trimEnd() + '\n\n' + block + '\n'`. `trimEnd()` прибирає лишні хвостові переноси, потім додається порожній рядок-розділювач.
|
|
117
|
-
|
|
118
|
-
**Кейси (контракт):**
|
|
119
|
-
|
|
120
|
-
| Вхідний `content` | Поведінка |
|
|
121
|
-
|---|---|
|
|
122
|
-
| Порожній рядок `''` | Дописує `\n\n<block>\n` (фактично починається з `\n\n` через `trimEnd('')`). |
|
|
123
|
-
| Markdown без маркерів | Дописує блок у кінець із розділювачем. |
|
|
124
|
-
| Markdown із валідною парою маркерів | Замінює вміст між ними (включно з самими маркерами) на свіжий блок. |
|
|
125
|
-
| Markdown із поодиноким `SUMMARY_START` без `SUMMARY_END` | Йде у гілку append (дописати в кінець). |
|
|
126
|
-
| Markdown із `SUMMARY_END` перед `SUMMARY_START` (`j <= i`) | Теж йде в append. |
|
|
127
|
-
|
|
128
|
-
### `writeSummaryToTaskRecord(taskPath, snapshot)`
|
|
129
|
-
|
|
130
|
-
**Сигнатура:**
|
|
131
|
-
|
|
132
|
-
```js
|
|
133
|
-
writeSummaryToTaskRecord(taskPath: string, snapshot: object): void
|
|
134
|
-
````
|
|
135
|
-
|
|
136
|
-
**Параметри:**
|
|
137
|
-
|
|
138
|
-
- `taskPath` — **абсолютний** шлях до task record-файла (типово `<repo>/docs/tasks/<id>.md`).
|
|
139
|
-
- `snapshot` — об'єкт snapshot (зазвичай результат `buildCompletionSnapshot`).
|
|
140
|
-
|
|
141
|
-
**Повертає:** `void`.
|
|
142
|
-
|
|
143
|
-
**Side effects:**
|
|
144
|
-
|
|
145
|
-
- Кидає `Error`, якщо `taskPath` **не** абсолютний (`isAbsolute(taskPath) === false`). Текст помилки: `writeSummaryToTaskRecord: очікується абсолютний шлях (отримано: <taskPath>)`.
|
|
146
|
-
- Якщо файл існує — читає його синхронно (`readFileSync(taskPath, 'utf8')`).
|
|
147
|
-
- Якщо файл не існує — використовує порожній рядок як base.
|
|
148
|
-
- Записує результат `upsertSummaryBlock(...)` у `taskPath` синхронно (`writeFileSync(..., 'utf8')`). Файл буде створено, якщо його не було.
|
|
149
|
-
|
|
150
|
-
**Особливості:**
|
|
151
|
-
|
|
152
|
-
- Усі IO — **синхронні**. Це свідомий вибір: функція викликається на cleanup-етапі диспетчера, де простота й детерміністичність важливіші за throughput.
|
|
153
|
-
- Не створює проміжних каталогів. Очікується, що `docs/tasks/` уже існує (інакше `writeFileSync` кине `ENOENT`).
|
|
154
|
-
- Не виконує бекапу: попередній блок Summary перезаписується новим (про що дбає `upsertSummaryBlock`).
|
|
155
|
-
|
|
156
|
-
## Залежності
|
|
157
|
-
|
|
158
|
-
**Зовнішні (Node.js core, через `node:` префікс):**
|
|
159
|
-
|
|
160
|
-
- `node:fs` — `existsSync`, `readFileSync`, `writeFileSync` (синхронний IO).
|
|
161
|
-
- `node:path` — `isAbsolute` (валідація вхідного шляху).
|
|
162
|
-
|
|
163
|
-
**Внутрішніх залежностей** (на інші модулі диспетчера) — **немає**. Модуль самодостатній і не імпортує нічого з `lib/` чи проєкту.
|
|
164
|
-
|
|
165
|
-
**Зовнішні npm-пакети:** відсутні.
|
|
166
|
-
|
|
167
|
-
**Тип/середовище:** ESM (`.mjs`, `import`-синтаксис), запускається у Node.js / Bun.
|
|
168
|
-
|
|
169
|
-
## Потік виконання / Використання
|
|
170
|
-
|
|
171
|
-
### Сценарій 1: завершення flow-задачі (основний use case)
|
|
172
|
-
|
|
173
|
-
Псевдокод callsite (типового місця виклику в диспетчері):
|
|
174
|
-
|
|
175
|
-
```js
|
|
176
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
177
|
-
import { join } from 'node:path'
|
|
178
|
-
import { buildCompletionSnapshot, writeSummaryToTaskRecord } from './lib/snapshot.mjs'
|
|
179
|
-
|
|
180
|
-
// 1. Зчитати transient-стан.
|
|
181
|
-
const state = JSON.parse(readFileSync('.flow.json', 'utf8'))
|
|
182
|
-
|
|
183
|
-
// 2. Побудувати completion snapshot.
|
|
184
|
-
const snapshot = buildCompletionSnapshot(state)
|
|
185
|
-
|
|
186
|
-
// 3. Уписати його в durable task record.
|
|
187
|
-
const taskPath = join(repoRoot, 'docs', 'tasks', `${state.id}.md`)
|
|
188
|
-
writeSummaryToTaskRecord(taskPath, snapshot)
|
|
189
|
-
|
|
190
|
-
// 4. Тепер безпечно видаляти `.flow.json` (cleanup).
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### Сценарій 2: тестування (інжекція часу)
|
|
194
|
-
|
|
195
|
-
`buildCompletionSnapshot` приймає `now` як параметр, тож тест може зафіксувати `finished_at`:
|
|
196
|
-
|
|
197
|
-
```js
|
|
198
|
-
const fixedNow = () => 1_700_000_000_000
|
|
199
|
-
const snap = buildCompletionSnapshot(state, fixedNow)
|
|
200
|
-
// snap.finished_at === '2023-11-14T22:13:20.000Z'
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### Сценарій 3: ідемпотентний повторний запис
|
|
204
|
-
|
|
205
|
-
Виклик `writeSummaryToTaskRecord` на тому ж шляху з тим самим snapshot **не** змінює файл змістовно (тільки переписує блок Summary тим самим вмістом). На різних snapshot — оновлює блок без дублювання та без впливу на решту markdown поза маркерами.
|
|
206
|
-
|
|
207
|
-
### Потік даних
|
|
208
|
-
|
|
209
|
-
```
|
|
210
|
-
.flow.json (transient)
|
|
211
|
-
│
|
|
212
|
-
▼ JSON.parse
|
|
213
|
-
state object
|
|
214
|
-
│
|
|
215
|
-
▼ buildCompletionSnapshot(state, now)
|
|
216
|
-
snapshot object ─────────────┐
|
|
217
|
-
▼ upsertSummaryBlock(content, snapshot)
|
|
218
|
-
docs/tasks/<id>.md (existing) ───┤
|
|
219
|
-
│ ▼
|
|
220
|
-
│ updated markdown
|
|
221
|
-
│ │
|
|
222
|
-
└─◀────── writeFileSync ──┘
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Інваріанти
|
|
226
|
-
|
|
227
|
-
- `finished_at` завжди є валідним ISO-8601 рядком.
|
|
228
|
-
- `gates` завжди є об'єктом (нехай і порожнім), ніколи не `undefined`.
|
|
229
|
-
- Файл task record після виклику завжди містить рівно один блок Summary між маркерами.
|
|
230
|
-
- HTML-маркери залишаються в markdown навмисно: вони не рендеряться у GitHub/MD-рендерерах, але слугують machine-readable якорями для idempotent upsert.
|
|
231
|
-
|
|
232
|
-
## Rebuild Test
|
|
233
|
-
|
|
234
|
-
Mental-rebuild перевірка (чи документація достатня для відтворення модуля з нуля без перегляду коду):
|
|
235
|
-
|
|
236
|
-
1. **Призначення зрозуміле?** Так — будувати completion snapshot і вписувати його між HTML-маркерами в `docs/tasks/<id>.md` перед cleanup `.flow.json`.
|
|
237
|
-
2. **Експорти й сигнатури повні?** Так — три функції з типами параметрів, дефолтами, типами повернення.
|
|
238
|
-
3. **Формат snapshot задокументовано?** Так — перелік полів, дефолти, fallback-логіка для `base_commit`, нормалізація `gates` до `'ok'`/`'fail'`.
|
|
239
|
-
4. **Формат блоку в markdown задокументовано?** Так — маркери, `## Summary`, fenced JSON-блок, pretty-print 2 пробіли.
|
|
240
|
-
5. **Алгоритм upsert описаний?** Так — пошук маркерів, заміна діапазону при валідній парі, інакше append із `trimEnd` + `\n\n`.
|
|
241
|
-
6. **Обробка помилок описана?** Так — `Error` на не-абсолютний шлях, відсутність файлу = порожній base.
|
|
242
|
-
7. **Залежності перелічено?** Так — `node:fs` (3 функції), `node:path` (`isAbsolute`).
|
|
243
|
-
8. **Side effects явні?** Так — синхронний IO лише в `writeSummaryToTaskRecord`; решта pure.
|
|
244
|
-
9. **Інваріанти й кейси edge?** Так — порожній content, поодинокий маркер, зворотний порядок маркерів, повторний запис.
|
|
245
|
-
10. **Інжекція часу для тестів?** Так — параметр `now`.
|
|
246
|
-
|
|
247
|
-
Рекомпіляція з документації → ідентичний за поведінкою модуль: можлива.
|