@nitra/cursor 4.1.1 → 5.0.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 (71) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/docs/n-cursor.md +1 -9
  3. package/bin/n-cursor.js +3 -25
  4. package/docs/stryker.config.md +37 -0
  5. package/docs/vitest.config.md +23 -0
  6. package/package.json +2 -1
  7. package/rules/docker/lib/docs/docker-mirror.md +1 -1
  8. package/rules/docker/lib/docs/docker-native-addon.md +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/scripts/dispatcher/docs/graph.md +0 -346
  19. package/scripts/dispatcher/docs/index.md +0 -236
  20. package/scripts/dispatcher/docs/trace.md +0 -296
  21. package/scripts/dispatcher/graph/lib/cmd-init.mjs +0 -112
  22. package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +0 -96
  23. package/scripts/dispatcher/graph/lib/cmd-kill.mjs +0 -141
  24. package/scripts/dispatcher/graph/lib/cmd-plan.mjs +0 -142
  25. package/scripts/dispatcher/graph/lib/cmd-run.mjs +0 -328
  26. package/scripts/dispatcher/graph/lib/cmd-scan.mjs +0 -115
  27. package/scripts/dispatcher/graph/lib/cmd-setup.mjs +0 -111
  28. package/scripts/dispatcher/graph/lib/cmd-signals.mjs +0 -328
  29. package/scripts/dispatcher/graph/lib/cmd-status.mjs +0 -131
  30. package/scripts/dispatcher/graph/lib/cmd-verify.mjs +0 -100
  31. package/scripts/dispatcher/graph/lib/cmd-watch.mjs +0 -128
  32. package/scripts/dispatcher/graph/lib/config.mjs +0 -103
  33. package/scripts/dispatcher/graph/lib/frontmatter.mjs +0 -224
  34. package/scripts/dispatcher/graph/lib/nnn.mjs +0 -127
  35. package/scripts/dispatcher/graph/lib/node-state.mjs +0 -157
  36. package/scripts/dispatcher/graph/lib/scanner.mjs +0 -235
  37. package/scripts/dispatcher/graph/lib/worktree-ops.mjs +0 -193
  38. package/scripts/dispatcher/graph-tasks.mjs +0 -92
  39. package/scripts/dispatcher/graph.mjs +0 -212
  40. package/scripts/dispatcher/index.mjs +0 -45
  41. package/scripts/dispatcher/lib/docs/active.md +0 -348
  42. package/scripts/dispatcher/lib/docs/artifact.md +0 -232
  43. package/scripts/dispatcher/lib/docs/budget.md +0 -167
  44. package/scripts/dispatcher/lib/docs/capability.md +0 -196
  45. package/scripts/dispatcher/lib/docs/commands.md +0 -210
  46. package/scripts/dispatcher/lib/docs/events.md +0 -183
  47. package/scripts/dispatcher/lib/docs/executor.md +0 -190
  48. package/scripts/dispatcher/lib/docs/gate.md +0 -231
  49. package/scripts/dispatcher/lib/docs/level.md +0 -335
  50. package/scripts/dispatcher/lib/docs/plan-panel.md +0 -181
  51. package/scripts/dispatcher/lib/docs/plan.md +0 -200
  52. package/scripts/dispatcher/lib/docs/planner.md +0 -269
  53. package/scripts/dispatcher/lib/docs/review.md +0 -255
  54. package/scripts/dispatcher/lib/docs/reviewer.md +0 -240
  55. package/scripts/dispatcher/lib/docs/snapshot.md +0 -247
  56. package/scripts/dispatcher/lib/docs/spec.md +0 -203
  57. package/scripts/dispatcher/lib/docs/state-store.md +0 -303
  58. package/scripts/dispatcher/lib/docs/subagent-runner.md +0 -173
  59. package/scripts/dispatcher/lib/events.mjs +0 -67
  60. package/scripts/dispatcher/lib/executor.mjs +0 -107
  61. package/scripts/dispatcher/lib/plan-panel.mjs +0 -76
  62. package/scripts/dispatcher/lib/state-store.mjs +0 -173
  63. package/scripts/dispatcher/lib/subagent-runner.mjs +0 -53
  64. package/scripts/graph/index.mjs +0 -115
  65. package/scripts/graph/lib/config.mjs +0 -62
  66. package/scripts/graph/lib/dag.mjs +0 -161
  67. package/scripts/graph/lib/frontmatter.mjs +0 -70
  68. package/scripts/graph/lib/nnn.mjs +0 -77
  69. package/scripts/graph/lib/state.mjs +0 -110
  70. package/scripts/graph/scan.mjs +0 -64
  71. package/scripts/graph/status.mjs +0 -86
