@nitra/cursor 5.2.0 → 5.3.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 +13 -0
- package/lib/docs/models.md +24 -17
- package/lib/llm.mjs +60 -47
- package/lib/omlx-trace.mjs +158 -0
- package/lib/omlx.mjs +49 -11
- package/package.json +1 -1
- package/rules/changelog/js/docs/consistency.md +36 -383
- package/rules/feedback/docs/fix.md +21 -131
- package/rules/ga/docs/fix.md +14 -12
- package/rules/ga/js/docs/lint.md +12 -9
- package/rules/ga/js/docs/workflows.md +20 -19
- package/rules/graphql/js/docs/tooling.md +18 -253
- package/rules/hasura/docs/fix.md +18 -111
- 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/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 +3 -15
- package/skills/doc-files/js/docgen-extract-anchors.mjs +28 -2
- package/skills/doc-files/js/docgen-extract.mjs +24 -1
- package/skills/doc-files/js/docgen-gen.mjs +54 -9
- package/skills/doc-files/js/docgen-prompts.mjs +28 -16
- package/skills/doc-files/js/units-js.mjs +139 -0
- package/skills/doc-files/js/units.mjs +19 -0
- package/skills/fix/js/llm-worker.mjs +10 -22
|
@@ -7,11 +7,12 @@ import { DEFAULT_OMLX_MODEL } from '../../../lib/omlx.mjs'
|
|
|
7
7
|
import { callLlm } from '../../../lib/llm.mjs'
|
|
8
8
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
9
9
|
import { extractFacts } from './docgen-extract.mjs'
|
|
10
|
-
import { extractAnchors } from './docgen-extract-anchors.mjs'
|
|
10
|
+
import { extractAnchors, anchorTokens } from './docgen-extract-anchors.mjs'
|
|
11
11
|
import { QUALITY_THRESHOLD } from './docgen-crc.mjs'
|
|
12
12
|
import {
|
|
13
13
|
oneShotMessages,
|
|
14
14
|
sectionMessages,
|
|
15
|
+
overviewMessages,
|
|
15
16
|
criticMessages,
|
|
16
17
|
refineMessages,
|
|
17
18
|
guaranteesFromMarkers
|
|
@@ -25,6 +26,15 @@ const SECTION_KEY_CLEAN_RE = /[^а-яіїєґa-z0-9]/gi
|
|
|
25
26
|
const CACHE_MENTION_RE = /кеш/i
|
|
26
27
|
const CACHE_NEGATION_RE = /(?:не|без)\s+(?:\S+\s+)?кеш|немає\s+кеш/i
|
|
27
28
|
const CRITIC_NONE_RE = /^\s*NONE\s*$/i
|
|
29
|
+
// R4: абстрактні «нічого-не-кажучі» формули, які обходять exact-blocklist і дають score=100
|
|
30
|
+
const GENERIC_RE =
|
|
31
|
+
/відповідност\S*\s+(?:даних\s+)?(?:визначеному\s+)?контракту|валідаці\S*\s+даних|перевірк\S*\s+(?:відповідності\s+)?даних|обробк\S*\s+даних|застосову\S*\s+логіку|інспекту\S*\s+та\s+збира\S*\s+дан/i
|
|
32
|
+
// R7: часті русизми/суржик (курований безпечний список — без false-positive на нормальній мові).
|
|
33
|
+
// Без \b: кирилиця не є ASCII-`\w`, тож межі слова в JS-regex не спрацьовують — терміни специфічні.
|
|
34
|
+
const SURZHIK_RE =
|
|
35
|
+
/пропуская|являється|в залежності|по замовчуванню|на протязі|відповідаюч|слідуюч|наступним разом|приймати участь|у відповідності/i
|
|
36
|
+
const ANCHOR_MISS_PENALTY = 5
|
|
37
|
+
const ANCHOR_MISS_CAP = 20
|
|
28
38
|
|
|
29
39
|
/**
|
|
30
40
|
* Прибирає код-фенс-обгортку (потрійні бектіки) й випадковий провідний
|
|
@@ -86,18 +96,26 @@ function hasName(text, sym) {
|
|
|
86
96
|
* Stage 2.5 — детермінований скоринг (0 токенів): перевіряє вихід проти фактів.
|
|
87
97
|
* @param {string} md зібраний документ
|
|
88
98
|
* @param {object} facts факт-лист про файл
|
|
99
|
+
* @param {{ anchors?: object|null, src?: string }} [ctx] анкори й джерело для R5
|
|
89
100
|
* @returns {{ score: number, issues: string[] }} оцінка 0–100 і коди проблем
|
|
90
101
|
*/
|
|
91
|
-
function scoreDoc(md, facts) {
|
|
102
|
+
export function scoreDoc(md, facts, { anchors = null, src = '' } = {}) {
|
|
92
103
|
const s = parseSections(md)
|
|
93
104
|
let score = 100
|
|
94
105
|
const issues = []
|
|
106
|
+
const overview = s['огляд'] ?? ''
|
|
95
107
|
|
|
96
108
|
if (!s['огляд']) {
|
|
97
109
|
score -= 25
|
|
98
110
|
issues.push('no-overview')
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
// R4: generic-Огляд (парафрази, які обходять exact-blocklist) — як майже-відсутній.
|
|
114
|
+
if (GENERIC_RE.test(overview)) {
|
|
115
|
+
score -= 35
|
|
116
|
+
issues.push('generic-overview')
|
|
117
|
+
}
|
|
118
|
+
|
|
101
119
|
const behavior = s['поведінка'] ?? ''
|
|
102
120
|
if (behavior.length < 60) {
|
|
103
121
|
score -= 20
|
|
@@ -113,14 +131,35 @@ function scoreDoc(md, facts) {
|
|
|
113
131
|
issues.push('cache-hallucination')
|
|
114
132
|
}
|
|
115
133
|
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
// R6: службові (неекспортовані) функції не мають фігурувати як публічні
|
|
135
|
+
const api = s['публічнийapi'] ?? ''
|
|
136
|
+
for (const sym of [...(facts.internalSymbols ?? []), ...(facts.localSymbols ?? [])]) {
|
|
137
|
+
const inDoc = hasName(guarantees, sym) || hasName(overview, sym) || hasName(behavior, sym) || hasName(api, sym)
|
|
118
138
|
if (inDoc) {
|
|
119
139
|
score -= 10
|
|
120
140
|
issues.push(`internal-name:${sym}`)
|
|
121
141
|
}
|
|
122
142
|
}
|
|
123
143
|
|
|
144
|
+
// R5: кожен валідний анкор (дослівний підрядок src) має зʼявитися в документі
|
|
145
|
+
if (anchors && src) {
|
|
146
|
+
let missPenalty = 0
|
|
147
|
+
for (const tok of anchorTokens(anchors)) {
|
|
148
|
+
if (!src.includes(tok)) continue // валідність: фейковий анкор не вимагаємо
|
|
149
|
+
if (!md.includes(tok) && missPenalty < ANCHOR_MISS_CAP) {
|
|
150
|
+
missPenalty += ANCHOR_MISS_PENALTY
|
|
151
|
+
issues.push(`anchor-miss:${tok}`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
score -= missPenalty
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// R7: суржик/русизми
|
|
158
|
+
if (SURZHIK_RE.test(md)) {
|
|
159
|
+
score -= 10
|
|
160
|
+
issues.push('surzhik')
|
|
161
|
+
}
|
|
162
|
+
|
|
124
163
|
return { score: Math.max(0, score), issues }
|
|
125
164
|
}
|
|
126
165
|
|
|
@@ -205,15 +244,21 @@ function orchestratedDoc(facts, src, model, timeoutMs, { anchors = null, tempera
|
|
|
205
244
|
const anc = anchors ?? extractAnchors(src)
|
|
206
245
|
// E3: «Гарантії» — детермінований шаблон з markers (0 LLM-запитів, 0 generic-фраз)
|
|
207
246
|
sections.guarantees = guaranteesFromMarkers(facts)
|
|
247
|
+
// Спершу Поведінка (+API) — секції з фактажем
|
|
208
248
|
for (const s of sectionMessages(facts, src, anc)) {
|
|
209
|
-
if (s.key === 'guarantees') continue // вже згенеровано детерміновано
|
|
210
249
|
let draft = stripSignatures(stripSection(callLlm(s.messages, model, { timeoutMs, temperature })))
|
|
211
|
-
// E2
|
|
212
|
-
if (s.key === '
|
|
250
|
+
// E2: critique→refine для API, коли всі описи порожні (модель зриває на generic)
|
|
251
|
+
if (s.key === 'api' && apiNeedsRefine(facts)) {
|
|
213
252
|
draft = critiqueRefineSection(s.key, draft, facts, anc, model, timeoutMs)
|
|
214
253
|
}
|
|
215
254
|
sections[s.key] = draft
|
|
216
255
|
}
|
|
256
|
+
// R3: «Огляд» — ОСТАННІМ, узагальненням уже написаної Поведінки (не голого факт-листа)
|
|
257
|
+
let overview = stripSignatures(
|
|
258
|
+
stripSection(callLlm(overviewMessages(facts, sections.behavior ?? '', anc), model, { timeoutMs, temperature }))
|
|
259
|
+
)
|
|
260
|
+
overview = critiqueRefineSection('overview', overview, facts, anc, model, timeoutMs)
|
|
261
|
+
sections.overview = overview
|
|
217
262
|
return { md: assemble(basename(facts.relPath), sections) }
|
|
218
263
|
}
|
|
219
264
|
|
|
@@ -253,13 +298,13 @@ export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUA
|
|
|
253
298
|
}
|
|
254
299
|
|
|
255
300
|
// Stage 2.5: детермінований скоринг (0 токенів)
|
|
256
|
-
let { score, issues } = scoreDoc(r.md, facts)
|
|
301
|
+
let { score, issues } = scoreDoc(r.md, facts, { anchors, src })
|
|
257
302
|
|
|
258
303
|
// E4: best-of-2 — один retry з вищою температурою, det-вибір кращого
|
|
259
304
|
if (score < threshold && env.N_CURSOR_DOCGEN_BEST_OF !== '0') {
|
|
260
305
|
try {
|
|
261
306
|
const r2 = orchestratedDoc(facts, src, model, LOCAL_TIMEOUT_MS, { anchors, temperature: 0.5 })
|
|
262
|
-
const s2 = scoreDoc(r2.md, facts)
|
|
307
|
+
const s2 = scoreDoc(r2.md, facts, { anchors, src })
|
|
263
308
|
if (s2.score > score) {
|
|
264
309
|
r = r2
|
|
265
310
|
score = s2.score
|
|
@@ -52,31 +52,26 @@ const msgs = (system, user) => [
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Секційні набори messages з МІНІМАЛЬНИМ контекстом під кожну секцію.
|
|
55
|
-
* Код потрапляє лише в `behavior`;
|
|
55
|
+
* Код потрапляє лише в `behavior`; «Огляд» генерується окремо ОСТАННІМ
|
|
56
|
+
* (`overviewMessages`) з уже написаної Поведінки — тут його немає.
|
|
56
57
|
* @param {object} facts факт-лист про файл
|
|
57
58
|
* @param {string} src вміст файлу
|
|
58
59
|
* @param {object|null} [anchors] анкори файлу для обовʼязкового включення
|
|
59
|
-
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів
|
|
60
|
+
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів (behavior[, api])
|
|
60
61
|
*/
|
|
61
62
|
export function sectionMessages(facts, src, anchors = null) {
|
|
62
63
|
const factsTxt = factsSummary(facts)
|
|
63
64
|
const anch = anchorsBlock(anchors)
|
|
64
65
|
const multi = (facts.exports?.length || 0) > 1
|
|
65
66
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
key: 'overview',
|
|
69
|
-
numPredict: 220,
|
|
70
|
-
messages: msgs(
|
|
71
|
-
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
|
|
72
|
-
'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій. Заборонені generic-фрази типу «забезпечує перевірку», «виконує валідацію» — пиши КОНКРЕТНО що саме і за яким контрактом.'
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Поведінка — ЄДИНА секція, якій потрібен код
|
|
67
|
+
// R6: Поведінка описує РІВНО експортовані імена, не службові помічники
|
|
68
|
+
const exportNames = (facts.exports ?? []).map(e => e.name)
|
|
77
69
|
const behaviorTask = multi
|
|
78
70
|
? 'для кожної публічної функції — один короткий пункт «що вона робить»'
|
|
79
71
|
: 'нумерований алгоритм у бізнес-термінах'
|
|
72
|
+
const onlyExports = exportNames.length
|
|
73
|
+
? ` Описуй РІВНО ці публічні імена і жодних інших: ${exportNames.join(', ')}.`
|
|
74
|
+
: ''
|
|
80
75
|
const noInternal = facts.internalSymbols?.length
|
|
81
76
|
? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.`
|
|
82
77
|
: ''
|
|
@@ -85,12 +80,12 @@ export function sectionMessages(facts, src, anchors = null) {
|
|
|
85
80
|
numPredict: 500,
|
|
86
81
|
messages: msgs(
|
|
87
82
|
`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
|
|
88
|
-
`Напиши вміст секції «Поведінка»: ${behaviorTask}
|
|
83
|
+
`Напиши вміст секції «Поведінка»: ${behaviorTask}.${onlyExports} Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
|
|
89
84
|
)
|
|
90
85
|
}
|
|
91
86
|
|
|
92
87
|
// API — лише список експортів (без коду)
|
|
93
|
-
if (!multi && !facts.exports?.some(e => e.desc)) return [
|
|
88
|
+
if (!multi && !facts.exports?.some(e => e.desc)) return [behavior]
|
|
94
89
|
const list = facts.exports.map(e => `- ${e.name}: ${e.desc || '(сформулюй стисло з наміру файлу)'}`).join('\n')
|
|
95
90
|
const api = {
|
|
96
91
|
key: 'api',
|
|
@@ -100,7 +95,24 @@ export function sectionMessages(facts, src, anchors = null) {
|
|
|
100
95
|
`Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка. Без generic-фраз «застосовує логіку», «перевіряє коректність» — пиши конкретно ЩО саме застосовує/перевіряє.`
|
|
101
96
|
)
|
|
102
97
|
}
|
|
103
|
-
return [
|
|
98
|
+
return [behavior, api]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* R3 — «Огляд» ОСТАННІМ: узагальнення вже написаної Поведінки, а не здогад із
|
|
103
|
+
* голого факт-листа. Лікує generic/хибний Огляд на складних файлах.
|
|
104
|
+
* @param {object} facts факт-лист про файл
|
|
105
|
+
* @param {string} behaviorText готовий текст секції «Поведінка»
|
|
106
|
+
* @param {object|null} [anchors] анкори файлу
|
|
107
|
+
* @returns {Array<{role:string,content:string}>} messages-масив для Огляду
|
|
108
|
+
*/
|
|
109
|
+
export function overviewMessages(facts, behaviorText, anchors = null) {
|
|
110
|
+
const factsTxt = factsSummary(facts)
|
|
111
|
+
const anch = anchorsBlock(anchors)
|
|
112
|
+
return msgs(
|
|
113
|
+
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
|
|
114
|
+
`На основі вже написаної секції «Поведінка» (нижче) напиши «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Узагальнюй САМЕ описану поведінку, не додавай нових фактів. Без заголовка, без переліку функцій. Заборонені абстрактні формули без конкретики («перевірка/валідація/обробка даних», «відповідність контракту», «застосовує логіку») — пиши, ЩО саме і за яким контрактом.\n\nПОВЕДІНКА:\n${behaviorText}`
|
|
115
|
+
)
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
/**
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** @see ./docs/units-js.md */
|
|
2
|
+
|
|
3
|
+
import { parseProgramOrNull, walkAstWithAncestors } from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
4
|
+
|
|
5
|
+
// JSDoc-блок, що стоїть впритул перед позицією (лише пробіли між ними).
|
|
6
|
+
const JSDOC_BEFORE_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
|
|
7
|
+
const JSDOC_OPEN_RE = /^\s*\/\*\*?/
|
|
8
|
+
const JSDOC_CLOSE_RE = /\*\/\s*$/
|
|
9
|
+
const STAR_PREFIX_RE = /^\s*\*?\s?/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Очищає JSDoc від обрамлення `/** */` і `*`-префіксів.
|
|
13
|
+
* @param {string} raw сирий блок або порожній рядок
|
|
14
|
+
* @returns {string} текст опису без тегів-обрамлення
|
|
15
|
+
*/
|
|
16
|
+
function cleanDoc(raw) {
|
|
17
|
+
if (!raw) return ''
|
|
18
|
+
return raw
|
|
19
|
+
.replace(JSDOC_OPEN_RE, '')
|
|
20
|
+
.replace(JSDOC_CLOSE_RE, '')
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map(l => l.replace(STAR_PREFIX_RE, '').trimEnd())
|
|
23
|
+
.join('\n')
|
|
24
|
+
.trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* JSDoc, що передує позиції `start` у джерелі (або порожній рядок).
|
|
29
|
+
* @param {string} src вміст файлу
|
|
30
|
+
* @param {number} start зміщення початку декларації
|
|
31
|
+
* @returns {string} очищений опис
|
|
32
|
+
*/
|
|
33
|
+
function precedingDoc(src, start) {
|
|
34
|
+
const m = src.slice(0, start).match(JSDOC_BEFORE_RE)
|
|
35
|
+
return cleanDoc(m ? m[0] : '')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Імʼя функції, що викликається (проста Identifier або `obj.method`).
|
|
40
|
+
* @param {Record<string, unknown>} node CallExpression
|
|
41
|
+
* @returns {string|null} імʼя callee або null
|
|
42
|
+
*/
|
|
43
|
+
function calleeName(node) {
|
|
44
|
+
const c = node.callee
|
|
45
|
+
if (!c || typeof c !== 'object') return null
|
|
46
|
+
if (c.type === 'Identifier') return c.name
|
|
47
|
+
if (c.type === 'MemberExpression' && !c.computed && c.property?.type === 'Identifier') return c.property.name
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Множина імен, що викликаються у тілі вузла (сирі callee — фільтрація на ребра
|
|
53
|
+
* call-graph робиться у `extractUnitsJs` після збору всіх імен юнітів).
|
|
54
|
+
* @param {unknown} node AST-вузол юніта
|
|
55
|
+
* @returns {Set<string>} імена викликів
|
|
56
|
+
*/
|
|
57
|
+
function collectCalls(node) {
|
|
58
|
+
const names = new Set()
|
|
59
|
+
walkAstWithAncestors(node, [], n => {
|
|
60
|
+
if (n.type === 'CallExpression') {
|
|
61
|
+
const name = calleeName(n)
|
|
62
|
+
if (name) names.add(name)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
return names
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Будує юніт із декларації, додає у `units`. Розпізнає function/class та
|
|
70
|
+
* const-функції (`const x = () => {}` / `function expression`).
|
|
71
|
+
* @param {Record<string, unknown>} decl декларація (function/class/variable)
|
|
72
|
+
* @param {boolean} exported чи експортується
|
|
73
|
+
* @param {number} docStart зміщення для пошуку JSDoc (зовнішній export-вузол)
|
|
74
|
+
* @param {string} src вміст файлу
|
|
75
|
+
* @param {Array<object>} units акумулятор
|
|
76
|
+
* @returns {void}
|
|
77
|
+
*/
|
|
78
|
+
function pushUnits(decl, exported, docStart, src, units) {
|
|
79
|
+
if (!decl || typeof decl !== 'object') return
|
|
80
|
+
const doc = precedingDoc(src, docStart)
|
|
81
|
+
if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
|
|
82
|
+
const name = decl.id?.name
|
|
83
|
+
if (!name) return
|
|
84
|
+
units.push({
|
|
85
|
+
name,
|
|
86
|
+
kind: decl.type === 'ClassDeclaration' ? 'class' : 'function',
|
|
87
|
+
exported,
|
|
88
|
+
span: { start: decl.start, end: decl.end },
|
|
89
|
+
body: src.slice(decl.start, decl.end),
|
|
90
|
+
calls: collectCalls(decl),
|
|
91
|
+
doc
|
|
92
|
+
})
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (decl.type === 'VariableDeclaration') {
|
|
96
|
+
for (const d of decl.declarations ?? []) {
|
|
97
|
+
const init = d.init
|
|
98
|
+
const isFn = init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
|
|
99
|
+
if (!isFn || d.id?.type !== 'Identifier') continue
|
|
100
|
+
units.push({
|
|
101
|
+
name: d.id.name,
|
|
102
|
+
kind: 'const',
|
|
103
|
+
exported,
|
|
104
|
+
span: { start: init.start, end: init.end },
|
|
105
|
+
body: src.slice(init.start, init.end),
|
|
106
|
+
calls: collectCalls(init),
|
|
107
|
+
doc
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Юніт-шар для js/mjs/ts: top-level функції/класи/const-функції з тілом, JSDoc,
|
|
115
|
+
* прапором експорту і ребрами call-graph (виклики ІНШИХ юнітів у тілі).
|
|
116
|
+
* @param {string} src вміст файлу
|
|
117
|
+
* @param {string} [relPath] шлях (для вибору мови oxc)
|
|
118
|
+
* @returns {Array<{name:string, kind:string, exported:boolean, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null} юніти або null, якщо файл не парситься
|
|
119
|
+
*/
|
|
120
|
+
export function extractUnitsJs(src, relPath = 'scan.ts') {
|
|
121
|
+
const program = parseProgramOrNull(src, relPath)
|
|
122
|
+
if (!program || !Array.isArray(program.body)) return null
|
|
123
|
+
|
|
124
|
+
const units = []
|
|
125
|
+
for (const node of program.body) {
|
|
126
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
127
|
+
pushUnits(node.declaration, true, node.start, src, units)
|
|
128
|
+
} else if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
|
|
129
|
+
pushUnits(node.declaration, true, node.start, src, units)
|
|
130
|
+
} else {
|
|
131
|
+
pushUnits(node, false, node.start, src, units)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Ребра call-graph: лишаємо тільки виклики інших внутрішніх юнітів
|
|
136
|
+
const names = new Set(units.map(u => u.name))
|
|
137
|
+
for (const u of units) u.calls = [...u.calls].filter(n => names.has(n) && n !== u.name)
|
|
138
|
+
return units
|
|
139
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @see ./docs/units.md */
|
|
2
|
+
|
|
3
|
+
import { extractUnitsJs } from './units-js.mjs'
|
|
4
|
+
|
|
5
|
+
const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Мовно-агностичний фасад юніт-шару (Інкремент 1). Диспатчить за розширенням:
|
|
9
|
+
* js/mjs/ts → oxc; vue/py — додаються наступними кроками (поки `null` → виклик
|
|
10
|
+
* відкочується на whole-file шлях, як і раніше).
|
|
11
|
+
* @param {string} src вміст файлу
|
|
12
|
+
* @param {string} relPath шлях файлу
|
|
13
|
+
* @returns {Array<object>|null} юніти або null, якщо мова ще не підтримана / файл не парситься
|
|
14
|
+
*/
|
|
15
|
+
export function extractUnits(src, relPath) {
|
|
16
|
+
const ext = (relPath.split('.').pop() || '').toLowerCase()
|
|
17
|
+
if (JS_EXT.has(ext)) return extractUnitsJs(src, relPath)
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
5
|
import { env } from 'node:process'
|
|
7
6
|
import { resolveModel } from '../../../lib/models.mjs'
|
|
8
|
-
import {
|
|
7
|
+
import { callLlm } from '../../../lib/llm.mjs'
|
|
9
8
|
|
|
10
9
|
// Тир за замовчуванням: min → avg при ескалації (каскад local→cloud).
|
|
11
10
|
// Перевизначення через N_CURSOR_FIX_MODEL / N_CURSOR_FIX_MODEL_HEAVY.
|
|
@@ -13,6 +12,7 @@ export const MODEL = env.N_CURSOR_FIX_MODEL ?? resolveModel('min')
|
|
|
13
12
|
export const MODEL_HEAVY = env.N_CURSOR_FIX_MODEL_HEAVY ?? resolveModel('avg')
|
|
14
13
|
|
|
15
14
|
const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
|
|
15
|
+
const API_KEY_RE = /api key/i
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Витягує відносні шляхи файлів із violation output.
|
|
@@ -87,29 +87,18 @@ function buildPrompt(ruleId, ruleMdc, output, files) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* Викликає LLM за model-id
|
|
91
|
-
*
|
|
90
|
+
* Викликає LLM через спільний `callLlm` (маршрут за префіксом model-id; wire-trace).
|
|
91
|
+
* Зберігає дружнє повідомлення про відсутній API-ключ для хмарних провайдерів.
|
|
92
92
|
* @param {string} prompt текст промпта
|
|
93
93
|
* @param {string} model назва моделі (provider/id, `omlx/...` або '')
|
|
94
94
|
* @returns {{ text: string, error?: string }} текст відповіді або повідомлення про помилку
|
|
95
95
|
*/
|
|
96
96
|
function callModel(prompt, model) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const modelArgs = model ? ['--model', model] : []
|
|
105
|
-
const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {
|
|
106
|
-
encoding: 'utf8',
|
|
107
|
-
timeout: 120_000
|
|
108
|
-
})
|
|
109
|
-
if (r.error) return { text: '', error: r.error.message }
|
|
110
|
-
if (r.status !== 0) {
|
|
111
|
-
const stderr = r.stderr?.slice(0, 300) ?? ''
|
|
112
|
-
if (stderr.toLowerCase().includes('no api key') || stderr.toLowerCase().includes('api key')) {
|
|
97
|
+
try {
|
|
98
|
+
return { text: callLlm([{ role: 'user', content: prompt }], model, { timeoutMs: 120_000, caller: 'fix' }) }
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const msg = String(error.message)
|
|
101
|
+
if (API_KEY_RE.test(msg)) {
|
|
113
102
|
const provider = model ? model.split('/')[0] : 'дефолтного провайдера'
|
|
114
103
|
return {
|
|
115
104
|
text: '',
|
|
@@ -120,9 +109,8 @@ function callModel(prompt, model) {
|
|
|
120
109
|
].join(' ')
|
|
121
110
|
}
|
|
122
111
|
}
|
|
123
|
-
return { text: '', error:
|
|
112
|
+
return { text: '', error: msg }
|
|
124
113
|
}
|
|
125
|
-
return { text: r.stdout?.trim() ?? '' }
|
|
126
114
|
}
|
|
127
115
|
|
|
128
116
|
/**
|