@nitra/cursor 5.1.0 → 5.2.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.
Files changed (204) hide show
  1. package/.claude-template/settings.template.json +22 -0
  2. package/.pi-template/extensions/n-cursor-adr/docs/index.md +15 -9
  3. package/CHANGELOG.md +12 -1
  4. package/bin/n-cursor.js +73 -16
  5. package/docs/stryker.config.md +6 -0
  6. package/docs/vitest.config.md +6 -0
  7. package/lib/docs/llm.md +29 -0
  8. package/lib/docs/omlx.md +32 -0
  9. package/lib/llm.mjs +137 -0
  10. package/lib/omlx.mjs +49 -4
  11. package/package.json +1 -1
  12. package/rules/abie/docs/fix.md +6 -0
  13. package/rules/abie/js/docs/applies.md +6 -0
  14. package/rules/abie/js/docs/env_dns.md +25 -22
  15. package/rules/abie/js/docs/firebase_hosting.md +6 -0
  16. package/rules/abie/js/docs/hc_pairing.md +21 -25
  17. package/rules/abie/js/docs/ua_http_route.md +27 -19
  18. package/rules/abie/js/docs/ua_node_selector.md +24 -19
  19. package/rules/abie/lib/docs/enabled.md +13 -7
  20. package/rules/abie/lib/docs/env-dns.md +9 -3
  21. package/rules/abie/lib/docs/hc-yaml.md +6 -0
  22. package/rules/abie/lib/docs/http-route.md +6 -0
  23. package/rules/abie/lib/docs/k8s-tree.md +6 -0
  24. package/rules/abie/lib/docs/kustomization-patches.md +6 -0
  25. package/rules/abie/lib/docs/overlay-paths.md +6 -0
  26. package/rules/abie/lib/docs/yaml.md +6 -0
  27. package/rules/adr/docs/fix.md +6 -0
  28. package/rules/adr/js/docs/hooks.md +29 -244
  29. package/rules/bun/docs/fix.md +6 -0
  30. package/rules/bun/js/docs/layout.md +37 -375
  31. package/rules/capacitor/docs/fix.md +22 -108
  32. package/rules/capacitor/js/docs/platforms.md +62 -268
  33. package/rules/changelog/docs/fix.md +6 -0
  34. package/rules/changelog/lib/docs/package-manifest.md +6 -0
  35. package/rules/ci4/docs/fix.md +23 -165
  36. package/rules/ci4/js/docs/marksman_config.md +9 -1
  37. package/rules/docker/docs/fix.md +6 -0
  38. package/rules/docker/js/docs/lint.md +55 -239
  39. package/rules/docker/lib/docs/docker-hadolint.md +6 -0
  40. package/rules/docker/lib/docs/docker-mirror.md +6 -0
  41. package/rules/docker/lib/docs/docker-native-addon.md +6 -0
  42. package/rules/docker/lib/docs/docker-nginx-user.md +6 -0
  43. package/rules/docker/lint/docs/lint.md +9 -1
  44. package/rules/efes/docs/fix.md +6 -0
  45. package/rules/ga/lint/docs/lint.md +6 -0
  46. package/rules/graphql/docs/fix.md +6 -0
  47. package/rules/graphql/lib/docs/graphql-gql-scan.md +6 -0
  48. package/rules/image-avif/docs/fix.md +6 -0
  49. package/rules/image-avif/js/docs/avif_generation.md +6 -0
  50. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +9 -3
  51. package/rules/js-bun-redis/lib/docs/redis-imports.md +6 -0
  52. package/rules/js-lint/js/docs/utils_imports.md +6 -0
  53. package/rules/js-lint-ci/docs/fix.md +7 -1
  54. package/rules/js-mssql/docs/fix.md +6 -0
  55. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +6 -0
  56. package/rules/js-run/docs/fix.md +6 -0
  57. package/rules/js-run/lib/docs/bunyan-imports.md +6 -0
  58. package/rules/js-run/lib/docs/check-env-scan.md +6 -0
  59. package/rules/js-run/lib/docs/conn-file-rules.md +6 -0
  60. package/rules/js-run/lib/docs/conn-imports-scan.md +6 -0
  61. package/rules/js-run/lib/docs/promise-settimeout-scan.md +6 -0
  62. package/rules/js-run/lib/docs/temporal-scan.md +6 -0
  63. package/rules/k8s/docs/fix.md +6 -0
  64. package/rules/k8s/lint/docs/lint.md +6 -0
  65. package/rules/nginx-default-tpl/docs/fix.md +6 -0
  66. package/rules/npm-module/js/docs/header_doc_pointer.md +7 -0
  67. package/rules/npm-module/js/header_doc_pointer.mjs +2 -8
  68. package/rules/php/docs/fix.md +6 -0
  69. package/rules/php/lint/docs/lint.md +6 -0
  70. package/rules/python/docs/fix.md +6 -0
  71. package/rules/python/lint/docs/lint.md +6 -0
  72. package/rules/rego/lint/docs/lint.md +6 -0
  73. package/rules/release/docs/change.md +6 -0
  74. package/rules/release/docs/fix.md +6 -0
  75. package/rules/release/docs/release.md +6 -0
  76. package/rules/release/lib/docs/aggregate.md +6 -0
  77. package/rules/release/lib/docs/change-file.md +6 -0
  78. package/rules/release/lib/docs/fallback.md +6 -0
  79. package/rules/rust/lib/docs/has-cargo-toml.md +6 -0
  80. package/rules/security/docs/fix.md +7 -1
  81. package/rules/security/js/docs/lint.md +6 -0
  82. package/rules/style-lint/docs/fix.md +6 -0
  83. package/rules/tauri/docs/fix.md +6 -0
  84. package/rules/test/docs/fix.md +6 -0
  85. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +6 -0
  86. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +6 -0
  87. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +6 -0
  88. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +6 -0
  89. package/rules/text/docs/fix.md +6 -0
  90. package/rules/text/lint/docs/lint.md +6 -0
  91. package/rules/text/lint/docs/run-dotenv-linter.md +6 -0
  92. package/rules/text/lint/docs/run-shellcheck.md +6 -0
  93. package/rules/text/lint/docs/run-v8r.md +6 -0
  94. package/rules/vue/lib/docs/vue-forbidden-imports.md +6 -0
  95. package/scripts/coverage-classify/cache.mjs +1 -1
  96. package/scripts/coverage-classify/docs/apply.md +6 -0
  97. package/scripts/coverage-classify/docs/cache.md +6 -0
  98. package/scripts/coverage-classify/docs/prompt.md +6 -0
  99. package/scripts/coverage-classify/docs/verdict-schema.md +6 -0
  100. package/scripts/coverage-classify/prompt.mjs +1 -1
  101. package/scripts/coverage-fix-extract.mjs +1 -1
  102. package/scripts/coverage-fix.mjs +2 -1
  103. package/scripts/docs/auto-skills.md +6 -0
  104. package/scripts/docs/build-agents-commands.md +7 -1
  105. package/scripts/docs/cli-entry.md +6 -0
  106. package/scripts/docs/coverage-fix-extract.md +6 -0
  107. package/scripts/docs/coverage-fix.md +6 -0
  108. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +6 -0
  109. package/scripts/docs/lint-cli.md +6 -0
  110. package/scripts/docs/post-tool-use-fix.md +6 -0
  111. package/scripts/docs/rename-yaml-extensions.md +6 -0
  112. package/scripts/docs/skills-cli.md +6 -0
  113. package/scripts/docs/sync-setup-bun-deps-action.md +6 -0
  114. package/scripts/docs/upgrade-nitra-cursor-and-install.md +6 -0
  115. package/scripts/docs/worktree-cli.md +6 -0
  116. package/scripts/lib/docs/assert-project-root.md +6 -0
  117. package/scripts/lib/docs/check-mdc-template-refs.md +6 -0
  118. package/scripts/lib/docs/check-reporter.md +6 -0
  119. package/scripts/lib/docs/diff-added-lines.md +6 -0
  120. package/scripts/lib/docs/discover-check-rules-from-cursor.md +6 -0
  121. package/scripts/lib/docs/discover-checkable-rules.md +6 -0
  122. package/scripts/lib/docs/ensure-tool.md +6 -0
  123. package/scripts/lib/docs/generated-markdown.md +6 -0
  124. package/scripts/lib/docs/gha-workflow.md +6 -0
  125. package/scripts/lib/docs/inline-template-links.md +6 -0
  126. package/scripts/lib/docs/list-rule-ids.md +6 -0
  127. package/scripts/lib/docs/load-cursor-config.md +6 -0
  128. package/scripts/lib/docs/mirror-parity.md +6 -0
  129. package/scripts/lib/docs/read-n-cursor-config-lite.md +6 -0
  130. package/scripts/lib/docs/resolve-target-files.md +6 -0
  131. package/scripts/lib/docs/root-notice.md +6 -0
  132. package/scripts/lib/docs/rule-meta-helpers.md +6 -0
  133. package/scripts/lib/docs/rule-meta.md +6 -0
  134. package/scripts/lib/docs/run-conftest-batch.md +6 -0
  135. package/scripts/lib/docs/run-lint-step.md +6 -0
  136. package/scripts/lib/docs/run-rule-cli.md +6 -0
  137. package/scripts/lib/docs/run-rule.md +6 -0
  138. package/scripts/lib/docs/run-standard-lint.md +6 -0
  139. package/scripts/lib/docs/run-standard-rule.md +6 -0
  140. package/scripts/lib/docs/skill-meta.md +6 -0
  141. package/scripts/lib/docs/template.md +6 -0
  142. package/scripts/lib/docs/timing-summary.md +6 -0
  143. package/scripts/lib/docs/workspaces.md +6 -0
  144. package/scripts/lib/docs/worktree-notice.md +6 -0
  145. package/scripts/lib/docs/worktree.md +6 -0
  146. package/scripts/lib/mirror-parity.mjs +1 -1
  147. package/scripts/lib/root-notice.mjs +1 -1
  148. package/scripts/lib/worktree-notice.mjs +5 -5
  149. package/scripts/lib/worktree.mjs +1 -1
  150. package/scripts/sync-claude-config.mjs +3 -0
  151. package/scripts/utils/docs/ast-scan-utils.md +6 -0
  152. package/scripts/utils/docs/ensure-gitignore-entries.md +6 -0
  153. package/scripts/utils/docs/find-package-json-paths.md +6 -0
  154. package/scripts/utils/docs/lock-cache-dir.md +6 -0
  155. package/scripts/utils/docs/pass.md +6 -0
  156. package/scripts/utils/docs/resolve-cargo-manifest.md +6 -0
  157. package/scripts/utils/docs/resolve-cmd.md +6 -0
  158. package/scripts/utils/docs/resolve-js-root.md +6 -0
  159. package/scripts/utils/docs/test-helpers.md +6 -0
  160. package/scripts/utils/docs/walk-cache.md +6 -0
  161. package/scripts/utils/docs/walkDir.md +6 -0
  162. package/scripts/utils/docs/worktree-fingerprint.md +6 -0
  163. package/scripts/utils/resolve-js-root.mjs +1 -1
  164. package/skills/doc-aggregate/SKILL.md +129 -0
  165. package/skills/doc-aggregate/js/docgen-ignore.mjs +9 -0
  166. package/skills/{docgen → doc-aggregate}/js/docgen-scan.mjs +22 -67
  167. package/skills/doc-aggregate/js/docs/docgen-ignore.md +21 -0
  168. package/skills/doc-files/SKILL.md +100 -0
  169. package/skills/doc-files/js/docgen-crc.mjs +164 -0
  170. package/skills/{docgen → doc-files}/js/docgen-extract-anchors.mjs +20 -11
  171. package/skills/{docgen → doc-files}/js/docgen-extract.mjs +15 -9
  172. package/skills/doc-files/js/docgen-files-batch.mjs +181 -0
  173. package/skills/doc-files/js/docgen-gen.mjs +291 -0
  174. package/skills/{docgen → doc-files}/js/docgen-prompts.mjs +43 -40
  175. package/skills/doc-files/js/docgen-scan.mjs +298 -0
  176. package/skills/doc-files/js/docs/docgen-crc.md +32 -0
  177. package/skills/doc-files/js/docs/docgen-extract-anchors.md +27 -0
  178. package/skills/doc-files/js/docs/docgen-extract.md +29 -0
  179. package/skills/doc-files/js/docs/docgen-files-batch.md +25 -0
  180. package/skills/doc-files/js/docs/docgen-gen.md +30 -0
  181. package/skills/doc-files/js/docs/docgen-prompts.md +32 -0
  182. package/skills/doc-files/js/docs/docgen-scan.md +25 -0
  183. package/skills/doc-files/meta.json +1 -0
  184. package/skills/fix/js/docs/llm-worker.md +6 -0
  185. package/skills/fix/js/docs/orchestrator.md +6 -0
  186. package/skills/fix/js/llm-worker.mjs +3 -3
  187. package/skills/fix/js/orchestrator.mjs +1 -1
  188. package/skills/start-check/js/check.mjs +5 -3
  189. package/skills/start-check/js/docs/check.md +6 -0
  190. package/skills/docgen/SKILL.md +0 -224
  191. package/skills/docgen/bench/etalon/firebase_hosting.md +0 -19
  192. package/skills/docgen/bench/etalon/k8s-tree.md +0 -24
  193. package/skills/docgen/bench/etalon/overlay-paths.md +0 -24
  194. package/skills/docgen/js/docgen-batch-omlx.mjs +0 -82
  195. package/skills/docgen/js/docgen-batch.mjs +0 -95
  196. package/skills/docgen/js/docgen-compare-pi-vs-direct.mjs +0 -95
  197. package/skills/docgen/js/docgen-gen.mjs +0 -306
  198. package/skills/docgen/js/docs/docgen-extract.md +0 -28
  199. package/skills/docgen/js/docs/docgen-gen.md +0 -41
  200. package/skills/docgen/js/docs/docgen-ignore.md +0 -24
  201. package/skills/docgen/js/docs/docgen-prompts.md +0 -24
  202. package/skills/docgen/js/docs/docgen-scan.md +0 -48
  203. /package/skills/{docgen → doc-aggregate}/meta.json +0 -0
  204. /package/skills/{docgen → doc-files}/js/docgen-ignore.mjs +0 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * JS-оркестрація генерації файлових док (local-only, ADR 260610-2228).
