@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,200 @@
|
|
|
1
|
+
# plan.mjs — фаза `plan` команди `flow`
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль `plan.mjs` реалізує фазу **плану** lifecycle-команди `flow` (модель «Пасивний Турнікет», §4 правила `n-flow`). Це чистий «turnstile»-крок: він **не пише кодові артефакти** — лише фіксує план роботи у persistent-стані `.flow.json` поточного worktree та супутньому event-log.
|
|
6
|
+
|
|
7
|
+
Команда CLI:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
flow plan [--panel] [<plan.md>]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Високорівнева семантика:
|
|
14
|
+
|
|
15
|
+
1. Резолвить активний `flow`-state (`.flow.json`) у поточному `cwd` або за параметром `branch`.
|
|
16
|
+
2. Перевіряє, що state існує (`flow init` був запущений раніше). Якщо ще не зафіксована spec — попереджає, але не блокує.
|
|
17
|
+
3. Бере **кроки плану** одним із двох способів (brainstorm-моделей з `flow.mdc`):
|
|
18
|
+
- **human↔agent** — читає `docs/plans/<date>-<slug>.md` (передано аргументом або резолвиться по бренчу) і витягує `## Кроки` як список steps.
|
|
19
|
+
- **agent↔agent (`--panel`)** — викликає `runPanel` (панель персон + суддя) через subagent-runner і отримує синтезовані кроки.
|
|
20
|
+
4. Нормалізує steps через `parsePlan` (валідація формату/структури → масив об'єктів кроків).
|
|
21
|
+
5. Read-only `verifyTrace` перевіряє цілісність ланцюга `spec → plan → flow` (front-matter лінки). При розриві лише попереджає у лог (не падає).
|
|
22
|
+
6. Записує transition у state-store: `plan` ← `normalized`, `plan_doc` ← шлях до md (або `null` для panel-режиму), `status` ← `'planned'`. Тип події — `plan`, payload — `{ steps: <кількість> }`.
|
|
23
|
+
7. Логує підсумок і повертає `0`.
|
|
24
|
+
|
|
25
|
+
Будь-яка помилка (відсутність state, відсутність плану-доку, невалідний формат, неможливість підняти runner) логується через `log` і повертає `1`.
|
|
26
|
+
|
|
27
|
+
## Експорти / API
|
|
28
|
+
|
|
29
|
+
| Експорт | Тип | Призначення |
|
|
30
|
+
| ------------------- | ---------------- | ---------------------------------------------------------- |
|
|
31
|
+
| `plan(rest, deps?)` | `async function` | Виконати фазу `plan` поточного `flow`. Іменований експорт. |
|
|
32
|
+
|
|
33
|
+
Інших експортів немає (немає `default`).
|
|
34
|
+
|
|
35
|
+
### Сигнатура
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
export async function plan(rest, deps = {}) → Promise<number>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Параметри
|
|
42
|
+
|
|
43
|
+
- `rest: string[]` — позиційні аргументи CLI (після `plan`). Розпізнаються:
|
|
44
|
+
- `--panel` — увімкнути agent↔agent режим (панель персон).
|
|
45
|
+
- перший елемент із суфіксом `.md` — явний шлях до plan-доку (інакше резолвиться через `resolveArtifact(cwd, 'plans', state.branch)`).
|
|
46
|
+
- `deps?: object` — bag залежностей для ін'єкції в тестах:
|
|
47
|
+
- `cwd?: string` — стартовий `cwd` (default: `process.cwd()`).
|
|
48
|
+
- `branch?: string` — підказка про активний flow для авторезолву поза worktree.
|
|
49
|
+
- `log?: (msg: string) => void` — sink для повідомлень (default: `console.error`).
|
|
50
|
+
- `runner?: object` — готовий subagent-runner (для `--panel`). Якщо немає — створюється через `createRunner(deps)`.
|
|
51
|
+
- `trace?: (cwd: string) => number` — кастомна імплементація trace-перевірки для `verifyTrace`.
|
|
52
|
+
- `now?: () => number` — джерело часу для transition-таймстампу (default: `Date.now`).
|
|
53
|
+
|
|
54
|
+
### Повертає
|
|
55
|
+
|
|
56
|
+
`Promise<number>` — exit code:
|
|
57
|
+
|
|
58
|
+
- `0` — план зафіксовано, state переведено у `planned`.
|
|
59
|
+
- `1` — будь-яка помилка: нема state-файлу/active flow, нема plan-доку у режимі без `--panel`, runner не піднявся, `parsePlan` упав на валідації, panel повернув пустий результат.
|
|
60
|
+
|
|
61
|
+
### Side effects
|
|
62
|
+
|
|
63
|
+
- Читає файлову систему: `.flow.json` (через `readState`), доки плану через `readFileSync`, `existsSync`.
|
|
64
|
+
- Пише через `recordTransition`: оновлює `.flow.json` та аппендить запис у `flowEventsPath(cwd)`.
|
|
65
|
+
- Викликає `log(...)` (за замовчуванням — `console.error`).
|
|
66
|
+
- У режимі `--panel` запускає підпроцес/subagent-runner через `createRunner` (мережа/LLM-виклики залежно від реалізації runner).
|
|
67
|
+
|
|
68
|
+
## Функції
|
|
69
|
+
|
|
70
|
+
У файлі **одна** експортована функція `plan` — описана вище. Внутрішніх допоміжних функцій немає.
|
|
71
|
+
|
|
72
|
+
### `plan(rest, deps)` — деталі потоку
|
|
73
|
+
|
|
74
|
+
**Сигнатура:** `(rest: string[], deps?: PlanDeps) → Promise<number>`
|
|
75
|
+
|
|
76
|
+
**Послідовність:**
|
|
77
|
+
|
|
78
|
+
1. **Резолв cwd і логера** — `cwd0 = deps.cwd ?? processCwd()`, `log = deps.log ?? console.error`.
|
|
79
|
+
2. **Резолв активного flow** — `resolveActiveFlowState({ cwd: cwd0, branch: deps.branch }, deps)`.
|
|
80
|
+
- Якщо `resolved.statePath` пустий — log `plan: <error>` і `return 1`.
|
|
81
|
+
- Якщо `resolved.autoResolved === true` — log повідомлення про авторезолв (cwd поза worktree).
|
|
82
|
+
- `cwd` для подальших операцій = `resolved.worktreeDir ?? cwd0`.
|
|
83
|
+
3. **Читання state** — `readState(statePath)`; якщо `null` → `'plan: стану нема — спершу `flow init`'` і `return 1`.
|
|
84
|
+
4. **Soft-гейт по spec** — якщо `state.status !== 'spec'` і `!state.spec_doc` — лише попередження (`'plan: дизайн ще не зафіксовано — рекомендовано спершу `flow spec` (не блокує)'`), виконання продовжується.
|
|
85
|
+
5. **Резолв plan-доку** — `doc = rest.find(a => a.endsWith('.md')) ?? resolveArtifact(cwd, 'plans', state.branch)`.
|
|
86
|
+
6. **Збір steps**:
|
|
87
|
+
- **Гілка `--panel`:**
|
|
88
|
+
- Якщо `deps.runner` не передано — пробує `createRunner(deps)`, на throw → log і `return 1`.
|
|
89
|
+
- `steps = await runPanel({ task: state.branch, cwd, runner, log, mode: 'plan' })`.
|
|
90
|
+
- Якщо `steps` falsy → `return 1` (panel вирішив не комітити).
|
|
91
|
+
- **Гілка з документом:**
|
|
92
|
+
- Якщо `!doc || !existsSync(doc)` → log `'plan: нема docs/plans/<date>-<slug>.md — спершу пройди brainstorm (див. flow.mdc)'` і `return 1`.
|
|
93
|
+
- `steps = extractSteps(readFileSync(doc, 'utf8'))`.
|
|
94
|
+
7. **Нормалізація** — `parsePlan(JSON.stringify(steps))` у try/catch; помилка → log і `return 1`. Результат — `normalized` (масив крокових об'єктів).
|
|
95
|
+
8. **Trace-перевірка** — `verifyTrace(cwd, deps.trace)`; якщо `false` — лише warning з пр **U+26A0**: `'⚠️ plan: trace виявив розрив ланцюга — перевір лінки spec/plan/flow'`.
|
|
96
|
+
9. **Transition** — `recordTransition`:
|
|
97
|
+
- target: `{ statePath, eventsPath: flowEventsPath(cwd) }`.
|
|
98
|
+
- event: `{ type: 'plan', steps: normalized.length }`.
|
|
99
|
+
- reducer: `s => ({ ...s, plan: normalized, plan_doc: doc ?? null, status: 'planned' })`.
|
|
100
|
+
- clock: `deps.now ?? Date.now`.
|
|
101
|
+
10. **Лог підсумку** — `'plan: зафіксовано <N> кроків → status: planned'`, **return 0**.
|
|
102
|
+
|
|
103
|
+
**Інваріанти:**
|
|
104
|
+
|
|
105
|
+
- Функція не модифікує state у разі будь-якого `return 1` до кроку 9.
|
|
106
|
+
- `plan_doc` свідомо може бути `null` (panel-режим без файлу).
|
|
107
|
+
- Trace-розрив **не блокує** запис — це м'який сигнал розробнику.
|
|
108
|
+
|
|
109
|
+
## Залежності
|
|
110
|
+
|
|
111
|
+
### Зовнішні (node:)
|
|
112
|
+
|
|
113
|
+
- `node:fs` — `existsSync`, `readFileSync` (читання plan-доку).
|
|
114
|
+
- `node:process` — `cwd as processCwd` (default-резолв робочої директорії).
|
|
115
|
+
|
|
116
|
+
### Внутрішні модулі lib/
|
|
117
|
+
|
|
118
|
+
- `./artifact.mjs` — `extractSteps` (парс `## Кроки` з md), `resolveArtifact` (резолв `docs/<kind>/<date>-<slug>.md` за бренчем), `verifyTrace` (read-only валідація ланцюга front-matter).
|
|
119
|
+
- `./events.mjs` — `flowEventsPath` (шлях до event-log поточного flow).
|
|
120
|
+
- `./planner.mjs` — `parsePlan` (нормалізація+валідація steps).
|
|
121
|
+
- `./plan-panel.mjs` — `runPanel` (agent↔agent режим, mode: `'plan'`).
|
|
122
|
+
- `./subagent-runner.mjs` — `createRunner` (фабрика runner-а для LLM-викликів у panel).
|
|
123
|
+
- `./state-store.mjs` — `readState` (читання `.flow.json`), `recordTransition` (атомарне оновлення стану + event).
|
|
124
|
+
- `./flow-resolve.mjs` — `resolveActiveFlowState` (резолв активного flow зі `cwd`/`branch`, авторезолв за межами worktree).
|
|
125
|
+
|
|
126
|
+
Жодних transitive npm-пакетів напряму звідси не використовується.
|
|
127
|
+
|
|
128
|
+
## Потік виконання / Використання
|
|
129
|
+
|
|
130
|
+
### Як цей модуль використовується
|
|
131
|
+
|
|
132
|
+
Файл є **обробником субкоманди** dispatcher-а команди `flow`. Очікувано викликається з вищого рівня (CLI-entry, наприклад `npm/scripts/dispatcher/cli.mjs` чи аналогічного), куди передаються розпарсені аргументи. Виклик можна представити так:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
import { plan } from './lib/plan.mjs'
|
|
136
|
+
|
|
137
|
+
const code = await plan(process.argv.slice(3))
|
|
138
|
+
process.exit(code)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Сценарії використання
|
|
142
|
+
|
|
143
|
+
1. **Human↔agent (типовий ручний flow)**
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
flow init my-feature # створив .flow.json
|
|
147
|
+
flow spec # зафіксував docs/specs/<...>.md
|
|
148
|
+
# brainstorm у Cursor/Claude → з'явився docs/plans/<date>-my-feature.md з '## Кроки'
|
|
149
|
+
flow plan # резолвить plan-doc за branch, фіксує steps, status → planned
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
2. **Agent↔agent (panel)**
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
flow plan --panel
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Без plan-доку: панель персон через `runPanel({ mode: 'plan' })` синтезує `steps`. Записує `plan_doc: null`.
|
|
159
|
+
|
|
160
|
+
3. **Явний шлях**
|
|
161
|
+
```
|
|
162
|
+
flow plan docs/plans/2026-06-03-my-feature.md
|
|
163
|
+
```
|
|
164
|
+
Перший `*.md`-аргумент у `rest` має пріоритет над `resolveArtifact`.
|
|
165
|
+
|
|
166
|
+
### Стан до/після
|
|
167
|
+
|
|
168
|
+
| Поле `.flow.json` | До `plan` | Після `plan` |
|
|
169
|
+
| ----------------- | --------------------------------- | ---------------------------------------- |
|
|
170
|
+
| `status` | `'spec'` (рекомендовано) або інше | `'planned'` |
|
|
171
|
+
| `plan` | відсутнє/попереднє | `normalized` (масив steps) |
|
|
172
|
+
| `plan_doc` | відсутнє | абсолютний шлях до md або `null` (panel) |
|
|
173
|
+
|
|
174
|
+
В event-log дописується `{ type: 'plan', steps: <N>, ts: <now> }` (фактичний формат — в `recordTransition`).
|
|
175
|
+
|
|
176
|
+
### Exit code контракт
|
|
177
|
+
|
|
178
|
+
- `0` — успіх, можна переходити до наступної фази (`flow code`/`flow review` за конвенцією `n-flow`).
|
|
179
|
+
- `1` — будь-яка помилка; стан **не модифікується**, кроки не записані.
|
|
180
|
+
|
|
181
|
+
### Обробка помилок
|
|
182
|
+
|
|
183
|
+
Жодних винятків назовні: усі try/catch перетворені у `log + return 1`. Виключення — баги нижчих рівнів (`recordTransition`, `readState`) — на цьому шарі не загорнуті, тож можуть пропагуватися як unhandled rejection у CLI.
|
|
184
|
+
|
|
185
|
+
## Rebuild Test
|
|
186
|
+
|
|
187
|
+
Цього модуля **достатньо** для відтворення з нуля за цією специфікацією за умови наявності таких контрактів-сусідів:
|
|
188
|
+
|
|
189
|
+
- `resolveActiveFlowState({ cwd, branch }, deps) → { statePath, worktreeDir?, autoResolved?, label?, error? }`.
|
|
190
|
+
- `readState(statePath) → object | null` з принаймні полями `{ branch, status, spec_doc? }`.
|
|
191
|
+
- `resolveArtifact(cwd, kind, branch) → string | undefined` (абсолютний шлях до `docs/<kind>/<date>-<slug>.md`).
|
|
192
|
+
- `extractSteps(mdString) → Array` (парсить `## Кроки`).
|
|
193
|
+
- `parsePlan(jsonString) → Array` (валідація; кидає `Error` з полем `.message`).
|
|
194
|
+
- `runPanel({ task, cwd, runner, log, mode }) → Promise<Array | undefined>`.
|
|
195
|
+
- `createRunner(deps) → Promise<runner>` (може кидати `Error`).
|
|
196
|
+
- `verifyTrace(cwd, traceFn?) → boolean`.
|
|
197
|
+
- `flowEventsPath(cwd) → string`.
|
|
198
|
+
- `recordTransition({ statePath, eventsPath }, event, reducer, now) → void`.
|
|
199
|
+
|
|
200
|
+
Маючи їх — модуль повністю відновлюється з опису вище: 10 кроків функції `plan`, дві гілки збору `steps`, exit-code-контракт, side-effects через `recordTransition`.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# planner.mjs
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль `planner.mjs` — **декларативний планувальник** диспетчера субагентів (відповідає Ф1 зі spec §3). Його завдання — отримати від користувача опис фічі/задачі, сформулювати спеціалізований промпт, передати його зовнішньому субагенту (через ін'єкцію `runner`), а потім **строго валідувати** повернутий JSON-план реалізації та нормалізувати його кроки до уніфікованої структури з полями `step`, `task`, `status`, `retry_count` (плюс опційне `acceptance`).
|
|
6
|
+
|
|
7
|
+
Поведінка модуля **fail-closed**: будь-яка невідповідність контракту (відсутність JSON-масиву, невалідний JSON, порожній масив, відсутність текстового `task` у кроці, плейсхолдер на кшталт `tbd`/`todo`/`fixme`/`...`/`placeholder`, помилка субагента) призводить до кидання `Error` без часткових результатів. Це гарантує, що подальші стадії (виконання плану) ніколи не отримають частково сформований/невалідований план.
|
|
8
|
+
|
|
9
|
+
Файл написано як чистий ES-модуль (`.mjs`) без зовнішніх залежностей — лише вбудовані JS-конструкції (`JSON.parse`, `String`, регулярні вирази, `Array.prototype.map`). Це робить його легким для unit-тестування й мокування `runner` у тестах.
|
|
10
|
+
|
|
11
|
+
## Експорти / API
|
|
12
|
+
|
|
13
|
+
Модуль експортує **три іменовані функції** (інших експортів немає, default-експорту немає):
|
|
14
|
+
|
|
15
|
+
| Експорт | Тип | Призначення |
|
|
16
|
+
| ------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `plannerPrompt(task)` | `(string) => string` | Формує текстовий промпт для субагента-планувальника. |
|
|
18
|
+
| `parsePlan(text)` | `(string) => Step[]` | Парсить і валідує JSON-план із сирого тексту відповіді; повертає нормалізований масив кроків. |
|
|
19
|
+
| `generatePlan({ runner, task, cwd })` | `async ({...}) => Promise<Step[]>` | Високорівнева оркестровка: будує промпт, викликає `runner.runStep`, парсить вихід. |
|
|
20
|
+
|
|
21
|
+
Внутрішня (не експортована) константа:
|
|
22
|
+
|
|
23
|
+
| Ім'я | Тип | Значення / опис |
|
|
24
|
+
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `PLACEHOLDER` | `RegExp` | `^(tbd\|todo\|fixme\|\.\.\.\|placeholder)$` із прапором `i` — список «заборонених» текстів кроку, що сигналізують про несформований план. |
|
|
26
|
+
|
|
27
|
+
### Тип `Step` (нормалізований крок плану)
|
|
28
|
+
|
|
29
|
+
Об'єкт, який повертають `parsePlan` і `generatePlan` (масивом):
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
{
|
|
33
|
+
step: number, // 0-індексований порядковий номер
|
|
34
|
+
task: string, // оригінальний (не тримлений!) текст задачі
|
|
35
|
+
status: 'pending', // початковий статус — завжди 'pending'
|
|
36
|
+
retry_count: 0, // лічильник повторів — завжди 0
|
|
37
|
+
acceptance?: string // опційний критерій приймання (як строка)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Функції
|
|
42
|
+
|
|
43
|
+
### `plannerPrompt(task)`
|
|
44
|
+
|
|
45
|
+
**Сигнатура.** `plannerPrompt(task: string): string`
|
|
46
|
+
|
|
47
|
+
**Параметри.**
|
|
48
|
+
|
|
49
|
+
- `task` — текстовий опис фічі/задачі, який отримав диспетчер від виклику верхнього рівня.
|
|
50
|
+
|
|
51
|
+
**Повертає.** Готовий **system-user** промпт у вигляді одного рядка, склеєного через `\n`. Зміст промпта:
|
|
52
|
+
|
|
53
|
+
1. Роль: «архітектор», який має розбити задачу.
|
|
54
|
+
2. Обмеження кроку: ≤ 5 хв розробки, чіткі критерії приймання.
|
|
55
|
+
3. Формат відповіді: **ЛИШЕ** JSON-масив без коментарів, кожен елемент — об'єкт `{ "task": "...", "acceptance": "..." }`.
|
|
56
|
+
4. Останній рядок — фактична задача: `Задача: <task>`.
|
|
57
|
+
|
|
58
|
+
**Side effects.** Немає — функція суто детермінована й pure. Її вихід можна логувати, кешувати, передавати в тести як snapshot.
|
|
59
|
+
|
|
60
|
+
**Особливості.**
|
|
61
|
+
|
|
62
|
+
- Не екранує і не санітизує `task` — викликач відповідає за те, що рядок не зламає форматування промпта (хоча для рядкового рендеру це й не критично).
|
|
63
|
+
- Не вставляє приклади чи додаткові few-shot блоки — мінімалістичний контракт.
|
|
64
|
+
|
|
65
|
+
### `parsePlan(text)`
|
|
66
|
+
|
|
67
|
+
**Сигнатура.** `parsePlan(text: string): Step[]`
|
|
68
|
+
|
|
69
|
+
**Параметри.**
|
|
70
|
+
|
|
71
|
+
- `text` — сира відповідь субагента; може містити markdown-огорожі (наприклад, ` ```json ... ``` `), пояснювальний текст до/після масиву тощо.
|
|
72
|
+
|
|
73
|
+
**Повертає.** Масив нормалізованих кроків (див. тип `Step` вище).
|
|
74
|
+
|
|
75
|
+
**Алгоритм.**
|
|
76
|
+
|
|
77
|
+
1. Конвертує вхід у рядок через `String(text)` — захист від `null`/`undefined`/буферів.
|
|
78
|
+
2. Шукає **перший `[`** через `indexOf('[')` і **останній `]`** через `lastIndexOf(']')`. Це робить парсер толерантним до markdown-огорож, преамбули чи коментарів навколо JSON-масиву.
|
|
79
|
+
3. Якщо хоча б одна з дужок не знайдена або порядок інвертовано (`end < start`) — кидає `Error('planner: не знайдено JSON-масив плану — fail-closed')`.
|
|
80
|
+
4. Виокремлює зріз `str.slice(start, end + 1)` і парсить через `JSON.parse`. На SyntaxError кидає `Error('planner: невалідний JSON плану — fail-closed')`.
|
|
81
|
+
5. Перевіряє, що результат — **непорожній масив**; інакше — `Error('planner: план має бути непорожнім масивом — fail-closed')`.
|
|
82
|
+
6. Мапить кожен елемент:
|
|
83
|
+
- Витягує `task`: якщо елемент сам є рядком — то він і є task; інакше `s?.task`.
|
|
84
|
+
- Якщо `task` відсутній або не рядок — кидає `Error('planner: крок <i> без текстового поля task — fail-closed')`.
|
|
85
|
+
- Тримить копію (`task.trim()`) і перевіряє: непорожній і **не** збігається з `PLACEHOLDER`. Інакше — `Error('planner: крок <i> — placeholder/порожній task (<task>) — fail-closed')`.
|
|
86
|
+
- Формує об'єкт `{ step: i, task, status: 'pending', retry_count: 0 }`. Важливо: у поле `task` зберігається **оригінальний** (нетримлений) рядок, валідується лише тримлений варіант.
|
|
87
|
+
- Якщо в сирому елементі є поле `acceptance` (truthy) — додає `step.acceptance = String(s.acceptance)`.
|
|
88
|
+
|
|
89
|
+
**Повертає.** Масив об'єктів `Step` у тому ж порядку, що й у вхідному масиві; індекс `step` — 0-індексований, відповідає позиції в масиві.
|
|
90
|
+
|
|
91
|
+
**Side effects.** Немає — pure-функція. Винятки кидаються синхронно; внутрішнього логування, мутацій глобального стану чи I/O немає.
|
|
92
|
+
|
|
93
|
+
**Покриті граничні випадки.**
|
|
94
|
+
|
|
95
|
+
- Markdown-огорожі / преамбула / postамбула навколо JSON.
|
|
96
|
+
- Елемент-рядок (`"крок 1"`) замість `{ task: "крок 1" }`.
|
|
97
|
+
- Цифрові `task` (`123`), `null`, відсутні — все відсіюється.
|
|
98
|
+
- Регістронезалежні плейсхолдери (`TBD`, `Todo`, `FIXME`, `...`, `Placeholder`).
|
|
99
|
+
- Порожній масив `[]` — відхиляється.
|
|
100
|
+
- Будь-який `task` із пробілами навколо плейсхолдера (наприклад, `" tbd "`) також відсіюється завдяки `.trim()` перед регуляркою.
|
|
101
|
+
|
|
102
|
+
### `generatePlan({ runner, task, cwd })`
|
|
103
|
+
|
|
104
|
+
**Сигнатура.**
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
async generatePlan(input: {
|
|
108
|
+
runner: { runStep: (prompt: string, opts?: { cwd?: string })
|
|
109
|
+
=> { ok: boolean, output: string } | Promise<{ ok: boolean, output: string }> },
|
|
110
|
+
task: string,
|
|
111
|
+
cwd?: string
|
|
112
|
+
}): Promise<Step[]>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Параметри (деструктуровані).**
|
|
116
|
+
|
|
117
|
+
- `runner` — обов'язкова ін'єкція «бігуна» субагента. Очікується об'єкт із методом `runStep(prompt, opts?)`, що повертає (або резолвить у Promise) `{ ok: boolean, output: string }`. Контракт навмисно мінімальний — це спрощує мокування в тестах.
|
|
118
|
+
- `task` — текстовий опис фічі/задачі; передається у `plannerPrompt`.
|
|
119
|
+
- `cwd` — опційний робочий каталог, який пробрасується у `runner.runStep` через `{ cwd }`.
|
|
120
|
+
|
|
121
|
+
**Повертає.** `Promise<Step[]>` — нормалізований план від `parsePlan`.
|
|
122
|
+
|
|
123
|
+
**Алгоритм.**
|
|
124
|
+
|
|
125
|
+
1. Формує промпт через `plannerPrompt(task)`.
|
|
126
|
+
2. Викликає `await runner.runStep(prompt, { cwd })` — підтримує і синхронний, і асинхронний `runStep` (await на не-promise — no-op).
|
|
127
|
+
3. Якщо `res.ok === false` — кидає `Error('planner: субагент-планувальник завершився помилкою[:\n<output>]')`. Якщо `res.output` truthy — він приєднується до повідомлення з префіксом `:\n` для зручної діагностики.
|
|
128
|
+
4. Передає `res.output` у `parsePlan` і повертає його результат (валідні винятки парсера пробулькують далі).
|
|
129
|
+
|
|
130
|
+
**Side effects.**
|
|
131
|
+
|
|
132
|
+
- Викликає `runner.runStep`, який у реальному середовищі стартує субагента (запис файлів, мережа, процеси — все на боці `runner`). Сам модуль `planner.mjs` ніяких I/O не виконує.
|
|
133
|
+
- Може кидати `Error` як від власних перевірок, так і від `parsePlan`.
|
|
134
|
+
|
|
135
|
+
**Обробка помилок.** Будь-яка помилка веде до **повного провалу** (без часткового плану) — fail-closed контракт, на який спираються наступні Ф2/Ф3 диспетчера.
|
|
136
|
+
|
|
137
|
+
## Залежності
|
|
138
|
+
|
|
139
|
+
**Зовнішні npm-залежності:** немає.
|
|
140
|
+
|
|
141
|
+
**Імпорти у файлі:** немає (`import`-ів нема — модуль самодостатній).
|
|
142
|
+
|
|
143
|
+
**Що використовується (вбудоване в JS-runtime):**
|
|
144
|
+
|
|
145
|
+
- `String(...)` — нормалізація вхідного значення в `parsePlan`.
|
|
146
|
+
- `JSON.parse` — парсинг витягнутого JSON-зрізу.
|
|
147
|
+
- `Array.isArray`, `Array.prototype.map` — перевірка типу й мапінг.
|
|
148
|
+
- `RegExp` (`/^(tbd|todo|fixme|\.\.\.|placeholder)$/i`) — детектор плейсхолдерів.
|
|
149
|
+
- `String.prototype.indexOf`/`lastIndexOf`/`slice`/`trim`.
|
|
150
|
+
|
|
151
|
+
**Ін'єкції (контракт ззовні):**
|
|
152
|
+
|
|
153
|
+
- `runner` із методом `runStep(prompt, opts?)` — інверсія залежностей; реалізація лежить за межами цього модуля (зазвичай — окремий модуль `dispatcher/lib/runner*.mjs`).
|
|
154
|
+
|
|
155
|
+
**Хто залежить від модуля (типові споживачі).**
|
|
156
|
+
|
|
157
|
+
- Оркестратор/диспетчер субагентів верхнього рівня — імпортує `generatePlan` для побудови плану перед виконанням.
|
|
158
|
+
- Юніт-тести — як правило, ймовірно імпортують `parsePlan` і `plannerPrompt` прямо для перевірки контракту без `runner`.
|
|
159
|
+
|
|
160
|
+
Точні шляхи споживачів у цьому документі **не фіксуються** (модуль документується ізольовано).
|
|
161
|
+
|
|
162
|
+
## Потік виконання / Використання
|
|
163
|
+
|
|
164
|
+
### Типовий happy-path (з боку диспетчера)
|
|
165
|
+
|
|
166
|
+
1. Диспетчер отримує `task` (опис фічі) і має `runner`-реалізацію.
|
|
167
|
+
2. Викликає `await generatePlan({ runner, task, cwd })`.
|
|
168
|
+
3. `generatePlan` будує промпт через `plannerPrompt(task)`.
|
|
169
|
+
4. `runner.runStep(prompt, { cwd })` стартує субагента-«архітектора», який повертає `{ ok: true, output: '[ ... JSON ... ]' }` (можливо з markdown-огорожею).
|
|
170
|
+
5. `parsePlan(output)` витягує JSON-зріз від `[` до `]`, парсить, валідує, нормалізує — повертає масив `Step`.
|
|
171
|
+
6. Диспетчер отримує `Step[]` і передає його наступній фазі (Ф2 — виконання кроків).
|
|
172
|
+
|
|
173
|
+
### Sad-path сценарії (fail-closed)
|
|
174
|
+
|
|
175
|
+
| Сценарій | Точка падіння | Текст помилки (рівно) |
|
|
176
|
+
| ------------------------------------------------------------ | -------------- | ---------------------------------------------------------------------- |
|
|
177
|
+
| `runner.runStep` повернув `{ ok: false, output: '' }` | `generatePlan` | `planner: субагент-планувальник завершився помилкою` |
|
|
178
|
+
| `runner.runStep` повернув `{ ok: false, output: '<lorem>' }` | `generatePlan` | `planner: субагент-планувальник завершився помилкою:\n<lorem>` |
|
|
179
|
+
| У відповіді немає `[` або `]` (або в зворотному порядку) | `parsePlan` | `planner: не знайдено JSON-масив плану — fail-closed` |
|
|
180
|
+
| JSON всередині `[...]` неваліден | `parsePlan` | `planner: невалідний JSON плану — fail-closed` |
|
|
181
|
+
| Результат — не масив або порожній | `parsePlan` | `planner: план має бути непорожнім масивом — fail-closed` |
|
|
182
|
+
| Крок без `task` (або не-рядок) | `parsePlan` | `planner: крок <i> без текстового поля task — fail-closed` |
|
|
183
|
+
| Крок з порожнім/плейсхолдер-`task` | `parsePlan` | `planner: крок <i> — placeholder/порожній task (<task>) — fail-closed` |
|
|
184
|
+
|
|
185
|
+
### Приклади використання
|
|
186
|
+
|
|
187
|
+
**Лише парсинг (наприклад, у тесті):**
|
|
188
|
+
|
|
189
|
+
````
|
|
190
|
+
import { parsePlan } from './planner.mjs'
|
|
191
|
+
|
|
192
|
+
const text = '```json\n[\n { "task": "init repo", "acceptance": "git init ok" },\n "lint config"\n]\n```'
|
|
193
|
+
const steps = parsePlan(text)
|
|
194
|
+
// steps[0] === { step: 0, task: 'init repo', status: 'pending', retry_count: 0, acceptance: 'git init ok' }
|
|
195
|
+
// steps[1] === { step: 1, task: 'lint config', status: 'pending', retry_count: 0 }
|
|
196
|
+
````
|
|
197
|
+
|
|
198
|
+
**Повна оркестровка з мок-runner:**
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
import { generatePlan } from './planner.mjs'
|
|
202
|
+
|
|
203
|
+
const runner = {
|
|
204
|
+
runStep: async (prompt) => ({
|
|
205
|
+
ok: true,
|
|
206
|
+
output: '[{ "task": "крок 1" }, { "task": "крок 2", "acceptance": "тести зелені" }]'
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const plan = await generatePlan({ runner, task: 'додати feature X', cwd: process.cwd() })
|
|
211
|
+
// plan.length === 2; plan[1].acceptance === 'тести зелені'
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Реакція на fail-closed:**
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
try {
|
|
218
|
+
await generatePlan({ runner, task: '...' })
|
|
219
|
+
} catch (e) {
|
|
220
|
+
// e.message починається з 'planner: ' — далі диспетчер може віддати помилку нагору
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Інваріанти, на які можна спиратися
|
|
225
|
+
|
|
226
|
+
- Поверне рівно стільки кроків, скільки було у вхідному JSON-масиві (mapping 1:1, порядок збережено).
|
|
227
|
+
- `step` зростає монотонно від `0` до `arr.length - 1`.
|
|
228
|
+
- `status === 'pending'`, `retry_count === 0` — для **кожного** свіжого кроку (новостворений план — завжди «незачеплений»).
|
|
229
|
+
- Поле `acceptance` присутнє **лише** якщо в сирому елементі воно truthy; інакше відсутнє (не `undefined`, а немає ключа взагалі).
|
|
230
|
+
- Поле `task` зберігається **в оригінальному вигляді** (з пробілами по краях), валідація використовує тримлений варіант — тобто можливі «візуально нетримлені» рядки в результаті, але вони гарантовано не плейсхолдери й не порожні.
|
|
231
|
+
|
|
232
|
+
## Rebuild Test
|
|
233
|
+
|
|
234
|
+
Цей розділ описує контракт настільки повно, що його можна використати як специфікацію для повторної реалізації файлу з нуля.
|
|
235
|
+
|
|
236
|
+
**Реалізаційні вимоги:**
|
|
237
|
+
|
|
238
|
+
1. ES-модуль `.mjs` без імпортів.
|
|
239
|
+
2. Внутрішня константа-регулярка `PLACEHOLDER = /^(tbd|todo|fixme|\.\.\.|placeholder)$/i`.
|
|
240
|
+
3. Експортувати рівно три функції: `plannerPrompt`, `parsePlan`, `generatePlan` (іменовані експорти, без default).
|
|
241
|
+
4. `plannerPrompt(task)` повертає рядок, склеєний через `\n` з 5 рядків (4 інструктивних + порожній + `Задача: <task>`).
|
|
242
|
+
5. `parsePlan(text)`:
|
|
243
|
+
- Конвертувати в рядок через `String(text)`.
|
|
244
|
+
- Знайти `start = indexOf('[')`, `end = lastIndexOf(']')`. Якщо `start === -1 || end === -1 || end < start` — кинути `'planner: не знайдено JSON-масив плану — fail-closed'`.
|
|
245
|
+
- Спробувати `JSON.parse(slice(start, end+1))`; на catch — кинути `'planner: невалідний JSON плану — fail-closed'`.
|
|
246
|
+
- Якщо результат не масив або порожній — кинути `'planner: план має бути непорожнім масивом — fail-closed'`.
|
|
247
|
+
- Для кожного `(s, i)`: визначити `task = typeof s === 'string' ? s : s?.task`; якщо falsy або не string — кинути `'planner: крок <i> без текстового поля task — fail-closed'`; перевірити `trimmed = task.trim()` — якщо falsy або матчить `PLACEHOLDER` — кинути `'planner: крок <i> — placeholder/порожній task (<task>) — fail-closed'`.
|
|
248
|
+
- Сформувати `{ step: i, task, status: 'pending', retry_count: 0 }`; якщо `s?.acceptance` — додати `acceptance: String(s.acceptance)`.
|
|
249
|
+
6. `generatePlan({ runner, task, cwd })` — `async`:
|
|
250
|
+
- `const res = await runner.runStep(plannerPrompt(task), { cwd })`.
|
|
251
|
+
- Якщо `!res.ok` — кинути `'planner: субагент-планувальник завершився помилкою'` плюс `':\n' + res.output` якщо `res.output` truthy.
|
|
252
|
+
- Інакше `return parsePlan(res.output)`.
|
|
253
|
+
|
|
254
|
+
**Контрольні приклади (повинні проходити після rebuild):**
|
|
255
|
+
|
|
256
|
+
| Вхід `parsePlan` | Очікуваний результат |
|
|
257
|
+
| -------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
258
|
+
| `'[{"task":"a"}]'` | `[{ step:0, task:'a', status:'pending', retry_count:0 }]` |
|
|
259
|
+
| `'prefix [{"task":"a","acceptance":"ok"}] suffix'` | `[{ step:0, task:'a', status:'pending', retry_count:0, acceptance:'ok' }]` |
|
|
260
|
+
| `'[\"a\",\"b\"]'` | `[{step:0,task:'a',...},{step:1,task:'b',...}]` |
|
|
261
|
+
| `'[]'` | throw `'planner: план має бути непорожнім масивом — fail-closed'` |
|
|
262
|
+
| `'no json here'` | throw `'planner: не знайдено JSON-масив плану — fail-closed'` |
|
|
263
|
+
| `'[{"task":"tbd"}]'` | throw `'planner: крок 0 — placeholder/порожній task (tbd) — fail-closed'` |
|
|
264
|
+
| `'[{"task":""}]'` | throw `'planner: крок 0 — placeholder/порожній task () — fail-closed'` |
|
|
265
|
+
| `'[{"task":123}]'` | throw `'planner: крок 0 без текстового поля task — fail-closed'` |
|
|
266
|
+
| `'[oops'` (нема `]`) | throw `'planner: не знайдено JSON-масив плану — fail-closed'` |
|
|
267
|
+
| `'[invalid json]'` | throw `'planner: невалідний JSON плану — fail-closed'` |
|
|
268
|
+
|
|
269
|
+
Якщо реалізація проходить усі ці кейси й тримає сигнатури/тексти помилок з таблиць — її можна вважати функціонально еквівалентною оригінальному `planner.mjs`.
|