@nitra/cursor 3.5.0 → 3.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.6.0] - 2026-06-01
4
+
5
+ ### Added
6
+
7
+ - flow: команда gate — структурований вердикт релізної готовності (PASS/CONCERNS/FAIL + score + причини, синтез verify-гейтів і review-findings); release м'яко попереджає на FAIL
8
+
3
9
  ## [3.5.0] - 2026-06-01
4
10
 
5
11
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -79,7 +79,17 @@ Composer, Claude Code) працює **Пасивний Турнікет**: ти,
79
79
  7. **На провал** — виправ код за виводом і виклич `flow verify` знову. Максимум
80
80
  **3 спроби**; якщо не вдається — зупинись і поклич людину.
81
81
 
82
- 8. **Фініш** лише після зеленого `verify`:
82
+ 8. **Gate (вердикт готовності)** перед фінішем:
83
+
84
+ ```
85
+ npx @nitra/cursor flow gate
86
+ ```
87
+
88
+ Синтезує verify-гейти і review-findings у `PASS / CONCERNS / FAIL` + score +
89
+ причини (пише у `.flow.json`). `FAIL` (провалений gate або high-severity
90
+ finding) → код 1; `release` на FAIL лише попередить (рішення за тобою).
91
+
92
+ 9. **Фініш** — після зеленого `verify` і бажано `gate` ≠ FAIL:
83
93
 