3
+ *
4
+ * Уся черга/батчинг/CRC-штамп живуть тут, а не в контексті моделі — тому
5
+ * масовий перший прогін на сотні файлів не «заморює» агента. Конвеєр суто
6
+ * локальний: жодних cloud-ескалацій; якщо det-score нижче порогу — дока все
7
+ * одно пишеться з degraded-маркером (`score`/`issues` у frontmatter), а
8
+ * `gen --retry-degraded` адресно переганяє лише такі доки пізніше.
9
+ *
10
+ * Перед масовим прогоном — health-check omlx: memory-guard зайнятої 8GB машини
11
+ * означає «відклади прогін», а не сотні хибних «✗» у звіті.
12
+ */
13
+ import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs'
14
+ import { dirname, join } from 'node:path'
15
+
16
+ import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
17
+ import { omlxHealthCheck, pickBackend } from '../../../lib/llm.mjs'
18
+ import { generateDoc, DEFAULT_LOCAL_MODEL } from './docgen-gen.mjs'
19
+ import { crc32, stampDoc, readDocQuality, QUALITY_THRESHOLD } from './docgen-crc.mjs'
20
+ import { resolveRoot, scanForDocFiles } from './docgen-scan.mjs'
21
+
22
+ /**
23
+ * Парсить `--limit N` / `--from N` / прапори режимів для дозапуску великого прогону.
24
+ * @param {string[]} argv аргументи
25
+ * @returns {{ from: number, limit: number, overwrite: boolean, retryDegraded: boolean }} зріз і режими
26
+ */
27
+ function parseGenArgs(argv) {
28
+ const num = (flag, dflt) => {
29
+ const i = argv.indexOf(flag)
30
+ return i !== -1 && argv[i + 1] ? Number(argv[i + 1]) || dflt : dflt
31
+ }
32
+ return {
33
+ from: num('--from', 0),
34
+ limit: num('--limit', Infinity),
35
+ overwrite: argv.includes('--overwrite'),
36
+ retryDegraded: argv.includes('--retry-degraded')
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Цілі генерації за режимом:
42
+ * - default → застарілі (stale);
43
+ * - `--overwrite` → усі;
44
+ * - `--retry-degraded` → свіжі за CRC, але зі `score < QUALITY_THRESHOLD`.
45
+ * @param {string} root абсолютний корінь
46
+ * @param {Array<object>} all результат scanForDocFiles
47
+ * @param {{ overwrite: boolean, retryDegraded: boolean }} mode режими
48
+ * @returns {Array<object>} відфільтровані цілі
49
+ */
50
+ function selectTargets(root, all, { overwrite, retryDegraded }) {
51
+ if (retryDegraded) {
52
+ return all.filter(f => {
53
+ if (f.stale) return false
54
+ const { score } = readDocQuality(join(root, f.docPath))
55
+ return score !== null && score < QUALITY_THRESHOLD
56
+ })
57
+ }
58
+ if (overwrite) return all
59
+ return all.filter(f => f.stale)
60
+ }
61
+
62
+ /**
63
+ * Preflight локального бекенда: для omlx-моделі — мінімальний chat-виклик.
64
+ * @returns {string|null} текст фатальної проблеми або null якщо можна генерувати
65
+ */
66
+ function preflightProblem() {
67
+ if (pickBackend(DEFAULT_LOCAL_MODEL) !== 'omlx') return null
68
+ const hc = omlxHealthCheck({ model: DEFAULT_LOCAL_MODEL })
69
+ if (hc.ok) return null
70
+ if (hc.reason === 'memory-guard') {
71
+ return `omlx memory-guard: модель не влазить у динамічну стелю пам'яті (машина зайнята).\n Звільни пам'ять або повтори прогін пізніше.\n ${hc.detail}`
72
+ }
73
+ if (hc.reason === 'down') {
74
+ return `omlx-сервер не відповідає. Запусти \`omlx serve\` і повтори.\n ${hc.detail}`
75
+ }
76
+ if (hc.reason === 'auth') {
77
+ return `omlx вимагає API-ключ. Вистав N_CURSOR_OMLX_KEY (auth.api_key з ~/.omlx/settings.json).\n ${hc.detail}`
78
+ }
79
+ return `omlx помилка: ${hc.detail}`
80
+ }
81
+
82
+ /**
83
+ * `doc-files gen` — згенерувати документацію для застарілих/відсутніх док.
84
+ * @param {string[]} argv аргументи після назви субкоманди
85
+ * @returns {Promise<number>} exit-код: 0 — без помилок, 1 — хоча б одна помилка або фейл preflight
86
+ */
87
+ export async function runDocFilesGenCli(argv) {
88
+ const root = resolveRoot(argv)
89
+ const { from, limit, overwrite, retryDegraded } = parseGenArgs(argv)
90
+
91
+ const all = scanForDocFiles(root)
92
+ const targets = selectTargets(root, all, { overwrite, retryDegraded }).slice(from, from + limit)
93
+
94
+ if (targets.length === 0) {
95
+ console.log(
96
+ retryDegraded
97
+ ? '✓ doc-files: degraded-док немає. Нічого переганяти.'
98
+ : '✓ doc-files: усі файлові доки свіжі. Нічого генерувати.'
99
+ )
100
+ return 0
101
+ }
102
+
103
+ const problem = preflightProblem()
104
+ if (problem) {
105
+ console.error(`✗ doc-files gen: ${problem}`)
106
+ return 1
107
+ }
108
+
109
+ let modeTxt = ''
110
+ if (overwrite) modeTxt = ' (--overwrite)'
111
+ else if (retryDegraded) modeTxt = ' (--retry-degraded)'
112
+ console.log(`📋 doc-files: до генерації ${targets.length} файл(ів)${modeTxt}`)
113
+ const stats = { ok: 0, degraded: 0, err: 0, errors: [] }
114
+
115
+ let done = 0
116
+ for (const file of targets) {
117
+ done++
118
+ const sourceAbs = join(root, file.sourcePath)
119
+ process.stdout.write(` [${done}/${targets.length}] ${file.sourcePath} … `)
120
+ try {
121
+ const result = await generateDoc(sourceAbs)
122
+ const crc = crc32(readFileSync(sourceAbs))
123
+ const docAbs = join(root, file.docPath)
124
+ mkdirSync(dirname(docAbs), { recursive: true })
125
+ const quality =
126
+ result.score === null ? null : { score: result.score, issues: result.degraded ? result.issues : [] }
127
+ writeFileSync(docAbs, stampDoc(result.md, file.sourcePath, crc, quality))
128
+ stats.ok++
129
+ if (result.degraded) {
130
+ stats.degraded++
131
+ process.stdout.write(`⚠ degraded score=${result.score} crc=${crc}\n`)
132
+ } else {
133
+ process.stdout.write(`✓ score=${result.score ?? '—'} crc=${crc}\n`)
134
+ }
135
+ } catch (error) {
136
+ stats.err++
137
+ stats.errors.push(file.sourcePath)
138
+ process.stdout.write(`✗ ${error.message}\n`)
139
+ }
140
+ }
141
+
142
+ console.log(`\n${'─'.repeat(50)}\n✓ OK: ${stats.ok} ⚠ degraded: ${stats.degraded} ✗ Err: ${stats.err}`)
143
+ if (stats.errors.length > 0) {
144
+ console.log('Помилки:')
145
+ for (const e of stats.errors) console.log(` - ${e}`)
146
+ }
147
+ if (stats.degraded > 0) {
148
+ console.log(`Degraded-доки перегенеровуються пізніше: npx @nitra/cursor doc-files gen --retry-degraded`)
149
+ }
150
+ return stats.err > 0 ? 1 : 0
151
+ }
152
+
153
+ /**
154
+ * `doc-files stamp` — детерміновано (пере)штампувати frontmatter `source`+`crc`
155
+ * у НАЯВНИХ доках без виклику LLM. Для міграції док, які ще не мають CRC.
156
+ * Поля якості (`score`/`issues`) при цьому зберігаються з наявного frontmatter.
157
+ * @param {string[]} argv аргументи після назви субкоманди
158
+ * @returns {number} exit-код: 0 — успіх
159
+ */
160
+ export function runDocFilesStampCli(argv) {
161
+ const root = resolveRoot(argv)
162
+ let stamped = 0
163
+ for (const file of scanForDocFiles(root)) {
164
+ const docAbs = join(root, file.docPath)
165
+ if (!existsSync(docAbs)) continue
166
+ const sourceAbs = join(root, file.sourcePath)
167
+ const crc = crc32(readFileSync(sourceAbs))
168
+ const md = readFileSync(docAbs, 'utf8')
169
+ const { score, issues } = readDocQuality(docAbs)
170
+ writeFileSync(docAbs, stampDoc(md, file.sourcePath, crc, score === null ? null : { score, issues }))
171
+ stamped++
172
+ }
173
+ console.log(`✓ doc-files stamp: оновлено frontmatter у ${stamped} доці(ах).`)
174
+ return 0
175
+ }
176
+
177
+ if (isRunAsCli(import.meta.url)) {
178
+ const [sub, ...rest] = process.argv.slice(2)
179
+ const argv = sub === 'gen' || sub === 'stamp' ? rest : process.argv.slice(2)
180
+ process.exitCode = sub === 'stamp' ? runDocFilesStampCli(argv) : await runDocFilesGenCli(argv)
181
+ }
@@ -0,0 +1,291 @@
1
+ /** @see ./docs/docgen-gen.md */
2
+ import { readFileSync } from 'node:fs'
3
+ import { basename } from 'node:path'
4
+ import { env } from 'node:process'
5
+ import { resolveModel } from '../../../lib/models.mjs'
6
+ import { DEFAULT_OMLX_MODEL } from '../../../lib/omlx.mjs'
7
+ import { callLlm } from '../../../lib/llm.mjs'
8
+ import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
9
+ import { extractFacts } from './docgen-extract.mjs'
10
+ import { extractAnchors } from './docgen-extract-anchors.mjs'
11
+ import { QUALITY_THRESHOLD } from './docgen-crc.mjs'
12
+ import {
13
+ oneShotMessages,
14
+ sectionMessages,
15
+ criticMessages,
16
+ refineMessages,
17
+ guaranteesFromMarkers
18
+ } from './docgen-prompts.mjs'
19
+
20
+ const FENCE_OPEN_RE = /^```[a-z]*\n?/
21
+ const FENCE_CLOSE_RE = /\n?```\s*$/
22
+ const LEADING_HEADING_RE = /^#{1,6}[ \t]{1,8}[^\n]{0,400}\n{1,8}/
23
+ const SECTION_HEADING_RE = /^##\s+(.+)/
24
+ const SECTION_KEY_CLEAN_RE = /[^а-яіїєґa-z0-9]/gi
25
+ const CACHE_MENTION_RE = /кеш/i
26
+ const CACHE_NEGATION_RE = /(?:не|без)\s+(?:\S+\s+)?кеш|немає\s+кеш/i
27
+ const CRITIC_NONE_RE = /^\s*NONE\s*$/i
28
+
29
+ /**
30
+ * Прибирає код-фенс-обгортку (потрійні бектіки) й випадковий провідний
31
+ * `##`-заголовок із секції.
32
+ * @param {string} text сирий вихід моделі
33
+ * @returns {string} очищений текст секції
34
+ */
35
+ function stripSection(text) {
36
+ let t = text.trim()
37
+ if (t.startsWith('```')) {
38
+ t = t.replace(FENCE_OPEN_RE, '').replace(FENCE_CLOSE_RE, '').trim()
39
+ }
40
+ t = t.replace(LEADING_HEADING_RE, '') // зрізати випадковий заголовок
41
+ return t.trim()
42
+ }
43
+
44
+ /**
45
+ * Stage 2 (детермінований лінт, 0 токенів): зрізає сигнатури `name(args)` → `name`.
46
+ * Два проходи — щоб зняти вкладені виклики на кшталт `check(cwd = process.cwd())`.
47
+ * Не чіпає дужки без ідентифікатора перед ними (напр. `(abie.mdc)`, «(наприклад)»).
48
+ * @param {string} text текст секції
49
+ * @returns {string} текст без сигнатур у дужках
50
+ */
51
+ function stripSignatures(text) {
52
+ let t = text
53
+ for (let i = 0; i < 2; i++) t = t.replaceAll(/([`\w$.]{1,80})\([^()]{0,300}\)/g, '$1')
54
+ return t
55
+ }
56
+
57
+ /**
58
+ * Розбиває md на секції за ## заголовками.
59
+ * @param {string} md зібраний документ
60
+ * @returns {Record<string, string>} нормалізований ключ секції → її тіло
61
+ */
62
+ function parseSections(md) {
63
+ const result = {}
64
+ let cur = null
65
+ for (const line of md.split('\n')) {
66
+ const m = line.match(SECTION_HEADING_RE)
67
+ if (m) {
68
+ cur = m[1].toLowerCase().replaceAll(SECTION_KEY_CLEAN_RE, '')
69
+ result[cur] = ''
70
+ } else if (cur) result[cur] += line + '\n'
71
+ }
72
+ return result
73
+ }
74
+
75
+ /**
76
+ * Чи містить текст бектік-обгорнуте імʼя символу (`sym`) — уникає substring false positives.
77
+ * @param {string} text текст секції
78
+ * @param {string} sym імʼя символу без бектіків
79
+ * @returns {boolean} true — імʼя згадано
80
+ */
81
+ function hasName(text, sym) {
82
+ return text.includes('`' + sym + '`')
83
+ }
84
+
85
+ /**
86
+ * Stage 2.5 — детермінований скоринг (0 токенів): перевіряє вихід проти фактів.
87
+ * @param {string} md зібраний документ
88
+ * @param {object} facts факт-лист про файл
89
+ * @returns {{ score: number, issues: string[] }} оцінка 0–100 і коди проблем
90
+ */
91
+ function scoreDoc(md, facts) {
92
+ const s = parseSections(md)
93
+ let score = 100
94
+ const issues = []
95
+
96
+ if (!s['огляд']) {
97
+ score -= 25
98
+ issues.push('no-overview')
99
+ }
100
+
101
+ const behavior = s['поведінка'] ?? ''
102
+ if (behavior.length < 60) {
103
+ score -= 20
104
+ issues.push('short-behavior')
105
+ }
106
+
107
+ const guarantees = s['гарантіїповедінки'] ?? ''
108
+ // Будь-яка згадка "кеш" у Гарантіях коли файл не кешує — галюцинація
109
+ // Негація: "не кешує", "не має кешування", "без кешування", "немає кешу"
110
+ const cacheHit = CACHE_MENTION_RE.test(guarantees) && !CACHE_NEGATION_RE.test(guarantees)
111
+ if (!facts.markers?.caches && cacheHit) {
112
+ score -= 20
113
+ issues.push('cache-hallucination')
114
+ }
115
+
116
+ for (const sym of facts.internalSymbols ?? []) {
117
+ const inDoc = hasName(guarantees, sym) || hasName(s['огляд'] ?? '', sym) || hasName(s['поведінка'] ?? '', sym)
118
+ if (inDoc) {
119
+ score -= 10
120
+ issues.push(`internal-name:${sym}`)
121
+ }
122
+ }
123
+
124
+ return { score: Math.max(0, score), issues }
125
+ }
126
+
127
+ /**
128
+ * E2 — один цикл critique→refine на секцію.
129
+ * Повертає або уточнену чорнетку, або оригінал якщо критик повідомив NONE.
130
+ * @param {'overview'|'behavior'|'api'} sectionKey ключ секції
131
+ * @param {string} draft чорнетка секції
132
+ * @param {object} facts факт-лист
133
+ * @param {object|null} anchors анкори файлу
134
+ * @param {string} model model-id
135
+ * @param {number} timeoutMs ліміт на один виклик
136
+ * @returns {string} фінальний текст секції
137
+ */
138
+ function critiqueRefineSection(sectionKey, draft, facts, anchors, model, timeoutMs) {
139
+ const critique = callLlm(criticMessages(sectionKey, draft, facts, anchors), model, { timeoutMs }).trim()
140
+ if (!critique || CRITIC_NONE_RE.test(critique) || critique.length < 12) return draft
141
+ const refined = callLlm(refineMessages(sectionKey, draft, critique, facts, anchors), model, { timeoutMs }).trim()
142
+ return stripSignatures(stripSection(refined)) || draft
143
+ }
144
+
145
+ /**
146
+ * Чи треба refine для секції API: тільки якщо є >1 експорту і всі desc-и порожні
147
+ * (саме там модель схильна писати «застосовує логіку до файлу»).
148
+ * @param {object} facts факт-лист
149
+ * @returns {boolean} true — секцію API варто прогнати через критика
150
+ */
151
+ function apiNeedsRefine(facts) {
152
+ const exps = facts.exports ?? []
153
+ if (exps.length <= 1) return false
154
+ return exps.every(e => !e.desc)
155
+ }
156
+
157
+ /**
158
+ * One-shot: один виклик LLM на весь документ (для unsupported-структур).
159
+ * @param {object} facts факт-лист
160
+ * @param {string} src вміст файлу
161
+ * @param {string} model model-id
162
+ * @param {number} [timeoutMs] ліміт на виклик
163
+ * @returns {{ md: string }} зібраний документ
164
+ */
165
+ function oneShotDoc(facts, src, model, timeoutMs = LOCAL_TIMEOUT_MS) {
166
+ const text = callLlm(oneShotMessages(facts, src), model, { timeoutMs })
167
+ let md = stripSignatures(stripSection(text))
168
+ if (!md.startsWith('#')) md = `# ${basename(facts.relPath)}\n\n${md}`
169
+ return { md: md + '\n' }
170
+ }
171
+
172
+ /**
173
+ * Stage 3: фіксовані заголовки у фіксованому порядку.
174
+ * @param {string} stem назва файлу для H1
175
+ * @param {Record<string, string>} sections тексти секцій за ключами
176
+ * @returns {string} зібраний md-документ
177
+ */
178
+ function assemble(stem, sections) {
179
+ const order = [
180
+ ['overview', '## Огляд'],
181
+ ['behavior', '## Поведінка'],
182
+ ['api', '## Публічний API'],
183
+ ['guarantees', '## Гарантії поведінки']
184
+ ]
185
+ const parts = [`# ${stem}`]
186
+ for (const [key, title] of order) {
187
+ const body = sections[key]
188
+ if (body && body.trim()) parts.push(`${title}\n\n${body.trim()}`)
189
+ }
190
+ return parts.join('\n\n') + '\n'
191
+ }
192
+
193
+ /**
194
+ * Orchestrated: N окремих LLM-викликів, по одному на секцію.
195
+ * Код потрапляє лише в `behavior`; решта секцій — на мінімальному факт-листі.
196
+ * @param {object} facts факт-лист
197
+ * @param {string} src вміст файлу
198
+ * @param {string} model model-id
199
+ * @param {number} timeoutMs ліміт на один виклик
200
+ * @param {{ anchors?: object|null, temperature?: number }} [opts] анкори й температура семплінгу
201
+ * @returns {{ md: string }} зібраний документ
202
+ */
203
+ function orchestratedDoc(facts, src, model, timeoutMs, { anchors = null, temperature = 0.2 } = {}) {
204
+ const sections = {}
205
+ const anc = anchors ?? extractAnchors(src)
206
+ // E3: «Гарантії» — детермінований шаблон з markers (0 LLM-запитів, 0 generic-фраз)
207
+ sections.guarantees = guaranteesFromMarkers(facts)
208
+ for (const s of sectionMessages(facts, src, anc)) {
209
+ if (s.key === 'guarantees') continue // вже згенеровано детерміновано
210
+ let draft = stripSignatures(stripSection(callLlm(s.messages, model, { timeoutMs, temperature })))
211
+ // E2 + E3: critique→refine лише для секцій, де мала модель зриває на generic
212
+ if (s.key === 'overview' || (s.key === 'api' && apiNeedsRefine(facts))) {
213
+ draft = critiqueRefineSection(s.key, draft, facts, anc, model, timeoutMs)
214
+ }
215
+ sections[s.key] = draft
216
+ }
217
+ return { md: assemble(basename(facts.relPath), sections) }
218
+ }
219
+
220
+ /** Максимальний час генерації одного LLM-виклику. */
221
+ const LOCAL_TIMEOUT_MS = 5 * 60 * 1000
222
+ /**
223
+ * Дефолтна модель: N_CURSOR_DOCGEN_MODEL → resolveModel('min') → omlx напряму.
224
+ * Останній fallback гарантує local-only шлях без жодних env (через pi CLI той
225
+ * самий локальний виклик виміряно повільніший на ~46%).
226
+ */
227
+ export const DEFAULT_LOCAL_MODEL = env.N_CURSOR_DOCGEN_MODEL ?? (resolveModel('min') || `omlx/${DEFAULT_OMLX_MODEL}`)
228
+
229
+ /**
230
+ * Головний API: файл → md-дока з det-оцінкою.
231
+ *
232
+ * Local-only (ADR 260610-2228): жодних cloud-ескалацій і pre-route — будь-який
233
+ * файл генерується локальною моделлю. Якщо det-score нижче порогу, один retry
234
+ * з вищою температурою (best-of-2); якщо й він не допоміг — результат
235
+ * позначається `degraded`, рішення про перегенерацію приймає batch/користувач.
236
+ * @param {string} file абсолютний шлях джерела
237
+ * @param {{ model?: string, threshold?: number }} [opts] model-id і поріг degraded
238
+ * @returns {{ md: string, ms: number, score: number|null, issues: string[], degraded: boolean, model: string }} документ і метадані генерації
239
+ */
240
+ export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUALITY_THRESHOLD } = {}) {
241
+ const src = readFileSync(file, 'utf8')
242
+ const facts = extractFacts(src, file)
243
+ const t0 = Date.now()
244
+
245
+ const anchors = facts.unsupported ? null : extractAnchors(src)
246
+ let r = facts.unsupported
247
+ ? oneShotDoc(facts, src, model)
248
+ : orchestratedDoc(facts, src, model, LOCAL_TIMEOUT_MS, { anchors })
249
+
250
+ // unsupported (vue/py до юніт-шару): скорер не застосовний — score=null, не degraded
251
+ if (facts.unsupported) {
252
+ return { ...r, ms: Date.now() - t0, score: null, issues: [], degraded: false, model }
253
+ }
254
+
255
+ // Stage 2.5: детермінований скоринг (0 токенів)
256
+ let { score, issues } = scoreDoc(r.md, facts)
257
+
258
+ // E4: best-of-2 — один retry з вищою температурою, det-вибір кращого
259
+ if (score < threshold && env.N_CURSOR_DOCGEN_BEST_OF !== '0') {
260
+ try {
261
+ const r2 = orchestratedDoc(facts, src, model, LOCAL_TIMEOUT_MS, { anchors, temperature: 0.5 })
262
+ const s2 = scoreDoc(r2.md, facts)
263
+ if (s2.score > score) {
264
+ r = r2
265
+ score = s2.score
266
+ issues = [...s2.issues, 'best-of-2:retry-won']
267
+ } else {
268
+ issues = [...issues, 'best-of-2:retry-lost']
269
+ }
270
+ } catch (error) {
271
+ issues = [...issues, `best-of-2:retry-error: ${error.message}`]
272
+ }
273
+ }
274
+
275
+ return { ...r, ms: Date.now() - t0, score, issues, degraded: score < threshold, model }
276
+ }
277
+
278
+ // CLI: node docgen-gen.mjs <file> [--model <m>]
279
+ if (isRunAsCli(import.meta.url)) {
280
+ const args = process.argv.slice(2)
281
+ const file = args.find(a => !a.startsWith('--'))
282
+ const mi = args.indexOf('--model')
283
+ const model = mi === -1 ? DEFAULT_LOCAL_MODEL : args[mi + 1]
284
+ if (!file) {
285
+ throw new Error('Usage: node docgen-gen.mjs <file> [--model <m>]')
286
+ }
287
+ const r = generateDoc(file, { model })
288
+ const issuesTxt = r.issues?.length ? ` issues=${r.issues.join(',')}` : ''
289
+ process.stderr.write(`[local ${r.model}] ${r.ms}ms / score=${r.score}${r.degraded ? ' DEGRADED' : ''}${issuesTxt}\n`)
290
+ process.stdout.write(r.md)
291
+ }
@@ -8,7 +8,11 @@ export const STYLE = [
8
8
  'Заборонено: сигнатури, типи, параметри функцій; перелік stdlib-модулів; опис regex чи внутрішніх приватних імен.'
9
9
  ].join(' ')
10
10
 
11
- /** Окремий блок інструкцій з анкорами — підставляється коли вони є. */
11
+ /**
12
+ * Окремий блок інструкцій з анкорами — підставляється коли вони є.
13
+ * @param {object|null} anchors анкори файлу (або null)
14
+ * @returns {string} текстовий блок для system-промпта або порожній рядок
15
+ */
12
16
  function anchorsBlock(anchors) {
13
17
  if (!anchors) return ''
14
18
  const txt = anchorsToPrompt(anchors)
@@ -35,6 +39,12 @@ function factsSummary(facts) {
35
39
  return lines.join('\n')
36
40
  }
37
41
 
42
+ /**
43
+ * Пара system+user messages для одного виклику.
44
+ * @param {string} system system-промпт
45
+ * @param {string} user user-промпт
46
+ * @returns {Array<{role:string, content:string}>} messages-масив
47
+ */
38
48
  const msgs = (system, user) => [
39
49
  { role: 'system', content: system },
40
50
  { role: 'user', content: user }
@@ -45,64 +55,68 @@ const msgs = (system, user) => [
45
55
  * Код потрапляє лише в `behavior`; решта секцій — на факт-листі.
46
56
  * @param {object} facts факт-лист про файл
47
57
  * @param {string} src вміст файлу
58
+ * @param {object|null} [anchors] анкори файлу для обовʼязкового включення
48
59
  * @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів
49
60
  */
50
61
  export function sectionMessages(facts, src, anchors = null) {
51
62
  const factsTxt = factsSummary(facts)
52
63
  const anch = anchorsBlock(anchors)
53
64
  const multi = (facts.exports?.length || 0) > 1
54
- const out = []
55
65
 
56
66
  // Огляд — лише факти (без коду)
57
- out.push({
67
+ const overview = {
58
68
  key: 'overview',
59
69
  numPredict: 220,
60
70
  messages: msgs(
61
71
  `${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
62
72
  'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій. Заборонені generic-фрази типу «забезпечує перевірку», «виконує валідацію» — пиши КОНКРЕТНО що саме і за яким контрактом.'
63
73
  )
64
- })
74
+ }
65
75
 
66
76
  // Поведінка — ЄДИНА секція, якій потрібен код
67
- out.push({
77
+ const behaviorTask = multi
78
+ ? 'для кожної публічної функції — один короткий пункт «що вона робить»'
79
+ : 'нумерований алгоритм у бізнес-термінах'
80
+ const noInternal = facts.internalSymbols?.length
81
+ ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.`
82
+ : ''
83
+ const behavior = {
68
84
  key: 'behavior',
69
85
  numPredict: 500,
70
86
  messages: msgs(
71
87
  `${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
72
- `Напиши вміст секції «Поведінка»: ${multi ? 'для кожної публічної функції — один короткий пункт «що вона робить»' : 'нумерований алгоритм у бізнес-термінах'}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${facts.internalSymbols?.length ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.` : ''} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
88
+ `Напиши вміст секції «Поведінка»: ${behaviorTask}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
73
89
  )
74
- })
90
+ }
75
91
 
76
92
  // API — лише список експортів (без коду)
77
- if (multi || facts.exports?.some(e => e.desc)) {
78
- const list = facts.exports.map(e => `- ${e.name}: ${e.desc || '(сформулюй стисло з наміру файлу)'}`).join('\n')
79
- out.push({
80
- key: 'api',
81
- numPredict: 320,
82
- messages: msgs(
83
- `${STYLE}${anch}`,
84
- `Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка. Без generic-фраз «застосовує логіку», «перевіряє коректність» — пиши конкретно ЩО саме застосовує/перевіряє.`
85
- )
86
- })
93
+ if (!multi && !facts.exports?.some(e => e.desc)) return [overview, behavior]
94
+ const list = facts.exports.map(e => `- ${e.name}: ${e.desc || '(сформулюй стисло з наміру файлу)'}`).join('\n')
95
+ const api = {
96
+ key: 'api',
97
+ numPredict: 320,
98
+ messages: msgs(
99
+ `${STYLE}${anch}`,
100
+ `Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка. Без generic-фраз «застосовує логіку», «перевіряє коректність» — пиши конкретно ЩО саме застосовує/перевіряє.`
101
+ )
87
102
  }
88
-
89
- return out
103
+ return [overview, behavior, api]
90
104
  }
91
105
 
92
106
  /**
93
107
  * E2-step 1 — критик. Перевіряє чорнетку секції на конкретні дефекти.
94
108
  * Повертає messages для LLM-запиту: вихід має бути СПИСКОМ issues або словом NONE.
95
- * @param {'overview'|'behavior'|'api'} sectionKey
109
+ * @param {'overview'|'behavior'|'api'} sectionKey ключ секції
96
110
  * @param {string} draft вже згенерована чорнетка секції
97
111
  * @param {object} facts факт-лист
98
- * @param {ReturnType<import('./docgen-extract-anchors.mjs').extractAnchors>} anchors
99
- * @returns {Array<{role:string,content:string}>}
112
+ * @param {ReturnType<import('./docgen-extract-anchors.mjs').extractAnchors>} anchors анкори файлу
113
+ * @returns {Array<{role:string,content:string}>} messages-масив для критика
100
114
  */
101
115
  export function criticMessages(sectionKey, draft, facts, anchors) {
102
116
  const anch = anchorsBlock(anchors)
103
117
  const criteria = [
104
118
  'generic-фрази без конкретики («забезпечує перевірку», «виконує валідацію», «застосовує логіку»)',
105
- 'пропущені обов\'язкові АНКОРИ з контексту (URLs, magic-string constants, error-маркери, конфіги, code-приклади)',
119
+ "пропущені обов'язкові АНКОРИ з контексту (URLs, magic-string constants, error-маркери, конфіги, code-приклади)",
106
120
  'граматичні помилки українською («перед їх застосування», «моделіне», англіцизми як «applys», «moduleline»)',
107
121
  'h1/h2/h3 підзаголовки всередині секції — їх не повинно бути',
108
122
  'дослівна копія JSDoc-сигнатури або параметрів у дужках',
@@ -122,12 +136,12 @@ export function criticMessages(sectionKey, draft, facts, anchors) {
122
136
 
123
137
  /**
124
138
  * E2-step 2 — refine. Переписує чорнетку, виправляючи перелічені issues.
125
- * @param {'overview'|'behavior'|'api'} sectionKey
126
- * @param {string} draft
139
+ * @param {'overview'|'behavior'|'api'} sectionKey ключ секції
140
+ * @param {string} draft чорнетка секції
127
141
  * @param {string} issues список issues від critic
128
- * @param {object} facts
129
- * @param {ReturnType<import('./docgen-extract-anchors.mjs').extractAnchors>} anchors
130
- * @returns {Array<{role:string,content:string}>}
142
+ * @param {object} facts факт-лист
143
+ * @param {ReturnType<import('./docgen-extract-anchors.mjs').extractAnchors>} anchors анкори файлу
144
+ * @returns {Array<{role:string,content:string}>} messages-масив для переписування
131
145
  */
132
146
  export function refineMessages(sectionKey, draft, issues, facts, anchors) {
133
147
  const anch = anchorsBlock(anchors)
@@ -146,7 +160,7 @@ export function refineMessages(sectionKey, draft, issues, facts, anchors) {
146
160
  /**
147
161
  * E3 — детермінований шаблон секції «Гарантії поведінки» з facts.markers.
148
162
  * НЕ використовує LLM: 0 запитів, 0 галюцинацій, 0 generic-фраз.
149
- * @param {object} facts
163
+ * @param {object} facts факт-лист
150
164
  * @returns {string} текст секції (без `## Гарантії` — це додає assemble())
151
165
  */
152
166
  export function guaranteesFromMarkers(facts) {
@@ -177,14 +191,3 @@ export function oneShotMessages(facts, src) {
177
191
  `Напиши документацію для файлу. Секції: ## Огляд (1-3 речення), ## Поведінка (нумерований/маркований алгоритм), ${multi ? '## Публічний API (назва + що робить), ' : ''}## Гарантії поведінки.\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\``
178
192
  )
179
193
  }
180
-
181
- /**
182
- * Лише текст user-промпту для one-shot (для хмарного fallback через Anthropic SDK).
183
- * @param {object} facts факт-лист про файл
184
- * @param {string} src вміст файлу
185
- * @returns {string} plain-text user-prompt
186
- */
187
- export function oneShotPromptText(facts, src) {
188
- const multi = (facts.exports?.length || 0) > 1
189
- return `Напиши документацію для файлу. Секції: ## Огляд (1-3 речення), ## Поведінка (нумерований/маркований алгоритм), ${multi ? '## Публічний API (назва + що робить), ' : ''}## Гарантії поведінки.\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\``
190
- }