@@ -1,232 +0,0 @@
1
- # artifact.mjs
2
-
3
- ## Огляд
4
-
5
- Модуль `artifact.mjs` — це набір **спільних утиліт для фаз `spec` і `plan`** у моделі «Пасивний Турнікет» dispatcher-конвеєра. Він вирішує три задачі:
6
-
7
- 1. **Резолв traceable-артефакту** в каталозі `docs/<kind>/` (де `<kind>` — `specs` або `plans`) з пріоритезацією за хвостом (`slug`) гілки і вибором найсвіжішого файлу за `mtime`.
8
- 2. **Екстракт нумерованих кроків** із секції `## Кроки` плану у структурований масив `{ task, acceptance }`.
9
- 3. **Read-only перевірка цілісності ланцюга артефактів** через CLI `n-cursor trace` (без мутацій).
10
-
11
- Ключова інваріанта: модуль **не пише** front-matter лінків (`spec.plan`, `plan.spec`, `plan.flow`) — це робить агент згідно з контрактом, описаним у правилі `flow.mdc`. Тут лише **верифікація**, бо мутатор `trace link` свідомо не реалізовано.
12
-
13
- Файл є чистим ES-модулем (`.mjs`), не має побічних ефектів на рівні імпорту і експортує три іменовані функції.
14
-
15
- ## Експорти / API
16
-
17
- | Експорт | Тип | Призначення |
18
- | ----------------- | ---------- | ------------------------------------------------------------- |
19
- | `resolveArtifact` | `function` | Знаходить актуальний markdown-артефакт у `docs/<kind>/`. |
20
- | `extractSteps` | `function` | Парсить нумерований список кроків із тексту плану. |
21
- | `verifyTrace` | `function` | Перевіряє цілісність ланцюга артефактів через runner `trace`. |
22
-
23
- Внутрішні (не експортовані) константи:
24
-
25
- | Ім'я | Значення | Призначення |
26
- | ----------------- | ----------------- | ---------------------------------------------------------------------- |
27
- | `ACCEPTANCE_MARK` | `'— acceptance:'` | Маркер критерію приймання в рядку кроку (порівняння case-insensitive). |
28
- | `DIGITS_RE` | `/^\d+$/u` | RegExp для перевірки, що префікс рядка перед `. ` є лише з цифр. |
29
-
30
- ## Функції
31
-
32
- ### `resolveArtifact(cwd, kind, branch)`
33
-
34
- **Сигнатура**
35
-
36
- ```js
37
- export function resolveArtifact(cwd, kind, branch)
38
- ```
39
-
40
- **Параметри**
41
-
42
- | Ім'я | Тип | Обов'язковий | Опис |
43
- | -------- | ----------------------- | ------------ | ------------------------------------------------------------------------------------------------------ |
44
- | `cwd` | `string` | так | Абсолютний шлях до кореня worktree. |
45
- | `kind` | `'specs' \| 'plans'` | так | Підкаталог усередині `docs/` (`docs/specs/` або `docs/plans/`). |
46
- | `branch` | `string` (опціональний) | ні | Назва гілки задачі (напр. `claude/flow-gate`). Використовується для пріоритезації за хвостом (`slug`). |
47
-
48
- **Повертає**
49
-
50
- `string | null` — абсолютний шлях до знайденого `.md`-файлу або `null`, якщо каталог `docs/<kind>/` відсутній чи в ньому немає markdown-файлів.
51
-
52
- **Алгоритм / поведінка**
53
-
54
- 1. Збирає `dir = join(cwd, 'docs', kind)`.
55
- 2. Якщо `dir` не існує — повертає `null`.
56
- 3. Сканує `dir` і фільтрує лише файли з розширенням `.md`. Якщо їх немає — `null`.
57
- 4. Обчислює `slug` як останній сегмент `branch` після `/` (`'claude/flow-gate' -> 'flow-gate'`). Якщо `branch` не задано — `slug = null`.
58
- 5. Формує `matched` — підмножину файлів, чиї назви **містять `slug`** (через `String.prototype.includes`). Якщо `slug` відсутній — `matched = []`.
59
- 6. Формує `pool`: якщо `matched.length > 0` — `pool = matched`; інакше `pool = md` (усі markdown-файли каталогу).
60
- 7. Серед `pool` обирає **найсвіжіший за `mtimeMs`** (а при рівності `mtime` — за зростанням за іменем, тож `.at(-1)` поверне лексикографічно більше ім'я як tie-breaker).
61
- 8. Повертає `join(dir, best.f)`.
62
-
63
- **Side effects**
64
-
65
- - **Тільки читання ФС**: `existsSync`, `readdirSync`, `statSync` із `node:fs`. Жодних записів.
66
-
67
- **Особливості / нюанси**
68
-
69
- - Сортування використовує `Array.prototype.toSorted` (іммутабельний варіант `sort`), тож вихідний `pool` не модифікується.
70
- - Документація у JSDoc наголошує: попередній «лексикографічний» вибір був **хибним** при кількох артефактах на одну дату — це виявлено dogfood'ом.
71
- - Якщо в `pool` лише один файл — `.at(-1)` поверне його.
72
-
73
- ### `extractSteps(text)`
74
-
75
- **Сигнатура**
76
-
77
- ```js
78
- export function extractSteps(text)
79
- ```
80
-
81
- **Параметри**
82
-
83
- | Ім'я | Тип | Обов'язковий | Опис |
84
- | ------ | -------- | ------------ | --------------------------------------------------------------------------------------------------- |
85
- | `text` | `string` | так | Повний вміст markdown-файлу плану (або довільний текст). Приводиться до рядка через `String(text)`. |
86
-
87
- **Повертає**
88
-
89
- `{ task: string, acceptance?: string }[]` — масив кроків у тому порядку, в якому вони зустрілися у вхідному тексті. Якщо нічого не знайдено — порожній масив.
90
-
91
- **Формат, який розпізнається**
92
-
93
- ```
94
- N. <текст задачі> — acceptance: <критерій приймання>
95
- ```
96
-
97
- де `N` — одне або більше десяткових цифр. Маркер `— acceptance:` розпізнається **case-insensitive** (через `toLowerCase`).
98
-
99
- Якщо рядок не починається з `<цифри>. `, він **ігнорується** (best-effort, без помилок).
100
-
101
- **Алгоритм / поведінка**
102
-
103
- 1. Розбиває вхід на рядки за `\n`.
104
- 2. Для кожного рядка обрізає пробіли (`trim`).
105
- 3. Шукає першу появу `. ` (`dot = line.indexOf('. ')`). Якщо `dot <= 0` (немає крапки з пробілом або вона на самому початку) — рядок пропускається.
106
- 4. Перевіряє, що префікс `line.slice(0, dot)` повністю складається з цифр (`DIGITS_RE`). Інакше — пропуск.
107
- 5. Виокремлює `body = line.slice(dot + 2).trim()`.
108
- 6. Шукає в `body.toLowerCase()` позицію маркера `ACCEPTANCE_MARK` (`'— acceptance:'`).
109
- 7. Якщо маркера немає — додає `{ task: body }`.
110
- 8. Якщо маркер є — ділить `body` на `task` (до маркера) і `acceptance` (після маркера), обрізає обидва і додає `{ task, acceptance }`.
111
-
112
- **Side effects**
113
-
114
- - Жодних. Чиста функція.
115
-
116
- **Особливості / нюанси**
117
-
118
- - Використано `indexOf` замість regex для уникнення **regex-backtracking**, що даватиме лінійну складність на довгих рядках.
119
- - Маркер `—` — це **em-dash** (U+2014), не звичайний дефіс. Це важливо для збігу.
120
- - Поле `acceptance` опціональне у вихідних об'єктах: відсутнє, якщо маркер не знайдено.
121
- - Нумерація кроків зчитується як префікс, але **не повертається** у результат: повертаються лише `task` і опціонально `acceptance`. Сам номер не використовується для впорядкування — порядок повністю визначається порядком появи в тексті.
122
-
123
- ### `verifyTrace(cwd, runTrace)`
124
-
125
- **Сигнатура**
126
-
127
- ```js
128
- export function verifyTrace(cwd, runTrace)
129
- ```
130
-
131
- **Параметри**
132
-
133
- | Ім'я | Тип | Обов'язковий | Опис |
134
- | ---------- | -------------------------------- | ------------ | ------------------------------------------------------------------------------------- |
135
- | `cwd` | `string` | так | Абсолютний шлях до кореня worktree, який передається у trace-runner. |
136
- | `runTrace` | `(cwd: string) => number` (опц.) | ні | Кастомний runner trace, що повертає exit-code. Призначений для **ін'єкції у тестах**. |
137
-
138
- **Повертає**
139
-
140
- `boolean` — `true`, якщо exit-code runner-а дорівнює `0` (ланцюг цілісний); `false` — інакше (розрив ланцюга або помилка).
141
-
142
- **Алгоритм / поведінка**
143
-
144
- 1. Якщо `runTrace` не передано — створюється дефолтний runner, який викликає `runTraceCli([], { cwd: c, log: () => {} })` з модуля `../trace.mjs`. Логування глушиться (`log: () => {}`), тож зовнішніх ефектів у stdout/stderr не буде.
145
- 2. Виконує `run(cwd)` і повертає `=== 0`.
146
-
147
- **Side effects**
148
-
149
- - Викликає `runTraceCli` з `../trace.mjs`, який у звичайному режимі **читає** артефакти й їхні front-matter. Жодних мутацій не передбачено — функція декларується як «read-only сигнал».
150
- - При використанні `runTrace` ін'єкції — side effects цілком визначаються переданим runner-ом.
151
-
152
- **Особливості / нюанси**
153
-
154
- - Семантика exit-code узгоджена з CLI: `0` — OK, `1` (або інше ненульове) — розрив.
155
- - Аргументи для `runTraceCli` фіксовані: порожній масив `[]` (без додаткових прапорців).
156
-
157
- ## Залежності
158
-
159
- ### Зовнішні (Node.js core)
160
-
161
- - `node:fs`:
162
- - `existsSync` — перевірка існування каталогу `docs/<kind>/`.
163
- - `readdirSync` — лістинг файлів у каталозі.
164
- - `statSync` — отримання `mtimeMs` для сортування.
165
- - `node:path`:
166
- - `join` — побудова шляхів `docs/<kind>/<file>.md`.
167
-
168
- ### Внутрішні (проєктні)
169
-
170
- - `../trace.mjs` → `runTraceCli` — CLI-runner перевірки цілісності trace-ланцюга. Викликається в `verifyTrace`.
171
-
172
- ### Контракти / правила
173
-
174
- - `flow.mdc` — описує контракт лінкування front-matter (`spec.plan`/`plan.spec`/`plan.flow`), яке робить агент, а не цей модуль.
175
- - Структура каталогів `docs/specs/` і `docs/plans/` — конвенція «Пасивний Турнікет».
176
-
177
- ## Потік виконання / Використання
178
-
179
- Модуль є **бібліотечним**: він не запускається самостійно, а імпортується іншими частинами dispatcher-pipeline (фази `spec`/`plan`).
180
-
181
- ### Типовий сценарій 1 — резолв plan-артефакту і парсинг кроків
182
-
183
- ```js
184
- import { resolveArtifact, extractSteps } from './artifact.mjs'
185
- import { readFileSync } from 'node:fs'
186
-
187
- const planPath = resolveArtifact(process.cwd(), 'plans', 'claude/flow-gate')
188
- if (!planPath) {
189
- // немає docs/plans/ або файлів — фаза має зупинитись
190
- return
191
- }
192
-
193
- const text = readFileSync(planPath, 'utf8')
194
- const steps = extractSteps(text)
195
- // steps: [{ task: '...', acceptance: '...' }, ...]
196
- ```
197
-
198
- ### Типовий сценарій 2 — read-only перевірка ланцюга перед мерджем
199
-
200
- ```js
201
- import { verifyTrace } from './artifact.mjs'
202
-
203
- if (!verifyTrace(process.cwd())) {
204
- // ланцюг spec ↔ plan ↔ flow має розрив — сигнал агенту
205
- }
206
- ```
207
-
208
- ### Типовий сценарій 3 — інверсія залежності у тестах
209
-
210
- ```js
211
- import { verifyTrace } from './artifact.mjs'
212
-
213
- // підставляємо фейковий runner, щоб не викликати реальний CLI
214
- const ok = verifyTrace('/tmp/fake-cwd', () => 0)
215
- // ok === true
216
- ```
217
-
218
- ### Контекст у dispatcher-конвеєрі
219
-
220
- 1. **Фаза `spec`**: викликає `resolveArtifact(cwd, 'specs', branch)` для пошуку специфікації.
221
- 2. **Фаза `plan`**: викликає `resolveArtifact(cwd, 'plans', branch)`, читає файл, потім `extractSteps(text)` — і отримує перелік кроків, які виконуватиме наступна фаза.
222
- 3. **Гейт цілісності**: перед переходом на наступну фазу — `verifyTrace(cwd)` як сигнал, чи зв'язки front-matter не порушені.
223
-
224
- ## Rebuild Test
225
-
226
- За цією документацією можна **відновити поведінку** модуля без оригінального коду:
227
-
228
- - Модуль експортує **рівно три функції**: `resolveArtifact`, `extractSteps`, `verifyTrace`.
229
- - `resolveArtifact(cwd, kind, branch)` шукає `.md` у `docs/<kind>/`, віддає пріоритет файлам, чиє ім'я містить `slug = branch.split('/').pop()`, серед них (або серед усіх, якщо збігу нема) обирає найсвіжіший за `mtime`; повертає абсолютний шлях або `null`.
230
- - `extractSteps(text)` парсить рядки виду `N. task — acceptance: crit` (em-dash, маркер case-insensitive), повертає масив `{ task, acceptance? }` у порядку появи; невалідні рядки ігноруються.
231
- - `verifyTrace(cwd, runTrace?)` повертає `true`, якщо `runTrace(cwd) === 0`; за замовчуванням викликає `runTraceCli([], { cwd, log: () => {} })` з `../trace.mjs`.
232
- - Жодних мутацій ФС, жодного запису front-matter — це робить агент за `flow.mdc`.
@@ -1,167 +0,0 @@
1
- # budget.mjs
2
-
3
- ## Огляд
4
-
5
- Модуль `budget.mjs` реалізує **запобіжник бюджету** (budget guard) для автономного режиму диспатчера (відповідно до spec §9.4). Його основна задача — обмежити кількість викликів зовнішнього API, які робить `SubagentRunner`, щоб запобігти неконтрольованим витратам у середовищах, де нема людини-оператора для ручної зупинки (наприклад, на сервері або в CI).
6
-
7
- Модуль реалізує патерн **декоратор/обгортка**: приймає базовий runner, що має метод `runStep`, і повертає функціонально еквівалентний об'єкт, який додатково:
8
-
9
- - веде лічильник викликів `runStep`;
10
- - порівнює лічильник із заданим лімітом `maxApiCalls` перед кожним викликом;
11
- - кидає кастомну помилку `BudgetExceeded` при перевищенні ліміту;
12
- - логує кожен виклик через переданий колбек `log`.
13
-
14
- Окремо експортується клас помилки `BudgetExceeded`, який споживачі модуля (зокрема `run`-функція диспатчера, §9.4) можуть ловити для коректного завершення з відповідним статусом.
15
-
16
- У коментарях файлу зазначено, що параметр `maxCostUsd` (бюджет за вартістю в доларах) поки не реалізований і запланований на майбутнє — на момент написання модуль рахує лише факти виклику, а не споживані токени чи вартість.
17
-
18
- ## Експорти / API
19
-
20
- Модуль експортує два ідентифікатори (обидва — іменовані експорти, default-експорту немає):
21
-
22
- | Експорт | Тип | Призначення |
23
- | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------ |
24
- | `BudgetExceeded` | `class extends Error` | Кастомний клас помилки, що сигналізує про вичерпання бюджету `maxApiCalls`. |
25
- | `withBudget` | функція | Фабрика, що обгортає переданий runner лічильником і поверненням нового runner-сумісного об'єкта. |
26
-
27
- ## Функції
28
-
29
- ### `class BudgetExceeded extends Error`
30
-
31
- **Сигнатура:** `class BudgetExceeded extends Error {}`
32
-
33
- **Опис:** Маркерний підклас стандартної `Error`. Тіло класу порожнє — використовується виключно для відрізнення цього типу помилки від інших через `instanceof BudgetExceeded` або зіставлення в `try/catch`. Конструктор успадковується від `Error` і приймає рядок-повідомлення.
34
-
35
- **Параметри:**
36
-
37
- - `message` (string, через `super`) — текст помилки. У місці кидання передається рядок виду `budget: вичерпано maxApiCalls=<N>`.
38
-
39
- **Side effects:** жодних.
40
-
41
- **Кидає:** не застосовно (це сам клас помилки, його екземпляр кидається в `withBudget`).
42
-
43
- ### `withBudget(runner, opts)`
44
-
45
- **Сигнатура:**
46
-
47
- ```
48
- withBudget(
49
- runner: { backend?: string, runStep: (prompt: string, opts?: object) => object },
50
- opts?: { maxApiCalls?: number, log?: (m: string) => void }
51
- ): {
52
- backend: string,
53
- runStep: (prompt: string, opts?: object) => Promise<object>,
54
- readonly calls: number
55
- }
56
- ```
57
-
58
- **Параметри:**
59
-
60
- - `runner` (object, обовʼязковий) — базовий runner, який потрібно обмежити бюджетом. Інтерфейс:
61
- - `backend?: string` — опційний ідентифікатор бекенду (пробросовується у поверненому обʼєкті).
62
- - `runStep(prompt, opts?) => object` — метод виконання одного кроку (виклику API). Повертати може як значення, так і `Promise`; в обгортці він викликається без `await`, але метод обгортки оголошено `async`, тож результат буде успішно "розгорнуто" викликаючою стороною.
63
- - `opts` (object, опційний, за замовчуванням `{}`) — налаштування бюджету:
64
- - `maxApiCalls?: number` — максимально дозволена кількість викликів `runStep`. Якщо не передано, використовується `Number.POSITIVE_INFINITY` (фактично — без ліміту).
65
- - `log?: (m: string) => void` — колбек логування. Якщо не передано, використовується no-op `() => {}`.
66
-
67
- **Повертає:** новий обʼєкт-обгортку з такими членами:
68
-
69
- - `backend` — копія значення `runner.backend` (один до одного, через звичайне присвоєння у літералі). Допомагає вищим шарам визначати, з яким бекендом працює runner, без розпаковки обгортки.
70
- - `get calls()` — getter (read-only ззовні), що повертає поточне значення внутрішньої змінної `calls` (кількість зроблених викликів). Дозволяє споживачам читати лічильник, але не дає його модифікувати.
71
- - `async runStep(prompt, stepOpts)` — обгортка над `runner.runStep`. Алгоритм:
72
- 1. Якщо `calls >= maxApiCalls` — кидає `new BudgetExceeded('budget: вичерпано maxApiCalls=<maxApiCalls>')`. Перевірка виконується **до** інкременту, тобто `maxApiCalls=N` дозволяє рівно `N` викликів `runStep`, які успішно дійдуть до делегування у внутрішній runner.
73
- 2. Інкрементує `calls` на 1.
74
- 3. Викликає `log` із повідомленням виду `budget: API-виклик <calls>/<maxApiCalls>`.
75
- 4. Повертає результат `runner.runStep(prompt, stepOpts)` (як значення промісу, бо метод `async`).
76
-
77
- **Side effects:**
78
-
79
- - Мутує закриту (замкнену в замиканні) змінну `calls` — інкремент на кожен валідний виклик.
80
- - Викликає переданий `log` як побічний ефект логування (виклик відбувається **після** інкременту, тому в логах перший виклик буде `1/<maxApiCalls>`).
81
- - Делегує виклик `runner.runStep`, що має власні side effects (мережа, файлова система тощо — залежно від реалізації runner).
82
-
83
- **Кидає:**
84
-
85
- - `BudgetExceeded` — коли бюджет уже вичерпано на момент чергового виклику `runStep`. У цьому випадку внутрішній `runner.runStep` **не** викликається.
86
- - Все, що кидає `runner.runStep`, проходить наскрізь без обгортання (бо результат повертається, а не `await`-иться у tryблоку).
87
-
88
- **Особливості реалізації:**
89
-
90
- - Лічильник зберігається в локальній змінній `let calls`, доступ ззовні — лише через getter, що робить значення фактично read-only. Прямого сетера немає.
91
- - `maxApiCalls ?? Number.POSITIVE_INFINITY` — використання nullish coalescing: значення `0` залишається `0` (тобто 0 викликів дозволено = одразу `BudgetExceeded`). Тільки `undefined`/`null` замінюються на `Infinity`.
92
- - `log` за замовчуванням — функція-заглушка `() => {}`, тож виклик безпечний у будь-якому випадку.
93
- - Перевірка `calls >= maxApiCalls` робиться перед інкрементом: для `maxApiCalls=3` третій виклик пройде (бо до інкременту `calls=2 < 3`), четвертий — впаде з `BudgetExceeded`.
94
-
95
- ## Залежності
96
-
97
- **Зовнішні (npm/standard):** жодних — модуль не імпортує нічого.
98
-
99
- **Внутрішні (проєктні):** жодних — модуль самодостатній.
100
-
101
- **Глобальні залежності runtime:**
102
-
103
- - `Error` — стандартний клас JavaScript (базовий для `BudgetExceeded`).
104
- - `Number.POSITIVE_INFINITY` — стандартна константа.
105
-
106
- **Стандарт мови:** використовуються ES-modules (`export`), `class` (ES2015), `async`/`await` (ES2017), nullish coalescing `??` (ES2020), getter у літералі обʼєкта (ES5+). Файл має розширення `.mjs`, що явно вказує Node.js на ESM-режим.
107
-
108
- ## Потік виконання / Використання
109
-
110
- ### Загальний потік
111
-
112
- 1. Споживач (наприклад, диспатчер у `run`) створює базовий `SubagentRunner` із власним методом `runStep`.
113
- 2. Перед запуском автономного циклу runner обгортається: `const guarded = withBudget(runner, { maxApiCalls: 50, log: logger.info })`.
114
- 3. У циклі дисперчера викликається `await guarded.runStep(prompt, opts)` стільки разів, скільки потрібно.
115
- 4. Поточне споживання можна читати через `guarded.calls`.
116
- 5. При перевищенні `maxApiCalls` наступний `runStep` кине `BudgetExceeded`, який споживач ловить і завершує сесію з відповідним статусом (§9.4).
117
-
118
- ### Приклад використання
119
-
120
- ```js
121
- import { withBudget, BudgetExceeded } from './budget.mjs'
122
-
123
- const baseRunner = {
124
- backend: 'claude',
125
- async runStep(prompt, opts) {
126
- // ... виклик API
127
- return { text: '...', tokens: 123 }
128
- }
129
- }
130
-
131
- const runner = withBudget(baseRunner, {
132
- maxApiCalls: 10,
133
- log: msg => console.log(msg)
134
- })
135
-
136
- try {
137
- for (const step of plan) {
138
- const result = await runner.runStep(step.prompt)
139
- // ... обробка
140
- }
141
- } catch (err) {
142
- if (err instanceof BudgetExceeded) {
143
- console.error('Зупинка: бюджет вичерпано після', runner.calls, 'викликів')
144
- process.exit(2)
145
- }
146
- throw err
147
- }
148
- ```
149
-
150
- ### Контракт з вищими шарами
151
-
152
- - **`SubagentRunner`** має надавати `{ backend, runStep(prompt, opts) }`. Обгортка не вимагає інших полів; усе, що поза `backend` і `runStep`, **губиться** при обгортанні (їх немає у поверненому літералі).
153
- - **`run`-функція (§9.4)** має ловити `BudgetExceeded` окремо від інших помилок, інакше бюджет проявиться як звичайний краш.
154
- - **Тестування:** клас `BudgetExceeded` пустий, тож порівнювати краще через `instanceof`, а не через `err.name` чи `err.constructor.name` (хоча обидва теж працюватимуть стандартно для підкласу `Error`).
155
-
156
- ### Граничні випадки
157
-
158
- - `maxApiCalls: 0` — будь-який перший виклик `runStep` одразу кидає `BudgetExceeded`.
159
- - `maxApiCalls: undefined` (опція не передана) — фактично без обмеження (Infinity).
160
- - `maxApiCalls: Infinity` явно — те саме, без обмеження.
161
- - `opts` не переданий — використовуються всі дефолти (`Infinity` і no-op log).
162
- - `runner.backend === undefined` — у поверненому обʼєкті `backend` буде `undefined` (типізація показує `string`, але рантайм цьому не перешкоджає).
163
- - `runner.runStep` синхронний — все одно повертає Promise, бо обгортка `async`.
164
-
165
- ### Звʼязок зі spec
166
-
167
- Файл явно посилається на **spec §9.4** ("Budget guard для автономного режиму"). Логіка цього модуля є реалізацією контракту, описаного в специфікації: лічильник API-викликів + кастомна помилка `BudgetExceeded` як механізм аварійного виходу.
@@ -1,196 +0,0 @@
1
- # capability.mjs
2
-
3
- ## Огляд
4
-
5
- Модуль `capability.mjs` реалізує **Capability Router** — шар резолюції режиму оркестрації для підкоманди `flow` диспетчера `n-cursor`. Завдання модуля — відповісти на запитання: «у якому режимі (`native` чи `polyfill`) виконувати flow для оголошеної моделі LLM?».
6
-
7
- Ключові архітектурні рішення модуля:
8
-
9
- - **Жодної рантайм-детекції моделі.** Модель не вгадується з оточення/процесу — її потрібно **явно оголосити** (CLI-прапорець, env, config). Це відповідає вимозі spec §2.2 проєкту.
10
- - **Чисті функції без I/O.** Усі вхідні джерела (`args`, `env`, `config`, `matrix`, `hasRunner`) приходять параметрами ззовні. Модуль не читає файлову систему, не звертається до мережі та не використовує `process.*` напряму. Завдяки цьому функції тривіально тестуються без моків.
11
- - **Розділення відповідальності.** Сам модуль лише **резолвить** режим і повідомляє про можливість запуску `polyfill`. Власне кидання помилок (`fail`) та інтеграція з runner-ом виконуються caller-ом — `polyfill` без доступного `SubagentRunner` (§15.1) не може стартувати, але рішення про помилку приймається вище за стеком.
12
-
13
- Модуль використовується як read-only утиліта диспетчером: спочатку парситься CLI-прапорець `--model`, потім збирається оголошена модель за пріоритетом, далі береться режим оркестрації з `capability-matrix`, і нарешті перевіряється, чи доступний `SubagentRunner` для polyfill-шляху.
14
-
15
- ## Експорти / API
16
-
17
- | Експорт | Тип | Опис |
18
- | --------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
19
- | `DEFAULT_ORCHESTRATION` | константа: `string` (`'polyfill'`) | Дефолтний режим оркестрації, що повертається, коли в `matrix` немає інформації для моделі і не задано `matrix.default.orchestration`. |
20
- | `parseModelFlag(args)` | функція | Витягує значення прапорця `--model <value>` з масиву argv. |
21
- | `declaredModel(sources)` | функція | Повертає оголошену модель за пріоритетом CLI > env > config. |
22
- | `orchestrationFor(model, matrix)` | функція | Резолвить режим оркестрації (`'native' | 'polyfill'`) для оголошеної моделі за матрицею. |
23
- | `polyfillStartable(ctx)` | функція | Перевіряє, чи доступний `SubagentRunner`, необхідний для старту polyfill-режиму. |
24
-
25
- Усі експорти — `named exports`; `default export` відсутній.
26
-
27
- ## Функції
28
-
29
- ### `parseModelFlag(args)`
30
-
31
- **Сигнатура:** `parseModelFlag(args: string[]): string | null`
32
-
33
- **Параметри:**
34
-
35
- - `args` — масив рядків, що представляє argv підкоманди `flow` (без імені виконуваного файлу). Зазвичай це частина `process.argv`, передана у диспетчер.
36
-
37
- **Повертає:**
38
-
39
- - `string` — значення, що йде безпосередньо за токеном `--model` в argv.
40
- - `null` — якщо токен `--model` відсутній або є останнім елементом масиву (тобто значення за ним немає).
41
-
42
- **Алгоритм:**
43
-
44
- 1. Знайти індекс першого входження рядка `'--model'` через `Array.prototype.indexOf`.
45
- 2. Якщо індекс не `-1` **і** наступний елемент існує (`i + 1 < args.length`) — повернути `args[i + 1]`.
46
- 3. Інакше — повернути `null`.
47
-
48
- **Side effects:** жодних. Вхідний масив не мутується, лише читається.
49
-
50
- **Зауваги:**
51
-
52
- - Розпізнається лише форма `--model <value>` через пробіл. Форма `--model=value` цією функцією **не** підтримується.
53
- - Враховується лише перше входження `--model` (наслідок `indexOf`).
54
- - Якщо аргумент після `--model` сам є прапорцем (наприклад, `--model --foo`), він однаково буде повернутий — валідація формату значення не виконується.
55
-
56
- ### `declaredModel(sources)`
57
-
58
- **Сигнатура:** `declaredModel(sources?: { cliModel?: string | null, envModel?: string | null, configModel?: string | null }): string | null`
59
-
60
- **Параметри (об’єкт-деструктуризація з дефолтами `null`):**
61
-
62
- - `cliModel` — модель, отримана з CLI (зазвичай результат `parseModelFlag`). Найвищий пріоритет.
63
- - `envModel` — модель з env-змінної (за конвенцією проєкту — `N_CURSOR_FLOW_MODEL`). Середній пріоритет.
64
- - `configModel` — модель з конфігураційного файла (ключ `flow.model`). Найнижчий пріоритет.
65
-
66
- Усі три поля **опціональні**; виклик без аргументів (`declaredModel()`) валідний завдяки дефолту `= {}`.
67
-
68
- **Повертає:**
69
-
70
- - `string` — перше істинне (truthy) значення серед `cliModel`, `envModel`, `configModel` у вказаному порядку.
71
- - `null` — якщо всі три джерела falsy (`null`, `undefined`, `''`).
72
-
73
- **Алгоритм:** короткозамкнений ланцюг `cliModel || envModel || configModel || null`.
74
-
75
- **Side effects:** жодних.
76
-
77
- **Зауваги:** оскільки використовується `||`, **порожній рядок** `''` трактується як «не оголошено» і пропускається — це узгоджується з намірами модуля (модель повинна бути не лише визначена, а й непорожня).
78
-
79
- ### `orchestrationFor(model, matrix)`
80
-
81
- **Сигнатура:** `orchestrationFor(model: string | null, matrix: { models?: Record<string, { orchestration?: string }>, default?: { orchestration?: string } }): 'native' | 'polyfill'`
82
-
83
- **Параметри:**
84
-
85
- - `model` — оголошена модель (зазвичай результат `declaredModel`). Може бути `null`.
86
- - `matrix` — capability-matrix:
87
- - `matrix.models` — мапа `модель → { orchestration }` з режимом для конкретних моделей.
88
- - `matrix.default.orchestration` — фолбек-режим для невідомих/неоголошених моделей.
89
-
90
- **Повертає:** літеральний рядок `'native'` або `'polyfill'` (типи з JSDoc; у реальності повертається будь-яке рядкове значення, прочитане з матриці, але інваріант протоколу — саме ці два).
91
-
92
- **Алгоритм каскадного фолбеку:**
93
-
94
- 1. Якщо `model` truthy **і** є `matrix.models` — узяти `entry = matrix.models[model]`; інакше `entry = null`.
95
- 2. Повернути перше істинне з трьох:
96
- - `entry?.orchestration` — режим, прописаний для конкретної моделі;
97
- - `matrix?.default?.orchestration` — дефолт із самої матриці;
98
- - `DEFAULT_ORCHESTRATION` — глобальний дефолт модуля (`'polyfill'`).
99
-
100
- **Side effects:** жодних. Матриця читається ad hoc без копіювання.
101
-
102
- **Зауваги:**
103
-
104
- - Функція стійка до `null`/`undefined` як `matrix`, так і `matrix.models`/`matrix.default` завдяки явним перевіркам та операторам `&&`.
105
- - Невідома модель (відсутня в `matrix.models`) автоматично потрапляє в гілку `matrix.default` → `DEFAULT_ORCHESTRATION`. Це означає, що нові/незареєстровані моделі за замовчуванням підуть через `polyfill` (за наявності runner-а).
106
-
107
- ### `polyfillStartable(ctx)`
108
-
109
- **Сигнатура:** `polyfillStartable(ctx: { hasRunner: boolean }): boolean`
110
-
111
- **Параметри:**
112
-
113
- - `ctx.hasRunner` — прапорець наявності `SubagentRunner` у середовищі (відповідно до §15.1 spec). Тип повинен бути саме `boolean`.
114
-
115
- **Повертає:**
116
-
117
- - `true` — якщо `ctx.hasRunner === true` (strict equality).
118
- - `false` — у будь-якому іншому випадку (`false`, `undefined`, truthy-не-`true`, тощо).
119
-
120
- **Алгоритм:** одна перевірка `hasRunner === true`.
121
-
122
- **Side effects:** жодних.
123
-
124
- **Зауваги:** strict-перевірка свідома — модуль не приймає «приблизно правда» значення (`1`, `'yes'`, об’єкти runner-а тощо). Це змушує caller-а передавати дискретний boolean-прапорець, що дисциплінує контракт.
125
-
126
- ## Залежності
127
-
128
- **Імпорти:** жодних. Модуль **самодостатній** — не залежить ні від npm-пакетів, ні від інших файлів проєкту.
129
-
130
- **Глобали / середовище:** не використовуються. Зокрема, не читається `process.argv`, `process.env`, файлова система, мережа.
131
-
132
- **Споживачі (caller-и):** модуль очікувано викликається з реалізації підкоманди `flow` диспетчера (`npm/scripts/dispatcher/...`), яка:
133
-
134
- 1. збирає `args` з `process.argv`;
135
- 2. підставляє `process.env.N_CURSOR_FLOW_MODEL` як `envModel`;
136
- 3. читає `flow.model` з config-файла як `configModel`;
137
- 4. вантажить `capability-matrix` (ймовірно, із статичного JSON/JS);
138
- 5. визначає `hasRunner` за наявністю `SubagentRunner` у рантаймі;
139
- 6. за результатом `orchestrationFor` + `polyfillStartable` або стартує flow, або кидає помилку.
140
-
141
- ## Потік виконання / Використання
142
-
143
- Типовий ланцюг викликів caller-а:
144
-
145
- ```js
146
- import {
147
- parseModelFlag,
148
- declaredModel,
149
- orchestrationFor,
150
- polyfillStartable,
151
- DEFAULT_ORCHESTRATION
152
- } from './capability.mjs'
153
-
154
- // 1. CLI-парсинг
155
- const cliModel = parseModelFlag(args)
156
-
157
- // 2. Резолюція оголошеної моделі за пріоритетом
158
- const model = declaredModel({
159
- cliModel,
160
- envModel: process.env.N_CURSOR_FLOW_MODEL ?? null,
161
- configModel: config?.flow?.model ?? null
162
- })
163
-
164
- // 3. Режим оркестрації за матрицею
165
- const mode = orchestrationFor(model, capabilityMatrix)
166
-
167
- // 4. Перевірка можливості старту polyfill
168
- if (mode === 'polyfill' && !polyfillStartable({ hasRunner })) {
169
- throw new Error('polyfill requires SubagentRunner (spec §15.1)')
170
- }
171
-
172
- // 5. Старт flow у режимі mode
173
- startFlow({ mode, model })
174
- ```
175
-
176
- **Інваріанти потоку:**
177
-
178
- - Якщо модель **не оголошена** в жодному з трьох джерел, `declaredModel` повертає `null`. `orchestrationFor(null, matrix)` пропустить `matrix.models` і впаде на `matrix.default` або `DEFAULT_ORCHESTRATION = 'polyfill'`. Тобто за замовчуванням flow без оголошеної моделі піде через `polyfill` — і вимагатиме `SubagentRunner`.
179
- - Caller відповідає за **fail-fast** у разі `mode === 'polyfill' && !hasRunner`. Сам модуль помилок не кидає.
180
- - `DEFAULT_ORCHESTRATION = 'polyfill'` означає: «дефолт — мати polyfill доступним»; це сумісно з ідеєю, що `polyfill` має «працювати з будь-якою моделлю» **лише** за наявності runner-а.
181
-
182
- **Таблиця рішень `orchestrationFor`:**
183
-
184
- | `model` | `matrix.models[model]` | `matrix.default.orchestration` | Результат |
185
- | ---------------- | ------------------------------- | ------------------------------ | -------------------------------------- |
186
- | `'modelA'` | `{ orchestration: 'native' }` | будь-що | `'native'` |
187
- | `'modelB'` | `{ orchestration: 'polyfill' }` | будь-що | `'polyfill'` |
188
- | `'unknownModel'` | відсутній | `'native'` | `'native'` |
189
- | `'unknownModel'` | відсутній | `'polyfill'` | `'polyfill'` |
190
- | `'unknownModel'` | відсутній | відсутній | `DEFAULT_ORCHESTRATION` (`'polyfill'`) |
191
- | `null` | (не дивимось) | `'native'` | `'native'` |
192
- | `null` | (не дивимось) | відсутній | `DEFAULT_ORCHESTRATION` (`'polyfill'`) |
193
-
194
- ## Rebuild Test
195
-
196
- На основі цієї документації модуль `capability.mjs` можна повністю відтворити: він складається з однієї константи `DEFAULT_ORCHESTRATION = 'polyfill'` та чотирьох чистих експортних функцій (`parseModelFlag`, `declaredModel`, `orchestrationFor`, `polyfillStartable`) з описаними вище сигнатурами, алгоритмами та інваріантами; жодних імпортів, жодного I/O, жодного default-export — тільки named exports.