@nitra/cursor 5.2.1 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/bin/n-cursor.js +72 -50
- package/lib/llm.mjs +60 -47
- package/lib/models.mjs +1 -1
- package/lib/omlx-trace.mjs +158 -0
- package/lib/omlx.mjs +49 -11
- package/package.json +1 -1
- package/rules/js-bun-db/js-bun-db.mdc +7 -7
- package/rules/js-lint/js-lint.mdc +14 -1
- package/rules/js-run/js-run.mdc +16 -16
- package/rules/k8s/js/manifests.mjs +144 -82
- package/rules/npm-module/js/header_doc_pointer.mjs +72 -27
- package/rules/npm-module/js/rule_meta.mjs +72 -36
- package/rules/npm-module/js/skill_meta.mjs +59 -35
- package/rules/style-lint/js/tooling.mjs +13 -4
- package/rules/style-lint/style-lint.mdc +1 -1
- package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +1 -1
- package/rules/test/js/data/stryker_config/stryker.config.vue.baseline.mjs +1 -1
- package/rules/test/js/stryker_config.mjs +33 -5
- package/rules/test/js/vitest-config-pool-forks.mjs +11 -7
- package/rules/test/test.mdc +9 -9
- package/rules/vue/vue.mdc +6 -6
- package/scripts/coverage-classify/index.mjs +5 -17
- package/scripts/coverage-classify/verdict-schema.mjs +1 -1
- package/scripts/lib/assert-project-root.mjs +1 -1
- package/scripts/lib/discover-check-rules-from-cursor.mjs +1 -1
- package/scripts/lib/rule-predicates.mjs +30 -18
- package/scripts/lib/run-rule-cli.mjs +1 -1
- package/scripts/lib/run-standard-rule.mjs +1 -1
- package/scripts/post-tool-use-fix.mjs +3 -3
- package/scripts/skills-cli.mjs +5 -5
- package/scripts/worktree-cli.mjs +5 -5
- package/skills/doc-files/js/docgen-extract.mjs +1 -1
- package/skills/doc-files/js/docgen-files-batch.mjs +65 -34
- package/skills/doc-files/js/docgen-gen.mjs +121 -36
- package/skills/doc-files/js/docgen-prompts.mjs +20 -5
- package/skills/fix/js/llm-worker.mjs +10 -22
- package/skills/fix/js/orchestrator.mjs +64 -35
- package/skills/fix/js/t0.mjs +44 -32
- package/skills/start-check/js/check.mjs +1 -1
|
@@ -11,18 +11,74 @@ const DEFAULT_MAX_ITER = 3
|
|
|
11
11
|
const ESCALATE_AFTER = 2
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* @param {string}
|
|
16
|
-
* @returns {
|
|
14
|
+
* Парсить `--max-iter N` і збирає rule-filter (позиційні аргументи без прапорців).
|
|
15
|
+
* @param {string[]} args CLI аргументи після 'fix'
|
|
16
|
+
* @returns {{ maxIter: number, ruleFilter: string[] }} ліміт ітерацій і фільтр правил
|
|
17
17
|
*/
|
|
18
|
-
|
|
19
|
-
const { runLlmWorker, MODEL, MODEL_HEAVY } = await import('./llm-worker.mjs')
|
|
20
|
-
|
|
18
|
+
function parseOrchestratorArgs(args) {
|
|
21
19
|
const maxIterIdx = args.indexOf('--max-iter')
|
|
22
20
|
const maxIter =
|
|
23
21
|
maxIterIdx === -1 ? DEFAULT_MAX_ITER : Number(args[maxIterIdx + 1] ?? DEFAULT_MAX_ITER) || DEFAULT_MAX_ITER
|
|
24
22
|
const skipIdxs = new Set(maxIterIdx === -1 ? [] : [maxIterIdx, maxIterIdx + 1])
|
|
25
23
|
const ruleFilter = args.filter((a, i) => !a.startsWith('-') && !skipIdxs.has(i))
|
|
24
|
+
return { maxIter, ruleFilter }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Крок T0-auto: детермінований фікс без LLM, повертає правила, що лишились.
|
|
29
|
+
* @param {string} cwd корінь проєкту
|
|
30
|
+
* @param {string[]} ruleFilter фільтр правил
|
|
31
|
+
* @param {Array<{ ruleId: string }>} failed правила перед кроком
|
|
32
|
+
* @returns {Array<{ ruleId: string, ok: boolean, output: string }>} правила після T0
|
|
33
|
+
*/
|
|
34
|
+
function runT0Step(cwd, ruleFilter, failed) {
|
|
35
|
+
spawnSync('bun', [N_CURSOR_BIN, 'fix-t0', ...ruleFilter], { cwd, stdio: 'pipe' })
|
|
36
|
+
|
|
37
|
+
const afterT0 = runFixCheck(cwd, ruleFilter)
|
|
38
|
+
const failedAfterT0 = afterT0?.rules.filter(r => !r.ok) ?? failed
|
|
39
|
+
const t0Fixed = failed.filter(r => !failedAfterT0.some(f => f.ruleId === r.ruleId))
|
|
40
|
+
|
|
41
|
+
if (t0Fixed.length > 0) {
|
|
42
|
+
console.log(` ⚙️ T0-auto: ${t0Fixed.map(r => r.ruleId).join(', ')}`)
|
|
43
|
+
}
|
|
44
|
+
return failedAfterT0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Крок T1: LLM через pi для кожного правила, з ескалацією моделі за провалами.
|
|
49
|
+
* @param {Array<{ ruleId: string, output: string }>} failed правила до фіксу
|
|
50
|
+
* @param {string} cwd корінь проєкту
|
|
51
|
+
* @param {Map<string, number>} failCount ruleId → кількість провалів підряд (мутується)
|
|
52
|
+
* @param {{ runLlmWorker: (ruleId: string, output: string, projectRoot: string, opts: {model: string}) => Promise<{ok: boolean, error?: string}>, MODEL: string, MODEL_HEAVY: string }} worker воркер і моделі
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
*/
|
|
55
|
+
async function runLlmStep(failed, cwd, failCount, { runLlmWorker, MODEL, MODEL_HEAVY }) {
|
|
56
|
+
for (const rule of failed) {
|
|
57
|
+
const prevFails = failCount.get(rule.ruleId) ?? 0
|
|
58
|
+
const model = prevFails >= ESCALATE_AFTER ? MODEL_HEAVY : MODEL
|
|
59
|
+
const label = model || 'pi'
|
|
60
|
+
|
|
61
|
+
const result = await runLlmWorker(rule.ruleId, rule.output, cwd, { model })
|
|
62
|
+
|
|
63
|
+
if (result.ok) {
|
|
64
|
+
console.log(` ⚡ LLM (${label}): ${rule.ruleId}`)
|
|
65
|
+
failCount.delete(rule.ruleId)
|
|
66
|
+
} else {
|
|
67
|
+
failCount.set(rule.ruleId, prevFails + 1)
|
|
68
|
+
const hint = (result.error ?? '').slice(0, 200)
|
|
69
|
+
console.log(` ⚡ LLM (${label}): ${rule.ruleId} ❌ ${hint}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string[]} args CLI аргументи після 'fix'
|
|
76
|
+
* @param {string} cwd корінь проєкту
|
|
77
|
+
* @returns {Promise<number>} 0 = all clean, 1 = unresolved
|
|
78
|
+
*/
|
|
79
|
+
export async function runOrchestratorCli(args, cwd) {
|
|
80
|
+
const worker = await import('./llm-worker.mjs')
|
|
81
|
+
const { maxIter, ruleFilter } = parseOrchestratorArgs(args)
|
|
26
82
|
|
|
27
83
|
/** @type {Map<string, number>} ruleId → кількість LLM-провалів підряд */
|
|
28
84
|
const failCount = new Map()
|
|
@@ -48,37 +104,10 @@ export async function runOrchestratorCli(args, cwd) {
|
|
|
48
104
|
if (ruleFilter.length) console.log(` filter: ${ruleFilter.join(', ')}`)
|
|
49
105
|
|
|
50
106
|
for (let iter = 1; iter <= maxIter; iter++) {
|
|
51
|
-
|
|
52
|
-
spawnSync('bun', [N_CURSOR_BIN, 'fix-t0', ...ruleFilter], { cwd, stdio: 'pipe' })
|
|
53
|
-
|
|
54
|
-
const afterT0 = runFixCheck(cwd, ruleFilter)
|
|
55
|
-
const failedAfterT0 = afterT0?.rules.filter(r => !r.ok) ?? failed
|
|
56
|
-
const t0Fixed = failed.filter(r => !failedAfterT0.some(f => f.ruleId === r.ruleId))
|
|
57
|
-
|
|
58
|
-
if (t0Fixed.length > 0) {
|
|
59
|
-
console.log(` ⚙️ T0-auto: ${t0Fixed.map(r => r.ruleId).join(', ')}`)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
failed = failedAfterT0
|
|
107
|
+
failed = runT0Step(cwd, ruleFilter, failed)
|
|
63
108
|
if (failed.length === 0) break
|
|
64
109
|
|
|
65
|
-
|
|
66
|
-
for (const rule of failed) {
|
|
67
|
-
const prevFails = failCount.get(rule.ruleId) ?? 0
|
|
68
|
-
const model = prevFails >= ESCALATE_AFTER ? MODEL_HEAVY : MODEL
|
|
69
|
-
const label = model || 'pi'
|
|
70
|
-
|
|
71
|
-
const result = await runLlmWorker(rule.ruleId, rule.output, cwd, { model })
|
|
72
|
-
|
|
73
|
-
if (result.ok) {
|
|
74
|
-
console.log(` ⚡ LLM (${label}): ${rule.ruleId}`)
|
|
75
|
-
failCount.delete(rule.ruleId)
|
|
76
|
-
} else {
|
|
77
|
-
failCount.set(rule.ruleId, prevFails + 1)
|
|
78
|
-
const hint = (result.error ?? '').slice(0, 200)
|
|
79
|
-
console.log(` ⚡ LLM (${label}): ${rule.ruleId} ❌ ${hint}`)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
110
|
+
await runLlmStep(failed, cwd, failCount, worker)
|
|
82
111
|
|
|
83
112
|
// Перевірка після LLM
|
|
84
113
|
const afterLLM = runFixCheck(cwd, ruleFilter)
|
package/skills/fix/js/t0.mjs
CHANGED
|
@@ -113,6 +113,43 @@ const HERE = dirname(fileURLToPath(import.meta.url))
|
|
|
113
113
|
/** Абсолютний шлях до npm/bin/n-cursor.js відносно цього файлу */
|
|
114
114
|
const N_CURSOR_BIN = join(HERE, '../../../bin/n-cursor.js')
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Запускає `_fix-check` і парсить JSON-результат.
|
|
118
|
+
* @param {string[]} ruleFilter список rule-ids (порожній — усі)
|
|
119
|
+
* @param {string} cwd корінь проєкту
|
|
120
|
+
* @returns {{ rules: Array<{ ruleId: string, ok: boolean, output: string }> } | { _empty: true } | { _badJson: true }} JSON або маркер помилки
|
|
121
|
+
*/
|
|
122
|
+
function fixCheck(ruleFilter, cwd) {
|
|
123
|
+
const r = spawnSync('bun', [N_CURSOR_BIN, '_fix-check', ...ruleFilter], { cwd, encoding: 'utf8', timeout: 120_000 })
|
|
124
|
+
const raw = r.stdout?.trim()
|
|
125
|
+
if (!raw) return { _empty: true, stderr: r.stderr }
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(raw)
|
|
128
|
+
} catch {
|
|
129
|
+
return { _badJson: true }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Застосовує T0-auto до кожного провального правила, розділяючи на applied/skipped.
|
|
135
|
+
* @param {Array<{ ruleId: string, output: string }>} failed провальні правила
|
|
136
|
+
* @param {string} cwd корінь проєкту
|
|
137
|
+
* @returns {{ applied: Array<{ ruleId: string, actions: string[] }>, skipped: string[] }} застосовані й пропущені
|
|
138
|
+
*/
|
|
139
|
+
function applyT0ToFailed(failed, cwd) {
|
|
140
|
+
const applied = []
|
|
141
|
+
const skipped = []
|
|
142
|
+
for (const r of failed) {
|
|
143
|
+
const result = applyT0Auto(r.ruleId, r.output, cwd)
|
|
144
|
+
if (result.applied) {
|
|
145
|
+
applied.push({ ruleId: r.ruleId, actions: result.actions })
|
|
146
|
+
} else {
|
|
147
|
+
skipped.push(r.ruleId)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { applied, skipped }
|
|
151
|
+
}
|
|
152
|
+
|
|
116
153
|
/**
|
|
117
154
|
* CLI підкоманда `n-cursor fix-t0 [rule...]`.
|
|
118
155
|
* Запускає `fix --json`, застосовує T0-auto для кожного violation,
|
|
@@ -126,22 +163,13 @@ export function runT0AutoCli(args, cwd) {
|
|
|
126
163
|
const verbose = args.includes('--verbose') || args.includes('-v')
|
|
127
164
|
|
|
128
165
|
// 1. Запустити fix --json
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
encoding: 'utf8',
|
|
132
|
-
timeout: 120_000
|
|
133
|
-
})
|
|
134
|
-
const raw = fixResult.stdout?.trim()
|
|
135
|
-
if (!raw) {
|
|
166
|
+
const fixJson = fixCheck(ruleFilter, cwd)
|
|
167
|
+
if (fixJson._empty) {
|
|
136
168
|
console.error(`n-cursor fix-t0: fix --json повернув порожній stdout`)
|
|
137
|
-
console.error(
|
|
169
|
+
console.error(fixJson.stderr?.slice(0, 300) ?? '')
|
|
138
170
|
return 1
|
|
139
171
|
}
|
|
140
|
-
|
|
141
|
-
let fixJson
|
|
142
|
-
try {
|
|
143
|
-
fixJson = JSON.parse(raw)
|
|
144
|
-
} catch {
|
|
172
|
+
if (fixJson._badJson) {
|
|
145
173
|
console.error(`n-cursor fix-t0: fix --json повернув невалідний JSON`)
|
|
146
174
|
return 1
|
|
147
175
|
}
|
|
@@ -153,16 +181,7 @@ export function runT0AutoCli(args, cwd) {
|
|
|
153
181
|
}
|
|
154
182
|
|
|
155
183
|
// 2. Застосувати T0-auto
|
|
156
|
-
const applied =
|
|
157
|
-
const skipped = []
|
|
158
|
-
for (const r of failed) {
|
|
159
|
-
const result = applyT0Auto(r.ruleId, r.output, cwd)
|
|
160
|
-
if (result.applied) {
|
|
161
|
-
applied.push({ ruleId: r.ruleId, actions: result.actions })
|
|
162
|
-
} else {
|
|
163
|
-
skipped.push(r.ruleId)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
184
|
+
const { applied, skipped } = applyT0ToFailed(failed, cwd)
|
|
166
185
|
|
|
167
186
|
if (applied.length === 0) {
|
|
168
187
|
console.log(`⏭️ fix-t0: T0-auto паттерн не підходить для: ${failed.map(r => r.ruleId).join(', ')}`)
|
|
@@ -176,18 +195,11 @@ export function runT0AutoCli(args, cwd) {
|
|
|
176
195
|
}
|
|
177
196
|
|
|
178
197
|
// 4. Check-gate: перевірити лише ті правила, що ми чіпали
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
encoding: 'utf8',
|
|
182
|
-
timeout: 120_000
|
|
183
|
-
})
|
|
184
|
-
const recheckRaw = recheck.stdout?.trim()
|
|
185
|
-
if (!recheckRaw) {
|
|
198
|
+
const recheckJson = fixCheck(applied.map(a => a.ruleId), cwd)
|
|
199
|
+
if (recheckJson._empty) {
|
|
186
200
|
console.error(`fix-t0: check-gate: fix --json повернув порожній stdout`)
|
|
187
201
|
return 1
|
|
188
202
|
}
|
|
189
|
-
|
|
190
|
-
const recheckJson = JSON.parse(recheckRaw)
|
|
191
203
|
const stillFailed = recheckJson.rules.filter(r => !r.ok)
|
|
192
204
|
|
|
193
205
|
if (verbose) {
|
|
@@ -111,7 +111,7 @@ function diffSideEffects(before, after) {
|
|
|
111
111
|
* Запускає `start` одного воркспейсу з grace-таймаутом і класифікує результат.
|
|
112
112
|
* @param {string} cwd корінь репозиторію
|
|
113
113
|
* @param {string} workspace відносний шлях воркспейсу
|
|
114
|
-
* @param {{graceMs?:number, type?:('server'|'cli'), spawnImpl?:
|
|
114
|
+
* @param {{graceMs?:number, type?:('server'|'cli'), spawnImpl?:typeof import('node:child_process').spawnSync}} [opts] grace-період, тип (інакше з package.json), інʼєкція spawn для тестів
|
|
115
115
|
* @returns {Promise<{workspace:string, type:string, exitCode:number|null, timedOut:boolean, status:('OK'|'FAIL'), ready:boolean, firstError:string|null, logTail:string, sideEffects:{newFiles:string[], changedTracked:string[]}}>} результат прогону
|
|
116
116
|
*/
|
|
117
117
|
export async function runWorkspaceStart(cwd, workspace, opts = {}) {
|