@nitra/cursor 3.26.0 → 3.27.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.
@@ -0,0 +1,229 @@
1
+ /**
2
+ * T0-auto: детермінований рівень виправлень для n-fix оркестратора.
3
+ *
4
+ * Парсить `output` з `n-cursor fix --json` → застосовує програмний фікс без LLM.
5
+ * Умова застосування: violation-message містить конкретне цільове значення
6
+ * (назву файлу, рядок для вставки, ім'я залежності), яке можна видобути regex.
7
+ *
8
+ * Ієрархія: T0 (rm/create, знаний тип) → T0-auto (parse violation) → T1 (LLM).
9
+ * T0-auto запускається першим у конвергентному циклі; T1 — лише для решти.
10
+ *
11
+ * Публічний API:
12
+ * applyT0Auto(ruleId, violationOutput, cwd) → { applied: boolean, actions: string[] }
13
+ * listT0AutoRules() → string[] (ids що мають хоч один паттерн)
14
+ * runT0AutoCli(args, cwd) → Promise<number> (exit 0=clean, 1=violation)
15
+ */
16
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
17
+ import { join } from 'node:path'
18
+ import { spawnSync } from 'node:child_process'
19
+ import { dirname } from 'node:path'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ /**
23
+ * Патерни T0-auto.
24
+ * Кожен паттерн: {
25
+ * id: string — унікальна назва паттерну (для логу)
26
+ * test: (output)=>bool — чи підходить цей output до паттерну
27
+ * apply: (match, cwd)=>{ ok: bool, action: string } — застосувати фікс
28
+ * }
29
+ */
30
+ const PATTERNS = [
31
+ // ── vscode-ext-add ──────────────────────────────────────────────────────────
32
+ // Violation: «recommendations має містити "tsandall.opa"»
33
+ // Fix: додати рядок у .vscode/extensions.json#recommendations
34
+ {
35
+ id: 'vscode-ext-add',
36
+ test: out => /recommendations має містити "[^"]+"/.test(out),
37
+ apply: (out, cwd) => {
38
+ const matches = [...out.matchAll(/recommendations має містити "([^"]+)"/g)]
39
+ if (matches.length === 0) return { ok: false, action: 'no match' }
40
+
41
+ const extPath = join(cwd, '.vscode/extensions.json')
42
+ if (!existsSync(extPath)) {
43
+ return { ok: false, action: '.vscode/extensions.json не знайдено' }
44
+ }
45
+
46
+ let parsed
47
+ try {
48
+ parsed = JSON.parse(readFileSync(extPath, 'utf8'))
49
+ } catch {
50
+ return { ok: false, action: '.vscode/extensions.json: невалідний JSON' }
51
+ }
52
+
53
+ const recs = Array.isArray(parsed.recommendations) ? parsed.recommendations : []
54
+ const toAdd = matches.map(m => m[1]).filter(e => !recs.includes(e))
55
+ if (toAdd.length === 0) return { ok: false, action: 'вже є' }
56
+
57
+ parsed.recommendations = [...recs, ...toAdd]
58
+ writeFileSync(extPath, JSON.stringify(parsed, null, 2) + '\n', 'utf8')
59
+ return { ok: true, action: `додано до extensions.json: ${toAdd.join(', ')}` }
60
+ },
61
+ },
62
+
63
+ // ── rm-forbidden-file ────────────────────────────────────────────────────────
64
+ // Violation: «Знайдено заборонений файл: package-lock.json»
65
+ // Fix: видалити файл
66
+ {
67
+ id: 'rm-forbidden-file',
68
+ test: out => /Знайдено заборонений файл: \S+/.test(out),
69
+ apply: (out, cwd) => {
70
+ const matches = [...out.matchAll(/Знайдено заборонений файл: (\S+)/g)]
71
+ if (matches.length === 0) return { ok: false, action: 'no match' }
72
+
73
+ const removed = []
74
+ for (const m of matches) {
75
+ const filePath = join(cwd, m[1])
76
+ if (existsSync(filePath)) {
77
+ rmSync(filePath, { force: true })
78
+ removed.push(m[1])
79
+ }
80
+ }
81
+ if (removed.length === 0) return { ok: false, action: 'файлів не знайдено' }
82
+ return { ok: true, action: `видалено: ${removed.join(', ')}` }
83
+ },
84
+ },
85
+ ]
86
+
87
+ /**
88
+ * Застосовує всі T0-auto паттерни до одного violation-output.
89
+ *
90
+ * @param {string} ruleId id правила (для логу)
91
+ * @param {string} violationOutput рядок з поля `output` у `fix --json`
92
+ * @param {string} cwd корінь проєкту
93
+ * @returns {{ applied: boolean, actions: string[] }}
94
+ */
95
+ export function applyT0Auto(ruleId, violationOutput, cwd) {
96
+ const actions = []
97
+ let applied = false
98
+
99
+ for (const p of PATTERNS) {
100
+ if (!p.test(violationOutput)) continue
101
+ const result = p.apply(violationOutput, cwd)
102
+ actions.push(`[${p.id}] ${result.action}`)
103
+ if (result.ok) {
104
+ applied = true
105
+ }
106
+ }
107
+
108
+ return { applied, actions }
109
+ }
110
+
111
+ /**
112
+ * Повертає список id правил, для яких є хоча б один T0-auto паттерн
113
+ * (визначається по violation-output із `fix --json`).
114
+ *
115
+ * @param {{ ruleId: string, output: string }[]} failedRules
116
+ * @returns {string[]}
117
+ */
118
+ export function filterT0AutoRules(failedRules) {
119
+ return failedRules
120
+ .filter(r => PATTERNS.some(p => p.test(r.output)))
121
+ .map(r => r.ruleId)
122
+ }
123
+
124
+ // ─── CLI entry-point ──────────────────────────────────────────────────────────
125
+
126
+ const HERE = dirname(fileURLToPath(import.meta.url))
127
+ /** Абсолютний шлях до npm/bin/n-cursor.js відносно цього файлу */
128
+ const N_CURSOR_BIN = join(HERE, '../../../bin/n-cursor.js')
129
+
130
+ /**
131
+ * CLI підкоманда `n-cursor fix-t0 [rule...]`.
132
+ * Запускає `fix --json`, застосовує T0-auto для кожного violation,
133
+ * повторно перевіряє check-gate, виводить підсумок.
134
+ *
135
+ * @param {string[]} args аргументи підкоманди (опційний список rule-ids)
136
+ * @param {string} cwd корінь проєкту
137
+ * @returns {Promise<number>} 0 — T0-auto закрив всі або немає порушень; 1 — лишились
138
+ */
139
+ export async function runT0AutoCli(args, cwd) {
140
+ const ruleFilter = args.filter(a => !a.startsWith('--'))
141
+ const verbose = args.includes('--verbose') || args.includes('-v')
142
+
143
+ // 1. Запустити fix --json
144
+ const fixResult = spawnSync(
145
+ 'bun',
146
+ [N_CURSOR_BIN, '_fix-check', ...ruleFilter],
147
+ { cwd, encoding: 'utf8', timeout: 120_000 }
148
+ )
149
+ const raw = fixResult.stdout?.trim()
150
+ if (!raw) {
151
+ console.error(`n-cursor fix-t0: fix --json повернув порожній stdout`)
152
+ console.error(fixResult.stderr?.slice(0, 300) ?? '')
153
+ return 1
154
+ }
155
+
156
+ let fixJson
157
+ try {
158
+ fixJson = JSON.parse(raw)
159
+ } catch {
160
+ console.error(`n-cursor fix-t0: fix --json повернув невалідний JSON`)
161
+ return 1
162
+ }
163
+
164
+ const failed = fixJson.rules.filter(r => !r.ok)
165
+ if (failed.length === 0) {
166
+ console.log(`✅ fix-t0: всі правила чисті — T0 не потрібен`)
167
+ return 0
168
+ }
169
+
170
+ // 2. Застосувати T0-auto
171
+ const applied = []
172
+ const skipped = []
173
+ for (const r of failed) {
174
+ const result = applyT0Auto(r.ruleId, r.output, cwd)
175
+ if (result.applied) {
176
+ applied.push({ ruleId: r.ruleId, actions: result.actions })
177
+ } else {
178
+ skipped.push(r.ruleId)
179
+ }
180
+ }
181
+
182
+ if (applied.length === 0) {
183
+ console.log(`⏭️ fix-t0: T0-auto паттерн не підходить для: ${failed.map(r => r.ruleId).join(', ')}`)
184
+ return 1
185
+ }
186
+
187
+ // 3. Вивести що зробили
188
+ for (const { ruleId, actions } of applied) {
189
+ console.log(`⚙️ ${ruleId}:`)
190
+ for (const a of actions) console.log(` ${a}`)
191
+ }
192
+
193
+ // 4. Check-gate: перевірити лише ті правила, що ми чіпали
194
+ const recheck = spawnSync(
195
+ 'bun',
196
+ [N_CURSOR_BIN, '_fix-check', ...applied.map(a => a.ruleId)],
197
+ { cwd, encoding: 'utf8', timeout: 120_000 }
198
+ )
199
+ const recheckRaw = recheck.stdout?.trim()
200
+ if (!recheckRaw) {
201
+ console.error(`fix-t0: check-gate: fix --json повернув порожній stdout`)
202
+ return 1
203
+ }
204
+
205
+ const recheckJson = JSON.parse(recheckRaw)
206
+ const stillFailed = recheckJson.rules.filter(r => !r.ok)
207
+
208
+ if (verbose) {
209
+ for (const r of recheckJson.rules) {
210
+ console.log(` ${r.ok ? '✅' : '❌'} ${r.ruleId}`)
211
+ }
212
+ }
213
+
214
+ if (stillFailed.length > 0) {
215
+ console.log(`❌ fix-t0 check-gate: ${stillFailed.map(r => r.ruleId).join(', ')} — лишились`)
216
+ if (skipped.length > 0) {
217
+ console.log(`⏭️ без T0 паттерну: ${skipped.join(', ')} → потрібен LLM`)
218
+ }
219
+ return 1
220
+ }
221
+
222
+ const totalFixed = applied.length
223
+ const total = failed.length
224
+ console.log(
225
+ `✅ fix-t0: ${totalFixed}/${total} правил закрито T0-auto` +
226
+ (skipped.length > 0 ? `; ${skipped.join(', ')} → T1 (LLM)` : '')
227
+ )
228
+ return 0
229
+ }
@@ -1 +1 @@
1
- { "auto": "завжди", "worktree": true }
1
+ { "auto": "завжди", "worktree": true, "orchestrator": true }