@nitra/cursor 12.0.3 → 12.2.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,315 @@
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
+ /**
111
+ * Стискає запис до полів, важливих для аналізу (без ts/ms-шуму).
112
+ * @param {object} r сирий запис рунга
113
+ * @returns {object} компактний запис
114
+ */
115
+ function summarizeRecord(r) {
116
+ return {
117
+ ruleId: r.ruleId,
118
+ tier: r.tier,
119
+ model: r.model,
120
+ callOk: r.callOk,
121
+ recheckOk: r.recheckOk,
122
+ callError: r.callError ?? null,
123
+ diagnosis: r.diagnosis ?? null,
124
+ remainingViolation: r.remainingViolation ?? null
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Ділить записи на чанки так, щоб JSON кожного чанка не перевищував `maxChars`.
130
+ * Працює на стиснених записах (саме вони йдуть у prompt).
131
+ * @param {object[]} records сирі записи
132
+ * @param {number} [maxChars] бюджет символів на чанк
133
+ * @returns {object[][]} чанки стиснених записів
134
+ */
135
+ export function chunkRecords(records, maxChars = DEFAULT_CHUNK_CHARS) {
136
+ const items = records.map(r => summarizeRecord(r))
137
+ const chunks = []
138
+ let cur = []
139
+ let size = 0
140
+ for (const it of items) {
141
+ const len = JSON.stringify(it).length + 1
142
+ if (cur.length > 0 && size + len > maxChars) {
143
+ chunks.push(cur)
144
+ cur = []
145
+ size = 0
146
+ }
147
+ cur.push(it)
148
+ size += len
149
+ }
150
+ if (cur.length > 0) chunks.push(cur)
151
+ return chunks
152
+ }
153
+
154
+ /**
155
+ * Prompt для аналізу одного чанка.
156
+ * @param {object[]} items стиснені записи чанка
157
+ * @param {number} idx індекс чанка (0-based)
158
+ * @param {number} total всього чанків
159
+ * @returns {string} текст prompt
160
+ */
161
+ function buildChunkPrompt(items, idx, total) {
162
+ return [
163
+ GOAL,
164
+ ``,
165
+ `Log chunk ${idx + 1}/${total} (${items.length} records):`,
166
+ JSON.stringify(items),
167
+ ``,
168
+ `Return concise markdown: per recommendation — target ruleId, type (A/B/C), and the concrete change`,
169
+ `(for A: the regex + mechanical fix; for B: the exact .mdc clarification; for C: the script change).`
170
+ ].join('\n')
171
+ }
172
+
173
+ /**
174
+ * Prompt для злиття часткових аналізів у фінальний звіт.
175
+ * @param {string[]} partials часткові аналізи чанків
176
+ * @returns {string} текст prompt
177
+ */
178
+ function buildSynthesisPrompt(partials) {
179
+ return [
180
+ GOAL,
181
+ ``,
182
+ `Below are partial analyses of separate log chunks. Merge, dedupe and prioritise into ONE report.`,
183
+ ``,
184
+ partials.map((p, i) => `--- chunk ${i + 1} ---\n${p}`).join('\n\n'),
185
+ ``,
186
+ `Return the final markdown report: recommendations ordered highest-impact first.`
187
+ ].join('\n')
188
+ }
189
+
190
+ /**
191
+ * Безпечний виклик моделі: ковтає помилку у `null` (аналіз не має валити lint).
192
+ * @param {(messages: object[], model: string, opts: object) => string} call функція callLlm
193
+ * @param {string} prompt текст prompt
194
+ * @param {string} model model-id
195
+ * @returns {string|null} текст відповіді або null
196
+ */
197
+ function safeCall(call, prompt, model) {
198
+ try {
199
+ const text = call([{ role: 'user', content: prompt }], model, { timeoutMs: ANALYZE_TIMEOUT_MS, caller: 'fix-analyze' })
200
+ return text || null
201
+ } catch {
202
+ return null
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Аналізує записи: чанкінг → виклик avg-моделі по чанках → синтез (якщо чанків >1).
208
+ * Синхронний (callLlm — spawnSync-based).
209
+ * @param {object[]} records записи escalation-логу
210
+ * @param {{ model?: string, callLlm?: (messages: object[], model: string, opts: object) => string, log?: (s: string) => void, maxChars?: number }} [opts]
211
+ * `model` — модель (default `CLOUD_AVG`); `callLlm` — інжекція для тестів; `log` — логер
212
+ * @returns {{ report: string|null, chunks: number, reason: string }} звіт і метадані
213
+ */
214
+ export function analyzeEscalations(records, opts = {}) {
215
+ const model = opts.model ?? CLOUD_AVG
216
+ const call = opts.callLlm ?? callLlm
217
+ const log = opts.log ?? NOOP_LOG
218
+ const maxChars = opts.maxChars ?? DEFAULT_CHUNK_CHARS
219
+
220
+ if (records.length === 0) return { report: null, chunks: 0, reason: 'no-records' }
221
+ if (!model) return { report: null, chunks: 0, reason: 'no-cloud-avg-model' }
222
+
223
+ const chunks = chunkRecords(records, maxChars)
224
+ const partials = []
225
+ for (const [i, chunk] of chunks.entries()) {
226
+ log(` 🔎 escalation-analysis: чанк ${i + 1}/${chunks.length} (${chunk.length} записів)`)
227
+ const text = safeCall(call, buildChunkPrompt(chunk, i, chunks.length), model)
228
+ if (text) partials.push(text)
229
+ }
230
+
231
+ if (partials.length === 0) return { report: null, chunks: chunks.length, reason: 'empty-responses' }
232
+ const report = partials.length === 1 ? partials[0] : safeCall(call, buildSynthesisPrompt(partials), model)
233
+ return { report, chunks: chunks.length, reason: report ? 'ok' : 'empty-responses' }
234
+ }
235
+
236
+ /**
237
+ * Шлях markdown-звіту аналізу.
238
+ * @param {string} [cwd] корінь
239
+ * @returns {string} шлях .n-cursor/fix-escalation-analysis.md
240
+ */
241
+ export function analysisReportPath(cwd = processCwd()) {
242
+ return join(cwd, '.n-cursor', 'fix-escalation-analysis.md')
243
+ }
244
+
245
+ /**
246
+ * Дописує звіт у markdown-файл із timestamp-заголовком.
247
+ * @param {string} report текст звіту
248
+ * @param {string} cwd корінь
249
+ * @param {string} ts ISO-час
250
+ * @returns {string} шлях файлу
251
+ */
252
+ export function writeAnalysisReport(report, cwd, ts) {
253
+ const path = analysisReportPath(cwd)
254
+ mkdirSync(dirname(path), { recursive: true })
255
+ appendFileSync(path, `\n## Аналіз ${ts}\n\n${report}\n`, 'utf8')
256
+ return path
257
+ }
258
+
259
+ /**
260
+ * Спільний шлях: аналіз записів → запис звіту → лог-підсумок.
261
+ * @param {object[]} records записи
262
+ * @param {string} cwd корінь
263
+ * @param {(s: string) => void} log логер
264
+ * @returns {number} 0 — ок/пропуск, 1 — модель не дала звіт
265
+ */
266
+ function analyzeAndReport(records, cwd, log) {
267
+ const res = analyzeEscalations(records, { log })
268
+ if (res.reason === 'no-records') {
269
+ log('ℹ️ escalation-analysis: немає записів для аналізу.')
270
+ return 0
271
+ }
272
+ if (res.reason === 'no-cloud-avg-model') {
273
+ log('⚠️ escalation-analysis: N_CLOUD_AVG_MODEL не заданий — аналіз пропущено.')
274
+ return 0
275
+ }
276
+ if (!res.report) {
277
+ log('⚠️ escalation-analysis: модель не повернула звіт.')
278
+ return 1
279
+ }
280
+ const reportPath = writeAnalysisReport(res.report, cwd, new Date().toISOString())
281
+ log(`📝 escalation-analysis: звіт → ${reportPath} (${res.chunks} чанк(и))`)
282
+ return 0
283
+ }
284
+
285
+ /**
286
+ * CLI `n-cursor analyze-escalation` — аналізує ВЕСЬ escalation-лог і пише звіт.
287
+ * @param {string[]} _args аргументи (зарезервовано)
288
+ * @param {string} [cwd] корінь
289
+ * @returns {number} exit code
290
+ */
291
+ export function runEscalationAnalysisCli(_args, cwd = processCwd()) {
292
+ const records = readEscalationRecords(escalationLogPath(), 0)
293
+ return analyzeAndReport(records, cwd, s => console.log(s))
294
+ }
295
+
296
+ /**
297
+ * Хук наприкінці `lint --full` (non-read-only): аналізує записи ЦЬОГО прогону
298
+ * (від `sinceOffset`). Gated: kill-switch, наявність cloud-avg, наявність записів.
299
+ * Помилки не валять lint.
300
+ * @param {string} cwd корінь
301
+ * @param {number} sinceOffset байтовий зсув логу перед прогоном
302
+ * @param {(s: string) => void} log логер
303
+ * @returns {void}
304
+ */
305
+ export function maybeAnalyzeEscalation(cwd, sinceOffset, log) {
306
+ if (!analysisEnabled()) return
307
+ const records = readEscalationRecords(escalationLogPath(), sinceOffset)
308
+ if (records.length === 0) return
309
+ if (!CLOUD_AVG) {
310
+ log('\nℹ️ escalation-analysis: були LLM-ескалації, але N_CLOUD_AVG_MODEL не заданий — аналіз пропущено.\n')
311
+ return
312
+ }
313
+ log('\n🔬 escalation-analysis: аналізую ескалації цього прогону…\n')
314
+ analyzeAndReport(records, cwd, log)
315
+ }
@@ -0,0 +1,31 @@
1
+ ---
2
+ type: JS Module
3
+ title: analyze-escalation.mjs
4
+ resource: npm/scripts/lib/fix/analyze-escalation.mjs
5
+ docgen:
6
+ crc: f802e47f
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ ---
10
+
11
+ Аналітика escalation-логу fix-конформності. Читає записи рунгів драбини (`escalation-log.mjs`) — весь лог або записи одного прогону (від байтового зсуву), — ділить на чанки за бюджетом символів і просить хмарну **avg**-модель запропонувати, як зменшити LLM-залежність: нові детерміновані T0-патерни, уточнення `.mdc`-інструкцій або зміни скриптів пакета. Результат — markdown-звіт у `.n-cursor/fix-escalation-analysis.md` (append із timestamp). Викликається CLI `n-cursor analyze-escalation` (весь лог) і наприкінці `lint --full` (записи прогону).
12
+
13
+ ## Поведінка
14
+
15
+ `readEscalationRecords` читає JSONL від байтового зсуву (зсув на межі рядка — мультибайт не б'ється; биті рядки пропускаються); `escalationLogSize` дає зсув для scope «цей прогін». `chunkRecords` стискає записи й ділить на чанки, щоб JSON кожного не перевищив бюджет. `analyzeEscalations` (синхронний — callLlm spawnSync-based) робить виклик avg-моделі по кожному чанку, а за кількох чанків — фінальний синтез; помилки моделі ковтаються в `null` (аналіз не валить lint). `maybeAnalyzeEscalation` — хук lint: gated kill-switch `N_CURSOR_FIX_ANALYZE`, наявністю `CLOUD_AVG` і записів.
16
+
17
+ ## Публічний API
18
+
19
+ - `analysisEnabled()` — чи дозволено авто-аналіз (kill-switch `N_CURSOR_FIX_ANALYZE`).
20
+ - `escalationLogSize(path?)` — розмір логу в байтах (since-offset).
21
+ - `readEscalationRecords(path, sinceOffset?)` — записи від зсуву.
22
+ - `chunkRecords(records, maxChars?)` — чанки стиснених записів.
23
+ - `analyzeEscalations(records, opts?)` — `{ report, chunks, reason }`; `opts.callLlm` інжектовний.
24
+ - `analysisReportPath(cwd?)` / `writeAnalysisReport(report, cwd, ts)` — шлях/запис звіту.
25
+ - `runEscalationAnalysisCli(args, cwd?)` — CLI: весь лог → звіт.
26
+ - `maybeAnalyzeEscalation(cwd, sinceOffset, log)` — хук наприкінці `lint --full`.
27
+
28
+ ## Гарантії поведінки
29
+
30
+ - Звертається до мережі лише при виклику avg-моделі (через pi/omlx за префіксом model-id).
31
+ - Помилки виклику моделі перехоплює (fail-safe): аналіз не впливає на exit-код lint.
@@ -0,0 +1,27 @@
1
+ ---
2
+ type: JS Module
3
+ title: escalation-log.mjs
4
+ resource: npm/scripts/lib/fix/escalation-log.mjs
5
+ docgen:
6
+ crc: 07ae959f
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ ---
10
+
11
+ Append-only JSONL-лог драбини ескалації конформність-фіксу. Один запис на рунг драбини: модель, чи виклик удався, чи правило стало зеленим після рунга («чи допомогло»), залишковий violation і само-аналіз моделі (`diagnosis`). Доповнює always-on wire-trace — той знає вміст викликів, але не результат re-check; join — за полем `caller` (`fix:<rule>:<rung>`).
12
+
13
+ ## Поведінка
14
+
15
+ `escalationLogPath` резолвить шлях: `N_CURSOR_FIX_ESCALATION_LOG` як kill-switch (`0|false|off|no` → лог вимкнено, повертає `null`) або як явний шлях; інакше дефолт `<cwd>/.n-cursor/fix-escalation.jsonl`.
16
+
17
+ `logEscalation` дописує один JSONL-рядок (no-op, якщо лог вимкнено). Поля `remainingViolation` і `diagnosis` обрізаються до межі; `recheckOk` обнуляє `remainingViolation`. Помилки запису ковтаються — лог діагностичний і не має валити сам фікс.
18
+
19
+ ## Публічний API
20
+
21
+ - `escalationLogPath()` — шлях активного логу або `null`, якщо вимкнено.
22
+ - `logEscalation(rec)` — дописує запис рунга у JSONL.
23
+
24
+ ## Гарантії поведінки
25
+
26
+ - Не звертається до мережі.
27
+ - Перехоплює помилки запису і не пропускає винятків назовні (fail-safe).
@@ -8,6 +8,8 @@ resource: npm/scripts/lib/fix/
8
8
 
9
9
  | Файл | Тип |
10
10
  |---|---|
11
+ | [analyze-escalation.mjs](analyze-escalation.md) | JS Module |
12
+ | [escalation-log.mjs](escalation-log.md) | JS Module |
11
13
  | [llm-fix-apply.mjs](llm-fix-apply.md) | JS Module |
12
14
  | [llm-lint-fix.mjs](llm-lint-fix.md) | JS Module |
13
15
  | [llm-worker.mjs](llm-worker.md) | JS Module |
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: llm-fix-apply.mjs
4
4
  resource: npm/scripts/lib/fix/llm-fix-apply.mjs
5
5
  docgen:
6
- crc: 80befb00
6
+ crc: 62e475fa
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -14,7 +14,7 @@ docgen:
14
14
 
15
15
  parseChangesResponse парсить сирий текст відповіді моделі, щоб витягнути структуру змін у форматі патчу або повернути null у разі невдачі парсингу.
16
16
  readFilesForFix зчитує вміст файлів, зазначених у списку відносних шляхів, відносно кореня проєкту, повертаючи список об'єктів з шляхом та вмістом або null для неіснуючих файлів.
17
- applyChanges записує нові вмісти в файли, використовуючи наданий список змін, відносно кореня проєкту, повертаючи статус успіху або помилки.
17
+ applyChanges записує нові вмісти в файли, використовуючи наданий список змін, відносно кореня проєкту; перед записом створює батьківську теку (`mkdirSync recursive`) — модель може запропонувати новий файл у ще неіснуючому каталозі. Повертає статус успіху або помилки.
18
18
 
19
19
  ## Публічний API
20
20
 
@@ -3,27 +3,24 @@ type: JS Module
3
3
  title: llm-worker.mjs
4
4
  resource: npm/scripts/lib/fix/llm-worker.mjs
5
5
  docgen:
6
- crc: 00730451
6
+ crc: de9eb68c
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
10
10
 
11
- Модуль визначає моделі для стандартних та складних виправлень коду. Він надає функції для ініціалізації моделей (`MODEL`, `MODEL_HEAVY`) та для виконання роботи з мовною моделлю (`runLlmWorker`), застосовуючи згенеровані зміни до файлів проєкту.
11
+ Модуль виправляє одне rule-порушення конформності через LLM: читає `.mdc` правила, дістає файли з violation-output, будує prompt і застосовує згенеровані зміни. Підтримує **feedback-режим** драбини ескалації — у повторний виклик передається контекст попереднього рунга, а модель спершу формулює `diagnosis` («чому попередня спроба не вдалася»), тоді виправляє.
12
12
 
13
13
  ## Поведінка
14
14
 
15
- MODEL визначає модель за замовчуванням для виправлення, використовуючи змінну середовища або значення за замовчуванням.
16
- MODEL_HEAVY визначає модель для важких виправлень, використовуючи змінну середовища або значення за замовчуванням.
17
- runLlmWorker виправляє одне правило-порушення, викликаючи LLM для генерації змін, а потім застосовує ці зміни до файлів проєкту.
15
+ `runLlmWorker` читає `.mdc` правила, читає файли з violation-output, будує prompt і викликає модель через спільний `callLlm` (з міткою `caller` для wire-trace). Відповідь очікується як JSON `{diagnosis, changes}`; зміни застосовуються до файлів повним вмістом.
16
+
17
+ Якщо переданий `feedback`, prompt додає блок попереднього рунга (модель, попередньо змінені файли, помилка) і просить діагностувати причину невдачі у полі `diagnosis`. Результат повертається з `changes` і `diagnosis` навіть при невдачі — щоб драбина (`orchestrator.mjs`) їх залогувала і прокинула далі.
18
18
 
19
19
  ## Публічний API
20
20
 
21
- MODELОсновна модель для обробки даних.
22
- MODEL_HEAVY — Важка модель для складних обчислень.
23
- runLlmWorker — Виправляє одне порушення правила, використовуючи шаблон C1.
21
+ - `runLlmWorker(ruleId, violationOutput, projectRoot, opts)` виправляє одне порушення; `opts`: `model`, `feedback`, `caller`. Повертає `{ ok, error, changes, diagnosis }`.
24
22
 
25
23
  ## Гарантії поведінки
26
24
 
27
- - Read-only: файл не виконує операцій запису у файлову систему.
28
- - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
29
- - Не звертається до мережі.
25
+ - Перехоплює помилки виклику моделі й не пропускає винятків назовні (fail-safe): дружнє повідомлення для відсутнього API-ключа хмарного провайдера.
26
+ - Звертається до мережі під час виклику моделі (omlx HTTP або pi CLI, залежно від префікса model-id).
@@ -3,24 +3,33 @@ type: JS Module
3
3
  title: orchestrator.mjs
4
4
  resource: npm/scripts/lib/fix/orchestrator.mjs
5
5
  docgen:
6
- crc: 3a66072d
6
+ crc: 78dfe86b
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
10
10
 
11
- Модуль керує процесом валідації та виправлення правил. Він ініціює перевірку всіх правил. Якщо виявлено порушення, процес ітеративно застосовує детермінований механізм виправлення (T0) та виправлення на основі великої мовної моделі (T1) до невирішених правил. Цей цикл повторюється до досягнення стану, коли всі правила відповідають вимогам, або до перевищення встановленого ліміту ітерацій. Функція `runOrchestratorCli` запускає цей процес.
11
+ Модуль керує процесом валідації та виправлення конформності правил. Він перевіряє правила, застосовує детермінований механізм (T0), а нерозв'язані порушення проводить по **драбині ескалації моделей**: локальна min-модель той самий локальний тир із feedback хмарна min хмарна avg. Кожен крок драбини логується для подальшого аналізу. Функція `runOrchestratorCli` запускає процес.
12
12
 
13
13
  ## Поведінка
14
14
 
15
- 1. `runOrchestratorCli` виконує початкову перевірку всіх правил. Якщо порушень немає, процес завершується успішно.
16
- 2. Якщо порушення виявлено, ініціюється ітеративний цикл до максимальної кількості ітерацій.
17
- 3. На кожній ітерації виконується детермінований фікс (крок T0). Це зменшує кількість правил, що потребують подальшої обробки.
18
- 4. Якщо після кроку T0 залишилися невирішені правила, виконується LLM-фікс (крок T1). Для кожного невирішеного правила застосовується відповідна модель, яка може ескалуватися при повторних провалах.
19
- 5. Після LLM-фіксу виконується повторна перевірка всіх правил.
20
- 6. Якщо після перевірки всі правила чисті, процес завершується успішно.
21
- 7. Якщо після завершення всіх ітерацій залишаються невирішені правила, процес завершується з позначкою про невирішені проблеми.
15
+ 1. `runOrchestratorCli` виконує початкову перевірку правил. Якщо порушень немає успіх.
16
+ 2. Один прохід детермінованого фіксу (T0) зменшує набір порушень без LLM.
17
+ 3. `buildLadder` будує драбину з доступних тирів (`N_LOCAL_MIN_MODEL`, `N_CLOUD_MIN_MODEL`, `N_CLOUD_AVG_MODEL`); незадані тири відсіюються. Якщо драбина порожня процес завершується з ознакою нерозв'язаних порушень.
18
+ 4. `escalateRule` проводить кожне правило по драбині до першого зеленого re-check:
19
+ - рунг `local-min` перший прохід без feedback;
20
+ - рунг `local-min-retry` той самий локальний тир, але з feedback попереднього рунга (попередні зміни + залишковий violation);
21
+ - рунги `cloud-min` / `cloud-avg` хмарні моделі (через pi), теж із feedback.
22
+ 5. Достроковий вихід драбини: systemic-помилка локального тиру пропускає рунги тієї ж моделі; відсутній API-ключ на хмарному обриває драбину; вичерпаний avg-кеп пропускає avg-рунг (із записом у лог).
23
+ 6. Після обробки всіх правил — фінальна перевірка. Усі чисті → успіх; інакше — ознака нерозв'язаних.
24
+
25
+ ## Публічний API
26
+
27
+ - `buildLadder({ localMin, cloudMin, cloudAvg })` — будує драбину рунгів із наявних тирів.
28
+ - `escalateRule(rule, cwd, deps)` — проводить одне правило по драбині; повертає `{ resolved, avgUsed }`. Залежності (`worker`, `check`, `clock`) інжектовні для тестів.
29
+ - `parseOrchestratorArgs(args)` — парсить `--max-avg N` (кеп на хмарні avg-виклики) і фільтр правил.
30
+ - `runOrchestratorCli(args, cwd)` — точка входу; повертає `0` (усе чисто) або `1` (нерозв'язані).
22
31
 
23
32
  ## Гарантії поведінки
24
33
 
25
- - Read-only: файл не виконує операцій запису у файлову систему.
26
- - Не звертається до мережі.
34
+ - Кожен рунг драбини дописується в escalation-лог (`escalation-log.mjs`): модель, чи виклик удався, чи правило стало зеленим, залишковий violation і само-аналіз моделі.
35
+ - Звертається до мережі лише на хмарних рунгах драбини (через pi).
@@ -3,19 +3,18 @@ type: JS Module
3
3
  title: t0.mjs
4
4
  resource: npm/scripts/lib/fix/t0.mjs
5
5
  docgen:
6
- crc: 0321ecc1
6
+ crc: 524d91e2
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
10
10
 
11
- Модуль керує автоматичним застосуванням паттернів до правил для виправлення виводів порушень. Він визначає, які автоматичні паттерни можуть бути застосовані до правил, використовуючи `filterT0AutoRules`, і запускає повний процес виправлення для всіх відповідних правил через `applyT0Auto` та `runT0AutoCli`. Код працює у режимі fail-safe, перехоплюючи помилки та не викидаючи винятків назовні. Робота модуля залежить від конфігурацій `extensions.json` та `package-lock.json`.
11
+ Модуль керує детермінованим (без LLM) застосуванням паттернів до виводів порушень конформності. Паттерни: `vscode-ext-add` (дописати розширення), `rm-forbidden-file` (видалити заборонений файл), `changelog-create-change-file` (створити change-файл через канонічну `writeChange` для воркспейсів із «немає change-файлу» прибирає ескалацію в LLM на цьому кейсі). `filterT0AutoRules` визначає, які правила мають придатний паттерн; `applyT0Auto`/`runT0AutoCli` застосовують. Паттерн може бути sync або async (await нормалізує). Fail-safe: помилки перехоплюються.
12
12
 
13
13
  ## Поведінка
14
14
 
15
- Поведінка
16
- applyT0Auto застосовує визначені автоматичні паттерни до одного виводу порушення, щоб виправити його.
17
- filterT0AutoRules повертає список ідентифікаторів правил, для яких існують відповідні автоматичні паттерни.
18
- runT0AutoCli запускає процес виправлення T0-автоматично для всіх провальних правил, застосовуючи паттерни, повторно перевіряючи стан і виводячи підсумок.
15
+ applyT0Auto застосовує придатні паттерни до одного виводу порушення (await на apply — паттерн може бути async, як `changelog-create-change-file`).
16
+ filterT0AutoRules повертає id правил, для яких існує придатний паттерн.
17
+ runT0AutoCli запускає T0-auto для всіх провальних правил, повторно перевіряє check-gate і виводить підсумок.
19
18
 
20
19
  ## Публічний API
21
20
 
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Append-only JSONL-лог ескалації конформність-фіксу (спека
3
+ * 2026-06-19-fix-escalation-cascade-design). Один запис **на рунг драбини** —
4
+ * фіксує `model`, `withFeedback`, чи виклик удався (`callOk`/`callError`), чи
5
+ * правило стало зеленим після цього рунга (`recheckOk` = «чи допомогло»),
6
+ * залишковий violation і `diagnosis` (само-аналіз моделі «чому не вдалося»).
7
+ *
8
+ * Це доповнення до always-on wire-trace (`lib/omlx-trace.mjs`): trace знає
9
+ * `messages`/`reasoning`/`usage` кожного виклику, але **не** знає результату
10
+ * re-check — а саме «чи допомогло» й потрібне для пост-аналізу драбини. Join із
11
+ * trace — за полем `caller` (`fix:<rule>:<rung>`), яке цей модуль і формує.
12
+ *
13
+ * Шлях — дзеркало `tracePath()`: `N_CURSOR_FIX_ESCALATION_LOG` (kill-switch
14
+ * `0|false|off|no` → не писати; інакше явний шлях) → дефолт
15
+ * `<cwd>/.n-cursor/fix-escalation.jsonl`.
16
+ */
17
+ import { appendFileSync, mkdirSync } from 'node:fs'
18
+ import { dirname, join } from 'node:path'
19
+ import { cwd, env } from 'node:process'
20
+
21
+ /** Значення `N_CURSOR_FIX_ESCALATION_LOG`, що вимикають лог повністю. */
22
+ const KILL_VALUES = new Set(['0', 'false', 'off', 'no'])
23
+
24
+ /** Межа обрізки `remainingViolation`/`diagnosis` у записі (символів). */
25
+ const MAX_FIELD_CHARS = 2000
26
+
27
+ /**
28
+ * Шлях активного escalation-логу або `null`, якщо вимкнено kill-switch-ем.
29
+ * @returns {string|null} шлях до .jsonl або null
30
+ */
31
+ export function escalationLogPath() {
32
+ const override = env.N_CURSOR_FIX_ESCALATION_LOG
33
+ if (override !== undefined) {
34
+ if (KILL_VALUES.has(override.toLowerCase())) return null
35
+ if (override) return override
36
+ }
37
+ return join(cwd(), '.n-cursor', 'fix-escalation.jsonl')
38
+ }
39
+
40
+ /**
41
+ * Обрізає рядок до `MAX_FIELD_CHARS` (null/undefined → null).
42
+ * @param {string|null|undefined} s вхід
43
+ * @returns {string|null} обрізаний рядок або null
44
+ */
45
+ function cap(s) {
46
+ if (s === null || s === undefined) return null
47
+ return s.length > MAX_FIELD_CHARS ? s.slice(0, MAX_FIELD_CHARS) : s
48
+ }
49
+
50
+ /**
51
+ * Дописує один запис рунга у JSONL-лог (no-op, якщо вимкнено). Помилки запису
52
+ * ковтаються — лог діагностичний, не має валити сам фікс.
53
+ * @param {object} rec запис рунга
54
+ * @param {string} rec.ts ISO-час завершення рунга
55
+ * @param {string} rec.ruleId id правила
56
+ * @param {number} rec.rung індекс рунга драбини (0-based)
57
+ * @param {string} rec.tier мітка тиру (`local-min`|`local-min-retry`|`cloud-min`|`cloud-avg`)
58
+ * @param {string} rec.model model-id (порожній → pi-дефолт)
59
+ * @param {boolean} rec.withFeedback чи передавався feedback попереднього рунга
60
+ * @param {boolean} rec.callOk чи виклик моделі+apply удався
61
+ * @param {string|null} rec.callError помилка виклику (null, якщо callOk)
62
+ * @param {boolean} rec.recheckOk чи правило стало зеленим після рунга («чи допомогло»)
63
+ * @param {string|null} rec.remainingViolation залишковий violation (null, якщо recheckOk)
64
+ * @param {string|null} rec.diagnosis само-аналіз моделі «чому попередній рунг не вдався»
65
+ * @param {number} rec.ms тривалість рунга (мс)
66
+ * @returns {void}
67
+ */
68
+ export function logEscalation(rec) {
69
+ const path = escalationLogPath()
70
+ if (!path) return
71
+ const line =
72
+ JSON.stringify({
73
+ ts: rec.ts,
74
+ ruleId: rec.ruleId,
75
+ rung: rec.rung,
76
+ tier: rec.tier,
77
+ model: rec.model,
78
+ withFeedback: rec.withFeedback,
79
+ callOk: rec.callOk,
80
+ callError: cap(rec.callError),
81
+ recheckOk: rec.recheckOk,
82
+ remainingViolation: rec.recheckOk ? null : cap(rec.remainingViolation),
83
+ diagnosis: cap(rec.diagnosis),
84
+ ms: rec.ms
85
+ }) + '\n'
86
+ try {
87
+ mkdirSync(dirname(path), { recursive: true })
88
+ appendFileSync(path, line, 'utf8')
89
+ } catch {
90
+ /* лог діагностичний — ковтаємо помилки запису */
91
+ }
92
+ }
@@ -3,8 +3,8 @@
3
3
  * під фікс і застосування змін. Використовують і `llm-worker.mjs` (конформність), і
4
4
  * `llm-lint-fix.mjs` (per-tool лінтер-фіксери) — щоб не дублювати парс/apply (knip/jscpd).
5
5
  */
6
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
7
- import { join } from 'node:path'
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
7
+ import { dirname, join } from 'node:path'
8
8
 
9
9
  const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
10
10
 
@@ -69,7 +69,11 @@ export function applyChanges(changes, projectRoot) {
69
69
  for (const change of changes) {
70
70
  if (!change.path || typeof change.content !== 'string') continue
71
71
  try {
72
- writeFileSync(join(projectRoot, change.path), change.content, 'utf8')
72
+ const abs = join(projectRoot, change.path)
73
+ // Створюємо батьківську теку перед записом: модель може запропонувати новий файл
74
+ // у ще неіснуючому каталозі (напр. `<ws>/.changes/…`) — інакше writeFileSync ENOENT.
75
+ mkdirSync(dirname(abs), { recursive: true })
76
+ writeFileSync(abs, change.content, 'utf8')
73
77
  } catch (error) {
74
78
  return { ok: false, error: `write ${change.path}: ${error.message}` }
75
79
  }