@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/bin/n-cursor.js +72 -50
  3. package/lib/llm.mjs +60 -47
  4. package/lib/models.mjs +1 -1
  5. package/lib/omlx-trace.mjs +158 -0
  6. package/lib/omlx.mjs +49 -11
  7. package/package.json +1 -1
  8. package/rules/js-bun-db/js-bun-db.mdc +7 -7
  9. package/rules/js-lint/js-lint.mdc +14 -1
  10. package/rules/js-run/js-run.mdc +16 -16
  11. package/rules/k8s/js/manifests.mjs +144 -82
  12. package/rules/npm-module/js/header_doc_pointer.mjs +72 -27
  13. package/rules/npm-module/js/rule_meta.mjs +72 -36
  14. package/rules/npm-module/js/skill_meta.mjs +59 -35
  15. package/rules/style-lint/js/tooling.mjs +13 -4
  16. package/rules/style-lint/style-lint.mdc +1 -1
  17. package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +1 -1
  18. package/rules/test/js/data/stryker_config/stryker.config.vue.baseline.mjs +1 -1
  19. package/rules/test/js/stryker_config.mjs +33 -5
  20. package/rules/test/js/vitest-config-pool-forks.mjs +11 -7
  21. package/rules/test/test.mdc +9 -9
  22. package/rules/vue/vue.mdc +6 -6
  23. package/scripts/coverage-classify/index.mjs +5 -17
  24. package/scripts/coverage-classify/verdict-schema.mjs +1 -1
  25. package/scripts/lib/assert-project-root.mjs +1 -1
  26. package/scripts/lib/discover-check-rules-from-cursor.mjs +1 -1
  27. package/scripts/lib/rule-predicates.mjs +30 -18
  28. package/scripts/lib/run-rule-cli.mjs +1 -1
  29. package/scripts/lib/run-standard-rule.mjs +1 -1
  30. package/scripts/post-tool-use-fix.mjs +3 -3
  31. package/scripts/skills-cli.mjs +5 -5
  32. package/scripts/worktree-cli.mjs +5 -5
  33. package/skills/doc-files/js/docgen-extract.mjs +1 -1
  34. package/skills/doc-files/js/docgen-files-batch.mjs +65 -34
  35. package/skills/doc-files/js/docgen-gen.mjs +121 -36
  36. package/skills/doc-files/js/docgen-prompts.mjs +20 -5
  37. package/skills/fix/js/llm-worker.mjs +10 -22
  38. package/skills/fix/js/orchestrator.mjs +64 -35
  39. package/skills/fix/js/t0.mjs +44 -32
  40. 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
- * @param {string[]} args CLI аргументи після 'fix'
15
- * @param {string} cwd корінь проєкту
16
- * @returns {Promise<number>} 0 = all clean, 1 = unresolved
14
+ * Парсить `--max-iter N` і збирає rule-filter (позиційні аргументи без прапорців).
15
+ * @param {string[]} args CLI аргументи після 'fix'
16
+ * @returns {{ maxIter: number, ruleFilter: string[] }} ліміт ітерацій і фільтр правил
17
17
  */
18
- export async function runOrchestratorCli(args, cwd) {
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
- // ── T0-auto: детермінований фікс без LLM ──
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
- // ── T1: LLM через pi ──
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)
@@ -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 fixResult = spawnSync('bun', [N_CURSOR_BIN, '_fix-check', ...ruleFilter], {
130
- cwd,
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(fixResult.stderr?.slice(0, 300) ?? '')
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 recheck = spawnSync('bun', [N_CURSOR_BIN, '_fix-check', ...applied.map(a => a.ruleId)], {
180
- cwd,
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?:Function}} [opts] grace-період, тип (інакше з package.json), інʼєкція spawn для тестів
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 = {}) {