@nitra/cursor 3.21.1 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
- package/CHANGELOG.md +37 -3
- package/bin/docs/n-cursor.md +636 -0
- package/bin/docs/rename-yaml-extensions.md +207 -0
- package/bin/n-cursor.js +30 -3
- package/package.json +1 -1
- package/rules/abie/docs/fix.md +18 -0
- package/rules/abie/js/docs/applies.md +26 -0
- package/rules/abie/js/docs/env_dns.md +32 -0
- package/rules/abie/js/docs/firebase_hosting.md +23 -0
- package/rules/abie/js/docs/hc_pairing.md +35 -0
- package/rules/abie/js/docs/ua_http_route.md +28 -0
- package/rules/abie/js/docs/ua_node_selector.md +28 -0
- package/rules/abie/lib/docs/enabled.md +29 -0
- package/rules/abie/lib/docs/env-dns.md +35 -0
- package/rules/abie/lib/docs/hc-yaml.md +33 -0
- package/rules/abie/lib/docs/http-route.md +44 -0
- package/rules/abie/lib/docs/k8s-tree.md +40 -0
- package/rules/abie/lib/docs/kustomization-patches.md +47 -0
- package/rules/abie/lib/docs/overlay-paths.md +38 -0
- package/rules/abie/lib/docs/yaml.md +29 -0
- package/rules/adr/docs/fix.md +148 -0
- package/rules/adr/js/docs/hooks.md +259 -0
- package/rules/bun/docs/fix.md +156 -0
- package/rules/bun/js/docs/layout.md +393 -0
- package/rules/capacitor/docs/fix.md +121 -0
- package/rules/capacitor/js/docs/platforms.md +295 -0
- package/rules/changelog/changelog.mdc +2 -2
- package/rules/changelog/docs/fix.md +174 -0
- package/rules/changelog/js/consistency.mjs +114 -13
- package/rules/changelog/js/docs/consistency.md +387 -0
- package/rules/changelog/lib/docs/package-manifest.md +210 -0
- package/rules/ci4/docs/fix.md +179 -0
- package/rules/ci4/js/docs/marksman_config.md +128 -0
- package/rules/docker/docker.mdc +8 -3
- package/rules/docker/docs/fix.md +171 -0
- package/rules/docker/js/docs/lint.md +258 -0
- package/rules/docker/lib/docs/docker-hadolint.md +184 -0
- package/rules/docker/lib/docs/docker-mirror.md +247 -0
- package/rules/docker/lib/docs/docker-native-addon.md +170 -0
- package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
- package/rules/docker/lint/docs/lint.md +193 -0
- package/rules/efes/docs/fix.md +203 -0
- package/rules/feedback/docs/fix.md +140 -0
- package/rules/flow/docs/fix.md +152 -0
- package/rules/ga/docs/fix.md +158 -0
- package/rules/ga/js/docs/lint.md +100 -0
- package/rules/ga/js/docs/workflows.md +217 -0
- package/rules/ga/lint/docs/lint.md +209 -0
- package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
- package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
- package/rules/graphql/docs/fix.md +126 -0
- package/rules/graphql/js/docs/tooling.md +264 -0
- package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
- package/rules/hasura/docs/fix.md +120 -0
- package/rules/hasura/hasura.mdc +14 -0
- package/rules/hasura/js/docs/internal_urls.md +326 -0
- package/rules/image-avif/docs/fix.md +132 -0
- package/rules/image-avif/js/docs/avif_generation.md +241 -0
- package/rules/image-compress/docs/fix.md +150 -0
- package/rules/image-compress/js/docs/package_setup.md +191 -0
- package/rules/js-bun-db/docs/fix.md +148 -0
- package/rules/js-bun-db/js/docs/safety.md +231 -0
- package/rules/js-bun-db/js-bun-db.mdc +42 -13
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
- package/rules/js-bun-redis/docs/fix.md +123 -0
- package/rules/js-bun-redis/js/docs/imports.md +176 -0
- package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
- package/rules/js-lint/docs/fix.md +117 -0
- package/rules/js-lint/js/docs/lint.md +250 -0
- package/rules/js-lint/js/docs/tooling.md +348 -0
- package/rules/js-lint/js/docs/utils_imports.md +207 -0
- package/rules/js-lint/js/lint-findings.mjs +110 -0
- package/rules/js-lint/js/lint.mjs +86 -15
- package/rules/js-lint-ci/docs/fix.md +154 -0
- package/rules/js-lint-ci/js/docs/lint.md +144 -0
- package/rules/js-mssql/docs/fix.md +128 -0
- package/rules/js-mssql/js/docs/deps.md +263 -0
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
- package/rules/js-run/docs/fix.md +144 -0
- package/rules/js-run/js/docs/runtime.md +388 -0
- package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
- package/rules/js-run/lib/docs/check-env-scan.md +433 -0
- package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
- package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
- package/rules/k8s/docs/fix.md +129 -0
- package/rules/k8s/js/docs/manifests.md +344 -0
- package/rules/k8s/js/manifests.mjs +6 -2
- package/rules/k8s/k8s.mdc +4 -2
- package/rules/k8s/lint/docs/lint.md +411 -0
- package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
- package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
- package/rules/nginx-default-tpl/docs/fix.md +124 -0
- package/rules/nginx-default-tpl/js/docs/template.md +378 -0
- package/rules/npm-module/docs/fix.md +98 -0
- package/rules/npm-module/js/docs/package_structure.md +274 -0
- package/rules/npm-module/js/docs/rule_meta.md +137 -0
- package/rules/npm-module/js/docs/skill_meta.md +190 -0
- package/rules/php/docs/fix.md +107 -0
- package/rules/php/js/docs/tooling.md +152 -0
- package/rules/php/lint/docs/lint.md +215 -0
- package/rules/python/docs/fix.md +163 -0
- package/rules/python/js/docs/applies.md +108 -0
- package/rules/python/js/docs/tooling.md +153 -0
- package/rules/python/lint/docs/lint.md +322 -0
- package/rules/rego/docs/fix.md +121 -0
- package/rules/rego/js/docs/applies.md +174 -0
- package/rules/rego/js/docs/lint.md +118 -0
- package/rules/rego/lint/docs/lint.md +204 -0
- package/rules/release/docs/change.md +185 -0
- package/rules/release/docs/fix.md +119 -0
- package/rules/release/docs/release.md +222 -0
- package/rules/release/lib/docs/aggregate.md +246 -0
- package/rules/release/lib/docs/change-file.md +200 -0
- package/rules/release/lib/docs/fallback.md +203 -0
- package/rules/rust/docs/fix.md +129 -0
- package/rules/rust/js/docs/applies.md +140 -0
- package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
- package/rules/security/docs/fix.md +86 -0
- package/rules/security/js/docs/lint.md +171 -0
- package/rules/security/js/docs/sample_secret.md +190 -0
- package/rules/security/js/docs/trufflehog.md +137 -0
- package/rules/security/js/lint.mjs +9 -1
- package/rules/style-lint/docs/fix.md +155 -0
- package/rules/style-lint/js/docs/lint.md +184 -0
- package/rules/style-lint/js/docs/tooling.md +194 -0
- package/rules/tauri/docs/fix.md +158 -0
- package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
- package/rules/tauri/js/docs/tooling.md +228 -0
- package/rules/test/coverage/coverage.mjs +15 -3
- package/rules/test/docs/fix.md +132 -0
- package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
- package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
- package/rules/test/js/docs/cargo_mutants_config.md +173 -0
- package/rules/test/js/docs/location.md +136 -0
- package/rules/test/js/docs/no-process-chdir.md +160 -0
- package/rules/test/js/docs/no-relative-fs-path.md +271 -0
- package/rules/test/js/docs/stryker_config.md +152 -0
- package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
- package/rules/text/docs/fix.md +118 -0
- package/rules/text/js/docs/forbidden-prettier.md +143 -0
- package/rules/text/js/docs/formatting.md +256 -0
- package/rules/text/js/docs/lint.md +122 -0
- package/rules/text/lint/docs/lint.md +220 -0
- package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
- package/rules/text/lint/docs/run-shellcheck.md +212 -0
- package/rules/text/lint/docs/run-v8r.md +197 -0
- package/rules/vue/docs/fix.md +127 -0
- package/rules/vue/js/docs/packages.md +335 -0
- package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
- package/rules/worktree/docs/fix.md +161 -0
- package/schemas/rule-meta.json +5 -1
- package/scripts/auto-rules.mjs +7 -4
- package/scripts/coverage-classify/docs/apply.md +202 -0
- package/scripts/coverage-classify/docs/cache.md +203 -0
- package/scripts/coverage-classify/docs/index.md +218 -0
- package/scripts/coverage-classify/docs/prompt.md +132 -0
- package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
- package/scripts/coverage-fix-extract.mjs +122 -0
- package/scripts/coverage-fix.mjs +1 -1
- package/scripts/dispatcher/docs/graph.md +346 -0
- package/scripts/dispatcher/docs/index.md +236 -0
- package/scripts/dispatcher/docs/trace.md +296 -0
- package/scripts/dispatcher/index.mjs +1 -1
- package/scripts/dispatcher/lib/active.mjs +4 -8
- package/scripts/dispatcher/lib/commands.mjs +7 -11
- package/scripts/dispatcher/lib/docs/active.md +348 -0
- package/scripts/dispatcher/lib/docs/artifact.md +232 -0
- package/scripts/dispatcher/lib/docs/budget.md +167 -0
- package/scripts/dispatcher/lib/docs/capability.md +196 -0
- package/scripts/dispatcher/lib/docs/commands.md +210 -0
- package/scripts/dispatcher/lib/docs/events.md +182 -0
- package/scripts/dispatcher/lib/docs/executor.md +190 -0
- package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
- package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
- package/scripts/dispatcher/lib/docs/gate.md +231 -0
- package/scripts/dispatcher/lib/docs/level.md +335 -0
- package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
- package/scripts/dispatcher/lib/docs/plan.md +200 -0
- package/scripts/dispatcher/lib/docs/planner.md +269 -0
- package/scripts/dispatcher/lib/docs/review.md +255 -0
- package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
- package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
- package/scripts/dispatcher/lib/docs/spec.md +203 -0
- package/scripts/dispatcher/lib/docs/state-store.md +303 -0
- package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
- package/scripts/dispatcher/lib/executor.mjs +6 -1
- package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
- package/scripts/dispatcher/lib/level.mjs +29 -3
- package/scripts/dispatcher/lib/review.mjs +1 -1
- package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
- package/scripts/docs/auto-rules.md +376 -0
- package/scripts/docs/auto-skills.md +173 -0
- package/scripts/docs/build-agents-commands.md +183 -0
- package/scripts/docs/cli-entry.md +153 -0
- package/scripts/docs/coverage-fix.md +177 -0
- package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
- package/scripts/lib/changed-files.mjs +4 -1
- package/scripts/lib/diff-added-lines.mjs +85 -0
- package/scripts/lib/docs/changed-files.md +149 -0
- package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
- package/scripts/lib/docs/check-reporter.md +175 -0
- package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
- package/scripts/lib/docs/discover-checkable-rules.md +165 -0
- package/scripts/lib/docs/ensure-tool.md +254 -0
- package/scripts/lib/docs/generated-markdown.md +275 -0
- package/scripts/lib/docs/gha-workflow.md +326 -0
- package/scripts/lib/docs/inline-template-links.md +303 -0
- package/scripts/lib/docs/list-rule-ids.md +156 -0
- package/scripts/lib/docs/load-cursor-config.md +147 -0
- package/scripts/lib/docs/mirror-parity.md +167 -0
- package/scripts/lib/worktree.mjs +26 -0
- package/scripts/worktree-cli.mjs +12 -2
- package/skills/coverage-fix/SKILL.md +34 -45
- package/skills/docgen/SKILL.md +44 -23
- package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
- package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
- package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
- package/skills/docgen/js/docgen-ignore.mjs +54 -0
- package/skills/docgen/js/docgen-scan.mjs +37 -21
- package/skills/llm-patch/SKILL.md +23 -2
- package/skills/start-check/SKILL.md +26 -53
- package/skills/start-check/js/check.mjs +211 -0
- package/skills/taze/SKILL.md +9 -3
- package/skills/taze/js/diff.mjs +154 -0
- package/types/bin/n-cursor.d.ts +1 -1
- package/skills/fix-tests/SKILL.md +0 -119
- package/skills/fix-tests/meta.json +0 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# review.mjs
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль реалізує команду `flow review` — adversarial-перевірку коду **після** його написання (концепція з BMAD quick-dev: спершу self-check, потім adversarial-review). Незалежний субагент-рецензент читає **лише** `git diff` від базового комміту й шукає логічні баги, ризики та smells, які не ловлять механічні гейти (lint + coverage) у команді `verify`.
|
|
6
|
+
|
|
7
|
+
Команда є **інформативною**: ворота м'які, тож exit code завжди `0`, якщо вдалося запустити рецензію. Код `1` повертається лише за технічної неможливості (немає стану flow, не вдалось створити runner). Кількість рецензентів визначається полем `level` стану flow (через `reviewersFor`), а додаткова security-лінза вмикається для `risk === 'high'`.
|
|
8
|
+
|
|
9
|
+
Усі сторонні залежності (запуск процесів, runner субагента, годинник) **ін'єктуються** через об'єкт `deps`, тож модуль повністю тестується без реального git та LLM.
|
|
10
|
+
|
|
11
|
+
Результати рецензії (`{ at, reviewers, findings }`) фіксуються в `.flow.json` через `recordTransition` і виводяться у лог зі зрозумілими емодзі-іконками за severity.
|
|
12
|
+
|
|
13
|
+
## Експорти / API
|
|
14
|
+
|
|
15
|
+
Модуль експортує чотири іменовані функції:
|
|
16
|
+
|
|
17
|
+
| Експорт | Тип | Призначення |
|
|
18
|
+
| ------------------------------ | ---------------- | ------------------------------------------------------------------------------------ |
|
|
19
|
+
| `diffFromBase(base, run, cwd)` | `function` | Будує текст diff: закомічене `base...HEAD` + working tree `git diff`. |
|
|
20
|
+
| `reviewerPrompt(diff, risk)` | `function` | Формує промпт для adversarial-рецензента з фокусом на diff (опційно security-лінза). |
|
|
21
|
+
| `parseFindings(text)` | `function` | Витягає JSON-масив findings з відповіді субагента (fail-soft). |
|
|
22
|
+
| `dedupeFindings(findings)` | `function` | Дедуплікує findings за ключем `(file, issue)`. |
|
|
23
|
+
| `review(_rest, deps)` | `async function` | Головна точка входу команди `flow review`. |
|
|
24
|
+
|
|
25
|
+
Внутрішня (не експортується) функція `severityIcon(severity)` повертає емодзі-маркер.
|
|
26
|
+
|
|
27
|
+
Константа модульного scope:
|
|
28
|
+
|
|
29
|
+
- `DIFF_LIMIT = 12_000` — максимальна кількість символів diff, що потрапляє у промпт рецензента (захист від роздування контексту).
|
|
30
|
+
|
|
31
|
+
## Функції
|
|
32
|
+
|
|
33
|
+
### `diffFromBase(base, run, cwd)`
|
|
34
|
+
|
|
35
|
+
**Сигнатура:** `(base: string, run: (cmd, args, opts) => { stdout: string }, cwd: string) => string`
|
|
36
|
+
|
|
37
|
+
**Параметри:**
|
|
38
|
+
|
|
39
|
+
- `base` — базовий комміт, від якого рахується diff (наприклад, `HEAD~1` або значення з `state.metadata.base_commit`).
|
|
40
|
+
- `run` — ін'єктований git-раннер. Виклик `run('git', args, { cwd })` має повертати об'єкт із полем `stdout`.
|
|
41
|
+
- `cwd` — шлях до worktree, у якому виконуються git-команди.
|
|
42
|
+
|
|
43
|
+
**Повертає:** склеєний рядок з двох частин — `git diff base...HEAD` (закомічене) і `git diff` (робоче дерево), розділених `\n`, з обрізаними пробілами по краях.
|
|
44
|
+
|
|
45
|
+
**Side effects:** виконує два процеси `git` через ін'єктований `run`. Без `run` — чиста функція.
|
|
46
|
+
|
|
47
|
+
**Особливості:** `stdout` нормалізується через `?? ''`, тому якщо одна з команд не повернула вивід, інша частина не "забивається" `undefined`.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### `reviewerPrompt(diff, risk)`
|
|
52
|
+
|
|
53
|
+
**Сигнатура:** `(diff: string, risk?: string) => string`
|
|
54
|
+
|
|
55
|
+
**Параметри:**
|
|
56
|
+
|
|
57
|
+
- `diff` — текст diff для рецензування (обрізається до `DIFF_LIMIT` символів).
|
|
58
|
+
- `risk` — рівень ризику flow: `'low'`, `'med'`, `'high'`. За `risk === 'high'` додається security-лінза з акцентом на auth/секрети/ін'єкції/незворотні операції.
|
|
59
|
+
|
|
60
|
+
**Повертає:** готовий текст промпта для adversarial-рецензента — рядки, склеєні через `\n` (порожні через `lens` або інші falsy-вставки відфільтровуються `.filter(Boolean)`).
|
|
61
|
+
|
|
62
|
+
**Side effects:** немає (чиста функція).
|
|
63
|
+
|
|
64
|
+
**Ключові вимоги в промпті:**
|
|
65
|
+
|
|
66
|
+
1. Рецензент шукає баги/ризики/smells, які **вносить або зачіпає** саме цей diff.
|
|
67
|
+
2. Якщо доступний інструмент `Read` — точково читає referenced-файли для верифікації cross-file тверджень; інакше працює лише з diff.
|
|
68
|
+
3. Сусідні файли — для контексту, **не** для пошуку преіснуючих багів.
|
|
69
|
+
4. Заборонено нефальсифіковні findings виду "з diff не видно / можливо" — або підтвердити читанням, або відкинути.
|
|
70
|
+
5. Формат відповіді — **лише** JSON-масив `[{ severity, file, issue, suggestion }]`; якщо проблем нема — `[]`.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### `parseFindings(text)`
|
|
75
|
+
|
|
76
|
+
**Сигнатура:** `(text: string) => Array<{ severity?: string, file?: string, issue?: string, suggestion?: string }>`
|
|
77
|
+
|
|
78
|
+
**Параметри:**
|
|
79
|
+
|
|
80
|
+
- `text` — сирий вивід субагента-рецензента.
|
|
81
|
+
|
|
82
|
+
**Повертає:** масив findings. Якщо JSON-масив не знайдено або парсинг впав — повертає `[]`.
|
|
83
|
+
|
|
84
|
+
**Алгоритм:**
|
|
85
|
+
|
|
86
|
+
1. Знаходить індекси першого `[` та останнього `]` у тексті.
|
|
87
|
+
2. Якщо хоча б одного нема, або `end < start` — повертає `[]`.
|
|
88
|
+
3. Парсить підрядок `[...]` через `JSON.parse`.
|
|
89
|
+
4. Якщо результат — масив, повертає його; інакше або при exception — `[]`.
|
|
90
|
+
|
|
91
|
+
**Side effects:** немає. Fail-soft: будь-яке сміття/невалідний JSON безпечно перетворюється на порожній масив.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### `dedupeFindings(findings)`
|
|
96
|
+
|
|
97
|
+
**Сигнатура:** `(findings: object[]) => object[]`
|
|
98
|
+
|
|
99
|
+
**Параметри:**
|
|
100
|
+
|
|
101
|
+
- `findings` — масив findings (можливо з дублікатами).
|
|
102
|
+
|
|
103
|
+
**Повертає:** новий масив без дублікатів за ключем `${file}::${issue}`, зі збереженням порядку першого входження.
|
|
104
|
+
|
|
105
|
+
**Side effects:** немає (чиста функція, але створює новий масив).
|
|
106
|
+
|
|
107
|
+
**Особливості:** `f?.file ?? ''` і `f?.issue ?? ''` — `undefined`/`null` нормалізуються до пустого рядка, тож два findings без обох полів вважаються дублікатами.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `severityIcon(severity)` _(internal)_
|
|
112
|
+
|
|
113
|
+
**Сигнатура:** `(severity: string) => string`
|
|
114
|
+
|
|
115
|
+
**Параметри:**
|
|
116
|
+
|
|
117
|
+
- `severity` — рівень: `'high'` | `'med'` | будь-що інше.
|
|
118
|
+
|
|
119
|
+
**Повертає:** емодзі-іконку:
|
|
120
|
+
|
|
121
|
+
- `'high'` → червоне коло;
|
|
122
|
+
- `'med'` → жовте коло;
|
|
123
|
+
- решта (включно з `'low'`, `undefined`) → біле коло.
|
|
124
|
+
|
|
125
|
+
**Side effects:** немає.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### `review(_rest, deps)`
|
|
130
|
+
|
|
131
|
+
**Сигнатура:** `(_rest: string[], deps?: object) => Promise<number>`
|
|
132
|
+
|
|
133
|
+
**Параметри:**
|
|
134
|
+
|
|
135
|
+
- `_rest` — позиційні аргументи CLI (не використовуються; передається для уніфікованої сигнатури команд диспатчера).
|
|
136
|
+
- `deps` — об'єкт ін'єкцій:
|
|
137
|
+
- `cwd` — стартова робоча тека (за замовчуванням `process.cwd()`);
|
|
138
|
+
- `branch` — гілка для авторезолву flow-стану (необов'язково);
|
|
139
|
+
- `log` — функція логування (за замовчуванням `console.error`);
|
|
140
|
+
- `run` — git-раннер (за замовчуванням `realRun` з `./commands.mjs`);
|
|
141
|
+
- `runner` — готовий runner субагента; якщо не передано — створюється через `createRunner(deps)`;
|
|
142
|
+
- `now` — джерело часу (за замовчуванням `Date.now`).
|
|
143
|
+
|
|
144
|
+
**Повертає:** `Promise<number>` — exit code:
|
|
145
|
+
|
|
146
|
+
- `0` — нормальне завершення (включно з випадком "нема змін" та з будь-якою кількістю findings);
|
|
147
|
+
- `1` — технічна неможливість виконати рецензію (нема активного flow-стану, нема `.flow.json`, не вдалось створити runner).
|
|
148
|
+
|
|
149
|
+
**Side effects:**
|
|
150
|
+
|
|
151
|
+
- Викликає `resolveActiveFlowState` для пошуку активного flow.
|
|
152
|
+
- Читає файл стану через `readState(statePath)`.
|
|
153
|
+
- Виконує git через `run` (всередині `diffFromBase`).
|
|
154
|
+
- Створює runner субагента (`createRunner`) і запускає `runner.runStep(prompt, { cwd })` стільки разів, скільки повернув `reviewersFor`.
|
|
155
|
+
- Записує транзицію в state-store через `recordTransition`, додаючи поле `review: { at, reviewers, findings }`.
|
|
156
|
+
- Логує кожен finding з емодзі за severity та підсумок `review: N findings (рецензентів: M)`. Якщо є high-severity — додатковий warning.
|
|
157
|
+
|
|
158
|
+
**Покроковий потік:**
|
|
159
|
+
|
|
160
|
+
1. Зчитує `cwd0`, `log`, `run`, `now` з `deps` із дефолтами.
|
|
161
|
+
2. Резолвить активний flow-стан через `resolveActiveFlowState({ cwd: cwd0, branch }, deps)`.
|
|
162
|
+
- Якщо `statePath` пустий — лог помилки + `return 1`.
|
|
163
|
+
- Якщо `autoResolved` — інформативний лог.
|
|
164
|
+
3. Робочий `cwd` = `resolved.worktreeDir ?? cwd0`.
|
|
165
|
+
4. Читає стан `state = readState(statePath)`. Якщо немає — лог `review: стану нема — спершу 'flow init'` + `return 1`.
|
|
166
|
+
5. Бере `base = state.metadata?.base_commit ?? 'HEAD~1'`.
|
|
167
|
+
6. Будує `diff = diffFromBase(base, run, cwd)`. Якщо пустий — лог `review: нема змін від base — нічого ревʼювити` + `return 0`.
|
|
168
|
+
7. Готує `runner`: бере з `deps.runner`, інакше пробує `await createRunner(deps)`. На exception — лог `review: ${error.message}` + `return 1`.
|
|
169
|
+
8. Обчислює `reviewers = reviewersFor(state.level ?? 1, state.risk)` — скільки паралельних рецензентів запустити.
|
|
170
|
+
9. Будує промпт `reviewerPrompt(diff, state.risk)`.
|
|
171
|
+
10. Запускає `reviewers` копій рецензента паралельно через `Promise.all` + `runner.runStep(prompt, { cwd })`.
|
|
172
|
+
11. Збирає findings: для кожного результату з `r.ok === true` парсить через `parseFindings(r.output)`, потім `flatMap` + `dedupeFindings`.
|
|
173
|
+
12. Викликає `recordTransition` з `type: 'review'`, кількістю findings, reducer-функцією, що додає поле `review`, і `now`.
|
|
174
|
+
13. Логує кожен finding: `${іконка} ${file ?? '?'}: ${issue ?? ''}`.
|
|
175
|
+
14. Якщо є high-severity findings — додатковий warning рядок.
|
|
176
|
+
15. Підсумковий лог `review: N findings (рецензентів: M)` і `return 0`.
|
|
177
|
+
|
|
178
|
+
## Залежності
|
|
179
|
+
|
|
180
|
+
### Зовнішні (Node.js standard)
|
|
181
|
+
|
|
182
|
+
- `cwd as processCwd` з `node:process` — дефолтний CWD для команди.
|
|
183
|
+
|
|
184
|
+
### Внутрішні модулі
|
|
185
|
+
|
|
186
|
+
- `./commands.mjs` — `realRun`: дефолтний раннер shell-команд (`git`).
|
|
187
|
+
- `./events.mjs` — `flowEventsPath`: шлях до файла подій flow для `recordTransition`.
|
|
188
|
+
- `./level.mjs` — `reviewersFor(level, risk)`: скільки adversarial-рецензентів запускати на даному рівні строгості та ризику.
|
|
189
|
+
- `./state-store.mjs`:
|
|
190
|
+
- `readState(statePath)` — читає `.flow.json`;
|
|
191
|
+
- `recordTransition(paths, event, reducer, now)` — атомарно оновлює стан і дописує подію в events-лог.
|
|
192
|
+
- `./flow-resolve.mjs` — `resolveActiveFlowState({ cwd, branch }, deps)`: знаходить активний flow за CWD або з авторезолвом по гілці; повертає `{ statePath, worktreeDir, label, autoResolved, error }`.
|
|
193
|
+
- `./subagent-runner.mjs` — `createRunner(deps)`: фабрика об'єкта з методом `runStep(prompt, opts) → Promise<{ ok, output }>` для запуску LLM-субагента.
|
|
194
|
+
|
|
195
|
+
### Зовнішні fail-points
|
|
196
|
+
|
|
197
|
+
- `git` (через `run`).
|
|
198
|
+
- Запуск LLM-субагента (через `runner.runStep`), що, ймовірно, використовує Claude CLI або аналог.
|
|
199
|
+
|
|
200
|
+
## Потік виконання / Використання
|
|
201
|
+
|
|
202
|
+
### Типове використання як CLI-команди
|
|
203
|
+
|
|
204
|
+
Команда `review` зареєстрована у dispatcher і викликається так:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
flow review
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Алгоритм:
|
|
211
|
+
|
|
212
|
+
1. Користувач має активний flow (створений через `flow init`) з `.flow.json` у поточному worktree або резолвиться авто.
|
|
213
|
+
2. У стані лежить `metadata.base_commit` (комміт-стартер flow).
|
|
214
|
+
3. Команда збирає diff від `base_commit` до HEAD + working tree, формує промпт і запускає рецензентів.
|
|
215
|
+
4. Findings виводяться в stderr і зберігаються в `.flow.json` під ключем `review`.
|
|
216
|
+
|
|
217
|
+
### Програмне використання (тести)
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
import { review, diffFromBase, reviewerPrompt, parseFindings, dedupeFindings } from './review.mjs'
|
|
221
|
+
|
|
222
|
+
// Тест без git та LLM
|
|
223
|
+
const fakeRun = (cmd, args) => ({ stdout: 'diff text' })
|
|
224
|
+
const fakeRunner = {
|
|
225
|
+
async runStep(prompt, { cwd }) {
|
|
226
|
+
return { ok: true, output: '[{"severity":"high","file":"a.js","issue":"npe","suggestion":"check null"}]' }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const exit = await review([], {
|
|
230
|
+
cwd: '/tmp/repo',
|
|
231
|
+
run: fakeRun,
|
|
232
|
+
runner: fakeRunner,
|
|
233
|
+
now: () => 0,
|
|
234
|
+
log: () => {}
|
|
235
|
+
})
|
|
236
|
+
// exit === 0
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Інваріанти та контракти
|
|
240
|
+
|
|
241
|
+
1. **Exit-code контракт:** `0` за успішного запуску рецензії (включно з нульовою кількістю findings); `1` лише за відсутності стану або runner-а.
|
|
242
|
+
2. **Ідемпотентність:** повторний запуск `review` перезаписує поле `review` у стані поточним підсумком; події накопичуються в events-лозі.
|
|
243
|
+
3. **Fail-soft парсинг:** будь-яке "сміття" від рецензента → пустий список findings, а не exception.
|
|
244
|
+
4. **Дедуплікація** — по `(file, issue)`, **не** по `suggestion`/`severity`. Різні рецензенти, що знайшли одну проблему по-різному, схлопуються в один finding.
|
|
245
|
+
5. **Контекст-ліміт:** diff обрізається до 12 000 символів — рецензент бачить лише початок великих diff'ів.
|
|
246
|
+
|
|
247
|
+
### Rebuild Test
|
|
248
|
+
|
|
249
|
+
Якщо файл видалити й переписати на основі цієї документації, відновлення має містити:
|
|
250
|
+
|
|
251
|
+
1. Імпорти: `cwd as processCwd` з `node:process`; `realRun` з `./commands.mjs`; `flowEventsPath` з `./events.mjs`; `reviewersFor` з `./level.mjs`; `readState`, `recordTransition` з `./state-store.mjs`; `resolveActiveFlowState` з `./flow-resolve.mjs`; `createRunner` з `./subagent-runner.mjs`.
|
|
252
|
+
2. Константу `DIFF_LIMIT = 12_000`.
|
|
253
|
+
3. Експорти `diffFromBase`, `reviewerPrompt`, `parseFindings`, `dedupeFindings`, `review` із сигнатурами та поведінкою, описаними вище.
|
|
254
|
+
4. Внутрішню `severityIcon` із трьома кейсами (`high` → червоний, `med` → жовтий, інше → білий).
|
|
255
|
+
5. У `review`: всі гілки `return 1` (нема `statePath`, нема стану, помилка `createRunner`); `return 0` за пустого diff; запуск `reviewers` копій рецензента через `Promise.all`; фільтрацію `r.ok` перед `parseFindings`; `dedupeFindings` після `flatMap`; виклик `recordTransition` з полем `review: { at, reviewers, findings }`; логування з іконками + warning для high-severity + підсумок.
|
|
@@ -0,0 +1,240 @@
|
|
|
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`; жодних додаткових.
|