@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,231 +0,0 @@
|
|
|
1
|
-
# `gate.mjs` — реалізація команди `flow gate`
|
|
2
|
-
|
|
3
|
-
## Огляд
|
|
4
|
-
|
|
5
|
-
Модуль реалізує підкоманду `flow gate` диспатчера — структурований вердикт релізної готовності у стилі BMAD `qa-gate`, адаптованого до внутрішнього флоу проєкту. Скрипт **синтезує** два джерела сигналів, що накопичуються у файлі стану флоу (`.flow.json`):
|
|
6
|
-
|
|
7
|
-
1. Механічні гейти, які заповнює крок `verify` (`state.gates` — масив `{ name, ok }`).
|
|
8
|
-
2. Adversarial-зауваження, які залишає крок `review` (`state.review.findings` — масив об'єктів із полем `severity`).
|
|
9
|
-
|
|
10
|
-
На виході формується єдиний вердикт `PASS | CONCERNS | FAIL`, числовий `score` у діапазоні `0..100` та список людиночитаних причин. Командa `gate` **не приймає рішень** за `verify` чи `review`, а лише агрегує їх — це забезпечує traceability «чому готово / не готово».
|
|
11
|
-
|
|
12
|
-
Архітектурно модуль складається з двох частин:
|
|
13
|
-
|
|
14
|
-
- `computeGate(state)` — **чиста** функція, повністю детермінована від вхідного стану, не торкається диска й часу. Призначена для unit-тестів без I/O-моків.
|
|
15
|
-
- `gate(_rest, deps)` — async-обгортка, що читає стан з диска, викликає `computeGate`, фіксує результат як подію в `events.jsonl` і повертає exit-код для CLI. Усі залежності IO (`cwd`, `log`, `now`) ін'єктуються через `deps`, що робить функцію тестопридатною.
|
|
16
|
-
|
|
17
|
-
## Експорти / API
|
|
18
|
-
|
|
19
|
-
| Експорт | Тип | Призначення |
|
|
20
|
-
| ------------- | ---------------- | --------------------------------------- |
|
|
21
|
-
| `computeGate` | `function` | Чистий синтез вердикту з об'єкта стану. |
|
|
22
|
-
| `gate` | `async function` | CLI-handler підкоманди `flow gate`. |
|
|
23
|
-
|
|
24
|
-
Модуль не має default-експорту. Внутрішня константа `PENALTY` (штрафи score) **не експортується** і є приватною деталлю реалізації.
|
|
25
|
-
|
|
26
|
-
### `computeGate(state)`
|
|
27
|
-
|
|
28
|
-
Сигнатура (JSDoc):
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
@param {{ gates?: { name: string, ok: boolean }[],
|
|
32
|
-
review?: { findings?: { severity?: string }[] } }} state
|
|
33
|
-
@returns {{ verdict: 'PASS' | 'CONCERNS' | 'FAIL',
|
|
34
|
-
score: number,
|
|
35
|
-
reasons: string[] }}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### `gate(_rest, deps)`
|
|
39
|
-
|
|
40
|
-
Сигнатура (JSDoc):
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
@param {string[]} _rest аргументи CLI (не використовуються)
|
|
44
|
-
@param {{ cwd?: string,
|
|
45
|
-
log?: (m: string) => void,
|
|
46
|
-
now?: () => number,
|
|
47
|
-
branch?: string }} [deps] ін'єкції
|
|
48
|
-
@returns {Promise<number>} exit-код (FAIL → 1; PASS/CONCERNS → 0)
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Параметр `_rest` зберігається в сигнатурі для уніфікації з рештою CLI-handlers диспатчера; підкреслення на початку позначає навмисне ігнорування. Крім задокументованих у JSDoc полів `deps`, реалізація читає також `deps.branch` — він передається в `resolveActiveFlowState`.
|
|
52
|
-
|
|
53
|
-
## Функції
|
|
54
|
-
|
|
55
|
-
### `computeGate(state)`
|
|
56
|
-
|
|
57
|
-
- **Сигнатура:** `computeGate(state) → { verdict, score, reasons }`.
|
|
58
|
-
- **Параметри:**
|
|
59
|
-
- `state.gates` (необов'язково) — масив об'єктів `{ name: string, ok: boolean }`. Якщо відсутній, береться `[]`.
|
|
60
|
-
- `state.review.findings` (необов'язково) — масив об'єктів із полем `severity` зі значенням `'high'` або `'med'` (інші значення ігноруються при підрахунку штрафів). Якщо відсутній — `[]`.
|
|
61
|
-
- **Повертає:** об'єкт `{ verdict, score, reasons }`.
|
|
62
|
-
- `verdict` — рядок `'PASS'`, `'CONCERNS'` або `'FAIL'`.
|
|
63
|
-
- `score` — ціле число `0..100`, обмежене через `Math.max(0, Math.min(100, …))`.
|
|
64
|
-
- `reasons` — масив рядкових пояснень (можливо порожній при `PASS`).
|
|
65
|
-
- **Side effects:** немає. Функція чиста, детермінована, без I/O і без використання глобального часу.
|
|
66
|
-
|
|
67
|
-
**Алгоритм синтезу:**
|
|
68
|
-
|
|
69
|
-
1. Розбиває `gates` на `failedGates = gates.filter(g => !g.ok)`.
|
|
70
|
-
2. Розбиває `findings` на дві групи за `severity`: `high` і `med`.
|
|
71
|
-
3. Прапор `noVerify = gates.length === 0` — стан, коли `verify` ще не запускався.
|
|
72
|
-
4. Формує `reasons`:
|
|
73
|
-
- для кожного провального гейта: `gate «<name>» провалено`;
|
|
74
|
-
- якщо `high.length > 0`: `<N> high-severity review finding(s)`;
|
|
75
|
-
- якщо `med.length > 0`: `<N> med-severity review finding(s)`;
|
|
76
|
-
- якщо `noVerify`: `verify ще не запускався`.
|
|
77
|
-
5. Визначає `verdict` за пріоритетом:
|
|
78
|
-
- `FAIL` — якщо є хоча б один провальний гейт **або** хоча б один `high`-finding;
|
|
79
|
-
- інакше `CONCERNS` — якщо є `med`-findings **або** `noVerify`;
|
|
80
|
-
- інакше `PASS`.
|
|
81
|
-
6. Обраховує `penalty` як суму штрафів `PENALTY`:
|
|
82
|
-
- `failedGate = 40` за кожен провальний гейт;
|
|
83
|
-
- `high = 25` за кожен `high`-finding;
|
|
84
|
-
- `med = 8` за кожен `med`-finding;
|
|
85
|
-
- `noVerify = 15` (одноразово, якщо `gates` порожній).
|
|
86
|
-
7. `score = max(0, min(100, 100 - penalty))`.
|
|
87
|
-
|
|
88
|
-
**Інваріанти:**
|
|
89
|
-
|
|
90
|
-
- `FAIL` ⇒ `score < 100` (`penalty ≥ 25` гарантовано через high або failedGate ≥ 40).
|
|
91
|
-
- `PASS` ⇒ `penalty = 0` ⇒ `score = 100`.
|
|
92
|
-
- `CONCERNS` досяжний при `penalty > 0` без `failedGates` і без `high`.
|
|
93
|
-
- `score` ніколи не виходить за `[0, 100]`.
|
|
94
|
-
|
|
95
|
-
### `gate(_rest, deps = {})`
|
|
96
|
-
|
|
97
|
-
- **Сигнатура:** `async gate(_rest, deps) → Promise<number>`.
|
|
98
|
-
- **Параметри:**
|
|
99
|
-
- `_rest` — масив рядків (CLI-аргументи), ігнорується.
|
|
100
|
-
- `deps.cwd` — поточна робоча директорія; default — `process.cwd()`.
|
|
101
|
-
- `deps.log` — функція логування; default — `console.error`.
|
|
102
|
-
- `deps.now` — провайдер часу `() => number`; default — `Date.now`.
|
|
103
|
-
- `deps.branch` — додаткова підказка для `resolveActiveFlowState` (необов'язково).
|
|
104
|
-
- **Повертає:** `Promise<number>` — exit-код процесу:
|
|
105
|
-
- `1` — якщо стан недоступний (немає активного флоу або `flow init` ще не виконувався) або вердикт `FAIL`;
|
|
106
|
-
- `0` — для `PASS` і `CONCERNS`.
|
|
107
|
-
- **Side effects:**
|
|
108
|
-
- Викликає `resolveActiveFlowState` (може читати worktree/гілку).
|
|
109
|
-
- Викликає `readState(statePath)` — синхронне читання `.flow.json`.
|
|
110
|
-
- Викликає `recordTransition(...)` — мутація `.flow.json` і дописування в `events.jsonl` (через `flowEventsPath(cwd)`).
|
|
111
|
-
- Логування в `deps.log` (за замовчуванням stderr).
|
|
112
|
-
|
|
113
|
-
**Покроковий потік:**
|
|
114
|
-
|
|
115
|
-
1. Розв'язує дефолтні залежності: `cwd0`, `log`, `now`.
|
|
116
|
-
2. `resolveActiveFlowState({ cwd: cwd0, branch })` визначає активний флоу:
|
|
117
|
-
- якщо `resolved.statePath` пустий — логує `gate: <error>` і повертає `1`;
|
|
118
|
-
- якщо `resolved.autoResolved` — логує повідомлення `flow: авторезолвлено активний flow «<label>» (cwd поза worktree)`.
|
|
119
|
-
3. Підставляє `cwd = resolved.worktreeDir ?? cwd0` і `statePath = resolved.statePath`.
|
|
120
|
-
4. Читає стан `readState(statePath)`; якщо `null/undefined` — логує `gate: стану нема — спершу `flow init``і повертає`1`.
|
|
121
|
-
5. Викликає `computeGate(state)` → `result`.
|
|
122
|
-
6. Викликає `recordTransition` з:
|
|
123
|
-
- шляхами `{ statePath, eventsPath: flowEventsPath(cwd) }`,
|
|
124
|
-
- подією `{ type: 'gate', verdict: result.verdict }`,
|
|
125
|
-
- редьюсером `s => ({ ...s, gate: { ...result, at: new Date(now()).toISOString() } })` — додає до стану секцію `gate` з `verdict`/`score`/`reasons`/`at` (ISO-таймстемп);
|
|
126
|
-
- провайдером часу `now`.
|
|
127
|
-
7. Логує рядок `gate: <VERDICT> (score <N>)`, потім по ` · <reason>` для кожної причини.
|
|
128
|
-
8. Повертає `1`, якщо `result.verdict === 'FAIL'`, інакше `0`.
|
|
129
|
-
|
|
130
|
-
## Залежності
|
|
131
|
-
|
|
132
|
-
### Зовнішні (Node.js core)
|
|
133
|
-
|
|
134
|
-
- `node:process` — імпорт `cwd as processCwd` для дефолтного значення `deps.cwd`.
|
|
135
|
-
|
|
136
|
-
### Внутрішні модулі диспатчера
|
|
137
|
-
|
|
138
|
-
- `./events.mjs` — функція `flowEventsPath(cwd)` будує шлях до файлу подій `events.jsonl` поточного флоу.
|
|
139
|
-
- `./state-store.mjs`:
|
|
140
|
-
- `readState(statePath)` — синхронне читання `.flow.json` (повертає `null`, якщо нема);
|
|
141
|
-
- `recordTransition({ statePath, eventsPath }, event, reducer, now)` — атомарна мутація стану + лог подій.
|
|
142
|
-
- `./flow-resolve.mjs` — `resolveActiveFlowState({ cwd, branch }, deps)` визначає активний worktree/гілку/`statePath`; повертає поля `statePath`, `worktreeDir`, `autoResolved`, `label`, `error`.
|
|
143
|
-
|
|
144
|
-
### Глобальні
|
|
145
|
-
|
|
146
|
-
- `console.error` — дефолтний логер (замінюється через `deps.log` у тестах).
|
|
147
|
-
- `Date.now`, `new Date(...).toISOString()` — джерело таймстемпів (час ін'єктується через `deps.now`, але `new Date(...).toISOString()` працює з результатом).
|
|
148
|
-
|
|
149
|
-
## Потік виконання / Використання
|
|
150
|
-
|
|
151
|
-
### Інтеграція в CLI
|
|
152
|
-
|
|
153
|
-
`gate` реєструється в основному диспатчері `flow` як handler підкоманди `gate`. Виклик відбувається у форматі:
|
|
154
|
-
|
|
155
|
-
```
|
|
156
|
-
flow gate
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Додаткові позиційні аргументи не зчитуються (`_rest` ігнорується).
|
|
160
|
-
|
|
161
|
-
### Типовий ланцюжок флоу
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
flow init → створює .flow.json
|
|
165
|
-
flow verify → заповнює state.gates
|
|
166
|
-
flow review → заповнює state.review.findings
|
|
167
|
-
flow gate → агрегує → state.gate = { verdict, score, reasons, at }
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Сценарії exit-коду
|
|
171
|
-
|
|
172
|
-
| Сценарій | Лог | Exit |
|
|
173
|
-
| ------------------------- | --------------------------------------- | ---- |
|
|
174
|
-
| Активний флоу не знайдено | `gate: <error>` | `1` |
|
|
175
|
-
| `.flow.json` відсутній | `gate: стану нема — спершу `flow init`` | `1` |
|
|
176
|
-
| `verdict = FAIL` | `gate: FAIL (score …)` + причини | `1` |
|
|
177
|
-
| `verdict = CONCERNS` | `gate: CONCERNS (score …)` + причини | `0` |
|
|
178
|
-
| `verdict = PASS` | `gate: PASS (score 100)` | `0` |
|
|
179
|
-
|
|
180
|
-
### Приклад чистого використання `computeGate`
|
|
181
|
-
|
|
182
|
-
```js
|
|
183
|
-
import { computeGate } from './gate.mjs'
|
|
184
|
-
|
|
185
|
-
const state = {
|
|
186
|
-
gates: [
|
|
187
|
-
{ name: 'lint', ok: true },
|
|
188
|
-
{ name: 'test', ok: false }
|
|
189
|
-
],
|
|
190
|
-
review: { findings: [{ severity: 'med' }] }
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
computeGate(state)
|
|
194
|
-
// → { verdict: 'FAIL', score: 52, reasons: [
|
|
195
|
-
// 'gate «test» провалено',
|
|
196
|
-
// '1 med-severity review finding(s)'
|
|
197
|
-
// ] }
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Приклад тестування `gate` через ін'єкції
|
|
201
|
-
|
|
202
|
-
```js
|
|
203
|
-
import { gate } from './gate.mjs'
|
|
204
|
-
|
|
205
|
-
const logs = []
|
|
206
|
-
const code = await gate([], {
|
|
207
|
-
cwd: '/tmp/fixture',
|
|
208
|
-
log: m => logs.push(m),
|
|
209
|
-
now: () => 1_700_000_000_000
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
// code === 0 | 1
|
|
213
|
-
// logs містить рядки виду 'gate: PASS (score 100)' / ' · …'
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
### Записи в `.flow.json`
|
|
217
|
-
|
|
218
|
-
Після успішного виконання у стані з'являється секція:
|
|
219
|
-
|
|
220
|
-
```json
|
|
221
|
-
{
|
|
222
|
-
"gate": {
|
|
223
|
-
"verdict": "CONCERNS",
|
|
224
|
-
"score": 92,
|
|
225
|
-
"reasons": ["1 med-severity review finding(s)"],
|
|
226
|
-
"at": "2024-01-01T00:00:00.000Z"
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
Подія `{ type: 'gate', verdict }` дописується в `events.jsonl` через `recordTransition`.
|
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
# level.mjs
|
|
2
|
-
|
|
3
|
-
## Огляд
|
|
4
|
-
|
|
5
|
-
Модуль `level.mjs` реалізує **scale-adaptive детекцію рівня складності й рівня ризику** задачі за її текстовим описом. Ідея запозичена з BMAD (project-levels / risk-profile) і адаптована до внутрішніх термінів проєкту.
|
|
6
|
-
|
|
7
|
-
Призначення модуля у контексті dispatcher:
|
|
8
|
-
|
|
9
|
-
- На етапі `init` (ініціалізація задачі/флоу) функція виклику аналізує текстовий опис задачі і відносить її до одного з чотирьох рівнів складності `L0..L3` та одного з трьох рівнів ризику `low | med | high`.
|
|
10
|
-
- Поєднання `level` + `risk` _right-size_-ить (масштабує) подальший пайплайн: зокрема, скільки adversarial-рецензентів спавнить команда `flow review` і яка її глибина/фокус, а також які фази робочого процесу рекомендовані як контракт.
|
|
11
|
-
- Усі рішення детермінуються пошуком ключових слів у нижньому регістрі без використання регулярних виразів — це свідомий вибір для уникнення slow-regex та проблем зі словесними межами для кириличних символів.
|
|
12
|
-
|
|
13
|
-
Файл є **чистою бібліотекою без побічних ефектів**: не звертається до файлової системи, мережі, не пише в `stdout/stderr`, не залежить від часу. Усі експорти — детерміновані функції від рядкового опису (або числа/рядка).
|
|
14
|
-
|
|
15
|
-
## Експорти / API
|
|
16
|
-
|
|
17
|
-
Модуль експортує чотири іменовані функції (усі — `export function`):
|
|
18
|
-
|
|
19
|
-
| Експорт | Сигнатура | Призначення |
|
|
20
|
-
| ------------------- | -------------------------------------------- | --------------------------------------------------------------------------- |
|
|
21
|
-
| `detectLevel` | `(desc: string) => 0 \| 1 \| 2 \| 3` | Визначає рівень складності задачі за описом. |
|
|
22
|
-
| `reviewersForLevel` | `(level: number) => number` | Кількість adversarial-рецензентів на основі рівня (1..3). |
|
|
23
|
-
| `detectRisk` | `(desc: string) => 'low' \| 'med' \| 'high'` | Визначає рівень ризику задачі за описом. |
|
|
24
|
-
| `reviewersForRisk` | `(risk: string) => number` | Кількість рецензентів на основі ризику (1..3). |
|
|
25
|
-
| `reviewersFor` | `(level: number, risk?: string) => number` | Підсумкова кількість рецензентів: максимум вимог рівня й ризику, з капом 3. |
|
|
26
|
-
|
|
27
|
-
Внутрішні (НЕ експортовані) допоміжні функції: `isAsciiAlnum`, `hasWord`.
|
|
28
|
-
|
|
29
|
-
Внутрішні константи з наборами ключових слів: `L3_KEYS`, `L0_WORD_KEYS`, `L0_SUBSTR_KEYS`, `L2_KEYS`, `HIGH_RISK_KEYS`, `MED_RISK_KEYS`.
|
|
30
|
-
|
|
31
|
-
## Константи (внутрішні)
|
|
32
|
-
|
|
33
|
-
### `L3_KEYS`
|
|
34
|
-
|
|
35
|
-
Ключові слова, що сигналізують про **архітектурні/великі/міграційні** задачі (рівень L3).
|
|
36
|
-
|
|
37
|
-
```text
|
|
38
|
-
['platform', 'migration', 'rewrite', 'architecture', 'enterprise',
|
|
39
|
-
'редизайн', 'міграц', 'переписат']
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Збігаються звичайним `indexOf` (підрядок, case-insensitive після приведення опису до lowercase). Кириличні корені `міграц` / `переписат` спеціально вкорочені до основи слова (стемінг), щоб ловити будь-які закінчення (`міграція`, `міграційний`, `переписати`, `переписану`).
|
|
43
|
-
|
|
44
|
-
### `L0_WORD_KEYS`
|
|
45
|
-
|
|
46
|
-
ASCII-дієслова, що сигналізують про **тривіальні** задачі (рівень L0). Матчаться **тільки як ціле слово** через `hasWord` — щоб `fix` не ловило `prefix` / `fixture`.
|
|
47
|
-
|
|
48
|
-
```text
|
|
49
|
-
['fix', 'typo', 'bump', 'rename', 'hotfix']
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### `L0_SUBSTR_KEYS`
|
|
53
|
-
|
|
54
|
-
Кириличні ключі для L0, матчаться **підрядком** (стемінг: `перейменув` ловить `перейменування`, `перейменувати` тощо).
|
|
55
|
-
|
|
56
|
-
```text
|
|
57
|
-
['опечат', 'перейменув']
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### `L2_KEYS`
|
|
61
|
-
|
|
62
|
-
Ключові слова для **багатофайлових фіч/рефакторів** (рівень L2). Матчаться підрядком.
|
|
63
|
-
|
|
64
|
-
```text
|
|
65
|
-
['feature', 'epic', 'refactor', 'рефактор', 'фіча']
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### `HIGH_RISK_KEYS`
|
|
69
|
-
|
|
70
|
-
Ключі **високого** ризику (безпека, гроші, доступи). Матчаються підрядком.
|
|
71
|
-
|
|
72
|
-
```text
|
|
73
|
-
['security', 'auth', 'crypto', 'payment', 'secret', 'token',
|
|
74
|
-
'permission', 'password', 'безпек']
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### `MED_RISK_KEYS`
|
|
78
|
-
|
|
79
|
-
Ключі **середнього** ризику (дані, незворотність). Матчаться підрядком.
|
|
80
|
-
|
|
81
|
-
```text
|
|
82
|
-
['data', ' db', 'database', 'migration', 'delete', 'gateway',
|
|
83
|
-
'міграц', 'видален']
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Зверніть увагу: ключ `' db'` навмисно записаний з провідним пробілом — це дешевий спосіб відрізнити окреме слово `db` від випадкових підрядків (наприклад, у `dbg`, `dbus`), не вдаючись до пошуку зі словесними межами.
|
|
87
|
-
|
|
88
|
-
## Функції
|
|
89
|
-
|
|
90
|
-
### `isAsciiAlnum(ch)` _(internal)_
|
|
91
|
-
|
|
92
|
-
**Сигнатура:** `function isAsciiAlnum(ch: string | undefined): boolean`
|
|
93
|
-
|
|
94
|
-
**Параметри:**
|
|
95
|
-
|
|
96
|
-
- `ch` — один символ (або `undefined`, що моделює "край рядка" перед нульовою позицією чи за останньою).
|
|
97
|
-
|
|
98
|
-
**Повертає:** `true`, якщо `ch` — ASCII-літера в нижньому регістрі (`a..z`) або ASCII-цифра (`0..9`). Інакше `false`. Для `undefined` повертає `false` (край рядка не вважається символом alnum).
|
|
99
|
-
|
|
100
|
-
**Side effects:** немає.
|
|
101
|
-
|
|
102
|
-
**Використання:** як перевірка межі слова в `hasWord`. Свідомо охоплює лише lowercase ASCII (бо вхід вже зведено до `.toLowerCase()` у `detectLevel`/`detectRisk`).
|
|
103
|
-
|
|
104
|
-
### `hasWord(text, word)` _(internal)_
|
|
105
|
-
|
|
106
|
-
**Сигнатура:** `function hasWord(text: string, word: string): boolean`
|
|
107
|
-
|
|
108
|
-
**Параметри:**
|
|
109
|
-
|
|
110
|
-
- `text` — основний текст (передбачається lowercase після `.toLowerCase()`).
|
|
111
|
-
- `word` — шукане ASCII-слово (lowercase).
|
|
112
|
-
|
|
113
|
-
**Повертає:** `true`, якщо в `text` зустрічається `word` і обидві його межі (символ безпосередньо до й безпосередньо після) **не є** ASCII-alnum (тобто це справді ціле слово); інакше `false`.
|
|
114
|
-
|
|
115
|
-
**Алгоритм:**
|
|
116
|
-
|
|
117
|
-
1. Шукає перше входження `word` через `text.indexOf(word)`.
|
|
118
|
-
2. Поки знайдено (`i !== -1`):
|
|
119
|
-
- Перевіряє ліву межу: `text[i - 1]` (або `undefined`, якщо `i === 0`).
|
|
120
|
-
- Перевіряє праву межу: `text[i + word.length]` (або `undefined`, якщо це кінець рядка).
|
|
121
|
-
- Якщо **жодна** з них не є ASCII-alnum — повертає `true`.
|
|
122
|
-
- Інакше шукає наступне входження через `text.indexOf(word, i + 1)`.
|
|
123
|
-
3. Якщо всі входження виявилися підрядками всередині більших слів — повертає `false`.
|
|
124
|
-
|
|
125
|
-
**Side effects:** немає.
|
|
126
|
-
|
|
127
|
-
**Чому без regex:** конвенція файлу — уникати `RegExp` для запобігання slow-regex атакам і неоднозначностям `\b` на кириличних межах слів (де `\b` в JS-regex поводиться непередбачувано). Ручний прохід через `indexOf` дешевший і явніший.
|
|
128
|
-
|
|
129
|
-
### `detectLevel(desc)`
|
|
130
|
-
|
|
131
|
-
**Експорт:** `export function detectLevel(desc): 0 | 1 | 2 | 3`
|
|
132
|
-
|
|
133
|
-
**Параметри:**
|
|
134
|
-
|
|
135
|
-
- `desc` — опис задачі (будь-який тип; усередині нормалізується через `String(desc ?? '')`).
|
|
136
|
-
|
|
137
|
-
**Повертає:** один із літералів `0 | 1 | 2 | 3` — рівень складності.
|
|
138
|
-
|
|
139
|
-
**Side effects:** немає.
|
|
140
|
-
|
|
141
|
-
**Алгоритм (з пріоритетом L3 > L0 > L2 > L1):**
|
|
142
|
-
|
|
143
|
-
1. Нормалізує `desc` до рядка: `String(desc ?? '')` (значення `null`/`undefined` стають порожнім рядком).
|
|
144
|
-
2. Зводить рядок у нижній регістр: `const d = ...toLowerCase()`.
|
|
145
|
-
3. Створює локальний хелпер `has = keys => keys.some(k => d.includes(k))` (підрядковий пошук).
|
|
146
|
-
4. Обчислює `isL0`:
|
|
147
|
-
- `L0_WORD_KEYS.some(k => hasWord(d, k))` — ASCII-дієслова як ціле слово, **АБО**
|
|
148
|
-
- `L0_SUBSTR_KEYS.some(k => d.includes(k))` — кириличні підрядки.
|
|
149
|
-
5. Послідовно перевіряє:
|
|
150
|
-
- Якщо `has(L3_KEYS)` → `3`.
|
|
151
|
-
- Інакше якщо `isL0` → `0`.
|
|
152
|
-
- Інакше якщо `has(L2_KEYS)` → `2`.
|
|
153
|
-
- Інакше → дефолт `1`.
|
|
154
|
-
|
|
155
|
-
**Семантика пріоритетів:**
|
|
156
|
-
|
|
157
|
-
- L3 виграє у всіх: навіть якщо опис містить і `migration`, і `fix`, рівень буде `3` — велика міграція важливіша за згадку дрібного фіксу.
|
|
158
|
-
- L0 наступний: тривіальні зміни не повинні маскуватися під L2-фічу через випадковий збіг.
|
|
159
|
-
- L2 — middle ground для багатофайлових задач.
|
|
160
|
-
- L1 — дефолт, коли жоден сигнал не зловлено.
|
|
161
|
-
|
|
162
|
-
**Приклади:**
|
|
163
|
-
|
|
164
|
-
- `'Fix typo in README'` → L0 (`fix` як слово; `typo`).
|
|
165
|
-
- `'Add database migration for users'` → L3 (через `migration`, що в L3_KEYS).
|
|
166
|
-
- `'Refactor auth module'` → L2 (`refactor`; `auth` тут не змінює рівень — це сигнал ризику, не рівня).
|
|
167
|
-
- `'Update copy on landing'` → L1 (нічого не зловлено).
|
|
168
|
-
- `'Prefix new fields with role_'` → L1 (`prefix` НЕ ловиться L0_WORD_KEYS через цілословний матч `fix`).
|
|
169
|
-
|
|
170
|
-
### `reviewersForLevel(level)`
|
|
171
|
-
|
|
172
|
-
**Експорт:** `export function reviewersForLevel(level: number): number`
|
|
173
|
-
|
|
174
|
-
**Параметри:**
|
|
175
|
-
|
|
176
|
-
- `level` — числовий рівень (зазвичай `0..3`, але приймає будь-яке число).
|
|
177
|
-
|
|
178
|
-
**Повертає:** ціле в діапазоні `1..3` — рекомендована кількість adversarial-рецензентів суто на основі рівня.
|
|
179
|
-
|
|
180
|
-
**Side effects:** немає.
|
|
181
|
-
|
|
182
|
-
**Таблиця рішень:**
|
|
183
|
-
|
|
184
|
-
| `level` | Повертає |
|
|
185
|
-
| -------------------------- | -------- |
|
|
186
|
-
| `>= 3` | `3` |
|
|
187
|
-
| `=== 2` | `2` |
|
|
188
|
-
| решта (`0`, `1`, від'ємні) | `1` |
|
|
189
|
-
|
|
190
|
-
**Семантика:** глибина review корелює з масштабом задачі.
|
|
191
|
-
|
|
192
|
-
### `detectRisk(desc)`
|
|
193
|
-
|
|
194
|
-
**Експорт:** `export function detectRisk(desc): 'low' | 'med' | 'high'`
|
|
195
|
-
|
|
196
|
-
**Параметри:**
|
|
197
|
-
|
|
198
|
-
- `desc` — опис задачі (будь-який тип; усередині нормалізується через `String(desc ?? '')`).
|
|
199
|
-
|
|
200
|
-
**Повертає:** літерал `'low' | 'med' | 'high'`.
|
|
201
|
-
|
|
202
|
-
**Side effects:** немає.
|
|
203
|
-
|
|
204
|
-
**Алгоритм:**
|
|
205
|
-
|
|
206
|
-
1. Нормалізує: `const d = String(desc ?? '').toLowerCase()`.
|
|
207
|
-
2. `has = keys => keys.some(k => d.includes(k))` — суто підрядковий пошук (для ризику немає різниці ASCII vs кирилиця: всі ключі матчаться як підрядки).
|
|
208
|
-
3. Якщо `has(HIGH_RISK_KEYS)` → `'high'`.
|
|
209
|
-
4. Інакше якщо `has(MED_RISK_KEYS)` → `'med'`.
|
|
210
|
-
5. Інакше → `'low'`.
|
|
211
|
-
|
|
212
|
-
**Семантика пріоритетів:** `high > med > low`. Якщо опис одночасно містить і high-, і med-сигнал — виграє `high`.
|
|
213
|
-
|
|
214
|
-
**Приклади:**
|
|
215
|
-
|
|
216
|
-
- `'Add password reset flow'` → `'high'` (`password`).
|
|
217
|
-
- `'Migrate user data to new schema'` → `'high'` (`migration`/`міграц` — НІ, але `data` в MED_RISK_KEYS… насправді тут `migration` теж у MED_RISK_KEYS, а high-ключів немає → `'med'`).
|
|
218
|
-
- `'Bump dep version'` → `'low'`.
|
|
219
|
-
|
|
220
|
-
### `reviewersForRisk(risk)`
|
|
221
|
-
|
|
222
|
-
**Експорт:** `export function reviewersForRisk(risk: string): number`
|
|
223
|
-
|
|
224
|
-
**Параметри:**
|
|
225
|
-
|
|
226
|
-
- `risk` — рядок (очікувано `'low' | 'med' | 'high'`; інші значення трактуються як low).
|
|
227
|
-
|
|
228
|
-
**Повертає:** ціле `1..3` — рекомендована кількість рецензентів суто за ризиком.
|
|
229
|
-
|
|
230
|
-
**Side effects:** немає.
|
|
231
|
-
|
|
232
|
-
**Таблиця рішень:**
|
|
233
|
-
|
|
234
|
-
| `risk` | Повертає |
|
|
235
|
-
| ----------------------------------------------------- | -------- |
|
|
236
|
-
| `'high'` | `3` |
|
|
237
|
-
| `'med'` | `2` |
|
|
238
|
-
| решта (включно з `'low'`, `undefined`, неочікуваними) | `1` |
|
|
239
|
-
|
|
240
|
-
### `reviewersFor(level, risk)`
|
|
241
|
-
|
|
242
|
-
**Експорт:** `export function reviewersFor(level: number, risk?: string): number`
|
|
243
|
-
|
|
244
|
-
**Параметри:**
|
|
245
|
-
|
|
246
|
-
- `level` — числовий рівень складності (`0..3`).
|
|
247
|
-
- `risk` — необов'язковий рядок ризику (`'low' | 'med' | 'high'`).
|
|
248
|
-
|
|
249
|
-
**Повертає:** ціле `1..3` — підсумкова кількість рецензентів, **максимум** вимог за рівнем і за ризиком, з верхнім капом `3`.
|
|
250
|
-
|
|
251
|
-
**Side effects:** немає.
|
|
252
|
-
|
|
253
|
-
**Формула:**
|
|
254
|
-
|
|
255
|
-
```text
|
|
256
|
-
Math.min(3, Math.max(reviewersForLevel(level), reviewersForRisk(risk)))
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
**Семантика:**
|
|
260
|
-
|
|
261
|
-
- Високий ризик піднімає глибину review навіть для L0/L1.
|
|
262
|
-
- Велика задача (L3) гарантує максимум рецензентів незалежно від ризику.
|
|
263
|
-
- Кап `3` страхує від екзотичних вхідних значень `level > 3` (не виходимо за межі узгодженого діапазону).
|
|
264
|
-
|
|
265
|
-
**Приклади (level, risk → reviewers):**
|
|
266
|
-
|
|
267
|
-
| `level` | `risk` | Результат |
|
|
268
|
-
| ------- | ----------- | --------------- |
|
|
269
|
-
| `0` | `'low'` | `max(1, 1) = 1` |
|
|
270
|
-
| `0` | `'high'` | `max(1, 3) = 3` |
|
|
271
|
-
| `2` | `'low'` | `max(2, 1) = 2` |
|
|
272
|
-
| `3` | `'med'` | `max(3, 2) = 3` |
|
|
273
|
-
| `1` | `undefined` | `max(1, 1) = 1` |
|
|
274
|
-
|
|
275
|
-
## Залежності
|
|
276
|
-
|
|
277
|
-
**Зовнішні (npm/Node.js):** немає. Файл повністю самодостатній.
|
|
278
|
-
|
|
279
|
-
**Стандартні API:** використовує лише вбудовані операції рядка (`String`, `toLowerCase`, `indexOf`, доступ за індексом, `length`) та `Math.min` / `Math.max`.
|
|
280
|
-
|
|
281
|
-
**Внутрішні (з інших модулів проєкту):** немає `import`/`require`.
|
|
282
|
-
|
|
283
|
-
**Хто залежить від цього модуля:** очікувано — модулі команди `init` (ініціалізація флоу) та `flow review` (вибір кількості рецензентів). Інші частини dispatcher можуть викликати `reviewersFor` як єдину точку входу для розрахунку review-глибини.
|
|
284
|
-
|
|
285
|
-
## Потік виконання / Використання
|
|
286
|
-
|
|
287
|
-
Типовий потік:
|
|
288
|
-
|
|
289
|
-
1. **Ініціалізація задачі (`init`).** Викликач має текстовий опис задачі (наприклад, з ADR-чернетки або з CLI-аргументу). Імпортує `detectLevel` і `detectRisk`:
|
|
290
|
-
|
|
291
|
-
```js
|
|
292
|
-
import { detectLevel, detectRisk, reviewersFor } from './level.mjs'
|
|
293
|
-
|
|
294
|
-
const level = detectLevel(taskDescription) // 0 | 1 | 2 | 3
|
|
295
|
-
const risk = detectRisk(taskDescription) // 'low' | 'med' | 'high'
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
2. **Запис у стан флоу.** Значення `level` і `risk` зберігаються у стейті задачі (state-store / spec / артефакти), щоб бути доступними наступним командам.
|
|
299
|
-
|
|
300
|
-
3. **Розрахунок глибини review (`flow review`).** Перед спавном adversarial-рецензентів викликається `reviewersFor`:
|
|
301
|
-
|
|
302
|
-
```js
|
|
303
|
-
import { reviewersFor } from './level.mjs'
|
|
304
|
-
|
|
305
|
-
const reviewers = reviewersFor(level, risk) // 1..3
|
|
306
|
-
for (let i = 0; i < reviewers; i++) {
|
|
307
|
-
spawnAdversarialReviewer({ index: i, level, risk })
|
|
308
|
-
}
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
4. **Рекомендовані фази (контракт).** Рівень і ризик також впливають на те, які фази робочого процесу обов'язкові (наприклад, для L3/high — повний цикл з планом, ревью, gate; для L0/low — спрощений шлях). Це використання вже виходить за межі цього файлу, але `detectLevel` + `detectRisk` — точка входу для таких рішень.
|
|
312
|
-
|
|
313
|
-
**Інваріанти й гарантії:**
|
|
314
|
-
|
|
315
|
-
- Усі функції чисті й детерміновані: однаковий вхід → однаковий вихід.
|
|
316
|
-
- `detectLevel` / `detectRisk` стійкі до `null` / `undefined` / нерядкових входів (приведення через `String(x ?? '')`).
|
|
317
|
-
- Жодна функція не кидає виключень для типових входів.
|
|
318
|
-
- Жодна функція не виконує I/O.
|
|
319
|
-
|
|
320
|
-
**Обмеження детекції:**
|
|
321
|
-
|
|
322
|
-
- Простий пошук підрядків може давати false-positive на природній мові (наприклад, фраза "no migration needed" все одно тригерить L3). Це свідомий компроміс на користь швидкості і детермінізму.
|
|
323
|
-
- ASCII-дієслова L0 потребують матчу цілим словом; кириличні L0 матчаться підрядком — це асиметрія, продиктована особливостями токенізації двох алфавітів.
|
|
324
|
-
|
|
325
|
-
## Rebuild Test
|
|
326
|
-
|
|
327
|
-
Документація достатня для відтворення модуля з нуля без перегляду оригіналу:
|
|
328
|
-
|
|
329
|
-
- Усі експорти, їхні сигнатури, параметри, тип повертального значення й семантика описані.
|
|
330
|
-
- Внутрішні константи (списки ключових слів) наведені дослівно, з поясненням, чому окремі ключі сформовані саме так (стемінг, провідний пробіл у `' db'`).
|
|
331
|
-
- Алгоритми `detectLevel` / `detectRisk` / `hasWord` / `isAsciiAlnum` описані покроково з порядком пріоритетів і дефолтами.
|
|
332
|
-
- Формула `reviewersFor` з капом і максимумом наведена явно.
|
|
333
|
-
- Зафіксовано принципову відмову від regex з обґрунтуванням.
|
|
334
|
-
|
|
335
|
-
За цією специфікацією модуль `level.mjs` можна повністю відтворити, і його поведінка на типових входах буде ідентичною оригіналу.
|