@7n/rules 1.49.26 → 1.50.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 +10 -0
- package/package.json +1 -1
- package/rules/doc-files/check/docs/index.md +10 -0
- package/rules/doc-files/check/docs/main.md +4 -2
- package/rules/doc-files/check/main.mjs +12 -4
- package/rules/doc-files/docgen-crc/docs/index.md +9 -0
- package/rules/doc-files/docgen-crc/docs/main.md +47 -26
- package/rules/doc-files/docgen-crc/main.mjs +21 -4
- package/rules/doc-files/docgen-files-batch/docs/main.md +20 -14
- package/rules/doc-files/docgen-files-batch/main.mjs +54 -20
- package/rules/doc-files/docgen-gen/docs/main.md +35 -3
- package/rules/doc-files/docgen-gen/main.mjs +253 -19
- package/rules/doc-files/docgen-prompts/docs/main.md +18 -12
- package/rules/doc-files/docgen-prompts/main.mjs +19 -12
- package/rules/doc-files/docgen-scan/docs/main.md +44 -32
- package/rules/doc-files/docgen-scan/main.mjs +6 -3
- package/rules/doc-files/docgen-test-context/docs/index.md +9 -0
- package/rules/doc-files/docgen-test-context/docs/main.md +57 -0
- package/rules/doc-files/docgen-test-context/main.mjs +212 -0
- package/rules/doc-files/main.mdc +37 -6
- package/rules/k8s/manifests/main.mjs +94 -38
- package/scripts/lib/lint-surface/lint-lock.mjs +81 -16
- package/scripts/lib/lint-surface/progress.mjs +15 -3
- package/scripts/lib/lint-surface/run-detectors.mjs +9 -1
- package/scripts/lib/lint-surface/types.mjs +2 -0
- package/skills/doc-files/SKILL.md +21 -6
|
@@ -8,6 +8,7 @@ import { startChain } from '@7n/llm-lib/chain'
|
|
|
8
8
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
9
9
|
import { docPathForSource } from '../docgen-scan/main.mjs'
|
|
10
10
|
import { loadDocFilesExtractors } from '../docgen-scan/lang-extensions.mjs'
|
|
11
|
+
import { buildTestEvidenceIndex, renderTestScenarios, testEvidenceForSource } from '../docgen-test-context/main.mjs'
|
|
11
12
|
import { extractAnchors, anchorTokens } from '../docgen-extract-anchors/main.mjs'
|
|
12
13
|
import { QUALITY_THRESHOLD } from '../docgen-crc/main.mjs'
|
|
13
14
|
import { JUDGE_ENABLED, JUDGE_MODEL, detectRefusalFiller, judgeDoc, judgeFailsDoc } from '../docgen-judge/main.mjs'
|
|
@@ -93,6 +94,13 @@ async function callLlm(messages, model, opts = {}) {
|
|
|
93
94
|
|
|
94
95
|
const FENCE_OPEN_RE = /^```[a-z]*\n?/
|
|
95
96
|
const FENCE_CLOSE_RE = /\n?```\s*$/
|
|
97
|
+
const EMPTY_INLINE_CODE_RE = /``/g
|
|
98
|
+
// Порожній code span — артефакт LLM, а не валідний факт про файл. Зрізаємо всю
|
|
99
|
+
// фразу: інакше після видалення лише `` лишається вигадане твердження довкола.
|
|
100
|
+
const EMPTY_INLINE_CODE_SENTENCE_RE = /[^.!?\n]*``[^.!?\n]*[.!?]/g
|
|
101
|
+
// Внутрішній приклад із коментарів на кшталт `(foo.mdc)` не є поведінкою
|
|
102
|
+
// модуля. Модель інколи переносить його у prose, тому таку фразу відкидаємо.
|
|
103
|
+
const PAREN_MDC_SENTENCE_RE = /[^.!?\n]*\([^()\n]+\.mdc\)[^.!?\n]*[.!?]/g
|
|
96
104
|
const LEADING_HEADING_RE = /^#{1,6}[ \t]{1,8}[^\n]{0,400}\n{1,8}/
|
|
97
105
|
// R9: чат-преамбули малих моделей — «озвучування завдання» перед відповіддю
|
|
98
106
|
// («Ось оновлена чорнетка секції…», «Як технічний письменник, я створю…»,
|
|
@@ -138,6 +146,11 @@ const PROTECTED_HEADING = 'Призначення'
|
|
|
138
146
|
const PROTECTED_START_RE = /^##\s+Призначення\s*$/
|
|
139
147
|
const H2_RE = /^##\s/
|
|
140
148
|
const H1_RE = /^#\s/
|
|
149
|
+
const SIGNATURE_CALL_RE = /([`\w$.]{1,80})\([^()]{0,300}\)/g
|
|
150
|
+
const NORMALIZED_SPACE_RE = /\s+/g
|
|
151
|
+
const H2_HEADING_RE = /^##\s.+$/gm
|
|
152
|
+
const TEST_SCENARIOS_HEADING = '## Сценарії використання'
|
|
153
|
+
const BEHAVIOR_HEADING = '## Поведінка'
|
|
141
154
|
|
|
142
155
|
/**
|
|
143
156
|
* R9: зрізає провідні чат-преамбули й дубль назви секції з початку тексту.
|
|
@@ -169,7 +182,11 @@ function stripSection(text) {
|
|
|
169
182
|
t = t.replace(FENCE_OPEN_RE, '').replace(FENCE_CLOSE_RE, '').trim()
|
|
170
183
|
}
|
|
171
184
|
t = t.replace(LEADING_HEADING_RE, '') // зрізати випадковий заголовок
|
|
172
|
-
return stripLeadingPreamble(t.trim())
|
|
185
|
+
return stripLeadingPreamble(t.trim())
|
|
186
|
+
.replaceAll(EMPTY_INLINE_CODE_SENTENCE_RE, '')
|
|
187
|
+
.replaceAll(PAREN_MDC_SENTENCE_RE, '')
|
|
188
|
+
.replaceAll(EMPTY_INLINE_CODE_RE, '')
|
|
189
|
+
.trim()
|
|
173
190
|
}
|
|
174
191
|
|
|
175
192
|
/**
|
|
@@ -181,7 +198,7 @@ function stripSection(text) {
|
|
|
181
198
|
*/
|
|
182
199
|
function stripSignatures(text) {
|
|
183
200
|
let t = text
|
|
184
|
-
for (let i = 0; i < 2; i++) t = t.replaceAll(
|
|
201
|
+
for (let i = 0; i < 2; i++) t = t.replaceAll(SIGNATURE_CALL_RE, '$1')
|
|
185
202
|
return t
|
|
186
203
|
}
|
|
187
204
|
|
|
@@ -424,6 +441,67 @@ export async function buildApiSection(facts, anchors, model, timeoutMs, temperat
|
|
|
424
441
|
return [coveredBlock, gapDraft].filter(Boolean).join('\n')
|
|
425
442
|
}
|
|
426
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Чи коментарі автора повністю покривають машинну документацію: header дає
|
|
446
|
+
* «Огляд», а змістовні описи всіх public API — відповідну секцію. У такому
|
|
447
|
+
* разі LLM не потрібна: текст зберігається дослівно для JS, Rust і Python.
|
|
448
|
+
* @param {object} facts факт-лист мовного екстрактора
|
|
449
|
+
* @returns {boolean} true, якщо можна зібрати документ без LLM
|
|
450
|
+
*/
|
|
451
|
+
export function hasCompleteCommentDocumentation(facts) {
|
|
452
|
+
return Boolean(facts.header?.trim()) && (facts.exports ?? []).every(exp => !isApiGap(exp))
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Мінімальний розмір header, який може бути лише pointer-ом, а не наративом. */
|
|
456
|
+
const SHORT_HEADER_CHARS = 240
|
|
457
|
+
/** Один докладний public API може сам закрити короткий header-pointer. */
|
|
458
|
+
const SUFFICIENT_SINGLE_API_CHARS = 160
|
|
459
|
+
/** Сигнали потоку, який доцільно стисло звʼязати окремою «Поведінкою». */
|
|
460
|
+
const BEHAVIOR_FLOW_RE =
|
|
461
|
+
/\b(?:await|Promise\.all|Semaphore|JoinSet|chunks|concurr|retry|queue|state|workflow|resolver|provider|catch|try|match)\b/i
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Вибирає гібридний режим для повністю прокоментованого source. Короткий
|
|
465
|
+
* header майже напевно є pointer-ом, а середній header разом із явним flow у
|
|
466
|
+
* коді потребує короткого LLM-доповнення. Детальний наратив лишається 0-LLM.
|
|
467
|
+
* @param {object} facts факт-лист мовного екстрактора
|
|
468
|
+
* @param {string} src вміст source-файлу
|
|
469
|
+
* @returns {'fallback'|'comment-only'|'comment+behavior'} режим генерації
|
|
470
|
+
*/
|
|
471
|
+
export function commentDocumentationMode(facts, src) {
|
|
472
|
+
if (!hasCompleteCommentDocumentation(facts)) return 'fallback'
|
|
473
|
+
const headerSize = facts.header.trim().replaceAll(NORMALIZED_SPACE_RE, ' ').length
|
|
474
|
+
const exports = facts.exports ?? []
|
|
475
|
+
const apiSize = exports
|
|
476
|
+
.map(exp => exp.desc?.replaceAll(NORMALIZED_SPACE_RE, ' ').length ?? 0)
|
|
477
|
+
.reduce((sum, size) => sum + size, 0)
|
|
478
|
+
// У маленькому модулі один ретельно описаний API уже є поведінковим
|
|
479
|
+
// контрактом. LLM тут найчастіше додає загальні фрази замість глибини.
|
|
480
|
+
if (headerSize < SHORT_HEADER_CHARS && exports.length === 1 && apiSize >= SUFFICIENT_SINGLE_API_CHARS) {
|
|
481
|
+
return 'comment-only'
|
|
482
|
+
}
|
|
483
|
+
if (headerSize < SHORT_HEADER_CHARS) return 'comment+behavior'
|
|
484
|
+
if (headerSize < 900 && BEHAVIOR_FLOW_RE.test(src)) return 'comment+behavior'
|
|
485
|
+
return 'comment-only'
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Збирає документ лише з авторських коментарів і детермінованих фактів.
|
|
490
|
+
* @param {object} facts факт-лист мовного екстрактора
|
|
491
|
+
* @param {string|null} intent захищена секція «Призначення»
|
|
492
|
+
* @param {string} [behavior] додаткова LLM-секція «Поведінка» для comment+behavior режиму
|
|
493
|
+
*/
|
|
494
|
+
function commentOnlyDoc(facts, intent, behavior = '') {
|
|
495
|
+
const sections = {
|
|
496
|
+
overview: facts.header.trim(),
|
|
497
|
+
api: (facts.exports ?? []).map(exp => renderApiLine(exp)).join('\n'),
|
|
498
|
+
behavior,
|
|
499
|
+
scenarios: renderTestScenarios(facts.testScenarioFiles ?? []),
|
|
500
|
+
guarantees: guaranteesFromMarkers(facts)
|
|
501
|
+
}
|
|
502
|
+
return { md: insertProtected(assemble(basename(facts.relPath), sections), intent) }
|
|
503
|
+
}
|
|
504
|
+
|
|
427
505
|
/**
|
|
428
506
|
* One-shot: один виклик LLM на весь документ (для unsupported-структур).
|
|
429
507
|
* @param {object} facts факт-лист
|
|
@@ -437,7 +515,7 @@ async function oneShotDoc(facts, src, model, timeoutMs = LOCAL_TIMEOUT_MS, { int
|
|
|
437
515
|
const text = await callLlm(oneShotMessages(facts, src), model, { timeoutMs })
|
|
438
516
|
let md = stripSignatures(stripSection(text))
|
|
439
517
|
if (!md.startsWith('#')) md = `# ${basename(facts.relPath)}\n\n${md}`
|
|
440
|
-
return { md: insertProtected(md + '\n', intent) }
|
|
518
|
+
return { md: insertProtected(insertTestScenarios(md + '\n', facts), intent) }
|
|
441
519
|
}
|
|
442
520
|
|
|
443
521
|
/**
|
|
@@ -451,6 +529,7 @@ function assemble(stem, sections) {
|
|
|
451
529
|
['overview', '## Огляд'],
|
|
452
530
|
['behavior', '## Поведінка'],
|
|
453
531
|
['api', '## Публічний API'],
|
|
532
|
+
['scenarios', '## Сценарії використання'],
|
|
454
533
|
['guarantees', '## Гарантії поведінки']
|
|
455
534
|
]
|
|
456
535
|
const parts = [`# ${stem}`]
|
|
@@ -461,6 +540,55 @@ function assemble(stem, sections) {
|
|
|
461
540
|
return parts.join('\n\n') + '\n'
|
|
462
541
|
}
|
|
463
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Видаляє H2-секцію з Markdown без regex з довільним тілом секції.
|
|
545
|
+
* @param {string} md зібраний Markdown-документ
|
|
546
|
+
* @param {string} heading точний H2-заголовок секції
|
|
547
|
+
* @returns {string} документ без секції
|
|
548
|
+
*/
|
|
549
|
+
function removeH2Section(md, heading) {
|
|
550
|
+
const lines = md.split('\n')
|
|
551
|
+
const start = lines.findIndex(line => line.trim() === heading)
|
|
552
|
+
if (start === -1) return md
|
|
553
|
+
const end = lines.findIndex((line, index) => index > start && H2_RE.test(line))
|
|
554
|
+
return [...lines.slice(0, start), ...lines.slice(end === -1 ? lines.length : end)].join('\n')
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Повертає тіло H2-секції з Markdown без regex з довільним тілом секції.
|
|
559
|
+
* @param {string} md зібраний Markdown-документ
|
|
560
|
+
* @param {string} heading точний H2-заголовок секції
|
|
561
|
+
* @returns {string} тіло секції або порожній рядок
|
|
562
|
+
*/
|
|
563
|
+
function h2SectionBody(md, heading) {
|
|
564
|
+
const lines = md.split('\n')
|
|
565
|
+
const start = lines.findIndex(line => line.trim() === heading)
|
|
566
|
+
if (start === -1) return ''
|
|
567
|
+
const end = lines.findIndex((line, index) => index > start && H2_RE.test(line))
|
|
568
|
+
return lines
|
|
569
|
+
.slice(start + 1, end === -1 ? lines.length : end)
|
|
570
|
+
.join('\n')
|
|
571
|
+
.trim()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Додає test-сценарії до one-shot/batch-документа. Для unsupported мов основний
|
|
576
|
+
* Markdown ще повертає LLM, але test-секція лишається виключно JS-рендером.
|
|
577
|
+
* @param {string} md зібраний Markdown-документ
|
|
578
|
+
* @param {object} facts факт-лист із `testScenarioFiles`
|
|
579
|
+
* @returns {string} документ із детермінованою секцією сценаріїв
|
|
580
|
+
*/
|
|
581
|
+
export function insertTestScenarios(md, facts) {
|
|
582
|
+
const scenarios = renderTestScenarios(facts.testScenarioFiles ?? [])
|
|
583
|
+
if (!scenarios) return md
|
|
584
|
+
const withoutOld = removeH2Section(md, TEST_SCENARIOS_HEADING).trimEnd()
|
|
585
|
+
const section = `## Сценарії використання\n\n${scenarios}`
|
|
586
|
+
const guarantees = '\n\n## Гарантії поведінки'
|
|
587
|
+
const at = withoutOld.indexOf(guarantees)
|
|
588
|
+
if (at === -1) return `${withoutOld}\n\n${section}\n`
|
|
589
|
+
return `${withoutOld.slice(0, at)}\n\n${section}${withoutOld.slice(at)}\n`
|
|
590
|
+
}
|
|
591
|
+
|
|
464
592
|
/**
|
|
465
593
|
* Orchestrated: N окремих LLM-викликів, по одному на секцію.
|
|
466
594
|
* Код потрапляє лише в `behavior`; решта секцій — на мінімальному факт-листі.
|
|
@@ -478,10 +606,21 @@ async function orchestratedDoc(
|
|
|
478
606
|
timeoutMs,
|
|
479
607
|
{ anchors = null, temperature = 0.2, intent = null } = {}
|
|
480
608
|
) {
|
|
609
|
+
const commentMode = commentDocumentationMode(facts, src)
|
|
610
|
+
if (commentMode === 'comment-only') return commentOnlyDoc(facts, intent)
|
|
611
|
+
if (commentMode === 'comment+behavior') {
|
|
612
|
+
const [behaviorPrompt] = sectionMessages(facts, src, anchors, intent, { complementaryBehavior: true })
|
|
613
|
+
const behavior = stripSignatures(
|
|
614
|
+
stripSection(await callLlm(behaviorPrompt.messages, model, { timeoutMs, temperature }))
|
|
615
|
+
)
|
|
616
|
+
return { md: commentOnlyDoc(facts, intent, CRITIC_NONE_RE.test(behavior.trim()) ? '' : behavior).md }
|
|
617
|
+
}
|
|
481
618
|
const sections = {}
|
|
482
619
|
const anc = anchors ?? extractAnchors(src)
|
|
483
620
|
// E3: «Гарантії» — детермінований шаблон з markers (0 LLM-запитів, 0 generic-фраз)
|
|
484
621
|
sections.guarantees = guaranteesFromMarkers(facts)
|
|
622
|
+
// Test/spec назви рендеряться JS-ом дослівно, а не потрапляють у LLM prompt.
|
|
623
|
+
sections.scenarios = renderTestScenarios(facts.testScenarioFiles ?? [])
|
|
485
624
|
// Спершу Поведінка — єдина секція з кодом (sectionMessages повертає лише її)
|
|
486
625
|
for (const s of sectionMessages(facts, src, anc, intent)) {
|
|
487
626
|
sections[s.key] = stripSignatures(stripSection(await callLlm(s.messages, model, { timeoutMs, temperature })))
|
|
@@ -502,6 +641,27 @@ async function orchestratedDoc(
|
|
|
502
641
|
return { md: insertProtected(assemble(basename(facts.relPath), sections), intent) }
|
|
503
642
|
}
|
|
504
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Semantic evidence для judge: лише source-код; test-сценарії формує JS.
|
|
646
|
+
* @param {string} src source-код
|
|
647
|
+
* @returns {string} evidence для judge
|
|
648
|
+
*/
|
|
649
|
+
function semanticEvidence(src) {
|
|
650
|
+
return src
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Витягує LLM-секцію «Поведінка» для вузького semantic judge. Авторські
|
|
655
|
+
* «Огляд»/API не оцінюються моделлю і не можуть бути нею переписані.
|
|
656
|
+
* @param {string} md зібраний Markdown-документ
|
|
657
|
+
* @returns {string} мінімальний документ з однією секцією або порожній рядок
|
|
658
|
+
*/
|
|
659
|
+
function behaviorOnlyDocument(md) {
|
|
660
|
+
const body = h2SectionBody(md, BEHAVIOR_HEADING)
|
|
661
|
+
return body ? `# Поведінка\n\n## Поведінка\n\n${body}\n` : ''
|
|
662
|
+
return match ? `# Поведінка\n\n## Поведінка\n\n${match[1].trim()}\n` : ''
|
|
663
|
+
}
|
|
664
|
+
|
|
505
665
|
/**
|
|
506
666
|
* №6 — judge-refine: суддя назвав конкретні неточності (`judge.reason`) — один
|
|
507
667
|
* локальний refine-прохід замість лише маркування degraded. Приймаємо виправлену
|
|
@@ -520,13 +680,13 @@ async function judgeRefinePass(r, judge, { facts, anchors, src, score, model, ch
|
|
|
520
680
|
if (!fixed.startsWith('#')) fixed = `# ${basename(facts.relPath)}\n\n${fixed}`
|
|
521
681
|
const fixedMd = insertProtected(fixed + '\n', intentBody)
|
|
522
682
|
// Guard 1: рерайт не має губити секції (малі моделі інколи повертають фрагмент)
|
|
523
|
-
const origHeadings = r.md.match(
|
|
683
|
+
const origHeadings = r.md.match(H2_HEADING_RE) ?? []
|
|
524
684
|
if (origHeadings.some(h => !fixedMd.includes(h))) return null
|
|
525
685
|
// Guard 2: det-score не має падати
|
|
526
686
|
const sFixed = scoreDoc(fixedMd, facts, { anchors, src })
|
|
527
687
|
if (sFixed.score < score) return null
|
|
528
688
|
// Guard 3: повторний суддя (той самий scope: inaccurate)
|
|
529
|
-
const judge2 = { ...(await judgeDoc(src, fixedMd, { chain })), model: JUDGE_MODEL }
|
|
689
|
+
const judge2 = { ...(await judgeDoc(semanticEvidence(src), fixedMd, { chain })), model: JUDGE_MODEL }
|
|
530
690
|
if (judgeFailsDoc(judge2)) return null
|
|
531
691
|
return { md: fixedMd, score: sFixed.score, issues: sFixed.issues, judge: judge2 }
|
|
532
692
|
}
|
|
@@ -541,7 +701,7 @@ async function judgeRefinePass(r, judge, { facts, anchors, src, score, model, ch
|
|
|
541
701
|
async function runJudgeGate({ r, score, issues, facts, anchors, src, model, chain }) {
|
|
542
702
|
let judge = null
|
|
543
703
|
try {
|
|
544
|
-
judge = { ...(await judgeDoc(src, r.md, { chain })), model: JUDGE_MODEL }
|
|
704
|
+
judge = { ...(await judgeDoc(semanticEvidence(src, facts), r.md, { chain })), model: JUDGE_MODEL }
|
|
545
705
|
// №6: суддя назвав конкретні неточності → один локальний refine-прохід
|
|
546
706
|
// (опт-аут: N_CURSOR_DOCGEN_JUDGE_REFINE=0). Прийнято лише коли всі
|
|
547
707
|
// guard-и judgeRefinePass пройдені; інакше — degraded, як раніше.
|
|
@@ -610,10 +770,13 @@ function srcTokenBudget() {
|
|
|
610
770
|
* (js/mjs/ts — lang-js, `.rs` — lang-rust; whole-file `unsupported`-fallback, якщо
|
|
611
771
|
* екстрактора для розширення нема).
|
|
612
772
|
* @param {string} file абсолютний шлях джерела
|
|
773
|
+
* @param {ReturnType<typeof buildTestEvidenceIndex>|null} [testIndex] source↔tests index
|
|
613
774
|
* @returns {Promise<{ src: string, estTokens: number, ext: string, langExtractors: Map<string, object>, facts: object }>} усе потрібне обом callers перед LLM-викликом
|
|
614
775
|
*/
|
|
615
|
-
async function loadSrcAndFacts(file) {
|
|
776
|
+
async function loadSrcAndFacts(file, testIndex = null) {
|
|
616
777
|
const src = readFileSync(file, 'utf8')
|
|
778
|
+
const evidenceIndex = testIndex ?? (existsSync(file) ? buildTestEvidenceIndex(process.cwd()) : null)
|
|
779
|
+
const testEvidence = evidenceIndex ? testEvidenceForSource(file, evidenceIndex) : { files: [] }
|
|
617
780
|
const estTokens = Math.round(Buffer.byteLength(src, 'utf8') / 4)
|
|
618
781
|
const budget = srcTokenBudget()
|
|
619
782
|
if (estTokens > budget) {
|
|
@@ -623,7 +786,7 @@ async function loadSrcAndFacts(file) {
|
|
|
623
786
|
}
|
|
624
787
|
const langExtractors = await loadDocFilesExtractors(process.cwd())
|
|
625
788
|
const ext = `.${file.split('.').pop()}`.toLowerCase()
|
|
626
|
-
const
|
|
789
|
+
const extractedFacts = langExtractors.get(ext)?.extractFacts?.(src, file) ?? {
|
|
627
790
|
relPath: file,
|
|
628
791
|
lang: ext.slice(1),
|
|
629
792
|
unsupported: true,
|
|
@@ -632,6 +795,10 @@ async function loadSrcAndFacts(file) {
|
|
|
632
795
|
imports: {},
|
|
633
796
|
markers: {}
|
|
634
797
|
}
|
|
798
|
+
const facts = {
|
|
799
|
+
...extractedFacts,
|
|
800
|
+
testScenarioFiles: testEvidence.files
|
|
801
|
+
}
|
|
635
802
|
return { src, estTokens, ext, langExtractors, facts }
|
|
636
803
|
}
|
|
637
804
|
|
|
@@ -674,7 +841,7 @@ function finishUnsupported(r, { t0, model, chainExtra }) {
|
|
|
674
841
|
* з вищою температурою (best-of-2); якщо й він не допоміг — результат
|
|
675
842
|
* позначається `degraded`, рішення про перегенерацію приймає batch/користувач.
|
|
676
843
|
* @param {string} file абсолютний шлях джерела
|
|
677
|
-
* @param {{ model?: string, threshold?: number, existingMd?: string|null, chainFactory?: typeof startChain, deadlineAt?: number|null }} [opts] model-id, поріг degraded, наявна дока (для збереження захищеної секції), фабрика ланцюжка (інжект для тестів), deadlineAt — мʼякий дедлайн fix-pipeline (epoch ms): per-call таймаути ріжуться під залишок бюджету, вичерпаний бюджет обриває генерацію transient
|
|
844
|
+
* @param {{ model?: string, threshold?: number, existingMd?: string|null, chainFactory?: typeof startChain, deadlineAt?: number|null, testIndex?: ReturnType<typeof buildTestEvidenceIndex>|null }} [opts] model-id, поріг degraded, наявна дока (для збереження захищеної секції), фабрика ланцюжка (інжект для тестів), deadlineAt — мʼякий дедлайн fix-pipeline (epoch ms): per-call таймаути ріжуться під залишок бюджету, вичерпаний бюджет обриває генерацію transient-помилкою; testIndex — спільний source↔tests index батчу
|
|
678
845
|
* @returns {{ md: string, ms: number, llmMs: number, llmCalls: number, score: number|null, issues: string[], degraded: boolean, model: string }} документ і метадані генерації (ms — увесь файл; llmMs/llmCalls — лише LLM; решта ms — оркестрація)
|
|
679
846
|
*/
|
|
680
847
|
export async function generateDoc(
|
|
@@ -684,11 +851,12 @@ export async function generateDoc(
|
|
|
684
851
|
threshold = QUALITY_THRESHOLD,
|
|
685
852
|
existingMd = null,
|
|
686
853
|
chainFactory = startChain,
|
|
687
|
-
deadlineAt = null
|
|
854
|
+
deadlineAt = null,
|
|
855
|
+
testIndex = null
|
|
688
856
|
} = {}
|
|
689
857
|
) {
|
|
690
858
|
// Guard ДО створення ланцюжка: skip без LLM — не задача.
|
|
691
|
-
const { src, estTokens, ext, langExtractors, facts } = await loadSrcAndFacts(file)
|
|
859
|
+
const { src, estTokens, ext, langExtractors, facts } = await loadSrcAndFacts(file, testIndex)
|
|
692
860
|
const t0 = Date.now()
|
|
693
861
|
llmMeter = { calls: 0, ms: 0 }
|
|
694
862
|
const chain = chainFactory({ kind: 'doc-generate', unit: facts.relPath, cwd: process.cwd() })
|
|
@@ -717,6 +885,7 @@ export async function generateDoc(
|
|
|
717
885
|
// Варіант B: захищена секція «Призначення» з наявної доки — зберегти й подати як контекст
|
|
718
886
|
const intent = existingMd ? splitProtected(existingMd).body : null
|
|
719
887
|
const anchors = facts.unsupported ? null : extractAnchors(src)
|
|
888
|
+
const commentMode = commentDocumentationMode(facts, src)
|
|
720
889
|
const promptSrc = resolvePromptSrc({ facts, estTokens, langExtractors, ext, src, file })
|
|
721
890
|
let r = facts.unsupported
|
|
722
891
|
? await oneShotDoc(facts, src, model, LOCAL_TIMEOUT_MS, { intent })
|
|
@@ -732,6 +901,49 @@ export async function generateDoc(
|
|
|
732
901
|
// Stage 2.5: детермінований скоринг (0 токенів)
|
|
733
902
|
let { score, issues } = scoreDoc(r.md, facts, { anchors, src })
|
|
734
903
|
|
|
904
|
+
// Авторські header/API-коментарі — уже джерело істини. Для comment-only
|
|
905
|
+
// LLM не запускається зовсім; comment+behavior судить лише LLM-секцію.
|
|
906
|
+
if (commentMode === 'comment-only') {
|
|
907
|
+
chainExtra.score = score
|
|
908
|
+
chainExtra.degraded = false
|
|
909
|
+
return {
|
|
910
|
+
...r,
|
|
911
|
+
ms: Date.now() - t0,
|
|
912
|
+
llmMs: llmMeter.ms,
|
|
913
|
+
llmCalls: llmMeter.calls,
|
|
914
|
+
score,
|
|
915
|
+
issues,
|
|
916
|
+
degraded: false,
|
|
917
|
+
model
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (commentMode === 'comment+behavior') {
|
|
922
|
+
const behaviorDoc = behaviorOnlyDocument(r.md)
|
|
923
|
+
let judge = null
|
|
924
|
+
if (JUDGE_ENABLED && behaviorDoc) {
|
|
925
|
+
judge = { ...(await judgeDoc(semanticEvidence(src), behaviorDoc, { chain })), model: JUDGE_MODEL }
|
|
926
|
+
if (judgeFailsDoc(judge)) {
|
|
927
|
+
r = commentOnlyDoc(facts, intent)
|
|
928
|
+
;({ score, issues } = scoreDoc(r.md, facts, { anchors, src }))
|
|
929
|
+
issues.push('behavior-judge-removed')
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
chainExtra.score = score
|
|
933
|
+
chainExtra.degraded = false
|
|
934
|
+
return {
|
|
935
|
+
...r,
|
|
936
|
+
ms: Date.now() - t0,
|
|
937
|
+
llmMs: llmMeter.ms,
|
|
938
|
+
llmCalls: llmMeter.calls,
|
|
939
|
+
score,
|
|
940
|
+
issues,
|
|
941
|
+
judge,
|
|
942
|
+
degraded: false,
|
|
943
|
+
model
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
735
947
|
// E4: best-of-2 — один retry з вищою температурою, det-вибір кращого
|
|
736
948
|
if (score < threshold && env.N_CURSOR_DOCGEN_BEST_OF !== '0') {
|
|
737
949
|
try {
|
|
@@ -786,14 +998,21 @@ export async function generateDoc(
|
|
|
786
998
|
* всі файли разом). Кидає ту саму помилку pre-send guard, що й `generateDoc`
|
|
787
999
|
* (класифікується `permanent` у batch-оркестраторі — skip, не помилка прогону).
|
|
788
1000
|
* @param {string} file абсолютний шлях джерела
|
|
789
|
-
* @param {{ existingMd?: string|null }} [opts] наявна дока (для захищеної секції «Призначення»)
|
|
790
|
-
* @returns {Promise<{ facts: object, anchors: object|null, src: string, messages: Array<{role:string,content:string}>, intent: string|null }>} усе потрібне для item-у batch-у й пізнішого
|
|
1001
|
+
* @param {{ existingMd?: string|null, testIndex?: ReturnType<typeof buildTestEvidenceIndex>|null }} [opts] наявна дока (для захищеної секції «Призначення») і спільний source↔tests index
|
|
1002
|
+
* @returns {Promise<{ facts: object, anchors: object|null, src: string, mode: string, messages: Array<{role:string,content:string}>, intent: string|null }>} усе потрібне для item-у batch-у й пізнішого фінішу; `comment-only` повертає порожні messages
|
|
791
1003
|
*/
|
|
792
|
-
export async function prepareBatchItem(file, { existingMd = null } = {}) {
|
|
793
|
-
const { src, facts } = await loadSrcAndFacts(file)
|
|
1004
|
+
export async function prepareBatchItem(file, { existingMd = null, testIndex = null } = {}) {
|
|
1005
|
+
const { src, facts } = await loadSrcAndFacts(file, testIndex)
|
|
794
1006
|
const anchors = facts.unsupported ? null : extractAnchors(src)
|
|
795
1007
|
const intent = existingMd ? splitProtected(existingMd).body : null
|
|
796
|
-
|
|
1008
|
+
const mode = facts.unsupported ? 'fallback' : commentDocumentationMode(facts, src)
|
|
1009
|
+
const messages =
|
|
1010
|
+
mode === 'comment-only'
|
|
1011
|
+
? []
|
|
1012
|
+
: mode === 'comment+behavior'
|
|
1013
|
+
? sectionMessages(facts, src, anchors, intent, { complementaryBehavior: true })[0].messages
|
|
1014
|
+
: oneShotMessages(facts, src)
|
|
1015
|
+
return { facts, anchors, src, mode, messages, intent }
|
|
797
1016
|
}
|
|
798
1017
|
|
|
799
1018
|
/**
|
|
@@ -803,13 +1022,28 @@ export async function prepareBatchItem(file, { existingMd = null } = {}) {
|
|
|
803
1022
|
* викликається (мінімальний обсяг T8 — генерація; judge лишається опційним
|
|
804
1023
|
* розширенням послідовного шляху).
|
|
805
1024
|
* @param {string} text сирий текст відповіді моделі для цього item-у
|
|
806
|
-
* @param {{ facts: object, anchors: object|null, src: string, intent: string|null, model: string, threshold?: number }} ctx контекст item-у (з `prepareBatchItem`)
|
|
1025
|
+
* @param {{ facts: object, anchors: object|null, src: string, intent: string|null, model: string, threshold?: number, mode?: string }} ctx контекст item-у (з `prepareBatchItem`)
|
|
807
1026
|
* @returns {{ md: string, score: number|null, issues: string[], degraded: boolean, model: string }} результат генерації для штампу/запису
|
|
808
1027
|
*/
|
|
809
|
-
export function finishBatchItem(
|
|
1028
|
+
export function finishBatchItem(
|
|
1029
|
+
text,
|
|
1030
|
+
{ facts, anchors, src, intent, model, threshold = QUALITY_THRESHOLD, mode = null }
|
|
1031
|
+
) {
|
|
1032
|
+
const commentMode = mode ?? (facts.unsupported ? 'fallback' : commentDocumentationMode(facts, src))
|
|
1033
|
+
if (commentMode === 'comment-only') {
|
|
1034
|
+
const md = commentOnlyDoc(facts, intent).md
|
|
1035
|
+
const { score, issues } = scoreDoc(md, facts, { anchors, src })
|
|
1036
|
+
return { md, score, issues, degraded: false, model }
|
|
1037
|
+
}
|
|
1038
|
+
if (commentMode === 'comment+behavior') {
|
|
1039
|
+
const behavior = stripSignatures(stripSection(text))
|
|
1040
|
+
const md = commentOnlyDoc(facts, intent, CRITIC_NONE_RE.test(behavior.trim()) ? '' : behavior).md
|
|
1041
|
+
const { score, issues } = scoreDoc(md, facts, { anchors, src })
|
|
1042
|
+
return { md, score, issues, degraded: false, model }
|
|
1043
|
+
}
|
|
810
1044
|
let md = stripSignatures(stripSection(text))
|
|
811
1045
|
if (!md.startsWith('#')) md = `# ${basename(facts.relPath)}\n\n${md}`
|
|
812
|
-
md = insertProtected(md + '\n', intent)
|
|
1046
|
+
md = insertProtected(insertTestScenarios(md + '\n', facts), intent)
|
|
813
1047
|
if (facts.unsupported) {
|
|
814
1048
|
const refusal = detectRefusalFiller(splitProtected(md).without)
|
|
815
1049
|
return {
|
|
@@ -3,33 +3,35 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/doc-files/docgen-prompts/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model: openai-codex/gpt-5.
|
|
8
|
-
tier: cloud-
|
|
6
|
+
crc: d09eacad
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
9
|
score: 100
|
|
10
|
-
issues: judge-refine:kept-original,judge:inaccurate:0.
|
|
10
|
+
issues: judge-refine:kept-original,judge:inaccurate:0.98
|
|
11
11
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
## Огляд
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
`STYLE` задає спільний тон і межі викладу, а `sectionMessages` і `overviewMessages` узгоджують текст секції з уже відомими фактами. `isApiGap`, `apiGapMessages` і `renderApiLine` допомагають окремо фіксувати прогалини в API, щоб опис лишався чесним і не приписував зайвого. `criticMessages`, `refineMessages`, `oneShotMessages` і `judgeRefineMessages` підтримують послідовне доопрацювання одного й того ж змісту, а `guaranteesFromMarkers` і `UNIT_DIGEST_TOKENS` тримають описи в межах підтверджених маркерів і зведених одиниць.
|
|
17
17
|
|
|
18
18
|
## Поведінка
|
|
19
19
|
|
|
20
|
-
STYLE задає
|
|
20
|
+
STYLE задає спільний тон і межі для всього генератора: з нього випливають правила лаконічності, безпечної опори на факти та узгодженості між секціями.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
sectionMessages збирає мінімальний контекст для поведінкових секцій і запускає основний потік: factsSummary дає стислий факт-витяг, anchorsBlock додає лише потрібні анкори, intentContext підключає захищене «Призначення» як read-only фон, а msgs поєднує все у формат одного LLM-виклику. Саме тут зароджується текст секції «Поведінка», і саме тут враховується позначка про complementarity, коли LLM має доповнювати прогалини, а не переписувати вже відоме.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
isApiGap відсікає експорти, які вже мають змістовний JSDoc, від тих, що потребують синтезу; далі цей поділ живить окремий шлях для API, щоб не змішувати покритий текст із прогалинами. renderApiLine використовує лише вже описані експорти і переносить їх у публічний список без перефразування, а apiGapMessages працює тільки з невкритими експортами, щоб LLM не контактував із уже зафіксованим авторським формулюванням.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
overviewMessages формує завершальний узагальнювальний крок: бере вже написану «Поведінку», підкріплює її factsSummary та, за потреби, intentContext, і на цій основі просить Огляд. Це замикає порядок секцій так, щоб Огляд узгоджувався з готовим змістом, а не вигадувався з сирих фактів.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
criticMessages і refineMessages працюють парою як цикл якості для окремих секцій: критик звіряє чорнетку з facts і anchorsBlock та повертає конкретні issues, після чого refineMessages переписує текст тільки на підставі цих зауважень. Завдяки цьому правки залишаються локальними й не розмивають уже добрі частини секції.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
guaranteesFromMarkers не входить у LLM-потік: він детерміновано виводить гарантовані властивості з facts.markers і слугує стабільним шаром, який не залежить від генеративної варіативності. Це тримає критичні твердження поза ризиком галюцинацій.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
oneShotMessages є базовою точкою порівняння для одного суцільного LLM-запиту: до нього потрапляють facts і src без розбиття на секції. buildUnitDigest підміняє сирий src компактним структурним дайджестом, коли великий файл починає розмивати фокус моделі; UNIT_DIGEST_TOKENS обмежує цей режим, щоб зберігати промпт коротким і передбачуваним.
|
|
33
|
+
|
|
34
|
+
judgeRefineMessages підхоплює фінальне локальне доопрацювання після judge-перевірки: reason від судді стає єдиною опорою для точкового виправлення doc-тексту без залучення захищеного «Призначення». Увесь потік спирається на спільний кеш у межах одного прогону, щоб повторні звернення до тих самих даних не змінювали результат і не множили витрати.
|
|
33
35
|
|
|
34
36
|
## Публічний API
|
|
35
37
|
|
|
@@ -71,6 +73,10 @@ LLM-судді (замість лише маркування degraded). Судд
|
|
|
71
73
|
хибне (`reason`) — мала модель добре виправляє точкові твердження, коли їй
|
|
72
74
|
сказано, які саме.
|
|
73
75
|
|
|
76
|
+
## Сценарії використання
|
|
77
|
+
|
|
78
|
+
- `npm/rules/doc-files/docgen-prompts/tests/docgen-prompts.test.mjs` (sectionMessages — Огляд більше не тут (R3); guaranteesFromMarkers — лише file-local твердження) — не повертає секцію overview; Поведінка обмежена експортованими іменами (R6); Поведінка не отримує test evidence: сценарії рендерить JS окремою секцією; гібридний режим просить доповнити comments лише відсутнім потоком; fail-safe маркер не обіцяє, що всі помилки лишаються всередині модуля; ще 10
|
|
79
|
+
|
|
74
80
|
## Гарантії поведінки
|
|
75
81
|
|
|
76
82
|
- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
|
|
@@ -15,7 +15,8 @@ export const STYLE = [
|
|
|
15
15
|
'Заборонено: сигнатури, типи, параметри функцій; перелік stdlib-модулів; опис regex чи внутрішніх приватних імен.',
|
|
16
16
|
// R9-профілактика: gemma-подібні малі моделі «озвучують завдання» перед відповіддю;
|
|
17
17
|
// явна заборона з прикладами різко знижує частоту (дет-зрізання у stripSection — страховка).
|
|
18
|
-
'Виведи ЛИШЕ текст секції. ЗАБОРОНЕНО починати з мета-фраз на кшталт «Ось оновлена чорнетка…», «Оновлений текст секції:», «Як технічний письменник, я створю…» — одразу перший змістовний рядок.'
|
|
18
|
+
'Виведи ЛИШЕ текст секції. ЗАБОРОНЕНО починати з мета-фраз на кшталт «Ось оновлена чорнетка…», «Оновлений текст секції:», «Як технічний письменник, я створю…» — одразу перший змістовний рядок.',
|
|
19
|
+
'Не вигадуй маркери, конфігурації або code identifiers; порожній inline code `` заборонений.'
|
|
19
20
|
].join(' ')
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -46,8 +47,8 @@ function factsSummary(facts) {
|
|
|
46
47
|
// безумовне «не пише» модель розганяє до хибного «гарантує безпечність» в Огляді.
|
|
47
48
|
if (m.readOnly) lines.push('Власних операцій запису (ФС/БД) у файлі немає (імпортовані модулі не аналізувались)')
|
|
48
49
|
if (m.network) lines.push('Звертається до мережі')
|
|
49
|
-
if (m.catchesErrors) lines.push('
|
|
50
|
-
if (m.returnsFalsyOnFail) lines.push('
|
|
50
|
+
if (m.catchesErrors) lines.push('Містить локальні fail-safe гілки; інші помилки можуть поширюватися назовні')
|
|
51
|
+
if (m.returnsFalsyOnFail) lines.push('Деякі локальні fail-safe гілки повертають порожнє значення (напр. null) замість винятку')
|
|
51
52
|
lines.push(m.caches ? 'Кешування: так, у межах прогону' : 'Кешування: НЕМАЄ — не згадуй кеш у гарантіях')
|
|
52
53
|
return lines.join('\n')
|
|
53
54
|
}
|
|
@@ -85,9 +86,10 @@ function intentContext(intent) {
|
|
|
85
86
|
* @param {string} src вміст файлу
|
|
86
87
|
* @param {object|null} [anchors] анкори файлу для обовʼязкового включення
|
|
87
88
|
* @param {string|null} [intent] захищена секція «Призначення» як read-only контекст
|
|
89
|
+
* @param {{ complementaryBehavior: boolean }} [opts] LLM доповнює лише прогалини між comments і кодом
|
|
88
90
|
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів (лише behavior)
|
|
89
91
|
*/
|
|
90
|
-
export function sectionMessages(facts, src, anchors = null, intent = null) {
|
|
92
|
+
export function sectionMessages(facts, src, anchors = null, intent = null, { complementaryBehavior = false } = {}) {
|
|
91
93
|
const factsTxt = factsSummary(facts)
|
|
92
94
|
const anch = anchorsBlock(anchors)
|
|
93
95
|
const intentCtx = intentContext(intent)
|
|
@@ -99,21 +101,26 @@ export function sectionMessages(facts, src, anchors = null, intent = null) {
|
|
|
99
101
|
// його іншими словами. Натомість — крос-функціональний наратив: те, чого
|
|
100
102
|
// немає в жодному окремому JSDoc за визначенням.
|
|
101
103
|
const exportNames = (facts.exports ?? []).map(e => e.name)
|
|
102
|
-
const behaviorTask =
|
|
103
|
-
? '
|
|
104
|
-
:
|
|
104
|
+
const behaviorTask = complementaryBehavior
|
|
105
|
+
? 'короткі поведінкові абзаци про відсутній користувацький контракт, без алгоритму'
|
|
106
|
+
: multi
|
|
107
|
+
? 'крос-функціональний потік: у якому порядку і як функції взаємодіють між собою, звідки приходять дані і куди йдуть результати, спільні правила чи стан. НЕ переказуй кожну функцію окремим пунктом — одно-рядкові описи вже є в секції «Публічний API»'
|
|
108
|
+
: 'нумерований алгоритм у бізнес-термінах'
|
|
105
109
|
const onlyExports = exportNames.length
|
|
106
110
|
? ` Описуй РІВНО ці публічні імена і жодних інших: ${exportNames.join(', ')}.`
|
|
107
111
|
: ''
|
|
108
112
|
const noInternal = facts.internalSymbols?.length
|
|
109
113
|
? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.`
|
|
110
114
|
: ''
|
|
115
|
+
const complementaryInstruction = complementaryBehavior
|
|
116
|
+
? ' «Огляд» і «Публічний API» уже дослівно зібрані з авторських коментарів. Додай ЛИШЕ відсутній для користувача контракт: умови, результат, error-flow, concurrency або інваріанти. Не перефразовуй авторський текст і НЕ переказуй реалізацію: заборонені обходи каталогів, цикли, читання файлів, AST-вузли, допоміжні виклики та нумерований алгоритм. Заборонені generic-фрази без факту з коду: «не вимагає налаштування», «виклик ініціює перевірку», «успішно повертається результат». Дай 1–4 короткі абзаци; якщо доповнювати нічого — поверни рівно NONE.'
|
|
117
|
+
: ''
|
|
111
118
|
const behavior = {
|
|
112
119
|
key: 'behavior',
|
|
113
120
|
numPredict: 500,
|
|
114
121
|
messages: msgs(
|
|
115
122
|
`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}${intentCtx}`,
|
|
116
|
-
`Напиши вміст секції «Поведінка»: ${behaviorTask}.${onlyExports} Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
|
|
123
|
+
`Напиши вміст секції «Поведінка»: ${behaviorTask}.${onlyExports}${complementaryInstruction} Сценарії з test/spec-файлів рендерить JS окремою секцією — не згадуй і не відтворюй їх. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
|
|
117
124
|
)
|
|
118
125
|
}
|
|
119
126
|
return [behavior]
|
|
@@ -127,7 +134,7 @@ const STUB_DESC_RE = /^опис\.?$/i
|
|
|
127
134
|
/**
|
|
128
135
|
* Stage 2 (gap-детект, 0 токенів): чи є опис експорту прогалиною — відсутній
|
|
129
136
|
* або JSDoc-заглушка без сенсу.
|
|
130
|
-
* @param {{desc
|
|
137
|
+
* @param {{desc:string}} exp запис експорту з факт-листа
|
|
131
138
|
* @returns {boolean} true — опис потрібно синтезувати LLM (Stage 3)
|
|
132
139
|
*/
|
|
133
140
|
export function isApiGap(exp) {
|
|
@@ -255,8 +262,8 @@ export function guaranteesFromMarkers(facts) {
|
|
|
255
262
|
// який file-local аналіз не може підтвердити. Обмежене твердження — може.
|
|
256
263
|
if (m.readOnly)
|
|
257
264
|
lines.push('- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.')
|
|
258
|
-
if (m.catchesErrors) lines.push('-
|
|
259
|
-
if (m.returnsFalsyOnFail) lines.push('-
|
|
265
|
+
if (m.catchesErrors) lines.push('- Містить локальні fail-safe гілки; інші помилки можуть поширюватися назовні.')
|
|
266
|
+
if (m.returnsFalsyOnFail) lines.push('- Деякі локальні fail-safe гілки повертають порожнє значення (напр. `null`) замість винятку.')
|
|
260
267
|
if (m.caches) lines.push('- Кешує результати в межах одного прогону.')
|
|
261
268
|
if (m.skips?.length) {
|
|
262
269
|
lines.push(`- Свідомо пропускає шляхи: ${m.skips.map(s => '`' + s + '`').join(', ')}.`)
|
|
@@ -275,7 +282,7 @@ export function oneShotMessages(facts, src) {
|
|
|
275
282
|
const multi = (facts.exports?.length || 0) > 1
|
|
276
283
|
return msgs(
|
|
277
284
|
STYLE,
|
|
278
|
-
`Напиши документацію для файлу. Секції: ## Огляд (1-3 речення), ## Поведінка (нумерований/маркований алгоритм), ${multi ? '## Публічний API (назва + що робить), ' : ''}## Гарантії
|
|
285
|
+
`Напиши документацію для файлу. Секції: ## Огляд (1-3 речення), ## Поведінка (нумерований/маркований алгоритм), ${multi ? '## Публічний API (назва + що робить), ' : ''}## Гарантії поведінки. Не додавай «Сценарії використання»: її детерміновано рендерить JS із повʼязаних test/spec-файлів.\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\``
|
|
279
286
|
)
|
|
280
287
|
}
|
|
281
288
|
|