@nitra/cursor 9.2.0 → 9.3.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
|
+
## [9.3.0] - 2026-06-14
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- per-tool omlx-фіксер (point 4): cspell autofix одруків через omlx у fix-режимі lint-text; спільне ядро llm-fix-apply (parse/read/apply, перевикористане llm-worker); generic llm-lint-fix; re-detect лишає валідні терміни для словника. Відновлено канон lint-text.yml --read-only (відкочений паралельним агентом)
|
|
8
|
+
|
|
3
9
|
## [9.2.0] - 2026-06-14
|
|
4
10
|
|
|
5
11
|
### Changed
|
package/package.json
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cspell у ланцюжку lint-text із omlx-автофіксом (point 4 спеки).
|
|
3
|
+
*
|
|
4
|
+
* cspell не має нативного `--fix`. У fix-режимі: детект (захоплення виводу) → групування
|
|
5
|
+
* знахідок по файлах → per-file omlx-фікс справжніх одруків (`llmLintFix`) → re-detect.
|
|
6
|
+
* У read-only: лише детект (нуль мутацій). Валідні терміни omlx лишає — їх ловить повторний
|
|
7
|
+
* cspell (далі — у словник `@nitra/cspell-dict`).
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
|
|
11
|
+
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
12
|
+
import { llmLintFix } from '../../../scripts/lib/fix/llm-lint-fix.mjs'
|
|
13
|
+
|
|
14
|
+
/** Рядок cspell: `<file>:<line>:<col> - Unknown word (xxx)`. */
|
|
15
|
+
const CSPELL_LINE_RE = /^(.+?):\d+:\d+\s+-\s+Unknown word/u
|
|
16
|
+
/** Максимум файлів під omlx-фікс за прогін (без тихого обрізання — логуємо надлишок). */
|
|
17
|
+
const MAX_FIX_FILES = 25
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Запускає `cspell .` із захопленням виводу.
|
|
21
|
+
* @param {string} cwd корінь
|
|
22
|
+
* @param {string} bin шлях до cspell (npx/локальний)
|
|
23
|
+
* @returns {{ code:number, out:string }} код + обʼєднаний stdout/stderr
|
|
24
|
+
*/
|
|
25
|
+
function detectCspell(cwd, bin) {
|
|
26
|
+
const r = spawnSync(bin, ['cspell', '.'], { cwd, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, env: process.env })
|
|
27
|
+
return { code: typeof r.status === 'number' ? r.status : 1, out: `${r.stdout ?? ''}${r.stderr ?? ''}` }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Групує cspell-знахідки за файлом.
|
|
32
|
+
* @param {string} out вивід cspell
|
|
33
|
+
* @returns {Map<string, string[]>} файл → рядки знахідок
|
|
34
|
+
*/
|
|
35
|
+
export function groupFindingsByFile(out) {
|
|
36
|
+
/** @type {Map<string, string[]>} */
|
|
37
|
+
const byFile = new Map()
|
|
38
|
+
for (const line of out.split('\n')) {
|
|
39
|
+
const m = CSPELL_LINE_RE.exec(line.trim())
|
|
40
|
+
if (!m) continue
|
|
41
|
+
const file = m[1]
|
|
42
|
+
if (!byFile.has(file)) byFile.set(file, [])
|
|
43
|
+
byFile.get(file).push(line.trim())
|
|
44
|
+
}
|
|
45
|
+
return byFile
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const CSPELL_INSTRUCTION = [
|
|
49
|
+
'Correct genuine spelling typos in the file(s).',
|
|
50
|
+
'Each flagged "Unknown word" is listed below.',
|
|
51
|
+
'ONLY fix obvious misspellings of real words.',
|
|
52
|
+
'If a flagged token is a valid identifier, technical term, abbreviation, proper noun, URL,',
|
|
53
|
+
'or an intentional non-English word, leave it UNCHANGED (it will be added to the dictionary).',
|
|
54
|
+
'Preserve all code, formatting, and unrelated text exactly.'
|
|
55
|
+
].join(' ')
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* cspell-крок lint-text з omlx-автофіксом.
|
|
59
|
+
* @param {string} [cwd] корінь
|
|
60
|
+
* @param {boolean} [readOnly] true → лише детект (нуль мутацій)
|
|
61
|
+
* @returns {number} 0 — чисто; 1 — лишились знахідки / помилка середовища
|
|
62
|
+
*/
|
|
63
|
+
export function runCspellText(cwd = process.cwd(), readOnly = false) {
|
|
64
|
+
const bin = resolveCmd('npx')
|
|
65
|
+
if (!bin) {
|
|
66
|
+
process.stderr.write('❌ npx не знайдено в PATH (cspell).\n')
|
|
67
|
+
return 1
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const first = detectCspell(cwd, bin)
|
|
71
|
+
if (first.code === 0) return 0
|
|
72
|
+
if (readOnly) {
|
|
73
|
+
process.stdout.write(first.out)
|
|
74
|
+
return first.code
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fix-режим: omlx по файлах зі справжніми одруками.
|
|
78
|
+
const byFile = groupFindingsByFile(first.out)
|
|
79
|
+
const files = [...byFile.keys()]
|
|
80
|
+
if (files.length === 0) {
|
|
81
|
+
process.stdout.write(first.out)
|
|
82
|
+
return first.code
|
|
83
|
+
}
|
|
84
|
+
const targets = files.slice(0, MAX_FIX_FILES)
|
|
85
|
+
if (files.length > MAX_FIX_FILES) {
|
|
86
|
+
process.stdout.write(`ℹ️ cspell: omlx-фікс перших ${MAX_FIX_FILES}/${files.length} файлів (решта — наступний прогін)\n`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const file of targets) {
|
|
90
|
+
const res = llmLintFix({
|
|
91
|
+
tool: 'cspell',
|
|
92
|
+
instruction: CSPELL_INSTRUCTION,
|
|
93
|
+
findings: byFile.get(file).join('\n'),
|
|
94
|
+
filePaths: [file],
|
|
95
|
+
projectRoot: cwd
|
|
96
|
+
})
|
|
97
|
+
process.stdout.write(res.ok ? ` ⚡ cspell omlx-фікс: ${file}\n` : ` ⚠️ cspell omlx-фікс пропущено (${file}): ${res.error}\n`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Re-detect: що лишилось (валідні терміни → у словник).
|
|
101
|
+
const second = detectCspell(cwd, bin)
|
|
102
|
+
if (second.code !== 0) process.stdout.write(second.out)
|
|
103
|
+
return second.code
|
|
104
|
+
}
|
package/rules/text/lint/lint.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { runLintStep } from '../../../scripts/lib/run-lint-step.mjs'
|
|
|
24
24
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
25
25
|
import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
|
|
26
26
|
import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
|
|
27
|
+
import { runCspellText } from './cspell-fix.mjs'
|
|
27
28
|
import { runDotenvLinter } from './run-dotenv-linter.mjs'
|
|
28
29
|
import { runShellcheckText } from './run-shellcheck.mjs'
|
|
29
30
|
import { runV8rWithGlobs } from './run-v8r.mjs'
|
|
@@ -106,7 +107,8 @@ function runLintTextSteps(readOnly = false) {
|
|
|
106
107
|
// patch потрібен лише для авто-фіксу shellcheck; у read-only пропускаємо preflight.
|
|
107
108
|
if (!readOnly && !preflight(PATCH_PREFLIGHT)) return 1
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
console.log(`\n▶ cspell (${readOnly ? 'перевірка' : 'omlx-автофікс одруків + перевірка'})`)
|
|
111
|
+
const cspellCode = runCspellText(process.cwd(), readOnly)
|
|
110
112
|
if (cspellCode !== 0) return cspellCode
|
|
111
113
|
|
|
112
114
|
console.log(`\n▶ shellcheck (${readOnly ? 'перевірка' : 'авто-фікс + фінальна перевірка'} *.sh)`)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Спільне ядро LLM-фіксу: парс відповіді `{changes:[{path,content}]}`, читання файлів
|
|
3
|
+
* під фікс і застосування змін. Використовують і `llm-worker.mjs` (конформність), і
|
|
4
|
+
* `llm-lint-fix.mjs` (per-tool лінтер-фіксери) — щоб не дублювати парс/apply (knip/jscpd).
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Парсить JSON-відповідь моделі: прямий JSON → ```json-блок``` → перший `{…}`-блок.
|
|
13
|
+
* @param {string} text сирий текст відповіді
|
|
14
|
+
* @returns {{ changes?: Array<{path:string,content:string}>, error?: string } | null} патч або null
|
|
15
|
+
*/
|
|
16
|
+
export function parseChangesResponse(text) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(text)
|
|
19
|
+
} catch {
|
|
20
|
+
/* fallthrough */
|
|
21
|
+
}
|
|
22
|
+
const block = text.match(JSON_CODE_BLOCK_RE)
|
|
23
|
+
if (block) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(block[1].trim())
|
|
26
|
+
} catch {
|
|
27
|
+
/* fallthrough */
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const start = text.indexOf('{')
|
|
31
|
+
const end = text.lastIndexOf('}')
|
|
32
|
+
if (start !== -1 && end > start) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(text.slice(start, end + 1))
|
|
35
|
+
} catch {
|
|
36
|
+
/* fallthrough */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Читає існуючі файли за відносними шляхами у форму `{path, content}` (для prompt).
|
|
44
|
+
* @param {string[]} filePaths відносні шляхи від кореня
|
|
45
|
+
* @param {string} projectRoot абсолютний корінь
|
|
46
|
+
* @returns {Array<{path:string, content:string}>} наявні файли з вмістом
|
|
47
|
+
*/
|
|
48
|
+
export function readFilesForFix(filePaths, projectRoot) {
|
|
49
|
+
return filePaths
|
|
50
|
+
.map(p => {
|
|
51
|
+
const abs = join(projectRoot, p)
|
|
52
|
+
if (!existsSync(abs)) return null
|
|
53
|
+
try {
|
|
54
|
+
return { path: p, content: readFileSync(abs, 'utf8') }
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Застосовує `changes` до ФС (повний вміст файлу, не diff).
|
|
64
|
+
* @param {Array<{path:string, content:string}>} changes зміни
|
|
65
|
+
* @param {string} projectRoot абсолютний корінь
|
|
66
|
+
* @returns {{ ok: boolean, error?: string }} статус
|
|
67
|
+
*/
|
|
68
|
+
export function applyChanges(changes, projectRoot) {
|
|
69
|
+
for (const change of changes) {
|
|
70
|
+
if (!change.path || typeof change.content !== 'string') continue
|
|
71
|
+
try {
|
|
72
|
+
writeFileSync(join(projectRoot, change.path), change.content, 'utf8')
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return { ok: false, error: `write ${change.path}: ${error.message}` }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { ok: true }
|
|
78
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool omlx-фікс лінтер-знахідок (point 4 спеки lint-orchestrator-fix-readonly).
|
|
3
|
+
*
|
|
4
|
+
* Для detect-only тулів без нативного `--fix` (cspell, knip, actionlint, v8r тощо): читає
|
|
5
|
+
* уражені файли, просить omlx виправити за tool-специфічною інструкцією, застосовує `{changes}`.
|
|
6
|
+
* Re-detect (перевірка, що знахідка закрита) — на стороні caller (convergence-патерн).
|
|
7
|
+
*
|
|
8
|
+
* Маршрут моделі — через `callLlm` за префіксом: `omlx/<model>` → локальний HTTP (дефолт
|
|
9
|
+
* `resolveModel('min')`); cloud — фолбек каскаду. Парс/застосування — спільне ядро `llm-fix-apply`.
|
|
10
|
+
*/
|
|
11
|
+
import { env } from 'node:process'
|
|
12
|
+
|
|
13
|
+
import { resolveModel } from '../../../lib/models.mjs'
|
|
14
|
+
import { callLlm } from '../../../lib/llm.mjs'
|
|
15
|
+
import { applyChanges, parseChangesResponse, readFilesForFix } from './llm-fix-apply.mjs'
|
|
16
|
+
|
|
17
|
+
/** Дефолтний локальний тир (omlx); env `N_CURSOR_FIX_MODEL` перекриває. */
|
|
18
|
+
const DEFAULT_MODEL = env.N_CURSOR_FIX_MODEL ?? resolveModel('min')
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Будує prompt для omlx: tool-інструкція + знахідки + повний вміст файлів.
|
|
22
|
+
* @param {string} tool назва тула (cspell/knip/…)
|
|
23
|
+
* @param {string} instruction що саме виправити (tool-специфічно)
|
|
24
|
+
* @param {string} findings сирий вивід тула (знахідки)
|
|
25
|
+
* @param {Array<{path:string, content:string}>} files файли під фікс
|
|
26
|
+
* @returns {string} prompt
|
|
27
|
+
*/
|
|
28
|
+
function buildLintFixPrompt(tool, instruction, findings, files) {
|
|
29
|
+
const filesBlock = files.map(f => `<file path="${f.path}">\n${f.content}\n</file>`).join('\n\n')
|
|
30
|
+
return [
|
|
31
|
+
`You fix ${tool} lint findings. Return ONLY valid JSON — no explanation, no markdown.`,
|
|
32
|
+
``,
|
|
33
|
+
`Task: ${instruction}`,
|
|
34
|
+
``,
|
|
35
|
+
`${tool} findings:`,
|
|
36
|
+
findings,
|
|
37
|
+
``,
|
|
38
|
+
`Current file contents:`,
|
|
39
|
+
filesBlock,
|
|
40
|
+
``,
|
|
41
|
+
`Return JSON with this exact shape:`,
|
|
42
|
+
`{"changes":[{"path":"relative/path","content":"full corrected file content"}]}`,
|
|
43
|
+
``,
|
|
44
|
+
`Rules:`,
|
|
45
|
+
`- "path" is relative to the project root (use the path from the <file> tag)`,
|
|
46
|
+
`- "content" is the COMPLETE new file content (not a diff)`,
|
|
47
|
+
`- Only include files that actually need to change; preserve everything unrelated verbatim`,
|
|
48
|
+
`- If nothing should be auto-fixed, return {"changes":[],"error":"reason"}`
|
|
49
|
+
].join('\n')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Виправляє лінтер-знахідки через omlx і застосовує зміни.
|
|
54
|
+
* @param {{ tool:string, instruction:string, findings:string, filePaths:string[], projectRoot:string, model?:string }} opts параметри
|
|
55
|
+
* @returns {{ ok:boolean, error?:string, fixed:string[] }} статус + список змінених шляхів
|
|
56
|
+
*/
|
|
57
|
+
export function llmLintFix({ tool, instruction, findings, filePaths, projectRoot, model }) {
|
|
58
|
+
const m = model ?? DEFAULT_MODEL
|
|
59
|
+
const files = readFilesForFix(filePaths, projectRoot)
|
|
60
|
+
if (files.length === 0) return { ok: false, error: 'no readable files to fix', fixed: [] }
|
|
61
|
+
|
|
62
|
+
let text
|
|
63
|
+
try {
|
|
64
|
+
text = callLlm([{ role: 'user', content: buildLintFixPrompt(tool, instruction, findings, files) }], m, {
|
|
65
|
+
timeoutMs: 120_000,
|
|
66
|
+
caller: `lint:${tool}`
|
|
67
|
+
})
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { ok: false, error: String(error.message), fixed: [] }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const parsed = parseChangesResponse(text)
|
|
73
|
+
if (!parsed) return { ok: false, error: `cannot parse omlx response: ${String(text).slice(0, 200)}`, fixed: [] }
|
|
74
|
+
if (parsed.error) return { ok: false, error: parsed.error, fixed: [] }
|
|
75
|
+
|
|
76
|
+
const changes = (parsed.changes ?? []).filter(c => c.path && typeof c.content === 'string')
|
|
77
|
+
if (changes.length === 0) return { ok: false, error: 'omlx returned no changes', fixed: [] }
|
|
78
|
+
|
|
79
|
+
const applied = applyChanges(changes, projectRoot)
|
|
80
|
+
if (!applied.ok) return { ok: false, error: applied.error, fixed: [] }
|
|
81
|
+
return { ok: true, fixed: changes.map(c => c.path) }
|
|
82
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/** @see ./docs/llm-worker.md */
|
|
2
2
|
|
|
3
|
-
import { existsSync, readFileSync
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { env } from 'node:process'
|
|
6
6
|
import { resolveModel } from '../../../lib/models.mjs'
|
|
7
7
|
import { callLlm } from '../../../lib/llm.mjs'
|
|
8
|
+
import { applyChanges, parseChangesResponse, readFilesForFix } from './llm-fix-apply.mjs'
|
|
8
9
|
|
|
9
10
|
// Тир за замовчуванням: min → avg при ескалації (каскад local→cloud).
|
|
10
11
|
// Перевизначення через N_CURSOR_FIX_MODEL / N_CURSOR_FIX_MODEL_HEAVY.
|
|
11
12
|
export const MODEL = env.N_CURSOR_FIX_MODEL ?? resolveModel('min')
|
|
12
13
|
export const MODEL_HEAVY = env.N_CURSOR_FIX_MODEL_HEAVY ?? resolveModel('avg')
|
|
13
14
|
|
|
14
|
-
const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
|
|
15
15
|
const API_KEY_RE = /api key/i
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -113,44 +113,6 @@ function callModel(prompt, model) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
/**
|
|
117
|
-
* Парсить JSON-відповідь від моделі.
|
|
118
|
-
* Модель може обгорнути JSON у ```json ... ```, тому пробуємо витягти.
|
|
119
|
-
* @param {string} text сирий текст відповіді
|
|
120
|
-
* @returns {{ changes: Array<{path:string,content:string}>, error?: string } | null} розпарсений патч або null
|
|
121
|
-
*/
|
|
122
|
-
function parseResponse(text) {
|
|
123
|
-
// Спроба 1: прямий JSON
|
|
124
|
-
try {
|
|
125
|
-
return JSON.parse(text)
|
|
126
|
-
} catch {
|
|
127
|
-
/* fallthrough */
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Спроба 2: витягти з ```json ... ```
|
|
131
|
-
const m = text.match(JSON_CODE_BLOCK_RE)
|
|
132
|
-
if (m) {
|
|
133
|
-
try {
|
|
134
|
-
return JSON.parse(m[1].trim())
|
|
135
|
-
} catch {
|
|
136
|
-
/* fallthrough */
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Спроба 3: перший { ... } блок
|
|
141
|
-
const start = text.indexOf('{')
|
|
142
|
-
const end = text.lastIndexOf('}')
|
|
143
|
-
if (start !== -1 && end > start) {
|
|
144
|
-
try {
|
|
145
|
-
return JSON.parse(text.slice(start, end + 1))
|
|
146
|
-
} catch {
|
|
147
|
-
/* fallthrough */
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return null
|
|
152
|
-
}
|
|
153
|
-
|
|
154
116
|
/**
|
|
155
117
|
* LLM-worker: виправляє одне rule-порушення через pi (C1 pattern).
|
|
156
118
|
* @param {string} ruleId ID правила
|
|
@@ -167,18 +129,7 @@ export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
|
|
|
167
129
|
const ruleMdc = existsSync(mdcPath) ? readFileSync(mdcPath, 'utf8') : '(rule file not found)'
|
|
168
130
|
|
|
169
131
|
// 2. Витягуємо файли з violation output і читаємо їх
|
|
170
|
-
const
|
|
171
|
-
const files = filePaths
|
|
172
|
-
.map(p => {
|
|
173
|
-
const abs = join(projectRoot, p)
|
|
174
|
-
if (!existsSync(abs)) return null
|
|
175
|
-
try {
|
|
176
|
-
return { path: p, content: readFileSync(abs, 'utf8') }
|
|
177
|
-
} catch {
|
|
178
|
-
return null
|
|
179
|
-
}
|
|
180
|
-
})
|
|
181
|
-
.filter(Boolean)
|
|
132
|
+
const files = readFilesForFix(extractFilePaths(violationOutput), projectRoot)
|
|
182
133
|
|
|
183
134
|
// 3. Будуємо prompt і викликаємо модель
|
|
184
135
|
const prompt = buildPrompt(ruleId, ruleMdc, violationOutput, files)
|
|
@@ -188,7 +139,7 @@ export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
|
|
|
188
139
|
if (!text) return { ok: false, error: 'model returned empty response' }
|
|
189
140
|
|
|
190
141
|
// 4. Парсимо відповідь
|
|
191
|
-
const parsed =
|
|
142
|
+
const parsed = parseChangesResponse(text)
|
|
192
143
|
if (!parsed) return { ok: false, error: `cannot parse pi response: ${text.slice(0, 200)}` }
|
|
193
144
|
if (parsed.error) return { ok: false, error: parsed.error }
|
|
194
145
|
|
|
@@ -196,15 +147,5 @@ export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
|
|
|
196
147
|
if (changes.length === 0) return { ok: false, error: 'pi returned no changes' }
|
|
197
148
|
|
|
198
149
|
// 5. Застосовуємо зміни
|
|
199
|
-
|
|
200
|
-
if (!change.path || typeof change.content !== 'string') continue
|
|
201
|
-
const abs = join(projectRoot, change.path)
|
|
202
|
-
try {
|
|
203
|
-
writeFileSync(abs, change.content, 'utf8')
|
|
204
|
-
} catch (error) {
|
|
205
|
-
return { ok: false, error: `write ${change.path}: ${error.message}` }
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return { ok: true }
|
|
150
|
+
return applyChanges(changes, projectRoot)
|
|
210
151
|
}
|