@nitra/cursor 3.25.1 → 3.26.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
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.26.0] - 2026-06-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Root-guard для всіх деструктивних точок: CLI-команди fix/lint/coverage/change/release + дефолтний sync хардом перевіряють cwd===git-toplevel (assertCwdIsProjectRoot); worktree-preflight підсилено root-assert (pwd vs toplevel); новий meta.json-флаг requireRoot + injectRootNotice для in-place root-only скілів (n-start-check); skillRequiresRoot як явна похідна ознака захисту; валідація requireRoot у npm-module
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- n-changelog: directional version-drift — лише version ВПЕРЕД (> опублікованої/git-бази) валить як ручний bump; version ПОЗАДУ (локаль відстала від CI-релізу, ще не git pull) більше не блокує коміт (compareSemverCore/versionIsAhead)
|
|
12
|
+
|
|
3
13
|
## [3.25.1] - 2026-06-06
|
|
4
14
|
|
|
5
15
|
### Fixed
|
package/package.json
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 0 docgen-конвеєра: детермінована екстракція «факт-листа» з коду (0 токенів LLM).
|
|
3
|
+
*
|
|
4
|
+
* Ідея: винести з-під локальної моделі все, що вона псує (імена експортів, stdlib-vs-internal,
|
|
5
|
+
* крайові деталі поведінки), і віддати їй лише перефразування вже відомих фактів. Тут — суто
|
|
6
|
+
* парсинг JS/MJS регулярками: жодних мереж, LLM чи запису.
|
|
7
|
+
*
|
|
8
|
+
* Повертає об'єкт-факт-лист, який Stage 1 (docgen-prompts) перетворює на точкові промпти.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const BUILTIN_MODULES = new Set([
|
|
12
|
+
'fs', 'path', 'crypto', 'os', 'util', 'stream', 'events', 'http', 'https',
|
|
13
|
+
'url', 'child_process', 'process', 'assert', 'buffer', 'zlib', 'readline'
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
/** Прибирає `/** */`-обрамлення й `*`-префікси, повертає чистий текст рядками. */
|
|
17
|
+
function cleanJsDoc(raw) {
|
|
18
|
+
return raw
|
|
19
|
+
.replace(/^\s*\/\*\*?/, '')
|
|
20
|
+
.replace(/\*\/\s*$/, '')
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map(l => l.replace(/^\s*\*?\s?/, '').trimEnd())
|
|
23
|
+
.join('\n')
|
|
24
|
+
.trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Опис (без @-тегів) + параметри з @param як «name — опис». */
|
|
28
|
+
function parseJsDoc(raw) {
|
|
29
|
+
const text = cleanJsDoc(raw)
|
|
30
|
+
const lines = text.split('\n')
|
|
31
|
+
const descLines = []
|
|
32
|
+
const params = []
|
|
33
|
+
let ret = ''
|
|
34
|
+
for (const l of lines) {
|
|
35
|
+
const pm = l.match(/^@param\s+(?:\{[^}]*\}\s+)?\[?([A-Za-z0-9_.]+)\]?\s*(.*)$/)
|
|
36
|
+
const rm = l.match(/^@returns?\s+(?:\{[^}]*\}\s+)?(.*)$/)
|
|
37
|
+
if (pm) {
|
|
38
|
+
const desc = pm[2].trim()
|
|
39
|
+
// «опис.» — JSDoc-заглушка без сенсу; не тягнемо її як факт
|
|
40
|
+
params.push({ name: pm[1], desc: desc === 'опис.' ? '' : desc })
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
if (rm) { ret = rm[1].trim(); continue }
|
|
44
|
+
if (l.startsWith('@')) continue
|
|
45
|
+
descLines.push(l)
|
|
46
|
+
}
|
|
47
|
+
return { desc: descLines.join('\n').trim(), params, ret }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Провідний блок-коментар файлу (намір), якщо він перед першим import/кодом. */
|
|
51
|
+
function extractFileHeader(src) {
|
|
52
|
+
const m = src.match(/^\s*\/\*\*([\s\S]*?)\*\//)
|
|
53
|
+
if (!m) return ''
|
|
54
|
+
// має бути на самому початку (до import/код)
|
|
55
|
+
if (src.slice(0, m.index).trim() !== '') return ''
|
|
56
|
+
return parseJsDoc(m[0]).desc
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Блок-коментар, що стоїть ВПРИТУЛ перед позицією (лише пробіли між ними).
|
|
61
|
+
* `(?:(?!\*/)[\s\S])*` гарантує, що тіло не містить `*/`, тож захоплюється рівно один
|
|
62
|
+
* найближчий блок — без жадібного «перестрибування» через імпорти/код.
|
|
63
|
+
*/
|
|
64
|
+
function precedingJsDoc(prefix) {
|
|
65
|
+
const m = prefix.match(/\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/)
|
|
66
|
+
return m ? m[0] : null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Експорти + JSDoc, що безпосередньо передує кожному. */
|
|
70
|
+
function extractExports(src) {
|
|
71
|
+
const out = []
|
|
72
|
+
const re = /export\s+(?:async\s+)?(function|const|class)\s+([A-Za-z0-9_]+)/g
|
|
73
|
+
let m
|
|
74
|
+
while ((m = re.exec(src))) {
|
|
75
|
+
const [, kind, name] = m
|
|
76
|
+
const jsdocRaw = precedingJsDoc(src.slice(0, m.index))
|
|
77
|
+
out.push({ name, kind, ...(jsdocRaw ? parseJsDoc(jsdocRaw) : { desc: '', params: [], ret: '' }) })
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Імпорти, класифіковані на stdlib / npm / internal. */
|
|
83
|
+
function extractImports(src) {
|
|
84
|
+
const stdlib = new Set(), npm = new Set(), internal = new Set()
|
|
85
|
+
const re = /^import\s+[\s\S]*?from\s+['"]([^'"]+)['"]/gm
|
|
86
|
+
let m
|
|
87
|
+
while ((m = re.exec(src))) {
|
|
88
|
+
const s = m[1]
|
|
89
|
+
if (s.startsWith('node:') || BUILTIN_MODULES.has(s.split('/')[0])) stdlib.add(s.replace(/^node:/, ''))
|
|
90
|
+
else if (s.startsWith('.') || s.startsWith('/')) internal.add(s)
|
|
91
|
+
else npm.add(s)
|
|
92
|
+
}
|
|
93
|
+
return { stdlib: [...stdlib], npm: [...npm], internal: [...internal] }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Імена символів, імпортованих із внутрішніх модулів — їх модель не має згадувати. */
|
|
97
|
+
function extractInternalSymbols(src) {
|
|
98
|
+
const out = new Set()
|
|
99
|
+
const re = /import\s+(?:([A-Za-z0-9_$]+)\s*,?\s*)?(?:\{([^}]+)\})?\s+from\s+['"](\.[^'"]+)['"]/g
|
|
100
|
+
let m
|
|
101
|
+
while ((m = re.exec(src))) {
|
|
102
|
+
if (m[1]) out.add(m[1].trim())
|
|
103
|
+
if (m[2]) for (const n of m[2].split(',')) {
|
|
104
|
+
const name = n.replace(/\s+as\s+.*/, '').trim()
|
|
105
|
+
if (name) out.add(name)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [...out]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Поведінкові маркери — евристики регулярками. */
|
|
112
|
+
function extractMarkers(src) {
|
|
113
|
+
// помітні «пропуски»: dir/segment-літерали у фільтрах
|
|
114
|
+
const skips = new Set()
|
|
115
|
+
for (const lit of ['.github', '.git', 'node_modules', 'base/', 'ua/', '.firebase']) {
|
|
116
|
+
if (src.includes(`'${lit}`) || src.includes(`"${lit}`) || src.includes(`/${lit}`)) skips.add(lit)
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
readOnly: !/\b(writeFile|mkdir|rmdir|unlink|appendFile|createWriteStream|rm\()/.test(src),
|
|
120
|
+
catchesErrors: /catch\s*\(/.test(src) || /\btry\s*\{/.test(src),
|
|
121
|
+
returnsFalsyOnFail: /return\s+(false|null|''|"")/.test(src),
|
|
122
|
+
network: /\bfetch\(|https?\.|axios|got\(/.test(src),
|
|
123
|
+
caches: /new Map\(\)|Cache|cache/.test(src),
|
|
124
|
+
skips: [...skips]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Головний екстрактор: код файлу → факт-лист.
|
|
130
|
+
* @param {string} src вміст файлу
|
|
131
|
+
* @param {string} relPath шлях (для контексту/мови екстрактора)
|
|
132
|
+
* @returns {{relPath:string, lang:string, header:string, exports:Array, imports:object, markers:object}}
|
|
133
|
+
*/
|
|
134
|
+
export function extractFacts(src, relPath) {
|
|
135
|
+
const lang = relPath.split('.').pop()
|
|
136
|
+
if (!['js', 'mjs', 'ts'].includes(lang)) {
|
|
137
|
+
return { relPath, lang, unsupported: true, header: '', exports: [], imports: {}, markers: {} }
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
relPath,
|
|
141
|
+
lang,
|
|
142
|
+
header: extractFileHeader(src),
|
|
143
|
+
exports: extractExports(src),
|
|
144
|
+
imports: extractImports(src),
|
|
145
|
+
internalSymbols: extractInternalSymbols(src),
|
|
146
|
+
markers: extractMarkers(src)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// CLI для інспекції: node docgen-extract.mjs <file>
|
|
151
|
+
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
152
|
+
import { readFileSync } from 'node:fs'
|
|
153
|
+
if (isRunAsCli(import.meta.url)) {
|
|
154
|
+
const file = process.argv[2]
|
|
155
|
+
if (!file) { console.error('Usage: node docgen-extract.mjs <file>'); process.exit(1) }
|
|
156
|
+
const facts = extractFacts(readFileSync(file, 'utf8'), file)
|
|
157
|
+
console.log(JSON.stringify(facts, null, 2))
|
|
158
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* docgen-конвеєр (входна точка): код файлу → .md-документація.
|
|
3
|
+
*
|
|
4
|
+
* Інверсія керування: веде цей JS, а локальна модель — лише сервіс перефразування.
|
|
5
|
+
* Stage 0 extractFacts — факти з коду (0 токенів)
|
|
6
|
+
* Stage 1 sectionInstructions — точкові промпти на кожну секцію (спільний KV-cached префікс)
|
|
7
|
+
* Stage 3 assemble — фіксовані заголовки/порядок + зрізання fence
|
|
8
|
+
* Режим `--oneshot` — база для порівняння (один промпт на весь документ).
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { basename } from 'node:path'
|
|
12
|
+
import { request } from 'node:http'
|
|
13
|
+
import { extractFacts } from './docgen-extract.mjs'
|
|
14
|
+
import { sectionMessages, oneShotMessages } from './docgen-prompts.mjs'
|
|
15
|
+
|
|
16
|
+
/** Один виклик чату до ollama зі streaming (токени стримуються → socket активний, жодного timeout). */
|
|
17
|
+
async function ollamaChat(messages, { model, numPredict = 600 }) {
|
|
18
|
+
const body = JSON.stringify({
|
|
19
|
+
model, messages, stream: true, think: false,
|
|
20
|
+
options: { num_ctx: 8192, temperature: 0.2, num_predict: numPredict },
|
|
21
|
+
keep_alive: '15m'
|
|
22
|
+
})
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const req = request(
|
|
25
|
+
{ hostname: 'localhost', port: 11434, path: '/api/chat', method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
|
|
27
|
+
(res) => {
|
|
28
|
+
let text = '', genTok = 0, buf = ''
|
|
29
|
+
res.on('data', chunk => {
|
|
30
|
+
buf += chunk.toString()
|
|
31
|
+
const lines = buf.split('\n')
|
|
32
|
+
buf = lines.pop()
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (!line.trim()) continue
|
|
35
|
+
try {
|
|
36
|
+
const j = JSON.parse(line)
|
|
37
|
+
text += j.message?.content ?? ''
|
|
38
|
+
if (j.done) genTok = j.eval_count ?? 0
|
|
39
|
+
} catch { /* partial line */ }
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
res.on('end', () => resolve({ text, genTok }))
|
|
43
|
+
res.on('error', reject)
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
req.on('error', reject)
|
|
47
|
+
req.write(body)
|
|
48
|
+
req.end()
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Прибирає ```-обгортку й випадковий провідний `##`-заголовок із секції. */
|
|
53
|
+
function stripSection(text) {
|
|
54
|
+
let t = text.trim()
|
|
55
|
+
if (t.startsWith('```')) {
|
|
56
|
+
t = t.replace(/^```[a-z]*\n?/, '').replace(/\n?```\s*$/, '').trim()
|
|
57
|
+
}
|
|
58
|
+
t = t.replace(/^#{1,6}\s+.*\n+/, '') // зрізати випадковий заголовок
|
|
59
|
+
return t.trim()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Stage 2 (детермінований лінт, 0 токенів): зрізає сигнатури `name(args)` → `name`.
|
|
64
|
+
* Два проходи — щоб зняти вкладені виклики на кшталт `check(cwd = process.cwd())`.
|
|
65
|
+
* Не чіпає дужки без ідентифікатора перед ними (напр. `(abie.mdc)`, «(наприклад)»).
|
|
66
|
+
*/
|
|
67
|
+
function stripSignatures(text) {
|
|
68
|
+
let t = text
|
|
69
|
+
for (let i = 0; i < 2; i++) t = t.replace(/([`\w$.]+)\([^()]*\)/g, '$1')
|
|
70
|
+
return t
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Stage 3: фіксовані заголовки у фіксованому порядку. */
|
|
74
|
+
function assemble(stem, sections) {
|
|
75
|
+
const order = [
|
|
76
|
+
['overview', '## Огляд'],
|
|
77
|
+
['behavior', '## Поведінка'],
|
|
78
|
+
['api', '## Публічний API'],
|
|
79
|
+
['guarantees', '## Гарантії поведінки']
|
|
80
|
+
]
|
|
81
|
+
const parts = [`# ${stem}`]
|
|
82
|
+
for (const [key, title] of order) {
|
|
83
|
+
const body = sections[key]
|
|
84
|
+
if (body && body.trim()) parts.push(`${title}\n\n${body.trim()}`)
|
|
85
|
+
}
|
|
86
|
+
return parts.join('\n\n') + '\n'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Оркестрований режим: секційно-мінімальний контекст — код інгестується лише в `behavior`. */
|
|
90
|
+
async function generateOrchestrated(facts, src, model) {
|
|
91
|
+
const sections = {}
|
|
92
|
+
let genTok = 0
|
|
93
|
+
for (const s of sectionMessages(facts, src)) {
|
|
94
|
+
const { text, genTok: g } = await ollamaChat(s.messages, { model, numPredict: s.numPredict })
|
|
95
|
+
sections[s.key] = stripSignatures(stripSection(text))
|
|
96
|
+
genTok += g
|
|
97
|
+
}
|
|
98
|
+
return { md: assemble(basename(facts.relPath), sections), genTok }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One-shot режим: один промпт на весь документ. */
|
|
102
|
+
async function generateOneShot(facts, src, model) {
|
|
103
|
+
const { text, genTok } = await ollamaChat(oneShotMessages(facts, src), { model, numPredict: 1500 })
|
|
104
|
+
let md = stripSignatures(stripSection(text)) // Stage-2 лінт і для one-shot
|
|
105
|
+
if (!md.startsWith('#')) md = `# ${basename(facts.relPath)}\n\n${md}`
|
|
106
|
+
return { md: md + '\n', genTok }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Головний API: файл → { md, genTok, ms }. */
|
|
110
|
+
export async function generateDoc(file, { model = 'gemma3:4b', mode = 'orchestrated' } = {}) {
|
|
111
|
+
const src = readFileSync(file, 'utf8')
|
|
112
|
+
const facts = extractFacts(src, file)
|
|
113
|
+
const t0 = Date.now()
|
|
114
|
+
const r = facts.unsupported
|
|
115
|
+
? await generateOneShot(facts, src, model) // fallback для не-JS
|
|
116
|
+
: mode === 'oneshot'
|
|
117
|
+
? await generateOneShot(facts, src, model)
|
|
118
|
+
: await generateOrchestrated(facts, src, model)
|
|
119
|
+
return { ...r, ms: Date.now() - t0 }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// CLI: node docgen-gen.mjs <file> [--oneshot] [--model <m>]
|
|
123
|
+
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
124
|
+
if (isRunAsCli(import.meta.url)) {
|
|
125
|
+
const args = process.argv.slice(2)
|
|
126
|
+
const file = args.find(a => !a.startsWith('--'))
|
|
127
|
+
const mode = args.includes('--oneshot') ? 'oneshot' : 'orchestrated'
|
|
128
|
+
const mi = args.indexOf('--model'); const model = mi >= 0 ? args[mi + 1] : 'gemma3:4b'
|
|
129
|
+
if (!file) { console.error('Usage: node docgen-gen.mjs <file> [--oneshot] [--model <m>]'); process.exit(1) }
|
|
130
|
+
const r = await generateDoc(file, { model, mode })
|
|
131
|
+
process.stderr.write(`[${mode}] ${r.ms}ms / ${r.genTok} tok\n`)
|
|
132
|
+
process.stdout.write(r.md)
|
|
133
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 1 docgen-конвеєра: факт-лист + код → точкові промпти на кожну секцію.
|
|
3
|
+
*
|
|
4
|
+
* v2 — СЕКЦІЙНО-МІНІМАЛЬНИЙ контекст: код іде ЛИШЕ у `Поведінку`. `Огляд` бере тільки
|
|
5
|
+
* header, `API` — лише список експортів, `Гарантії` — лише markers. Так інгест коду
|
|
6
|
+
* оплачується один раз (а не на кожну секцію), і оркестрація перестає програвати в часі.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const STYLE = [
|
|
10
|
+
'Ти технічний письменник. Пишеш лаконічну ПОВЕДІНКОВУ документацію до коду українською, чистим Markdown.',
|
|
11
|
+
'Пиши ЩО і НАВІЩО, не ЯК. Без вступів і висновків. Не обгортай у ```-блок.',
|
|
12
|
+
'Заборонено: сигнатури, типи, параметри функцій; перелік stdlib-модулів; опис regex чи внутрішніх приватних імен.'
|
|
13
|
+
].join(' ')
|
|
14
|
+
|
|
15
|
+
/** Короткий людиночитний витяг фактів (без коду). */
|
|
16
|
+
function factsSummary(facts) {
|
|
17
|
+
const m = facts.markers || {}
|
|
18
|
+
const lines = []
|
|
19
|
+
if (facts.header) lines.push(`Намір файлу: ${facts.header.replace(/\n/g, ' ')}`)
|
|
20
|
+
if (facts.exports?.length) lines.push(`Публічні функції: ${facts.exports.map(e => e.name).join(', ')}`)
|
|
21
|
+
if (m.skips?.length) lines.push(`Свідомо пропускає шляхи: ${m.skips.join(', ')}`)
|
|
22
|
+
lines.push(`Read-only: ${m.readOnly ? 'так' : 'ні'}`)
|
|
23
|
+
if (m.catchesErrors) lines.push('Перехоплює помилки (fail-safe), не кидає винятків назовні')
|
|
24
|
+
if (m.returnsFalsyOnFail) lines.push('За невдачі повертає false/null замість винятку')
|
|
25
|
+
lines.push(m.caches ? 'Кешування: так, у межах прогону' : 'Кешування: НЕМАЄ — не згадуй кеш у гарантіях')
|
|
26
|
+
if (m.network) lines.push('Звертається до мережі')
|
|
27
|
+
else lines.push('Робота з мережею: немає')
|
|
28
|
+
return lines.join('\n')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const msgs = (system, user) => [{ role: 'system', content: system }, { role: 'user', content: user }]
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Секційні набори messages з МІНІМАЛЬНИМ контекстом під кожну секцію.
|
|
35
|
+
* Код потрапляє лише в `behavior`; решта секцій — на факт-листі.
|
|
36
|
+
* @returns {Array<{key:string, messages:object[], numPredict:number}>}
|
|
37
|
+
*/
|
|
38
|
+
export function sectionMessages(facts, src) {
|
|
39
|
+
const factsTxt = factsSummary(facts)
|
|
40
|
+
const multi = (facts.exports?.length || 0) > 1
|
|
41
|
+
const out = []
|
|
42
|
+
|
|
43
|
+
// Огляд — лише факти (без коду)
|
|
44
|
+
out.push({
|
|
45
|
+
key: 'overview', numPredict: 220,
|
|
46
|
+
messages: msgs(`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
|
|
47
|
+
'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій.')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Поведінка — ЄДИНА секція, якій потрібен код
|
|
51
|
+
out.push({
|
|
52
|
+
key: 'behavior', numPredict: 500,
|
|
53
|
+
messages: msgs(`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
|
|
54
|
+
`Напиши вміст секції «Поведінка»: ${multi ? 'для кожної публічної функції — один короткий пункт «що вона робить»' : 'нумерований алгоритм у бізнес-термінах'}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${facts.internalSymbols?.length ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.` : ''} Без заголовка.`)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// API — лише список експортів (без коду)
|
|
58
|
+
if (multi || facts.exports?.some(e => e.desc)) {
|
|
59
|
+
const list = facts.exports.map(e => `- ${e.name}: ${e.desc || '(сформулюй стисло з наміру файлу)'}`).join('\n')
|
|
60
|
+
out.push({
|
|
61
|
+
key: 'api', numPredict: 320,
|
|
62
|
+
messages: msgs(STYLE,
|
|
63
|
+
`Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка.`)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Гарантії — лише markers (без коду)
|
|
68
|
+
out.push({
|
|
69
|
+
key: 'guarantees', numPredict: 300,
|
|
70
|
+
messages: msgs(`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
|
|
71
|
+
'Напиши вміст секції «Гарантії поведінки» як маркери-інваріанти СУВОРО на основі ВІДОМИХ ФАКТІВ (read-only, fail-safe, пропуски). Згадуй кеш ЛИШЕ якщо у фактах прямо є «Кешує». Без сигнатур у дужках і без імен внутрішніх структур/Map-ів/кешів. Не вигадуй гарантій, яких немає у фактах. Без заголовка.')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One-shot messages (база для порівняння). */
|
|
78
|
+
export function oneShotMessages(facts, src) {
|
|
79
|
+
const multi = (facts.exports?.length || 0) > 1
|
|
80
|
+
return msgs(STYLE,
|
|
81
|
+
`Напиши документацію для файлу. Секції: ## Огляд (1-3 речення), ## Поведінка (нумерований/маркований алгоритм), ${multi ? '## Публічний API (назва + що робить), ' : ''}## Гарантії поведінки.\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\``)
|
|
82
|
+
}
|