@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
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: llm-lint-fix.mjs
|
|
4
|
-
resource: npm/scripts/lib/fix/llm-lint-fix.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: de4439e9
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 90
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
Модуль реалізує механізм `omlx-фікс` для обробки знахідок лінтера, що стосуються `detect-only` тулів, які не мають нативного механізму виправлення (відповідно до `lint-orchestrator-fix-readonly`). Він зчитує уражені файли, ініціює запит до моделі через `callLlm` (маршрутизація через `omlx/<model>` з фолбеком каскаду), щоб отримати пропоновані зміни. Ці зміни застосовуються до файлової системи за допомогою спільного ядра `llm-fix-apply`.
|
|
12
|
-
|
|
13
|
-
## Поведінка
|
|
14
|
-
|
|
15
|
-
1. Зчитує вміст файлів, зазначених у `filePaths`, використовуючи `projectRoot`.
|
|
16
|
-
2. Формує детальний запит для моделі, включаючи назву тула, інструкцію, сирий вивід знахідок та вміст зчитаних файлів.
|
|
17
|
-
3. Надсилає сформований запит до моделі для отримання виправлень.
|
|
18
|
-
4. Парсить відповідь моделі для вилучення пропонованих змін.
|
|
19
|
-
5. Перевіряє, чи містить відповідь помилку або не містить жодних змін.
|
|
20
|
-
6. Застосовує вилучені зміни до файлової системи, використовуючи `projectRoot`.
|
|
21
|
-
7. Повертає результат виконання `llmLintFix`, що включає статус успіху, можливу помилку або список шляхів, які були успішно виправлені.
|
|
22
|
-
|
|
23
|
-
## Публічний API
|
|
24
|
-
|
|
25
|
-
llmLintFix — автоматично виправляє помилки, знайдені лінтером, використовуючи omlx.
|
|
26
|
-
|
|
27
|
-
## Гарантії поведінки
|
|
28
|
-
|
|
29
|
-
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
30
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
31
|
-
- Не звертається до мережі.
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: llm-worker.mjs
|
|
4
|
-
resource: npm/scripts/lib/fix/llm-worker.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 5d019a98
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Огляд
|
|
12
|
-
|
|
13
|
-
Модуль виділяє унікальні відносні шляхи файлів, пов'язаних із порушеннями, та виконує LLM-виклик для виправлення одного rule-порушення. Підтримує вибір sub-check `.mdc` за конкретним target-файлом — замість повного `n-{id}.mdc` передає моделі лише релевантну секцію правила (1–2 KB замість 20+ KB).
|
|
14
|
-
|
|
15
|
-
## Поведінка
|
|
16
|
-
|
|
17
|
-
`extractFilePaths` витягує унікальні відносні шляхи файлів із violation output, пріоритетно розбираючи рядки ❌ (файли що потребують фіксу), потім generic-regex для контексту. Розуміє workspace-prefix `[npm] path/file.ext → npm/path/file.ext`.
|
|
18
|
-
|
|
19
|
-
`selectSubCheckMdc` читає `policy/<concern>/target.json` з каталогу `npm/rules/{ruleId}/` пакету, знаходить concerns із `files.single`, що відповідають failing-файлам із violation output, і повертає конкатенацію відповідних `.mdc`. Повертає `null` якщо правило не має policy-підкаталогу або жоден concern не збігається з ❌-файлами.
|
|
20
|
-
|
|
21
|
-
`runLlmWorker` спочатку викликає `selectSubCheckMdc` — і якщо отримує match, передає моделі лише той sub-check `.mdc`. Якщо match немає — fallback на повний `n-{id}.mdc`. Далі читає файли з violation output, будує prompt, викликає LLM через `callLlmRich` і застосовує зміни.
|
|
22
|
-
|
|
23
|
-
## Публічний API
|
|
24
|
-
|
|
25
|
-
`extractFilePaths(output)` — повертає унікальні відносні шляхи файлів з violation output (❌-рядки першими).
|
|
26
|
-
|
|
27
|
-
`runLlmWorker(ruleId, violationOutput, projectRoot, opts)` — виправляє одне порушення правила через LLM. Повертає `{ ok, error?, changes, diagnosis, reasoning, reasoningSource, promptSummary }`. `promptSummary.subCheckMdc: boolean` вказує чи використано точковий sub-check замість повного mdc.
|
|
28
|
-
|
|
29
|
-
## Гарантії поведінки
|
|
30
|
-
|
|
31
|
-
- Читає `policy/` безпосередньо з пакету (`import.meta.dirname/../../../rules/`), без pre-generation файлів.
|
|
32
|
-
- Fallback на повний `n-{id}.mdc` при: відсутності policy-підкаталогу, `walkGlob`-таргетах, відсутності ❌-файлів у violation, нульовому match.
|
|
33
|
-
- Перехоплює всі помилки LLM і повертає структурований `{ ok: false, error }`.
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: verbose-block.mjs
|
|
4
|
-
resource: npm/scripts/lib/fix/verbose-block.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 1a921dca
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
Цей файл виводить детальний блок інформації після кожного LLM-рунга у режимі `--full`. Він друкує стислий опис промпту, включаючи деталі про правила, порушення та зворотний зв'язок. Також виводиться монолог моделі, якщо він присутній. Вивід блоку вимикається при встановленні `N_CURSOR_FIX_VERBOSE=off`.
|
|
12
|
-
|
|
13
|
-
## Поведінка
|
|
14
|
-
|
|
15
|
-
1. Викликається `printVerboseBlock` для виведення детальної інформації після кожного LLM-рунга у режимі `--full`.
|
|
16
|
-
2. `printVerboseBlock` виводить стислий опис промпту, включаючи ID правила, довжину правила, довжину порушення, кількість та розмір файлів, а також інформацію про зворотний зв'язок (наявність, модель, кількість змін, помилка).
|
|
17
|
-
3. Якщо надано монолог моделі (reasoning), `printVerboseBlock` виводить його, показуючи прев'ю (з можливим зазначенням загальної довжини), і вказує джерело цього монологу.
|
|
18
|
-
4. Якщо монологу моделі немає, `printVerboseBlock` виводить повідомлення про відсутність монологу.
|
|
19
|
-
5. Виведення здійснюється лише якщо змінна середовища `N_CURSOR_FIX_VERBOSE` не встановлена як `off`.
|
|
20
|
-
|
|
21
|
-
## Публічний API
|
|
22
|
-
|
|
23
|
-
printVerboseBlock — виводить детальний опис промпту та внутрішній роздум моделі після символу завершення рядка рунга.
|
|
24
|
-
|
|
25
|
-
## Гарантії поведінки
|
|
26
|
-
|
|
27
|
-
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Спільне ядро LLM-фіксу: парс відповіді `{changes:[{path,content}]}`, читання файлів
|
|
3
|
-
* під фікс і застосування змін. Використовують і `llm-worker.mjs` (конформність), і
|
|
4
|
-
* `llm-lint-fix.mjs` (per-tool лінтер-фіксери) — щоб не дублювати парс/apply (knip/jscpd).
|
|
5
|
-
*/
|
|
6
|
-
import { execSync } from 'node:child_process'
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
-
import { basename, dirname, join } from 'node:path'
|
|
9
|
-
|
|
10
|
-
const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Парсить JSON-відповідь моделі: прямий JSON → ```json-блок``` → перший `{…}`-блок.
|
|
14
|
-
* @param {string} text сирий текст відповіді
|
|
15
|
-
* @returns {{ changes?: Array<{path:string,content:string}>, error?: string } | null} патч або null
|
|
16
|
-
*/
|
|
17
|
-
export function parseChangesResponse(text) {
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(text)
|
|
20
|
-
} catch {
|
|
21
|
-
/* fallthrough */
|
|
22
|
-
}
|
|
23
|
-
const block = text.match(JSON_CODE_BLOCK_RE)
|
|
24
|
-
if (block) {
|
|
25
|
-
try {
|
|
26
|
-
return JSON.parse(block[1].trim())
|
|
27
|
-
} catch {
|
|
28
|
-
/* fallthrough */
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const start = text.indexOf('{')
|
|
32
|
-
const end = text.lastIndexOf('}')
|
|
33
|
-
if (start !== -1 && end > start) {
|
|
34
|
-
try {
|
|
35
|
-
return JSON.parse(text.slice(start, end + 1))
|
|
36
|
-
} catch {
|
|
37
|
-
/* fallthrough */
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return null
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Шукає файл за basename у дереві проєкту (fallback коли прямий шлях не існує).
|
|
45
|
-
* Повертає відносний шлях якщо знайдено рівно один матч, інакше `null` (ambiguous/not found).
|
|
46
|
-
* @param {string} name basename файлу
|
|
47
|
-
* @param {string} projectRoot абсолютний корінь
|
|
48
|
-
* @returns {string|null} відносний шлях або null
|
|
49
|
-
*/
|
|
50
|
-
function findByBasename(name, projectRoot) {
|
|
51
|
-
try {
|
|
52
|
-
const raw = execSync(
|
|
53
|
-
`find . -maxdepth 7 -name '${name.replace(/'/g, "'\\''")}' -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.worktrees/*'`,
|
|
54
|
-
{ cwd: projectRoot, encoding: 'utf8', timeout: 3000 }
|
|
55
|
-
).trim()
|
|
56
|
-
const hits = raw.split('\n').filter(Boolean)
|
|
57
|
-
return hits.length === 1 ? hits[0].replace(/^\.\//, '') : null
|
|
58
|
-
} catch {
|
|
59
|
-
return null
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Читає існуючі файли за відносними шляхами у форму `{path, content}` (для prompt).
|
|
65
|
-
* Якщо файл не знайдений за прямим шляхом — намагається знайти за basename через `find`.
|
|
66
|
-
* Повертає resolved path (може відрізнятись від вхідного коли `find` знайшов реальне місце).
|
|
67
|
-
* @param {string[]} filePaths відносні шляхи від кореня
|
|
68
|
-
* @param {string} projectRoot абсолютний корінь
|
|
69
|
-
* @returns {Array<{path:string, content:string}>} наявні файли з вмістом
|
|
70
|
-
*/
|
|
71
|
-
export function readFilesForFix(filePaths, projectRoot) {
|
|
72
|
-
return filePaths
|
|
73
|
-
.map(p => {
|
|
74
|
-
let abs = join(projectRoot, p)
|
|
75
|
-
let resolvedPath = p
|
|
76
|
-
if (!existsSync(abs)) {
|
|
77
|
-
const found = findByBasename(basename(p), projectRoot)
|
|
78
|
-
if (found) {
|
|
79
|
-
resolvedPath = found
|
|
80
|
-
abs = join(projectRoot, found)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (!existsSync(abs)) return null
|
|
84
|
-
try {
|
|
85
|
-
return { path: resolvedPath, content: readFileSync(abs, 'utf8') }
|
|
86
|
-
} catch {
|
|
87
|
-
return null
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
.filter(Boolean)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Застосовує `changes` до ФС (повний вміст файлу, не diff).
|
|
95
|
-
* @param {Array<{path:string, content:string}>} changes зміни
|
|
96
|
-
* @param {string} projectRoot абсолютний корінь
|
|
97
|
-
* @returns {{ ok: boolean, error?: string }} статус
|
|
98
|
-
*/
|
|
99
|
-
export function applyChanges(changes, projectRoot) {
|
|
100
|
-
for (const change of changes) {
|
|
101
|
-
if (!change.path || typeof change.content !== 'string') continue
|
|
102
|
-
try {
|
|
103
|
-
const abs = join(projectRoot, change.path)
|
|
104
|
-
// Створюємо батьківську теку перед записом: модель може запропонувати новий файл
|
|
105
|
-
// у ще неіснуючому каталозі (напр. `<ws>/.changes/…`) — інакше writeFileSync ENOENT.
|
|
106
|
-
mkdirSync(dirname(abs), { recursive: true })
|
|
107
|
-
writeFileSync(abs, change.content, 'utf8')
|
|
108
|
-
} catch (error) {
|
|
109
|
-
return { ok: false, error: `write ${change.path}: ${error.message}` }
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return { ok: true }
|
|
113
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
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,332 +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
|
-
* Для правил з `policy/<concern>/` підбирає лише ті concern-.mdc, що відповідають
|
|
51
|
-
* файлам з ❌-рядків violation output. Читає безпосередньо з пакету (`PACKAGE_RULES_DIR`),
|
|
52
|
-
* тому не потребує pre-generation. Fallback — null (→ повний n-{id}.mdc у caller).
|
|
53
|
-
* @param {string} ruleId ID правила
|
|
54
|
-
* @param {string} violationOutput violation output
|
|
55
|
-
* @returns {string|null} конкатенація релевантних .mdc або null
|
|
56
|
-
*/
|
|
57
|
-
function selectSubCheckMdc(ruleId, violationOutput) {
|
|
58
|
-
const policyDir = join(PACKAGE_RULES_DIR, ruleId, 'policy')
|
|
59
|
-
if (!existsSync(policyDir)) return null
|
|
60
|
-
|
|
61
|
-
const failPaths = extractFailPaths(violationOutput)
|
|
62
|
-
if (failPaths.length === 0) return null
|
|
63
|
-
|
|
64
|
-
const matched = []
|
|
65
|
-
let concerns
|
|
66
|
-
try {
|
|
67
|
-
concerns = readdirSync(policyDir, { withFileTypes: true })
|
|
68
|
-
} catch {
|
|
69
|
-
return null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
for (const entry of concerns) {
|
|
73
|
-
if (!entry.isDirectory()) continue
|
|
74
|
-
const concernDir = join(policyDir, entry.name)
|
|
75
|
-
const targetPath = join(concernDir, 'target.json')
|
|
76
|
-
if (!existsSync(targetPath)) continue
|
|
77
|
-
|
|
78
|
-
let target
|
|
79
|
-
try {
|
|
80
|
-
target = JSON.parse(readFileSync(targetPath, 'utf8'))
|
|
81
|
-
} catch {
|
|
82
|
-
continue
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const targetFile = target?.files?.single
|
|
86
|
-
if (!targetFile) continue // walkGlob та інші типи → skip, fallback на main.mdc
|
|
87
|
-
|
|
88
|
-
// Перевіряємо чи хоч один failing path закінчується на targetFile
|
|
89
|
-
const hit = failPaths.some(p => p === targetFile || p.endsWith(`/${targetFile}`))
|
|
90
|
-
if (!hit) continue
|
|
91
|
-
|
|
92
|
-
const mdcEntry = readdirSync(concernDir).find(f => f.endsWith('.mdc'))
|
|
93
|
-
if (!mdcEntry) continue
|
|
94
|
-
matched.push(readFileSync(join(concernDir, mdcEntry), 'utf8').trim())
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return matched.length > 0 ? matched.join('\n\n') : null
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Витягує відносні шляхи файлів із violation output.
|
|
102
|
-
* Розуміє workspace-prefix: `[npm] skills/foo.mjs` → `npm/skills/foo.mjs`.
|
|
103
|
-
* Спочатку явно парсить рядки ❌ (найвищий сигнал — файл потребує фіксу),
|
|
104
|
-
* потім підхоплює решту файлів generic-regex (контекст для читання).
|
|
105
|
-
* @param {string} output violation output з fix check
|
|
106
|
-
* @returns {string[]} унікальні відносні шляхи (від кореня проєкту)
|
|
107
|
-
*/
|
|
108
|
-
export function extractFilePaths(output) {
|
|
109
|
-
const seen = new Set()
|
|
110
|
-
const results = []
|
|
111
|
-
const add = p => {
|
|
112
|
-
if (!seen.has(p)) {
|
|
113
|
-
seen.add(p)
|
|
114
|
-
results.push(p)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 1. Явні рядки ❌ — найвищий сигнал: саме ці файли потребують фіксу.
|
|
119
|
-
// Формати: `❌ [ws] path/file.ext:line — msg` та `❌ path/file.ext: msg`
|
|
120
|
-
// Роздільник після шляху: `:` (з пробілом або цифрою), `—` (em-dash), або кінець рядка.
|
|
121
|
-
const failSep = `(?::\\d+)?(?::\\s|[\\s—]|$)`
|
|
122
|
-
const failWsRe = new RegExp(`^\\s*❌\\s+\\[([\\w-]+)\\]\\s+([\\w./][\\w./-]*\\.(?:${FILE_EXTS}))${failSep}`, 'gm')
|
|
123
|
-
for (const m of output.matchAll(failWsRe)) add(`${m[1]}/${m[2]}`)
|
|
124
|
-
|
|
125
|
-
const failRe = new RegExp(`^\\s*❌\\s+(\\.?[\\w][\\w./-]*\\.(?:${FILE_EXTS}))${failSep}`, 'gm')
|
|
126
|
-
for (const m of output.matchAll(failRe)) add(m[1])
|
|
127
|
-
|
|
128
|
-
// 2. Generic-regex: підхоплює файли з ✅-рядків та описів (контекст для читання).
|
|
129
|
-
// Workspace: [npm] skills/foo.mjs
|
|
130
|
-
const wsRe = new RegExp(`\\[([\\w-]+)\\]\\s+([\\w./][\\w./-]*\\.(?:${FILE_EXTS}))(?::\\d+)?`, 'gm')
|
|
131
|
-
for (const m of output.matchAll(wsRe)) add(`${m[1]}/${m[2]}`)
|
|
132
|
-
|
|
133
|
-
// Без workspace: path/to/file.ext або ./file.ext
|
|
134
|
-
const re = new RegExp(`(?:^|\\s)(\\.?\\w[\\w./-]*\\.(?:${FILE_EXTS}))(?::\\d+)?`, 'gm')
|
|
135
|
-
for (const m of output.matchAll(re)) add(m[1])
|
|
136
|
-
|
|
137
|
-
return results
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Будує prompt для pi: правило + порушення + поточний вміст файлів.
|
|
142
|
-
* Будує опційний feedback-блок драбини ескалації: попередній рунг застосував
|
|
143
|
-
* зміни, але re-check лишився червоним. Просимо модель спершу (в полі `diagnosis`)
|
|
144
|
-
* сформулювати, **чому** попередня спроба не задовольнила правило, тоді виправити.
|
|
145
|
-
* @param {{ previousModel?: string, previousChanges?: Array<{path:string}>, previousError?: string|null } | null} feedback контекст попереднього рунга
|
|
146
|
-
* @returns {string[]} рядки prompt-блоку (порожній масив, якщо feedback немає)
|
|
147
|
-
*/
|
|
148
|
-
function buildFeedbackBlock(feedback) {
|
|
149
|
-
if (!feedback) return []
|
|
150
|
-
const changedPaths = (feedback.previousChanges ?? []).map(c => c.path).filter(Boolean)
|
|
151
|
-
return [
|
|
152
|
-
``,
|
|
153
|
-
`A PREVIOUS attempt (model: ${feedback.previousModel || 'pi'}) did NOT resolve this violation.`,
|
|
154
|
-
changedPaths.length > 0
|
|
155
|
-
? `Previously changed files: ${changedPaths.join(', ')}`
|
|
156
|
-
: `The previous attempt produced no usable changes.`,
|
|
157
|
-
feedback.previousError ? `Previous attempt error: ${feedback.previousError}` : ``,
|
|
158
|
-
`The violation output below is what STILL fails after that attempt.`,
|
|
159
|
-
`In the "diagnosis" field, briefly state WHY the previous attempt failed, then provide a corrected fix.`
|
|
160
|
-
].filter(line => line !== ``)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* @param {string} ruleId ID правила
|
|
165
|
-
* @param {string} ruleMdc вміст .mdc-файлу правила
|
|
166
|
-
* @param {string} output violation output
|
|
167
|
-
* @param {Array<{path:string, content:string}>} files прочитані файли (path + content)
|
|
168
|
-
* @param {{ previousModel?: string, previousChanges?: Array<{path:string}>, previousError?: string|null } | null} [feedback] контекст попереднього рунга драбини (для retry-with-feedback)
|
|
169
|
-
* @returns {string} текст промпта для pi
|
|
170
|
-
*/
|
|
171
|
-
function buildPrompt(ruleId, ruleMdc, output, files, feedback = null) {
|
|
172
|
-
const filesBlock =
|
|
173
|
-
files.length === 0
|
|
174
|
-
? '(no files identified)'
|
|
175
|
-
: files.map(f => `<file path="${f.path}">\n${f.content}\n</file>`).join('\n\n')
|
|
176
|
-
|
|
177
|
-
return [
|
|
178
|
-
`You fix project structure violations. Return ONLY valid JSON — no explanation, no markdown.`,
|
|
179
|
-
...buildFeedbackBlock(feedback),
|
|
180
|
-
``,
|
|
181
|
-
`Rule (n-${ruleId}.mdc):`,
|
|
182
|
-
`---`,
|
|
183
|
-
ruleMdc,
|
|
184
|
-
`---`,
|
|
185
|
-
``,
|
|
186
|
-
`Violation output:`,
|
|
187
|
-
output,
|
|
188
|
-
``,
|
|
189
|
-
`Current file contents:`,
|
|
190
|
-
filesBlock,
|
|
191
|
-
``,
|
|
192
|
-
`Return JSON with this exact shape:`,
|
|
193
|
-
`{"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"}]}`,
|
|
194
|
-
``,
|
|
195
|
-
`Rules:`,
|
|
196
|
-
`- "path" is relative to the project root`,
|
|
197
|
-
`- "content" is the complete new file content (not a diff)`,
|
|
198
|
-
`- Only include files that actually need to change`,
|
|
199
|
-
`- "diagnosis" is plain text inside the JSON — do NOT emit prose outside the JSON`,
|
|
200
|
-
`- If nothing can be fixed automatically, return {"diagnosis":"...","changes":[],"error":"reason"}`
|
|
201
|
-
].join('\n')
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Викликає LLM через `callLlmRich` (маршрут за префіксом model-id; wire-trace).
|
|
206
|
-
* Повертає reasoning поряд із текстом — для verbose-блоку оркестратора.
|
|
207
|
-
* Зберігає дружнє повідомлення про відсутній API-ключ для хмарних провайдерів.
|
|
208
|
-
* @param {string} prompt текст промпта
|
|
209
|
-
* @param {string} model назва моделі (provider/id, `omlx/...` або '')
|
|
210
|
-
* @param {string} caller мітка викликача для wire-trace (`fix:<rule>:<rung>`)
|
|
211
|
-
* @param {number} [timeoutMs] ліміт виклику (драбина задає per-tier; undefined → дефолт callLlmRich)
|
|
212
|
-
* @param {number} [thinkingBudget] бюджет thinking-токенів (лише omlx; 0 = вимкнено)
|
|
213
|
-
* @returns {{ text: string, reasoning: string|null, reasoningSource: string|null, error?: string }}
|
|
214
|
-
*/
|
|
215
|
-
function callModel(prompt, model, caller, timeoutMs, thinkingBudget) {
|
|
216
|
-
try {
|
|
217
|
-
const { content, reasoning, reasoningSource } = callLlmRich([{ role: 'user', content: prompt }], model, {
|
|
218
|
-
timeoutMs,
|
|
219
|
-
caller,
|
|
220
|
-
thinkingBudget
|
|
221
|
-
})
|
|
222
|
-
return { text: content, reasoning, reasoningSource }
|
|
223
|
-
} catch (error) {
|
|
224
|
-
const msg = String(error.message)
|
|
225
|
-
if (API_KEY_RE.test(msg)) {
|
|
226
|
-
const provider = model ? model.split('/')[0] : 'дефолтного провайдера'
|
|
227
|
-
return {
|
|
228
|
-
text: '',
|
|
229
|
-
reasoning: null,
|
|
230
|
-
reasoningSource: null,
|
|
231
|
-
error: [
|
|
232
|
-
`pi: немає ключа для ${provider}.`,
|
|
233
|
-
`Встановіть N_CLOUD_MIN_MODEL=provider/model-id`,
|
|
234
|
-
`(напр.: openai/gpt-5.4-mini, google/gemini-2.5-flash, ollama/gemma3:4b)`
|
|
235
|
-
].join(' ')
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return { text: '', reasoning: null, reasoningSource: null, error: msg }
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* LLM-worker: виправляє одне rule-порушення через pi (C1 pattern).
|
|
244
|
-
* Повертає `changes`/`diagnosis` навіть при невдачі — драбина ескалації
|
|
245
|
-
* (`orchestrator.mjs`) логує їх і прокидає як feedback у наступний рунг.
|
|
246
|
-
* Поля `reasoning`/`reasoningSource`/`promptSummary` використовує оркестратор
|
|
247
|
-
* для verbose-блоку після кожного рунга (`--full` режим).
|
|
248
|
-
* @param {string} ruleId ID правила
|
|
249
|
-
* @param {string} violationOutput output з fix check для цього rule
|
|
250
|
-
* @param {string} projectRoot абсолютний шлях до кореня проєкту
|
|
251
|
-
* @param {{ model?: string, feedback?: object|null, caller?: string, timeoutMs?: number, thinkingBudget?: number }} opts опції:
|
|
252
|
-
* `model` — перевизначення моделі; `feedback` — контекст попереднього рунга
|
|
253
|
-
* драбини (retry-with-feedback); `caller` — мітка для wire-trace; `timeoutMs` —
|
|
254
|
-
* per-tier ліміт виклику (драбина: локалі fail-fast, хмара повний);
|
|
255
|
-
* `thinkingBudget` — кількість thinking-токенів для omlx (дефолт `DEFAULT_THINKING_BUDGET`)
|
|
256
|
-
* @returns {{ ok: boolean, error?: string, changes: Array<{path:string}>, diagnosis: string|null, reasoning: string|null, reasoningSource: string|null, promptSummary: object }}
|
|
257
|
-
*/
|
|
258
|
-
export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
|
|
259
|
-
const model = opts.model ?? MODEL
|
|
260
|
-
const feedback = opts.feedback ?? null
|
|
261
|
-
const caller = opts.caller ?? 'fix'
|
|
262
|
-
const timeoutMs = opts.timeoutMs
|
|
263
|
-
const thinkingBudget = opts.thinkingBudget ?? DEFAULT_THINKING_BUDGET
|
|
264
|
-
|
|
265
|
-
// 1. Читаємо rule .mdc: спробуємо sub-check mdc для конкретної перевірки,
|
|
266
|
-
// якщо не вдалося — fallback на повний n-{id}.mdc.
|
|
267
|
-
const subMdc = selectSubCheckMdc(ruleId, violationOutput)
|
|
268
|
-
const mdcPath = join(projectRoot, '.cursor', 'rules', `n-${ruleId}.mdc`)
|
|
269
|
-
const ruleMdc = subMdc ?? (existsSync(mdcPath) ? readFileSync(mdcPath, 'utf8') : '(rule file not found)')
|
|
270
|
-
|
|
271
|
-
// 2. Витягуємо файли з violation output і читаємо їх
|
|
272
|
-
const files = readFilesForFix(extractFilePaths(violationOutput), projectRoot)
|
|
273
|
-
|
|
274
|
-
// 3. Будуємо summary промпту (для verbose-блоку) до виклику моделі
|
|
275
|
-
const promptSummary = {
|
|
276
|
-
ruleMdcLen: ruleMdc.length,
|
|
277
|
-
subCheckMdc: !!subMdc,
|
|
278
|
-
violationLen: violationOutput.length,
|
|
279
|
-
filesCount: files.length,
|
|
280
|
-
filesTotalBytes: files.reduce((s, f) => s + f.content.length, 0),
|
|
281
|
-
hasFeedback: !!feedback,
|
|
282
|
-
feedbackModel: feedback?.previousModel ?? null,
|
|
283
|
-
feedbackChangesCount: feedback?.previousChanges?.length ?? 0,
|
|
284
|
-
feedbackError: feedback?.previousError ?? null
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// 4. Будуємо prompt і викликаємо модель
|
|
288
|
-
const prompt = buildPrompt(ruleId, ruleMdc, violationOutput, files, feedback)
|
|
289
|
-
const {
|
|
290
|
-
text,
|
|
291
|
-
error: modelError,
|
|
292
|
-
reasoning,
|
|
293
|
-
reasoningSource
|
|
294
|
-
} = callModel(prompt, model, caller, timeoutMs, thinkingBudget)
|
|
295
|
-
|
|
296
|
-
if (modelError)
|
|
297
|
-
return { ok: false, error: modelError, changes: [], diagnosis: null, reasoning, reasoningSource, promptSummary }
|
|
298
|
-
if (!text)
|
|
299
|
-
return {
|
|
300
|
-
ok: false,
|
|
301
|
-
error: 'model returned empty response',
|
|
302
|
-
changes: [],
|
|
303
|
-
diagnosis: null,
|
|
304
|
-
reasoning,
|
|
305
|
-
reasoningSource,
|
|
306
|
-
promptSummary
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// 5. Парсимо відповідь
|
|
310
|
-
const parsed = parseChangesResponse(text)
|
|
311
|
-
if (!parsed) {
|
|
312
|
-
return {
|
|
313
|
-
ok: false,
|
|
314
|
-
error: `cannot parse pi response: ${text.slice(0, 200)}`,
|
|
315
|
-
changes: [],
|
|
316
|
-
diagnosis: null,
|
|
317
|
-
reasoning,
|
|
318
|
-
reasoningSource,
|
|
319
|
-
promptSummary
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const diagnosis = typeof parsed.diagnosis === 'string' && parsed.diagnosis ? parsed.diagnosis : null
|
|
323
|
-
const changes = parsed.changes ?? []
|
|
324
|
-
if (parsed.error)
|
|
325
|
-
return { ok: false, error: parsed.error, changes, diagnosis, reasoning, reasoningSource, promptSummary }
|
|
326
|
-
if (changes.length === 0)
|
|
327
|
-
return { ok: false, error: 'pi returned no changes', changes, diagnosis, reasoning, reasoningSource, promptSummary }
|
|
328
|
-
|
|
329
|
-
// 6. Застосовуємо зміни
|
|
330
|
-
const applied = applyChanges(changes, projectRoot)
|
|
331
|
-
return { ...applied, changes, diagnosis, reasoning, reasoningSource, promptSummary }
|
|
332
|
-
}
|