@nitra/cursor 12.15.0 → 12.16.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 +16 -0
- package/bin/n-cursor.js +2 -11
- 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/bun/docs/main.md +7 -6
- 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/python/docs/main.md +11 -11
- package/rules/rust/docs/main.md +5 -5
- 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/docs/update-blue-oak.md +8 -8
- 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/discover-checkable-rules.md +6 -6
- package/scripts/lib/docs/inline-template-links.md +8 -6
- package/scripts/lib/docs/list-project-rules-mdc.md +5 -3
- package/scripts/lib/docs/root-notice.md +13 -16
- package/scripts/lib/docs/run-lint.md +10 -8
- package/scripts/lib/docs/skill-meta.md +29 -10
- package/scripts/lib/fix/docs/discover-t0-patterns.md +10 -13
- package/scripts/lib/fix/docs/escalation-log.md +10 -9
- package/scripts/lib/fix/docs/index.md +0 -1
- package/scripts/lib/fix/docs/orchestrator.md +15 -13
- package/scripts/lib/fix/escalation-log.mjs +1 -1
- package/scripts/lib/fix/orchestrator.mjs +67 -32
- package/scripts/lib/run-lint.mjs +2 -10
- 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/scripts/utils/docs/walkDir.md +17 -20
- 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/analyze-escalation.mjs +0 -353
- package/scripts/lib/fix/docs/analyze-escalation.md +0 -44
- 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 -33
- 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 -332
- package/scripts/lib/fix/verbose-block.mjs +0 -82
package/lib/omlx.mjs
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Спільний транспорт до локального omlx-сервера (OpenAI-сумісний MLX,
|
|
3
|
-
* `http://localhost:8000/v1/chat/completions`). Text-only: жодних `tools`/
|
|
4
|
-
* `tool_calls` — сервер їх не підтримує (див. ADR
|
|
5
|
-
* `260610-1349-агентна-пастка-js-owned-loop-через-omlx-замість-pi-tool-loop`).
|
|
6
|
-
*
|
|
7
|
-
* Маршрутизація між omlx і pi — за конвенцією префікса в model-id:
|
|
8
|
-
* `omlx/<model>` → прямий HTTP до omlx (локальний inference, без pi)
|
|
9
|
-
* будь-що інше → pi CLI (хмарні провайдери або pi-дефолт)
|
|
10
|
-
*
|
|
11
|
-
* Так `resolveModel(tier)` лишається незмінним: достатньо виставити локальний
|
|
12
|
-
* тир у форматі `N_LOCAL_MIN_MODEL=omlx/mlx-community--gemma-4-e2b-it-4bit`, і
|
|
13
|
-
* виклик сам піде напряму в omlx замість pi.
|
|
14
|
-
*
|
|
15
|
-
* Auth: якщо в omlx увімкнено API-ключ, він резолвиться через
|
|
16
|
-
* `resolveOmlxApiKey` (opts → `N_CURSOR_OMLX_KEY` → `~/.omlx/settings.json`)
|
|
17
|
-
* і шлеться як `Authorization: Bearer …`.
|
|
18
|
-
*/
|
|
19
|
-
import { spawnSync } from 'node:child_process'
|
|
20
|
-
import { readFileSync } from 'node:fs'
|
|
21
|
-
import { homedir } from 'node:os'
|
|
22
|
-
import { join } from 'node:path'
|
|
23
|
-
import { env } from 'node:process'
|
|
24
|
-
|
|
25
|
-
/** Дефолтний endpoint omlx (override — `N_CURSOR_OMLX_URL`). */
|
|
26
|
-
export const DEFAULT_OMLX_URL = 'http://127.0.0.1:8000/v1/chat/completions'
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* API-ключ для omlx-сервера, коли в ньому ввімкнено auth
|
|
30
|
-
* (`~/.omlx/settings.json` → `auth.skip_api_key_verification: false`).
|
|
31
|
-
* Порядок: явний `apiKey` → env `N_CURSOR_OMLX_KEY` → `auth.api_key` із
|
|
32
|
-
* локального `~/.omlx/settings.json` (zero-config для власної машини; читання
|
|
33
|
-
* fail-safe) → `null` (заголовок не шлеться).
|
|
34
|
-
* @param {string} [apiKey] явний ключ із opts виклику
|
|
35
|
-
* @returns {string|null} ключ для `Authorization: Bearer …` або null
|
|
36
|
-
*/
|
|
37
|
-
export function resolveOmlxApiKey(apiKey) {
|
|
38
|
-
if (apiKey) return apiKey
|
|
39
|
-
if (env.N_CURSOR_OMLX_KEY) return env.N_CURSOR_OMLX_KEY
|
|
40
|
-
try {
|
|
41
|
-
const settings = JSON.parse(readFileSync(join(homedir(), '.omlx', 'settings.json'), 'utf8'))
|
|
42
|
-
return settings?.auth?.api_key || null
|
|
43
|
-
} catch {
|
|
44
|
-
return null
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const OMLX_PREFIX = 'omlx/'
|
|
49
|
-
|
|
50
|
-
/** Backoff між transient-ретраями curl (мс): 2 паузи на 3 спроби. */
|
|
51
|
-
const BACKOFF_MS = [2000, 8000]
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Блокуюча пауза без зайнятого циклу (sync — для retry-loop у `callOmlxRaw`).
|
|
55
|
-
* @param {number} ms тривалість паузи
|
|
56
|
-
* @returns {void}
|
|
57
|
-
*/
|
|
58
|
-
function sleepSync(ms) {
|
|
59
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Чи цей model-id адресує локальний omlx-бекенд (префікс `omlx/`).
|
|
64
|
-
* @param {unknown} model перевірюваний model-id
|
|
65
|
-
* @returns {boolean} true, якщо рядок починається з `omlx/`
|
|
66
|
-
*/
|
|
67
|
-
export function isOmlxModel(model) {
|
|
68
|
-
return typeof model === 'string' && model.startsWith(OMLX_PREFIX)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Прибирає `omlx/`-префікс → чистий model-id для omlx API.
|
|
73
|
-
* Не-omlx-рядки повертає без змін.
|
|
74
|
-
* @param {string} model model-id (можливо з префіксом)
|
|
75
|
-
* @returns {string} model-id без `omlx/`
|
|
76
|
-
*/
|
|
77
|
-
export function omlxModelId(model) {
|
|
78
|
-
return isOmlxModel(model) ? model.slice(OMLX_PREFIX.length) : model
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const THINK_TAG_RE = /<think>([\s\S]*?)<\/think>/
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Витягує reasoning (думки моделі) з omlx-`message`. Джерела за пріоритетом:
|
|
85
|
-
* - `field` — окреме поле `message.reasoning_content` (Qwen3-Thinking тощо);
|
|
86
|
-
* - `think_tag` — `<think>…</think>` усередині `content` (інші thinking-моделі);
|
|
87
|
-
* - `truncated` — `finish_reason: "length"` зрізав думку в `content` до закриття
|
|
88
|
-
* тега → сирий reasoning лишився в `content` без `</think>`;
|
|
89
|
-
* - `null` — reasoning немає (не-thinking модель).
|
|
90
|
-
* @param {{content?:string, reasoning_content?:string}} message обʼєкт `choices[0].message`
|
|
91
|
-
* @param {string|null} finishReason `choices[0].finish_reason`
|
|
92
|
-
* @returns {{ reasoning: string|null, reasoningSource: 'field'|'think_tag'|'truncated'|null }} текст думок і його джерело
|
|
93
|
-
*/
|
|
94
|
-
export function extractReasoning(message, finishReason) {
|
|
95
|
-
const field = message?.reasoning_content
|
|
96
|
-
if (field && field.trim()) return { reasoning: field, reasoningSource: 'field' }
|
|
97
|
-
const content = message?.content ?? ''
|
|
98
|
-
const m = content.match(THINK_TAG_RE)
|
|
99
|
-
if (m) return { reasoning: m[1].trim(), reasoningSource: 'think_tag' }
|
|
100
|
-
if (finishReason === 'length' && content.trim()) return { reasoning: content, reasoningSource: 'truncated' }
|
|
101
|
-
return { reasoning: null, reasoningSource: null }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Парсить успішну (curl-exit 0) omlx-відповідь у багатий обʼєкт.
|
|
106
|
-
* @param {string} stdout сире тіло відповіді curl
|
|
107
|
-
* @param {number} attempt номер успішної спроби (для поля `attempts`)
|
|
108
|
-
* @returns {{ content:string, reasoning:string|null, reasoningSource:string|null, finishReason:string|null, usage:object|null, attempts:number }} багатий результат
|
|
109
|
-
* @throws {Error} на поганому JSON, api-помилці чи порожньому контенті
|
|
110
|
-
*/
|
|
111
|
-
function parseOmlxResponse(stdout, attempt) {
|
|
112
|
-
let j
|
|
113
|
-
try {
|
|
114
|
-
j = JSON.parse(stdout)
|
|
115
|
-
} catch {
|
|
116
|
-
throw new Error(`omlx bad json: ${stdout?.slice(0, 200) ?? ''}`)
|
|
117
|
-
}
|
|
118
|
-
if (j.error) throw new Error(`omlx api: ${JSON.stringify(j.error).slice(0, 300)}`)
|
|
119
|
-
const message = j.choices?.[0]?.message ?? {}
|
|
120
|
-
const finishReason = j.choices?.[0]?.finish_reason ?? null
|
|
121
|
-
const content = message.content?.trim() ?? ''
|
|
122
|
-
if (!content) throw new Error(`omlx empty content (finish=${finishReason})`)
|
|
123
|
-
const { reasoning, reasoningSource } = extractReasoning(message, finishReason)
|
|
124
|
-
return { content, reasoning, reasoningSource, finishReason, usage: j.usage ?? null, attempts: attempt }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Ядро прямого HTTP-виклику до omlx через `curl` (spawnSync). Повертає **багатий**
|
|
129
|
-
* обʼєкт: контент + reasoning + usage + finish_reason + кількість спроб. Ретраїть
|
|
130
|
-
* transient-помилки (curl 18/28/52/56 + spawnSync ETIMEDOUT) із backoff 2s→8s.
|
|
131
|
-
* @param {Array<{role:string, content:string}>} messages OpenAI-messages (system+user збережено)
|
|
132
|
-
* @param {string} model model-id (з/без `omlx/`-префікса); порожній і без `fallbackModel` → throw
|
|
133
|
-
* @param {{ url?: string, timeoutMs?: number, temperature?: number, maxTokens?: number, fallbackModel?: string, apiKey?: string, backoffMs?: number[], thinkingBudget?: number }} [opts] URL, timeout, температура, ліміт виходу, fallback-модель, API-ключ, backoff між ретраями (мс), бюджет thinking-токенів (0 = вимкнено)
|
|
134
|
-
* @returns {{ content:string, reasoning:string|null, reasoningSource:string|null, finishReason:string|null, usage:object|null, attempts:number }} багатий результат виклику
|
|
135
|
-
* @throws {Error} на curl-помилці, не-200 exit, поганому JSON чи порожньому контенті
|
|
136
|
-
*/
|
|
137
|
-
export function callOmlxRaw(messages, model, opts = {}) {
|
|
138
|
-
const {
|
|
139
|
-
url = env.N_CURSOR_OMLX_URL ?? DEFAULT_OMLX_URL,
|
|
140
|
-
timeoutMs = 60_000,
|
|
141
|
-
temperature = 0.2,
|
|
142
|
-
maxTokens = 4096,
|
|
143
|
-
fallbackModel = env.N_CURSOR_OMLX_MODEL ?? '',
|
|
144
|
-
apiKey,
|
|
145
|
-
backoffMs = BACKOFF_MS,
|
|
146
|
-
thinkingBudget = 0
|
|
147
|
-
} = opts
|
|
148
|
-
|
|
149
|
-
const m = omlxModelId(model) || fallbackModel
|
|
150
|
-
if (!m) {
|
|
151
|
-
throw new Error('omlx: модель не задано — постав N_LOCAL_MIN_MODEL (або N_CURSOR_OMLX_MODEL)')
|
|
152
|
-
}
|
|
153
|
-
const body = JSON.stringify({
|
|
154
|
-
model: m,
|
|
155
|
-
messages,
|
|
156
|
-
max_tokens: maxTokens,
|
|
157
|
-
temperature,
|
|
158
|
-
...(thinkingBudget > 0 ? { thinking_budget: thinkingBudget } : {})
|
|
159
|
-
})
|
|
160
|
-
// Ключ локального сервера в argv допустимий: localhost-секрет власної машини,
|
|
161
|
-
// короткоживучий процес; stdin уже зайнятий body (`--data-binary @-`).
|
|
162
|
-
const key = resolveOmlxApiKey(apiKey)
|
|
163
|
-
const authArgs = key ? ['-H', `Authorization: Bearer ${key}`] : []
|
|
164
|
-
|
|
165
|
-
// 18=transfer closed, 28=operation timeout, 52=empty reply, 56=recv failure — усі transient.
|
|
166
|
-
const TRANSIENT_CURL_CODES = new Set([18, 28, 52, 56])
|
|
167
|
-
let lastErr
|
|
168
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
169
|
-
const r = spawnSync(
|
|
170
|
-
'curl',
|
|
171
|
-
[
|
|
172
|
-
'-sS',
|
|
173
|
-
'-X',
|
|
174
|
-
'POST',
|
|
175
|
-
url,
|
|
176
|
-
'-H',
|
|
177
|
-
'Content-Type: application/json',
|
|
178
|
-
'-H',
|
|
179
|
-
'Connection: close',
|
|
180
|
-
...authArgs,
|
|
181
|
-
'--max-time',
|
|
182
|
-
String(Math.ceil(timeoutMs / 1000)),
|
|
183
|
-
'--data-binary',
|
|
184
|
-
'@-'
|
|
185
|
-
],
|
|
186
|
-
{ input: body, encoding: 'utf8', timeout: timeoutMs + 5000 }
|
|
187
|
-
)
|
|
188
|
-
if (r.error) {
|
|
189
|
-
lastErr = new Error(`omlx curl error: ${r.error.message}`)
|
|
190
|
-
// spawnSync-таймаут (ETIMEDOUT) — transient: сервер перевантажений, ретраїмо з backoff.
|
|
191
|
-
if (r.error.code === 'ETIMEDOUT' && attempt < 3) {
|
|
192
|
-
sleepSync(backoffMs[attempt - 1])
|
|
193
|
-
continue
|
|
194
|
-
}
|
|
195
|
-
break
|
|
196
|
-
}
|
|
197
|
-
if (r.status !== 0) {
|
|
198
|
-
if (TRANSIENT_CURL_CODES.has(r.status) && attempt < 3) {
|
|
199
|
-
lastErr = new Error(`omlx curl exit ${r.status} (transient, retry ${attempt})`)
|
|
200
|
-
sleepSync(backoffMs[attempt - 1])
|
|
201
|
-
continue
|
|
202
|
-
}
|
|
203
|
-
throw new Error(`omlx curl exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
|
|
204
|
-
}
|
|
205
|
-
return parseOmlxResponse(r.stdout, attempt)
|
|
206
|
-
}
|
|
207
|
-
throw lastErr ?? new Error('omlx unknown failure')
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Тонка обгортка над `callOmlxRaw` для споживачів, яким потрібен лише текст.
|
|
212
|
-
* Контракт незмінний: повертає непорожній `choices[0].message.content`.
|
|
213
|
-
* @param {Array<{role:string, content:string}>} messages OpenAI-messages
|
|
214
|
-
* @param {string} model model-id (з/без `omlx/`-префікса)
|
|
215
|
-
* @param {object} [opts] ті самі опції, що й у `callOmlxRaw`
|
|
216
|
-
* @returns {string} непорожній контент відповіді
|
|
217
|
-
*/
|
|
218
|
-
export function callOmlx(messages, model, opts = {}) {
|
|
219
|
-
return callOmlxRaw(messages, model, opts).content
|
|
220
|
-
}
|
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Аналітика escalation-логу (спека 2026-06-19-fix-escalation-analysis-design).
|
|
3
|
-
*
|
|
4
|
-
* Читає записи рунгів драбини (`escalation-log.mjs`) — за один прогін (від байтового
|
|
5
|
-
* зсуву) або весь лог, — ділить на чанки за бюджетом символів і просить хмарну
|
|
6
|
-
* **avg**-модель проаналізувати: як зменшити LLM-залежність fix-конформності.
|
|
7
|
-
* Мета аналізу — конкретні правки пакета `@nitra/cursor`:
|
|
8
|
-
* (A) новий ДЕТЕРМІНОВАНИЙ T0-патерн (`t0.mjs`) — прибирає LLM зовсім;
|
|
9
|
-
* (B) уточнення `.mdc`-інструкцій правила, щоб локальна min-модель влучала з першого рунга;
|
|
10
|
-
* (C) зміна скрипта/чека в пакеті.
|
|
11
|
-
* Результат — markdown-звіт у `.n-cursor/fix-escalation-analysis.md` (append із timestamp).
|
|
12
|
-
*
|
|
13
|
-
* Викликається CLI `n-cursor analyze-escalation` (весь лог) і наприкінці `lint --full`
|
|
14
|
-
* (записи цього прогону). Кожен виклик моделі йде через спільний `callLlm` (wire-trace).
|
|
15
|
-
*/
|
|
16
|
-
import { appendFileSync, mkdirSync, readFileSync, statSync } from 'node:fs'
|
|
17
|
-
import { dirname, join } from 'node:path'
|
|
18
|
-
import { cwd as processCwd, env } from 'node:process'
|
|
19
|
-
|
|
20
|
-
import { callLlm } from '../../../lib/llm.mjs'
|
|
21
|
-
import { CLOUD_AVG } from '../../../lib/models.mjs'
|
|
22
|
-
import { escalationLogPath } from './escalation-log.mjs'
|
|
23
|
-
|
|
24
|
-
/** Значення `N_CURSOR_FIX_ANALYZE`, що вимикають авто-аналіз наприкінці lint. */
|
|
25
|
-
const KILL_VALUES = new Set(['0', 'false', 'off', 'no'])
|
|
26
|
-
/** Бюджет символів на один чанк (щоб великий лог не перевищив контекст моделі). */
|
|
27
|
-
const DEFAULT_CHUNK_CHARS = 40_000
|
|
28
|
-
/** Timeout одного аналітичного виклику (мс) — аналіз може бути об'ємним. */
|
|
29
|
-
const ANALYZE_TIMEOUT_MS = 180_000
|
|
30
|
-
|
|
31
|
-
/** No-op логер за замовчуванням. */
|
|
32
|
-
const NOOP_LOG = () => {
|
|
33
|
-
/* тихо */
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Спільна мета-інструкція для аналітичних викликів.
|
|
38
|
-
*/
|
|
39
|
-
const GOAL = [
|
|
40
|
-
`You analyze logs from @nitra/cursor's automated rule-conformance fixer ("fix").`,
|
|
41
|
-
`Each record = one attempt by a model to fix a rule-conformance violation on a rung of`,
|
|
42
|
-
`an escalation ladder. Fields: ruleId; tier (local-min|local-min-retry|cloud-min|cloud-avg);`,
|
|
43
|
-
`model; callOk (model call+apply succeeded); recheckOk (rule PASSED after this rung — "did it help");`,
|
|
44
|
-
`callError; diagnosis (model's self-stated reason a prior attempt failed); remainingViolation.`,
|
|
45
|
-
``,
|
|
46
|
-
`Goal: reduce LLM dependence and time-to-green. For RECURRING patterns recommend CONCRETE changes`,
|
|
47
|
-
`to the @nitra/cursor package, in priority order:`,
|
|
48
|
-
`(A) a new DETERMINISTIC T0-auto pattern for npm/scripts/lib/fix/t0.mjs — give a regex that matches`,
|
|
49
|
-
` the violation output + the mechanical fix. PREFERRED: removes the LLM entirely.`,
|
|
50
|
-
`(B) a clarification to a rule's .mdc instructions so the LOCAL min-model succeeds on the FIRST rung.`,
|
|
51
|
-
`(C) a script/check change elsewhere in the package.`,
|
|
52
|
-
`Prioritise rules that escalated to cloud (cloud-min/cloud-avg) or failed repeatedly — they cost most.`,
|
|
53
|
-
`Ignore rules resolved at local-min with no retry — they already work.`
|
|
54
|
-
].join('\n')
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Чи увімкнено авто-аналіз наприкінці lint (default — так; kill-switch `N_CURSOR_FIX_ANALYZE`).
|
|
58
|
-
* @returns {boolean} true, якщо аналіз дозволено
|
|
59
|
-
*/
|
|
60
|
-
export function analysisEnabled() {
|
|
61
|
-
const v = env.N_CURSOR_FIX_ANALYZE
|
|
62
|
-
if (v === undefined) return true
|
|
63
|
-
return !KILL_VALUES.has(v.toLowerCase())
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Розмір escalation-логу в байтах (0, якщо файлу немає/вимкнено) — для since-offset.
|
|
68
|
-
* @param {string|null} [path] шлях логу
|
|
69
|
-
* @returns {number} розмір у байтах
|
|
70
|
-
*/
|
|
71
|
-
export function escalationLogSize(path = escalationLogPath()) {
|
|
72
|
-
if (!path) return 0
|
|
73
|
-
try {
|
|
74
|
-
return statSync(path).size
|
|
75
|
-
} catch {
|
|
76
|
-
return 0
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Читає записи escalation-логу від байтового зсуву (default 0 — весь лог). Зсув завжди
|
|
82
|
-
* на межі рядка (захоплюється після завершеного append), тож мультибайтні символи не б'ються.
|
|
83
|
-
* Биті JSON-рядки пропускаються.
|
|
84
|
-
* @param {string|null} path шлях логу
|
|
85
|
-
* @param {number} [sinceOffset] байтовий зсув початку читання
|
|
86
|
-
* @returns {object[]} розпарсені записи
|
|
87
|
-
*/
|
|
88
|
-
export function readEscalationRecords(path, sinceOffset = 0) {
|
|
89
|
-
if (!path) return []
|
|
90
|
-
let buf
|
|
91
|
-
try {
|
|
92
|
-
buf = readFileSync(path)
|
|
93
|
-
} catch {
|
|
94
|
-
return []
|
|
95
|
-
}
|
|
96
|
-
const text = (sinceOffset > 0 ? buf.subarray(sinceOffset) : buf).toString('utf8')
|
|
97
|
-
const out = []
|
|
98
|
-
for (const line of text.split('\n')) {
|
|
99
|
-
const t = line.trim()
|
|
100
|
-
if (!t) continue
|
|
101
|
-
try {
|
|
102
|
-
out.push(JSON.parse(t))
|
|
103
|
-
} catch {
|
|
104
|
-
/* битий рядок — пропускаємо */
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return out
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Маркер skip-запису avg-рунга (кеп вичерпано) — НЕ фактичний виклик моделі. */
|
|
111
|
-
const AVG_SKIP_MARKER = 'cloud-avg cap reached'
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Рахує фактичні виклики моделей за тирами (skip-записи avg-кепу не рахуються).
|
|
115
|
-
* @param {object[]} records записи рунгів
|
|
116
|
-
* @returns {{ local: number, cloudMin: number, cloudAvg: number }} лічильники викликів
|
|
117
|
-
*/
|
|
118
|
-
export function summarizeCalls(records) {
|
|
119
|
-
const stats = { local: 0, cloudMin: 0, cloudAvg: 0 }
|
|
120
|
-
for (const r of records) {
|
|
121
|
-
if (r.callError === AVG_SKIP_MARKER) continue
|
|
122
|
-
if (r.tier === 'cloud-avg') stats.cloudAvg++
|
|
123
|
-
else if (r.tier === 'cloud-min') stats.cloudMin++
|
|
124
|
-
else if (typeof r.tier === 'string' && r.tier.startsWith('local')) stats.local++
|
|
125
|
-
}
|
|
126
|
-
return stats
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Друкує резюме викликів моделей за цей прогін (локальна / cloud-min / cloud-avg).
|
|
131
|
-
* No-op, якщо викликів не було. Читає записи від `sinceOffset`.
|
|
132
|
-
* @param {number} sinceOffset байтовий зсув логу перед прогоном
|
|
133
|
-
* @param {(s: string) => void} log логер
|
|
134
|
-
* @returns {void}
|
|
135
|
-
*/
|
|
136
|
-
export function reportRunStats(sinceOffset, log) {
|
|
137
|
-
const { local, cloudMin, cloudAvg } = summarizeCalls(readEscalationRecords(escalationLogPath(), sinceOffset))
|
|
138
|
-
if (local + cloudMin + cloudAvg === 0) return
|
|
139
|
-
log(
|
|
140
|
-
`\n📊 LLM-виклики fix-конформності (цей прогін): ` +
|
|
141
|
-
`локальна ${local} · cloud-min ${cloudMin} · cloud-avg ${cloudAvg}\n`
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Стискає запис до полів, важливих для аналізу (без ts/ms-шуму).
|
|
147
|
-
* @param {object} r сирий запис рунга
|
|
148
|
-
* @returns {object} компактний запис
|
|
149
|
-
*/
|
|
150
|
-
function summarizeRecord(r) {
|
|
151
|
-
return {
|
|
152
|
-
ruleId: r.ruleId,
|
|
153
|
-
tier: r.tier,
|
|
154
|
-
model: r.model,
|
|
155
|
-
callOk: r.callOk,
|
|
156
|
-
recheckOk: r.recheckOk,
|
|
157
|
-
callError: r.callError ?? null,
|
|
158
|
-
diagnosis: r.diagnosis ?? null,
|
|
159
|
-
remainingViolation: r.remainingViolation ?? null
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Ділить записи на чанки так, щоб JSON кожного чанка не перевищував `maxChars`.
|
|
165
|
-
* Працює на стиснених записах (саме вони йдуть у prompt).
|
|
166
|
-
* @param {object[]} records сирі записи
|
|
167
|
-
* @param {number} [maxChars] бюджет символів на чанк
|
|
168
|
-
* @returns {object[][]} чанки стиснених записів
|
|
169
|
-
*/
|
|
170
|
-
export function chunkRecords(records, maxChars = DEFAULT_CHUNK_CHARS) {
|
|
171
|
-
const items = records.map(r => summarizeRecord(r))
|
|
172
|
-
const chunks = []
|
|
173
|
-
let cur = []
|
|
174
|
-
let size = 0
|
|
175
|
-
for (const it of items) {
|
|
176
|
-
const len = JSON.stringify(it).length + 1
|
|
177
|
-
if (cur.length > 0 && size + len > maxChars) {
|
|
178
|
-
chunks.push(cur)
|
|
179
|
-
cur = []
|
|
180
|
-
size = 0
|
|
181
|
-
}
|
|
182
|
-
cur.push(it)
|
|
183
|
-
size += len
|
|
184
|
-
}
|
|
185
|
-
if (cur.length > 0) chunks.push(cur)
|
|
186
|
-
return chunks
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Prompt для аналізу одного чанка.
|
|
191
|
-
* @param {object[]} items стиснені записи чанка
|
|
192
|
-
* @param {number} idx індекс чанка (0-based)
|
|
193
|
-
* @param {number} total всього чанків
|
|
194
|
-
* @returns {string} текст prompt
|
|
195
|
-
*/
|
|
196
|
-
function buildChunkPrompt(items, idx, total) {
|
|
197
|
-
return [
|
|
198
|
-
GOAL,
|
|
199
|
-
``,
|
|
200
|
-
`Log chunk ${idx + 1}/${total} (${items.length} records):`,
|
|
201
|
-
JSON.stringify(items),
|
|
202
|
-
``,
|
|
203
|
-
`Return concise markdown: per recommendation — target ruleId, type (A/B/C), and the concrete change`,
|
|
204
|
-
`(for A: the regex + mechanical fix; for B: the exact .mdc clarification; for C: the script change).`
|
|
205
|
-
].join('\n')
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Prompt для злиття часткових аналізів у фінальний звіт.
|
|
210
|
-
* @param {string[]} partials часткові аналізи чанків
|
|
211
|
-
* @returns {string} текст prompt
|
|
212
|
-
*/
|
|
213
|
-
function buildSynthesisPrompt(partials) {
|
|
214
|
-
return [
|
|
215
|
-
GOAL,
|
|
216
|
-
``,
|
|
217
|
-
`Below are partial analyses of separate log chunks. Merge, dedupe and prioritise into ONE report.`,
|
|
218
|
-
``,
|
|
219
|
-
partials.map((p, i) => `--- chunk ${i + 1} ---\n${p}`).join('\n\n'),
|
|
220
|
-
``,
|
|
221
|
-
`Return the final markdown report: recommendations ordered highest-impact first.`
|
|
222
|
-
].join('\n')
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Безпечний виклик моделі: ковтає помилку у `null` (аналіз не має валити lint).
|
|
227
|
-
* @param {(messages: object[], model: string, opts: object) => string} call функція callLlm
|
|
228
|
-
* @param {string} prompt текст prompt
|
|
229
|
-
* @param {string} model model-id
|
|
230
|
-
* @returns {string|null} текст відповіді або null
|
|
231
|
-
*/
|
|
232
|
-
function safeCall(call, prompt, model) {
|
|
233
|
-
try {
|
|
234
|
-
const text = call([{ role: 'user', content: prompt }], model, {
|
|
235
|
-
timeoutMs: ANALYZE_TIMEOUT_MS,
|
|
236
|
-
caller: 'fix-analyze'
|
|
237
|
-
})
|
|
238
|
-
return text || null
|
|
239
|
-
} catch {
|
|
240
|
-
return null
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Аналізує записи: чанкінг → виклик avg-моделі по чанках → синтез (якщо чанків >1).
|
|
246
|
-
* Синхронний (callLlm — spawnSync-based).
|
|
247
|
-
* @param {object[]} records записи escalation-логу
|
|
248
|
-
* @param {{ model?: string, callLlm?: (messages: object[], model: string, opts: object) => string, log?: (s: string) => void, maxChars?: number }} [opts]
|
|
249
|
-
* `model` — модель (default `CLOUD_AVG`); `callLlm` — інжекція для тестів; `log` — логер
|
|
250
|
-
* @returns {{ report: string|null, chunks: number, reason: string }} звіт і метадані
|
|
251
|
-
*/
|
|
252
|
-
export function analyzeEscalations(records, opts = {}) {
|
|
253
|
-
const model = opts.model ?? CLOUD_AVG
|
|
254
|
-
const call = opts.callLlm ?? callLlm
|
|
255
|
-
const log = opts.log ?? NOOP_LOG
|
|
256
|
-
const maxChars = opts.maxChars ?? DEFAULT_CHUNK_CHARS
|
|
257
|
-
|
|
258
|
-
if (records.length === 0) return { report: null, chunks: 0, reason: 'no-records' }
|
|
259
|
-
if (!model) return { report: null, chunks: 0, reason: 'no-cloud-avg-model' }
|
|
260
|
-
|
|
261
|
-
const chunks = chunkRecords(records, maxChars)
|
|
262
|
-
const partials = []
|
|
263
|
-
for (const [i, chunk] of chunks.entries()) {
|
|
264
|
-
log(` 🔎 escalation-analysis: чанк ${i + 1}/${chunks.length} (${chunk.length} записів)`)
|
|
265
|
-
const text = safeCall(call, buildChunkPrompt(chunk, i, chunks.length), model)
|
|
266
|
-
if (text) partials.push(text)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (partials.length === 0) return { report: null, chunks: chunks.length, reason: 'empty-responses' }
|
|
270
|
-
const report = partials.length === 1 ? partials[0] : safeCall(call, buildSynthesisPrompt(partials), model)
|
|
271
|
-
return { report, chunks: chunks.length, reason: report ? 'ok' : 'empty-responses' }
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Шлях markdown-звіту аналізу.
|
|
276
|
-
* @param {string} [cwd] корінь
|
|
277
|
-
* @returns {string} шлях .n-cursor/fix-escalation-analysis.md
|
|
278
|
-
*/
|
|
279
|
-
export function analysisReportPath(cwd = processCwd()) {
|
|
280
|
-
return join(cwd, '.n-cursor', 'fix-escalation-analysis.md')
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Дописує звіт у markdown-файл із timestamp-заголовком.
|
|
285
|
-
* @param {string} report текст звіту
|
|
286
|
-
* @param {string} cwd корінь
|
|
287
|
-
* @param {string} ts ISO-час
|
|
288
|
-
* @returns {string} шлях файлу
|
|
289
|
-
*/
|
|
290
|
-
export function writeAnalysisReport(report, cwd, ts) {
|
|
291
|
-
const path = analysisReportPath(cwd)
|
|
292
|
-
mkdirSync(dirname(path), { recursive: true })
|
|
293
|
-
appendFileSync(path, `\n## Аналіз ${ts}\n\n${report}\n`, 'utf8')
|
|
294
|
-
return path
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Спільний шлях: аналіз записів → запис звіту → лог-підсумок.
|
|
299
|
-
* @param {object[]} records записи
|
|
300
|
-
* @param {string} cwd корінь
|
|
301
|
-
* @param {(s: string) => void} log логер
|
|
302
|
-
* @returns {number} 0 — ок/пропуск, 1 — модель не дала звіт
|
|
303
|
-
*/
|
|
304
|
-
function analyzeAndReport(records, cwd, log) {
|
|
305
|
-
const res = analyzeEscalations(records, { log })
|
|
306
|
-
if (res.reason === 'no-records') {
|
|
307
|
-
log('ℹ️ escalation-analysis: немає записів для аналізу.')
|
|
308
|
-
return 0
|
|
309
|
-
}
|
|
310
|
-
if (res.reason === 'no-cloud-avg-model') {
|
|
311
|
-
log('⚠️ escalation-analysis: N_CLOUD_AVG_MODEL не заданий — аналіз пропущено.')
|
|
312
|
-
return 0
|
|
313
|
-
}
|
|
314
|
-
if (!res.report) {
|
|
315
|
-
log('⚠️ escalation-analysis: модель не повернула звіт.')
|
|
316
|
-
return 1
|
|
317
|
-
}
|
|
318
|
-
const reportPath = writeAnalysisReport(res.report, cwd, new Date().toISOString())
|
|
319
|
-
log(`📝 escalation-analysis: звіт → ${reportPath} (${res.chunks} чанк(и))`)
|
|
320
|
-
return 0
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* CLI `n-cursor analyze-escalation` — аналізує ВЕСЬ escalation-лог і пише звіт.
|
|
325
|
-
* @param {string[]} _args аргументи (зарезервовано)
|
|
326
|
-
* @param {string} [cwd] корінь
|
|
327
|
-
* @returns {number} exit code
|
|
328
|
-
*/
|
|
329
|
-
export function runEscalationAnalysisCli(_args, cwd = processCwd()) {
|
|
330
|
-
const records = readEscalationRecords(escalationLogPath(), 0)
|
|
331
|
-
return analyzeAndReport(records, cwd, s => console.log(s))
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Хук наприкінці `lint --full` (non-read-only): аналізує записи ЦЬОГО прогону
|
|
336
|
-
* (від `sinceOffset`). Gated: kill-switch, наявність cloud-avg, наявність записів.
|
|
337
|
-
* Помилки не валять lint.
|
|
338
|
-
* @param {string} cwd корінь
|
|
339
|
-
* @param {number} sinceOffset байтовий зсув логу перед прогоном
|
|
340
|
-
* @param {(s: string) => void} log логер
|
|
341
|
-
* @returns {void}
|
|
342
|
-
*/
|
|
343
|
-
export function maybeAnalyzeEscalation(cwd, sinceOffset, log) {
|
|
344
|
-
if (!analysisEnabled()) return
|
|
345
|
-
const records = readEscalationRecords(escalationLogPath(), sinceOffset)
|
|
346
|
-
if (records.length === 0) return
|
|
347
|
-
if (!CLOUD_AVG) {
|
|
348
|
-
log('\nℹ️ escalation-analysis: були LLM-ескалації, але N_CLOUD_AVG_MODEL не заданий — аналіз пропущено.\n')
|
|
349
|
-
return
|
|
350
|
-
}
|
|
351
|
-
log('\n🔬 escalation-analysis: аналізую ескалації цього прогону…\n')
|
|
352
|
-
analyzeAndReport(records, cwd, log)
|
|
353
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: analyze-escalation.mjs
|
|
4
|
-
resource: npm/scripts/lib/fix/analyze-escalation.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: f26cd4c7
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
Аналізує записи рунгів драбини з `escalation-log.mjs` для виявлення шляхів зменшення LLM-залежності у fix-конформності. Лог обробляється за один прогін (від байтового зсуву) або повністю, ділячись на чанки для аналізу хмарною **avg**-моделлю. Мета аналізу — визначити конкретні правки пакета `@nitra/cursor`: (A) новий ДЕТЕРМІНОВАНИЙ T0-патерн (`t0.mjs`), (B) уточнення `.mdc`-інструкцій правила, або (C) зміна скрипта/чека. Результат зберігається у markdown-звіт `.n-cursor/fix-escalation-analysis.md` (append із timestamp) після виклику CLI `n-cursor analyze-escalation`.
|
|
12
|
-
|
|
13
|
-
## Поведінка
|
|
14
|
-
|
|
15
|
-
analysisEnabled визначає, чи дозволено виконувати автоматичний аналіз ескалації наприкінці процесу `lint`.
|
|
16
|
-
escalationLogSize повертає розмір файлу логу ескалації у байтах.
|
|
17
|
-
readEscalationRecords читає записи логу ескалації, починаючи з заданого байтового зсуву, та повертає їх як масив об'єктів.
|
|
18
|
-
summarizeCalls рахує кількість викликів моделей для різних рівнів (локальний, cloud-min, cloud-avg) у наданих записах.
|
|
19
|
-
reportRunStats друкує резюме кількості викликів моделей для поточного прогону, використовуючи заданий байтовий зсув.
|
|
20
|
-
chunkRecords ділить масив стиснених записів на менші чанки, щоб кожен чанк не перевищив заданий бюджет символів.
|
|
21
|
-
analyzeEscalations аналізує надані записи, ділить їх на чанки, викликає модель для аналізу кожного чанка, а потім синтезує фінальний звіт.
|
|
22
|
-
analysisReportPath повертає шлях до файлу, де зберігається звіт аналізу ескалації.
|
|
23
|
-
writeAnalysisReport дописує згенерований звіт у markdown-файл у відповідному шляху, додаючи до нього мітку часу.
|
|
24
|
-
runEscalationAnalysisCli виконує повний аналіз всього логу ескалації та записує звіт.
|
|
25
|
-
maybeAnalyzeEscalation виконує аналіз ескалацій лише для записів поточного прогону, якщо дозволено та є необхідні умови.
|
|
26
|
-
|
|
27
|
-
## Публічний API
|
|
28
|
-
|
|
29
|
-
analysisEnabled — Вмикає автоматичний аналіз після завершення lint.
|
|
30
|
-
escalationLogSize — Визначає максимальний розмір escalation-логу в байтах.
|
|
31
|
-
readEscalationRecords — Зчитує записи з логу, починаючи з заданого байтового зсуву.
|
|
32
|
-
summarizeCalls — Підраховує реальні виклики моделей за тирами, ігноруючи записи про середнє кешування.
|
|
33
|
-
reportRunStats — Виводить підсумок викликів моделей за поточний запуск.
|
|
34
|
-
chunkRecords — Розбиває записи на менші частини, щоб розмір JSON кожного чанку не перевищував заданий ліміт.
|
|
35
|
-
analyzeEscalations — Обробляє записи: розбиває їх на чанки, викликає модель для кожного чанку та синтезує результат, якщо чанків більше одного.
|
|
36
|
-
analysisReportPath — Вказує шлях для збереження markdown-звіту аналізу.
|
|
37
|
-
writeAnalysisReport — Додає звіт до markdown-файлу, додаючи до нього мітку часу.
|
|
38
|
-
runEscalationAnalysisCli — Виконує повний аналіз всього логу escalation через інтерфейс командного рядка.
|
|
39
|
-
maybeAnalyzeEscalation — Запускає аналіз записів поточного прогону після `lint --full`, якщо це дозволено налаштуваннями.
|
|
40
|
-
|
|
41
|
-
## Гарантії поведінки
|
|
42
|
-
|
|
43
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
44
|
-
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: llm-fix-apply.mjs
|
|
4
|
-
resource: npm/scripts/lib/fix/llm-fix-apply.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 49f989ca
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Огляд
|
|
12
|
-
|
|
13
|
-
Це спільне ядро LLM-фіксу, призначене для оркестрації процесу застосування змін, згенерованих моделлю. Модуль використовує `llm-worker.mjs` та `llm-lint-fix.mjs` для забезпечення конформності та виконання лінтер-фіксів. Основний функціонал включає: парсинг відповіді LLM за схемою `{changes:[{path,content}]}` (через `parseChangesResponse`), зчитування необхідних файлів (`readFilesForFix`), та безпечне застосування змін (`applyChanges`). Система реалізує механізм fail-safe, перехоплюючи помилки та повертаючи `null` замість винятків для певних сценаріїв. При цьому шляхи `.git` та `node_modules` свідомо ігноруються.
|
|
14
|
-
|
|
15
|
-
## Поведінка
|
|
16
|
-
|
|
17
|
-
parseChangesResponse парсить сирий текст відповіді моделі, витягуючи структуру змін у форматі патчу, або повертає null, якщо парсинг неможливий.
|
|
18
|
-
readFilesForFix читає вміст файлів, вказаних у списку, відносно кореня проєкту. Якщо прямий шлях не знайдено, шукає файл за його базовим ім'ям у проєкті, ігноруючи каталоги `.git` та `node_modules`.
|
|
19
|
-
applyChanges записує вміст, наданий у змінних, у відповідні файли проєкту, створюючи необхідні каталоги, якщо вони відсутні.
|
|
20
|
-
|
|
21
|
-
## Публічний API
|
|
22
|
-
|
|
23
|
-
parseChangesResponse — розбирає JSON-відповідь моделі, витягуючи вміст першого об'єкта.
|
|
24
|
-
readFilesForFix — зчитує вміст файлів за заданими шляхами, шукаючи їх у системі, якщо прямий шлях не знайдено.
|
|
25
|
-
applyChanges — замінює вміст файлів на новий, наданий у змінній `changes`.
|
|
26
|
-
|
|
27
|
-
## Гарантії поведінки
|
|
28
|
-
|
|
29
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
30
|
-
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
31
|
-
- Свідомо пропускає шляхи: `.git`, `node_modules`.
|