@nitra/cursor 12.15.1 → 12.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/bin/n-cursor.js +1 -1
- package/lib/docs/index.md +9 -6
- package/lib/docs/pi-agent-fix.md +28 -0
- package/lib/docs/pi-agent-skill.md +36 -0
- package/lib/docs/pi-model-tiers.md +46 -0
- package/lib/docs/pi-one-shot.md +34 -0
- package/lib/docs/pi-telemetry-store.md +33 -0
- package/lib/docs/pi-trace.md +27 -0
- package/lib/docs/pi-write-guard.md +32 -0
- package/lib/pi-agent-fix.mjs +253 -0
- package/lib/pi-agent-skill.mjs +181 -0
- package/lib/pi-model-tiers.mjs +109 -0
- package/lib/pi-one-shot.mjs +129 -0
- package/lib/pi-telemetry-store.mjs +0 -0
- package/lib/pi-trace.mjs +40 -0
- package/lib/pi-write-guard.mjs +147 -0
- package/package.json +5 -1
- package/rules/doc-files/js/docgen-files-batch.mjs +20 -5
- package/rules/doc-files/js/docgen-gen.mjs +42 -25
- package/rules/doc-files/js/docgen-judge-measure.mjs +16 -13
- package/rules/doc-files/js/docgen-judge.mjs +11 -9
- package/rules/doc-files/js/docs/docgen-files-batch.md +3 -20
- package/rules/doc-files/js/docs/docgen-gen.md +3 -20
- package/rules/doc-files/js/docs/docgen-judge-measure.md +3 -18
- package/rules/doc-files/js/docs/docgen-judge.md +3 -22
- package/rules/npm-module/js/docs/skill_meta.md +22 -15
- package/rules/npm-module/js/skill_meta.mjs +5 -1
- package/rules/text/js/cspell-fix.mjs +15 -16
- package/rules/text/js/docs/cspell-fix.md +16 -9
- package/rules/text/main.mjs +4 -4
- package/schemas/skill-meta.json +8 -0
- package/scripts/docs/skills-cli.md +21 -25
- package/scripts/lib/adr/docs/normalize-cli.md +3 -20
- package/scripts/lib/adr/docs/normalize-pipeline.md +3 -33
- package/scripts/lib/adr/normalize-cli.mjs +2 -2
- package/scripts/lib/adr/normalize-pipeline.mjs +78 -44
- package/scripts/lib/docs/skill-meta.md +27 -10
- package/scripts/lib/fix/docs/escalation-log.md +10 -9
- package/scripts/lib/fix/docs/orchestrator.md +13 -20
- package/scripts/lib/fix/escalation-log.mjs +1 -1
- package/scripts/lib/fix/orchestrator.mjs +65 -31
- package/scripts/lib/skill-meta.mjs +22 -0
- package/scripts/skills-cli.mjs +52 -14
- package/scripts/utils/ast-extract.mjs +105 -0
- package/scripts/utils/docs/ast-extract.md +30 -0
- package/lib/docs/llm.md +0 -33
- package/lib/docs/models.md +0 -48
- package/lib/docs/omlx-trace.md +0 -49
- package/lib/docs/omlx.md +0 -41
- package/lib/llm.mjs +0 -215
- package/lib/models.mjs +0 -75
- package/lib/omlx-trace.mjs +0 -158
- package/lib/omlx.mjs +0 -220
- package/scripts/lib/fix/docs/llm-fix-apply.md +0 -31
- package/scripts/lib/fix/docs/llm-lint-fix.md +0 -31
- package/scripts/lib/fix/docs/llm-worker.md +0 -28
- package/scripts/lib/fix/docs/verbose-block.md +0 -27
- package/scripts/lib/fix/llm-fix-apply.mjs +0 -113
- package/scripts/lib/fix/llm-lint-fix.mjs +0 -82
- package/scripts/lib/fix/llm-worker.mjs +0 -346
- package/scripts/lib/fix/verbose-block.mjs +0 -82
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
/** @see ./docs/llm-worker.md */
|
|
2
|
-
|
|
3
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { env } from 'node:process'
|
|
6
|
-
import { resolveModel } from '../../../lib/models.mjs'
|
|
7
|
-
import { callLlmRich } from '../../../lib/llm.mjs'
|
|
8
|
-
import { applyChanges, parseChangesResponse, readFilesForFix } from './llm-fix-apply.mjs'
|
|
9
|
-
|
|
10
|
-
// Дефолтна модель, коли викликач не задав `opts.model` (legacy/прямі виклики).
|
|
11
|
-
// Драбина ескалації (`orchestrator.mjs`) завжди передає модель рунга явно, тож
|
|
12
|
-
// тут лишається тільки fallback на min-тир. Перевизначення — `N_CURSOR_FIX_MODEL`.
|
|
13
|
-
const MODEL = env.N_CURSOR_FIX_MODEL ?? resolveModel('min')
|
|
14
|
-
|
|
15
|
-
// Бюджет thinking-токенів для omlx-моделей (Gemma 4 та ін., що підтримують thinking_budget).
|
|
16
|
-
// Значення 0 вимикає thinking. Перевизначення — `N_CURSOR_OMLX_THINKING_BUDGET`.
|
|
17
|
-
const DEFAULT_THINKING_BUDGET = Number(env.N_CURSOR_OMLX_THINKING_BUDGET ?? 4096)
|
|
18
|
-
|
|
19
|
-
const API_KEY_RE = /api key/i
|
|
20
|
-
|
|
21
|
-
const FILE_EXTS = 'json|js|mjs|ts|vue|yml|yaml|toml|mdc|md|sh|py'
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Каталог `npm/rules/` у пакеті — для вибору sub-check .mdc.
|
|
25
|
-
* Шлях: <package>/npm/scripts/lib/fix/ → ../../.. → npm/ → rules/.
|
|
26
|
-
*/
|
|
27
|
-
const PACKAGE_RULES_DIR = join(import.meta.dirname, '..', '..', '..', 'rules')
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Витягує шляхи файлів лише з рядків ❌ у violation output.
|
|
31
|
-
* Без workspace-розгортання — повертає bare path для звірки з target.json.
|
|
32
|
-
* @param {string} output violation output
|
|
33
|
-
* @returns {string[]} унікальні шляхи з ❌-рядків
|
|
34
|
-
*/
|
|
35
|
-
function extractFailPaths(output) {
|
|
36
|
-
const seen = new Set()
|
|
37
|
-
const add = p => {
|
|
38
|
-
seen.add(p)
|
|
39
|
-
}
|
|
40
|
-
const failSep = `(?::\\d+)?(?::\\s|[\\s—]|$)`
|
|
41
|
-
// ❌ [ws] path/file.ext → strip workspace, зберігаємо bare file
|
|
42
|
-
const failWsRe = new RegExp(`^\\s*❌\\s+\\[[\\w-]+\\]\\s+([\\w./][\\w./-]*\\.(?:${FILE_EXTS}))${failSep}`, 'gm')
|
|
43
|
-
for (const m of output.matchAll(failWsRe)) add(m[1])
|
|
44
|
-
const failRe = new RegExp(`^\\s*❌\\s+(\\.?[\\w][\\w./-]*\\.(?:${FILE_EXTS}))${failSep}`, 'gm')
|
|
45
|
-
for (const m of output.matchAll(failRe)) add(m[1])
|
|
46
|
-
return [...seen]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Збирає .mdc-контекст правила з двох джерел у пакеті (`PACKAGE_RULES_DIR`):
|
|
51
|
-
* - `js/<check>.mdc` — описи check-логіки (завжди всі, відсортовані за іменем);
|
|
52
|
-
* - `policy/<c>/<c>.mdc` — concern-специфічний контент: вибираємо лише ті concern/.mdc,
|
|
53
|
-
* чий `target.json → files.single` збігається з failing paths
|
|
54
|
-
* у violation output; якщо жоден не збігається — включаємо всі.
|
|
55
|
-
* @param {string} ruleId ID правила
|
|
56
|
-
* @param {string} violationOutput violation output
|
|
57
|
-
* @returns {string|null} конкатенація зібраних .mdc або null, якщо нічого не знайдено
|
|
58
|
-
*/
|
|
59
|
-
function readRuleMdc(ruleId, violationOutput) {
|
|
60
|
-
const ruleDir = join(PACKAGE_RULES_DIR, ruleId)
|
|
61
|
-
const parts = []
|
|
62
|
-
|
|
63
|
-
// 1. js/**/*.mdc — завжди всі
|
|
64
|
-
const jsDir = join(ruleDir, 'js')
|
|
65
|
-
if (existsSync(jsDir)) {
|
|
66
|
-
let jsFiles
|
|
67
|
-
try {
|
|
68
|
-
jsFiles = readdirSync(jsDir)
|
|
69
|
-
.filter(f => f.endsWith('.mdc'))
|
|
70
|
-
.sort()
|
|
71
|
-
} catch {
|
|
72
|
-
jsFiles = []
|
|
73
|
-
}
|
|
74
|
-
for (const f of jsFiles) parts.push(readFileSync(join(jsDir, f), 'utf8').trim())
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 2. policy/**/*.mdc — matched через target.json; fallback — всі
|
|
78
|
-
const policyDir = join(ruleDir, 'policy')
|
|
79
|
-
if (existsSync(policyDir)) {
|
|
80
|
-
const failPaths = extractFailPaths(violationOutput)
|
|
81
|
-
let concerns
|
|
82
|
-
try {
|
|
83
|
-
concerns = readdirSync(policyDir, { withFileTypes: true })
|
|
84
|
-
} catch {
|
|
85
|
-
concerns = []
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const all = []
|
|
89
|
-
const matched = []
|
|
90
|
-
for (const entry of concerns) {
|
|
91
|
-
if (!entry.isDirectory()) continue
|
|
92
|
-
const concernDir = join(policyDir, entry.name)
|
|
93
|
-
const mdcEntry = readdirSync(concernDir).find(f => f.endsWith('.mdc'))
|
|
94
|
-
if (!mdcEntry) continue
|
|
95
|
-
const content = readFileSync(join(concernDir, mdcEntry), 'utf8').trim()
|
|
96
|
-
all.push(content)
|
|
97
|
-
|
|
98
|
-
const targetPath = join(concernDir, 'target.json')
|
|
99
|
-
if (!existsSync(targetPath)) continue
|
|
100
|
-
let target
|
|
101
|
-
try {
|
|
102
|
-
target = JSON.parse(readFileSync(targetPath, 'utf8'))
|
|
103
|
-
} catch {
|
|
104
|
-
continue
|
|
105
|
-
}
|
|
106
|
-
const targetFile = target?.files?.single
|
|
107
|
-
if (!targetFile) continue
|
|
108
|
-
if (failPaths.some(p => p === targetFile || p.endsWith(`/${targetFile}`))) matched.push(content)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
parts.push(...(matched.length > 0 ? matched : all))
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return parts.length > 0 ? parts.join('\n\n') : null
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Витягує відносні шляхи файлів із violation output.
|
|
119
|
-
* Розуміє workspace-prefix: `[npm] skills/foo.mjs` → `npm/skills/foo.mjs`.
|
|
120
|
-
* Спочатку явно парсить рядки ❌ (найвищий сигнал — файл потребує фіксу),
|
|
121
|
-
* потім підхоплює решту файлів generic-regex (контекст для читання).
|
|
122
|
-
* @param {string} output violation output з fix check
|
|
123
|
-
* @returns {string[]} унікальні відносні шляхи (від кореня проєкту)
|
|
124
|
-
*/
|
|
125
|
-
export function extractFilePaths(output) {
|
|
126
|
-
const seen = new Set()
|
|
127
|
-
const results = []
|
|
128
|
-
const add = p => {
|
|
129
|
-
if (!seen.has(p)) {
|
|
130
|
-
seen.add(p)
|
|
131
|
-
results.push(p)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// 1. Явні рядки ❌ — найвищий сигнал: саме ці файли потребують фіксу.
|
|
136
|
-
// Формати: `❌ [ws] path/file.ext:line — msg` та `❌ path/file.ext: msg`
|
|
137
|
-
// Роздільник після шляху: `:` (з пробілом або цифрою), `—` (em-dash), або кінець рядка.
|
|
138
|
-
const failSep = `(?::\\d+)?(?::\\s|[\\s—]|$)`
|
|
139
|
-
const failWsRe = new RegExp(`^\\s*❌\\s+\\[([\\w-]+)\\]\\s+([\\w./][\\w./-]*\\.(?:${FILE_EXTS}))${failSep}`, 'gm')
|
|
140
|
-
for (const m of output.matchAll(failWsRe)) add(`${m[1]}/${m[2]}`)
|
|
141
|
-
|
|
142
|
-
const failRe = new RegExp(`^\\s*❌\\s+(\\.?[\\w][\\w./-]*\\.(?:${FILE_EXTS}))${failSep}`, 'gm')
|
|
143
|
-
for (const m of output.matchAll(failRe)) add(m[1])
|
|
144
|
-
|
|
145
|
-
// 2. Generic-regex: підхоплює файли з ✅-рядків та описів (контекст для читання).
|
|
146
|
-
// Workspace: [npm] skills/foo.mjs
|
|
147
|
-
const wsRe = new RegExp(`\\[([\\w-]+)\\]\\s+([\\w./][\\w./-]*\\.(?:${FILE_EXTS}))(?::\\d+)?`, 'gm')
|
|
148
|
-
for (const m of output.matchAll(wsRe)) add(`${m[1]}/${m[2]}`)
|
|
149
|
-
|
|
150
|
-
// Без workspace: path/to/file.ext або ./file.ext
|
|
151
|
-
const re = new RegExp(`(?:^|\\s)(\\.?\\w[\\w./-]*\\.(?:${FILE_EXTS}))(?::\\d+)?`, 'gm')
|
|
152
|
-
for (const m of output.matchAll(re)) add(m[1])
|
|
153
|
-
|
|
154
|
-
return results
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Будує prompt для pi: правило + порушення + поточний вміст файлів.
|
|
159
|
-
* Будує опційний feedback-блок драбини ескалації: попередній рунг застосував
|
|
160
|
-
* зміни, але re-check лишився червоним. Просимо модель спершу (в полі `diagnosis`)
|
|
161
|
-
* сформулювати, **чому** попередня спроба не задовольнила правило, тоді виправити.
|
|
162
|
-
* @param {{ previousModel?: string, previousChanges?: Array<{path:string}>, previousError?: string|null } | null} feedback контекст попереднього рунга
|
|
163
|
-
* @returns {string[]} рядки prompt-блоку (порожній масив, якщо feedback немає)
|
|
164
|
-
*/
|
|
165
|
-
function buildFeedbackBlock(feedback) {
|
|
166
|
-
if (!feedback) return []
|
|
167
|
-
const changedPaths = (feedback.previousChanges ?? []).map(c => c.path).filter(Boolean)
|
|
168
|
-
return [
|
|
169
|
-
``,
|
|
170
|
-
`A PREVIOUS attempt (model: ${feedback.previousModel || 'pi'}) did NOT resolve this violation.`,
|
|
171
|
-
changedPaths.length > 0
|
|
172
|
-
? `Previously changed files: ${changedPaths.join(', ')}`
|
|
173
|
-
: `The previous attempt produced no usable changes.`,
|
|
174
|
-
feedback.previousError ? `Previous attempt error: ${feedback.previousError}` : ``,
|
|
175
|
-
`The violation output below is what STILL fails after that attempt.`,
|
|
176
|
-
`In the "diagnosis" field, briefly state WHY the previous attempt failed, then provide a corrected fix.`
|
|
177
|
-
].filter(line => line !== ``)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* @param {string} ruleId ID правила
|
|
182
|
-
* @param {string} ruleMdc вміст .mdc-файлу правила
|
|
183
|
-
* @param {string} output violation output
|
|
184
|
-
* @param {Array<{path:string, content:string}>} files прочитані файли (path + content)
|
|
185
|
-
* @param {{ previousModel?: string, previousChanges?: Array<{path:string}>, previousError?: string|null } | null} [feedback] контекст попереднього рунга драбини (для retry-with-feedback)
|
|
186
|
-
* @returns {string} текст промпта для pi
|
|
187
|
-
*/
|
|
188
|
-
function buildPrompt(ruleId, ruleMdc, output, files, feedback = null) {
|
|
189
|
-
const filesBlock =
|
|
190
|
-
files.length === 0
|
|
191
|
-
? '(no files identified)'
|
|
192
|
-
: files.map(f => `<file path="${f.path}">\n${f.content}\n</file>`).join('\n\n')
|
|
193
|
-
|
|
194
|
-
return [
|
|
195
|
-
`You fix project structure violations. Return ONLY valid JSON — no explanation, no markdown.`,
|
|
196
|
-
...buildFeedbackBlock(feedback),
|
|
197
|
-
``,
|
|
198
|
-
`Rule (n-${ruleId}.mdc):`,
|
|
199
|
-
`---`,
|
|
200
|
-
ruleMdc,
|
|
201
|
-
`---`,
|
|
202
|
-
``,
|
|
203
|
-
`Violation output:`,
|
|
204
|
-
output,
|
|
205
|
-
``,
|
|
206
|
-
`Current file contents:`,
|
|
207
|
-
filesBlock,
|
|
208
|
-
``,
|
|
209
|
-
`Return JSON with this exact shape:`,
|
|
210
|
-
`{"diagnosis":"short reason the rule fails (or why prior attempt failed); empty string if first attempt","changes":[{"path":"relative/path/to/file","content":"full corrected file content"}]}`,
|
|
211
|
-
``,
|
|
212
|
-
`Rules:`,
|
|
213
|
-
`- "path" is relative to the project root`,
|
|
214
|
-
`- "content" is the complete new file content (not a diff)`,
|
|
215
|
-
`- Only include files that actually need to change`,
|
|
216
|
-
`- "diagnosis" is plain text inside the JSON — do NOT emit prose outside the JSON`,
|
|
217
|
-
`- If nothing can be fixed automatically, return {"diagnosis":"...","changes":[],"error":"reason"}`
|
|
218
|
-
].join('\n')
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Викликає LLM через `callLlmRich` (маршрут за префіксом model-id; wire-trace).
|
|
223
|
-
* Повертає reasoning поряд із текстом — для verbose-блоку оркестратора.
|
|
224
|
-
* Зберігає дружнє повідомлення про відсутній API-ключ для хмарних провайдерів.
|
|
225
|
-
* @param {string} prompt текст промпта
|
|
226
|
-
* @param {string} model назва моделі (provider/id, `omlx/...` або '')
|
|
227
|
-
* @param {string} caller мітка викликача для wire-trace (`fix:<rule>:<rung>`)
|
|
228
|
-
* @param {number} [timeoutMs] ліміт виклику (драбина задає per-tier; undefined → дефолт callLlmRich)
|
|
229
|
-
* @param {number} [thinkingBudget] бюджет thinking-токенів (лише omlx; 0 = вимкнено)
|
|
230
|
-
* @returns {{ text: string, reasoning: string|null, reasoningSource: string|null, error?: string }}
|
|
231
|
-
*/
|
|
232
|
-
function callModel(prompt, model, caller, timeoutMs, thinkingBudget) {
|
|
233
|
-
try {
|
|
234
|
-
const { content, reasoning, reasoningSource } = callLlmRich([{ role: 'user', content: prompt }], model, {
|
|
235
|
-
timeoutMs,
|
|
236
|
-
caller,
|
|
237
|
-
thinkingBudget
|
|
238
|
-
})
|
|
239
|
-
return { text: content, reasoning, reasoningSource }
|
|
240
|
-
} catch (error) {
|
|
241
|
-
const msg = String(error.message)
|
|
242
|
-
if (API_KEY_RE.test(msg)) {
|
|
243
|
-
const provider = model ? model.split('/')[0] : 'дефолтного провайдера'
|
|
244
|
-
return {
|
|
245
|
-
text: '',
|
|
246
|
-
reasoning: null,
|
|
247
|
-
reasoningSource: null,
|
|
248
|
-
error: [
|
|
249
|
-
`pi: немає ключа для ${provider}.`,
|
|
250
|
-
`Встановіть N_CLOUD_MIN_MODEL=provider/model-id`,
|
|
251
|
-
`(напр.: openai/gpt-5.4-mini, google/gemini-2.5-flash, ollama/gemma3:4b)`
|
|
252
|
-
].join(' ')
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return { text: '', reasoning: null, reasoningSource: null, error: msg }
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* LLM-worker: виправляє одне rule-порушення через pi (C1 pattern).
|
|
261
|
-
* Повертає `changes`/`diagnosis` навіть при невдачі — драбина ескалації
|
|
262
|
-
* (`orchestrator.mjs`) логує їх і прокидає як feedback у наступний рунг.
|
|
263
|
-
* Поля `reasoning`/`reasoningSource`/`promptSummary` використовує оркестратор
|
|
264
|
-
* для verbose-блоку після кожного рунга (`--full` режим).
|
|
265
|
-
* @param {string} ruleId ID правила
|
|
266
|
-
* @param {string} violationOutput output з fix check для цього rule
|
|
267
|
-
* @param {string} projectRoot абсолютний шлях до кореня проєкту
|
|
268
|
-
* @param {{ model?: string, feedback?: object|null, caller?: string, timeoutMs?: number, thinkingBudget?: number }} opts опції:
|
|
269
|
-
* `model` — перевизначення моделі; `feedback` — контекст попереднього рунга
|
|
270
|
-
* драбини (retry-with-feedback); `caller` — мітка для wire-trace; `timeoutMs` —
|
|
271
|
-
* per-tier ліміт виклику (драбина: локалі fail-fast, хмара повний);
|
|
272
|
-
* `thinkingBudget` — кількість thinking-токенів для omlx (дефолт `DEFAULT_THINKING_BUDGET`).
|
|
273
|
-
* `timeoutMs` — per-tier ліміт: локальні 300s (4b повільна, backstop — turn-ceiling), хмарні 120s.
|
|
274
|
-
* @returns {{ ok: boolean, error?: string, changes: Array<{path:string}>, diagnosis: string|null, reasoning: string|null, reasoningSource: string|null, promptSummary: object }}
|
|
275
|
-
*/
|
|
276
|
-
export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
|
|
277
|
-
const model = opts.model ?? MODEL
|
|
278
|
-
const feedback = opts.feedback ?? null
|
|
279
|
-
const caller = opts.caller ?? 'fix'
|
|
280
|
-
const timeoutMs = opts.timeoutMs
|
|
281
|
-
const thinkingBudget = opts.thinkingBudget ?? DEFAULT_THINKING_BUDGET
|
|
282
|
-
|
|
283
|
-
// 1. Читаємо rule .mdc з джерела пакету: js/**/*.mdc + policy/**/*.mdc.
|
|
284
|
-
const ruleMdc = readRuleMdc(ruleId, violationOutput) ?? '(rule file not found)'
|
|
285
|
-
|
|
286
|
-
// 2. Витягуємо файли з violation output і читаємо їх
|
|
287
|
-
const files = readFilesForFix(extractFilePaths(violationOutput), projectRoot)
|
|
288
|
-
|
|
289
|
-
// 3. Будуємо summary промпту (для verbose-блоку) до виклику моделі
|
|
290
|
-
const promptSummary = {
|
|
291
|
-
ruleMdcLen: ruleMdc.length,
|
|
292
|
-
violationLen: violationOutput.length,
|
|
293
|
-
filesCount: files.length,
|
|
294
|
-
filesTotalBytes: files.reduce((s, f) => s + f.content.length, 0),
|
|
295
|
-
hasFeedback: !!feedback,
|
|
296
|
-
feedbackModel: feedback?.previousModel ?? null,
|
|
297
|
-
feedbackChangesCount: feedback?.previousChanges?.length ?? 0,
|
|
298
|
-
feedbackError: feedback?.previousError ?? null
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// 4. Будуємо prompt і викликаємо модель
|
|
302
|
-
const prompt = buildPrompt(ruleId, ruleMdc, violationOutput, files, feedback)
|
|
303
|
-
const {
|
|
304
|
-
text,
|
|
305
|
-
error: modelError,
|
|
306
|
-
reasoning,
|
|
307
|
-
reasoningSource
|
|
308
|
-
} = callModel(prompt, model, caller, timeoutMs, thinkingBudget)
|
|
309
|
-
|
|
310
|
-
if (modelError)
|
|
311
|
-
return { ok: false, error: modelError, changes: [], diagnosis: null, reasoning, reasoningSource, promptSummary }
|
|
312
|
-
if (!text)
|
|
313
|
-
return {
|
|
314
|
-
ok: false,
|
|
315
|
-
error: 'model returned empty response',
|
|
316
|
-
changes: [],
|
|
317
|
-
diagnosis: null,
|
|
318
|
-
reasoning,
|
|
319
|
-
reasoningSource,
|
|
320
|
-
promptSummary
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// 5. Парсимо відповідь
|
|
324
|
-
const parsed = parseChangesResponse(text)
|
|
325
|
-
if (!parsed) {
|
|
326
|
-
return {
|
|
327
|
-
ok: false,
|
|
328
|
-
error: `cannot parse pi response: ${text.slice(0, 200)}`,
|
|
329
|
-
changes: [],
|
|
330
|
-
diagnosis: null,
|
|
331
|
-
reasoning,
|
|
332
|
-
reasoningSource,
|
|
333
|
-
promptSummary
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
const diagnosis = typeof parsed.diagnosis === 'string' && parsed.diagnosis ? parsed.diagnosis : null
|
|
337
|
-
const changes = parsed.changes ?? []
|
|
338
|
-
if (parsed.error)
|
|
339
|
-
return { ok: false, error: parsed.error, changes, diagnosis, reasoning, reasoningSource, promptSummary }
|
|
340
|
-
if (changes.length === 0)
|
|
341
|
-
return { ok: false, error: 'pi returned no changes', changes, diagnosis, reasoning, reasoningSource, promptSummary }
|
|
342
|
-
|
|
343
|
-
// 6. Застосовуємо зміни
|
|
344
|
-
const applied = applyChanges(changes, projectRoot)
|
|
345
|
-
return { ...applied, changes, diagnosis, reasoning, reasoningSource, promptSummary }
|
|
346
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verbose-блок після кожного LLM-рунга у `--full` режимі.
|
|
3
|
-
* Друкує стислий опис промпту і thinking-монолог моделі (якщо є).
|
|
4
|
-
* Вимикається через `N_CURSOR_FIX_VERBOSE=off`.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const THINKING_PREVIEW_LEN = 500
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Форматує рядок файлів для prompt-блоку.
|
|
11
|
-
* @param {number} count кількість файлів
|
|
12
|
-
* @param {number} totalBytes сумарний розмір у байтах
|
|
13
|
-
* @returns {string}
|
|
14
|
-
*/
|
|
15
|
-
function formatFiles(count, totalBytes) {
|
|
16
|
-
if (count === 0) return '(none)'
|
|
17
|
-
const kb = (totalBytes / 1024).toFixed(1)
|
|
18
|
-
const word = count === 1 ? 'файл' : count < 5 ? 'файли' : 'файлів'
|
|
19
|
-
return `${count} ${word} (${kb} KB)`
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Форматує рядок feedback для prompt-блоку.
|
|
24
|
-
* @param {boolean} hasFeedback чи є feedback
|
|
25
|
-
* @param {string|null} feedbackModel модель попереднього рунга
|
|
26
|
-
* @param {number} feedbackChangesCount кількість змін попереднього рунга
|
|
27
|
-
* @param {string|null} feedbackError помилка попереднього рунга
|
|
28
|
-
* @returns {string}
|
|
29
|
-
*/
|
|
30
|
-
function formatFeedback(hasFeedback, feedbackModel, feedbackChangesCount, feedbackError) {
|
|
31
|
-
if (!hasFeedback) return '(none)'
|
|
32
|
-
const parts = []
|
|
33
|
-
if (feedbackModel) parts.push(`model=${feedbackModel}`)
|
|
34
|
-
parts.push(`${feedbackChangesCount} changes`)
|
|
35
|
-
if (feedbackError) parts.push(`error="${feedbackError.slice(0, 60)}"`)
|
|
36
|
-
return parts.join(', ')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Друкує verbose-блок після рядка рунга (`⚡`/`✅`).
|
|
41
|
-
* Містить стислий опис промпту і thinking-монолог моделі (якщо є).
|
|
42
|
-
* Не викликається якщо `N_CURSOR_FIX_VERBOSE=off`.
|
|
43
|
-
* @param {string} ruleId ID правила
|
|
44
|
-
* @param {{ ruleMdcLen: number, violationLen: number, filesCount: number, filesTotalBytes: number, hasFeedback: boolean, feedbackModel: string|null, feedbackChangesCount: number, feedbackError: string|null }} promptSummary стислий опис промпту
|
|
45
|
-
* @param {string|null} reasoning thinking-монолог моделі
|
|
46
|
-
* @param {string|null} reasoningSource джерело reasoning ('field'|'think_tag'|'truncated'|null)
|
|
47
|
-
*/
|
|
48
|
-
export function printVerboseBlock(ruleId, promptSummary, reasoning, reasoningSource) {
|
|
49
|
-
const {
|
|
50
|
-
ruleMdcLen,
|
|
51
|
-
violationLen,
|
|
52
|
-
filesCount,
|
|
53
|
-
filesTotalBytes,
|
|
54
|
-
hasFeedback,
|
|
55
|
-
feedbackModel,
|
|
56
|
-
feedbackChangesCount,
|
|
57
|
-
feedbackError
|
|
58
|
-
} = promptSummary
|
|
59
|
-
|
|
60
|
-
console.log(``)
|
|
61
|
-
console.log(` prompt:`)
|
|
62
|
-
console.log(` rule: n-${ruleId}.mdc (${ruleMdcLen} chars)`)
|
|
63
|
-
console.log(` violation: ${violationLen} chars`)
|
|
64
|
-
console.log(` files: ${formatFiles(filesCount, filesTotalBytes)}`)
|
|
65
|
-
console.log(` feedback: ${formatFeedback(hasFeedback, feedbackModel, feedbackChangesCount, feedbackError)}`)
|
|
66
|
-
|
|
67
|
-
if (reasoning) {
|
|
68
|
-
const preview =
|
|
69
|
-
reasoning.length > THINKING_PREVIEW_LEN
|
|
70
|
-
? reasoning.slice(0, THINKING_PREVIEW_LEN) + ` … (+${reasoning.length - THINKING_PREVIEW_LEN} chars)`
|
|
71
|
-
: reasoning
|
|
72
|
-
console.log(``)
|
|
73
|
-
console.log(` thinking [${reasoningSource}, ${reasoning.length} chars]:`)
|
|
74
|
-
for (const line of preview.split('\n')) {
|
|
75
|
-
console.log(` ${line}`)
|
|
76
|
-
}
|
|
77
|
-
} else {
|
|
78
|
-
console.log(``)
|
|
79
|
-
console.log(` thinking: (none)`)
|
|
80
|
-
}
|
|
81
|
-
console.log(``)
|
|
82
|
-
}
|