84
94
  ```
85
95
  npx @nitra/cursor flow release --bump <patch|minor|major> --section <Added|Changed|Fixed> --message "<що зроблено>"
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { cancel, repair, resume, run } from './lib/active.mjs'
11
11
  import { init, release, verify } from './lib/commands.mjs'
12
+ import { gate } from './lib/gate.mjs'
12
13
  import { plan } from './lib/plan.mjs'
13
14
  import { review } from './lib/review.mjs'
14
15
  import { spec } from './lib/spec.mjs'
@@ -20,6 +21,7 @@ const USAGE = [
20
21
  ' npx @nitra/cursor flow plan [--panel] # Фасад A: фаза плану → docs/plans/<…> + state',
21
22
  ' npx @nitra/cursor flow verify # Фасад A: Quality Gates (pass/fail)',
22
23
  ' npx @nitra/cursor flow review # Фасад A: adversarial diff-review (за level)',
24
+ ' npx @nitra/cursor flow gate # Фасад A: вердикт PASS/CONCERNS/FAIL (verify+review)',
23
25
  ' npx @nitra/cursor flow release # Фасад A: .changes + completion snapshot',
24
26
  ' npx @nitra/cursor flow run "<опис>" # Фасад B: повний 5-фазний цикл',
25
27
  ' npx @nitra/cursor flow resume # продовжити з чекпойнта',
@@ -28,13 +30,13 @@ const USAGE = [
28
30
  ].join('\n')
29
31
 
30
32
  /** Підкоманди flow. */
31
- export const SUBCOMMANDS = ['init', 'spec', 'plan', 'verify', 'review', 'release', 'run', 'resume', 'cancel', 'repair']
33
+ export const SUBCOMMANDS = ['init', 'spec', 'plan', 'verify', 'review', 'gate', 'release', 'run', 'resume', 'cancel', 'repair']
32
34
 
33
35
  /**
34
36
  * Усі handler-и реальні (Ф1 Spec/Plan + Ф2 Турнікет + Ф4 Активний Раннер).
35
37
  * @type {Record<string, (rest: string[], deps: object) => Promise<number>>}
36
38
  */
37
- export const DEFAULT_HANDLERS = { init, spec, plan, verify, review, release, run, resume, cancel, repair }
39
+ export const DEFAULT_HANDLERS = { init, spec, plan, verify, review, gate, release, run, resume, cancel, repair }
38
40
 
39
41
  /**
40
42
  * Точка входу `case 'flow'` у `bin/n-cursor.js`. Парсить підкоманду й
@@ -179,6 +179,10 @@ export async function release(rest, deps = {}) {
179
179
  log('release: стану нема — спершу `flow init`')
180
180
  return 1
181
181
  }
182
+ // М'які ворота: FAIL-гейт — лише попередження, рішення за людиною.
183
+ if (state.gate?.verdict === 'FAIL') {
184
+ log(`⚠️ release: gate = FAIL (score ${state.gate.score}) — релізиш свідомо? (див. flow gate)`)
185
+ }
182
186
 
183
187
  const ch = run('npx', ['@nitra/cursor', 'change', ...rest], { cwd })
184
188
  if ((ch.status ?? 1) !== 0) {
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `flow gate` — структурований вердикт релізної готовності (ідея BMAD qa-gate, у
3
+ * нашому стані). Синтезує механічні гейти `verify` (`state.gates`) і adversarial
4
+ * findings `review` (`state.review.findings`) у єдине PASS/CONCERNS/FAIL + score
5
+ * + причини. Дає traceability «чому готово/не готово». `gate` лише агрегує —
6
+ * рішення verify/review не дублює.
7
+ *
8
+ * Уся IO (`now`) ін'єктується; `computeGate` — чиста (тестується без стану на диску).
9
+ */
10
+ import { cwd as processCwd } from 'node:process'
11
+
12
+ import { flowEventsPath } from './events.mjs'
13
+ import { flowStatePath, readState, recordTransition } from './state-store.mjs'
14
+
15
+ /** Штрафи score за кожен тип проблеми. */
16
+ const PENALTY = { failedGate: 40, high: 25, med: 8, noVerify: 15 }
17
+
18
+ /**
19
+ * Чистий синтез вердикту з наявного стану.
20
+ * @param {{ gates?: { name: string, ok: boolean }[], review?: { findings?: { severity?: string }[] } }} state стан flow
21
+ * @returns {{ verdict: 'PASS' | 'CONCERNS' | 'FAIL', score: number, reasons: string[] }} вердикт
22
+ */
23
+ export function computeGate(state) {
24
+ const gates = state.gates ?? []
25
+ const findings = state.review?.findings ?? []
26
+ const failedGates = gates.filter(g => !g.ok)
27
+ const high = findings.filter(f => f.severity === 'high')
28
+ const med = findings.filter(f => f.severity === 'med')
29
+ const noVerify = gates.length === 0
30
+
31
+ const reasons = []
32
+ for (const g of failedGates) reasons.push(`gate «${g.name}» провалено`)
33
+ if (high.length > 0) reasons.push(`${high.length} high-severity review finding(s)`)
34
+ if (med.length > 0) reasons.push(`${med.length} med-severity review finding(s)`)
35
+ if (noVerify) reasons.push('verify ще не запускався')
36
+
37
+ let verdict = 'PASS'
38
+ if (failedGates.length > 0 || high.length > 0) {
39
+ verdict = 'FAIL'
40
+ } else if (med.length > 0 || noVerify) {
41
+ verdict = 'CONCERNS'
42
+ }
43
+
44
+ const penalty =
45
+ PENALTY.failedGate * failedGates.length +
46
+ PENALTY.high * high.length +
47
+ PENALTY.med * med.length +
48
+ (noVerify ? PENALTY.noVerify : 0)
49
+ const score = Math.max(0, Math.min(100, 100 - penalty))
50
+
51
+ return { verdict, score, reasons }
52
+ }
53
+
54
+ /**
55
+ * `flow gate` — обчислює й фіксує вердикт у `.flow.json`.
56
+ * @param {string[]} _rest аргументи (не використовуються)
57
+ * @param {{ cwd?: string, log?: (m: string) => void, now?: () => number }} [deps] ін'єкції
58
+ * @returns {Promise<number>} exit code (FAIL → 1; PASS/CONCERNS → 0)
59
+ */
60
+ export async function gate(_rest, deps = {}) {
61
+ const cwd = deps.cwd ?? processCwd()
62
+ const log = deps.log ?? console.error
63
+ const now = deps.now ?? Date.now
64
+
65
+ const statePath = flowStatePath(cwd)
66
+ const state = readState(statePath)
67
+ if (!state) {
68
+ log('gate: стану нема — спершу `flow init`')
69
+ return 1
70
+ }
71
+
72
+ const result = computeGate(state)
73
+ recordTransition(
74
+ { statePath, eventsPath: flowEventsPath(cwd) },
75
+ { type: 'gate', verdict: result.verdict },
76
+ s => ({ ...s, gate: { ...result, at: new Date(now()).toISOString() } }),
77
+ now
78
+ )
79
+
80
+ log(`gate: ${result.verdict} (score ${result.score})`)
81
+ for (const r of result.reasons) log(` · ${r}`)
82
+ return result.verdict === 'FAIL' ? 1 : 0
83
+ }