@nitra/cursor 5.1.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) 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 +18 -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/models.md +24 -17
  9. package/lib/docs/omlx.md +32 -0
  10. package/lib/llm.mjs +137 -0
  11. package/lib/omlx.mjs +49 -4
  12. package/package.json +1 -1
  13. package/rules/abie/docs/fix.md +6 -0
  14. package/rules/abie/js/docs/applies.md +6 -0
  15. package/rules/abie/js/docs/env_dns.md +25 -22
  16. package/rules/abie/js/docs/firebase_hosting.md +6 -0
  17. package/rules/abie/js/docs/hc_pairing.md +21 -25
  18. package/rules/abie/js/docs/ua_http_route.md +27 -19
  19. package/rules/abie/js/docs/ua_node_selector.md +24 -19
  20. package/rules/abie/lib/docs/enabled.md +13 -7
  21. package/rules/abie/lib/docs/env-dns.md +9 -3
  22. package/rules/abie/lib/docs/hc-yaml.md +6 -0
  23. package/rules/abie/lib/docs/http-route.md +6 -0
  24. package/rules/abie/lib/docs/k8s-tree.md +6 -0
  25. package/rules/abie/lib/docs/kustomization-patches.md +6 -0
  26. package/rules/abie/lib/docs/overlay-paths.md +6 -0
  27. package/rules/abie/lib/docs/yaml.md +6 -0
  28. package/rules/adr/docs/fix.md +6 -0
  29. package/rules/adr/js/docs/hooks.md +29 -244
  30. package/rules/bun/docs/fix.md +6 -0
  31. package/rules/bun/js/docs/layout.md +37 -375
  32. package/rules/capacitor/docs/fix.md +22 -108
  33. package/rules/capacitor/js/docs/platforms.md +62 -268
  34. package/rules/changelog/docs/fix.md +6 -0
  35. package/rules/changelog/js/docs/consistency.md +36 -383
  36. package/rules/changelog/lib/docs/package-manifest.md +6 -0
  37. package/rules/ci4/docs/fix.md +23 -165
  38. package/rules/ci4/js/docs/marksman_config.md +9 -1
  39. package/rules/docker/docs/fix.md +6 -0
  40. package/rules/docker/js/docs/lint.md +55 -239
  41. package/rules/docker/lib/docs/docker-hadolint.md +6 -0
  42. package/rules/docker/lib/docs/docker-mirror.md +6 -0
  43. package/rules/docker/lib/docs/docker-native-addon.md +6 -0
  44. package/rules/docker/lib/docs/docker-nginx-user.md +6 -0
  45. package/rules/docker/lint/docs/lint.md +9 -1
  46. package/rules/efes/docs/fix.md +6 -0
  47. package/rules/feedback/docs/fix.md +21 -131
  48. package/rules/ga/docs/fix.md +14 -12
  49. package/rules/ga/js/docs/lint.md +12 -9
  50. package/rules/ga/js/docs/workflows.md +20 -19
  51. package/rules/ga/lint/docs/lint.md +6 -0
  52. package/rules/graphql/docs/fix.md +6 -0
  53. package/rules/graphql/js/docs/tooling.md +18 -253
  54. package/rules/graphql/lib/docs/graphql-gql-scan.md +6 -0
  55. package/rules/hasura/docs/fix.md +18 -111
  56. package/rules/image-avif/docs/fix.md +6 -0
  57. package/rules/image-avif/js/docs/avif_generation.md +6 -0
  58. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +9 -3
  59. package/rules/js-bun-redis/lib/docs/redis-imports.md +6 -0
  60. package/rules/js-lint/js/docs/utils_imports.md +6 -0
  61. package/rules/js-lint-ci/docs/fix.md +7 -1
  62. package/rules/js-mssql/docs/fix.md +6 -0
  63. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +6 -0
  64. package/rules/js-run/docs/fix.md +6 -0
  65. package/rules/js-run/lib/docs/bunyan-imports.md +6 -0
  66. package/rules/js-run/lib/docs/check-env-scan.md +6 -0
  67. package/rules/js-run/lib/docs/conn-file-rules.md +6 -0
  68. package/rules/js-run/lib/docs/conn-imports-scan.md +6 -0
  69. package/rules/js-run/lib/docs/promise-settimeout-scan.md +6 -0
  70. package/rules/js-run/lib/docs/temporal-scan.md +6 -0
  71. package/rules/k8s/docs/fix.md +6 -0
  72. package/rules/k8s/lint/docs/lint.md +6 -0
  73. package/rules/nginx-default-tpl/docs/fix.md +6 -0
  74. package/rules/npm-module/js/docs/header_doc_pointer.md +7 -0
  75. package/rules/npm-module/js/header_doc_pointer.mjs +2 -8
  76. package/rules/php/docs/fix.md +6 -0
  77. package/rules/php/lint/docs/lint.md +6 -0
  78. package/rules/python/docs/fix.md +6 -0
  79. package/rules/python/lint/docs/lint.md +6 -0
  80. package/rules/rego/lint/docs/lint.md +6 -0
  81. package/rules/release/docs/change.md +6 -0
  82. package/rules/release/docs/fix.md +6 -0
  83. package/rules/release/docs/release.md +6 -0
  84. package/rules/release/lib/docs/aggregate.md +6 -0
  85. package/rules/release/lib/docs/change-file.md +6 -0
  86. package/rules/release/lib/docs/fallback.md +6 -0
  87. package/rules/rust/lib/docs/has-cargo-toml.md +6 -0
  88. package/rules/security/docs/fix.md +7 -1
  89. package/rules/security/js/docs/lint.md +6 -0
  90. package/rules/style-lint/docs/fix.md +6 -0
  91. package/rules/tauri/docs/fix.md +6 -0
  92. package/rules/test/docs/fix.md +6 -0
  93. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +6 -0
  94. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +6 -0
  95. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +6 -0
  96. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +6 -0
  97. package/rules/text/docs/fix.md +6 -0
  98. package/rules/text/lint/docs/lint.md +6 -0
  99. package/rules/text/lint/docs/run-dotenv-linter.md +6 -0
  100. package/rules/text/lint/docs/run-shellcheck.md +6 -0
  101. package/rules/text/lint/docs/run-v8r.md +6 -0
  102. package/rules/vue/lib/docs/vue-forbidden-imports.md +6 -0
  103. package/scripts/coverage-classify/cache.mjs +1 -1
  104. package/scripts/coverage-classify/docs/apply.md +6 -0
  105. package/scripts/coverage-classify/docs/cache.md +6 -0
  106. package/scripts/coverage-classify/docs/prompt.md +6 -0
  107. package/scripts/coverage-classify/docs/verdict-schema.md +6 -0
  108. package/scripts/coverage-classify/prompt.mjs +1 -1
  109. package/scripts/coverage-fix-extract.mjs +1 -1
  110. package/scripts/coverage-fix.mjs +2 -1
  111. package/scripts/docs/auto-skills.md +6 -0
  112. package/scripts/docs/build-agents-commands.md +7 -1
  113. package/scripts/docs/cli-entry.md +6 -0
  114. package/scripts/docs/coverage-fix-extract.md +6 -0
  115. package/scripts/docs/coverage-fix.md +6 -0
  116. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +6 -0
  117. package/scripts/docs/lint-cli.md +6 -0
  118. package/scripts/docs/post-tool-use-fix.md +6 -0
  119. package/scripts/docs/rename-yaml-extensions.md +6 -0
  120. package/scripts/docs/skills-cli.md +6 -0
  121. package/scripts/docs/sync-setup-bun-deps-action.md +6 -0
  122. package/scripts/docs/upgrade-nitra-cursor-and-install.md +6 -0
  123. package/scripts/docs/worktree-cli.md +6 -0
  124. package/scripts/lib/docs/assert-project-root.md +6 -0
  125. package/scripts/lib/docs/check-mdc-template-refs.md +6 -0
  126. package/scripts/lib/docs/check-reporter.md +6 -0
  127. package/scripts/lib/docs/diff-added-lines.md +6 -0
  128. package/scripts/lib/docs/discover-check-rules-from-cursor.md +6 -0
  129. package/scripts/lib/docs/discover-checkable-rules.md +6 -0
  130. package/scripts/lib/docs/ensure-tool.md +6 -0
  131. package/scripts/lib/docs/generated-markdown.md +6 -0
  132. package/scripts/lib/docs/gha-workflow.md +6 -0
  133. package/scripts/lib/docs/inline-template-links.md +6 -0
  134. package/scripts/lib/docs/list-rule-ids.md +6 -0
  135. package/scripts/lib/docs/load-cursor-config.md +6 -0
  136. package/scripts/lib/docs/mirror-parity.md +6 -0
  137. package/scripts/lib/docs/read-n-cursor-config-lite.md +6 -0
  138. package/scripts/lib/docs/resolve-target-files.md +6 -0
  139. package/scripts/lib/docs/root-notice.md +6 -0
  140. package/scripts/lib/docs/rule-meta-helpers.md +6 -0
  141. package/scripts/lib/docs/rule-meta.md +6 -0
  142. package/scripts/lib/docs/run-conftest-batch.md +6 -0
  143. package/scripts/lib/docs/run-lint-step.md +6 -0
  144. package/scripts/lib/docs/run-rule-cli.md +6 -0
  145. package/scripts/lib/docs/run-rule.md +6 -0
  146. package/scripts/lib/docs/run-standard-lint.md +6 -0
  147. package/scripts/lib/docs/run-standard-rule.md +6 -0
  148. package/scripts/lib/docs/skill-meta.md +6 -0
  149. package/scripts/lib/docs/template.md +6 -0
  150. package/scripts/lib/docs/timing-summary.md +6 -0
  151. package/scripts/lib/docs/workspaces.md +6 -0
  152. package/scripts/lib/docs/worktree-notice.md +6 -0
  153. package/scripts/lib/docs/worktree.md +6 -0
  154. package/scripts/lib/mirror-parity.mjs +1 -1
  155. package/scripts/lib/root-notice.mjs +1 -1
  156. package/scripts/lib/worktree-notice.mjs +5 -5
  157. package/scripts/lib/worktree.mjs +1 -1
  158. package/scripts/sync-claude-config.mjs +3 -0
  159. package/scripts/utils/docs/ast-scan-utils.md +6 -0
  160. package/scripts/utils/docs/ensure-gitignore-entries.md +6 -0
  161. package/scripts/utils/docs/find-package-json-paths.md +6 -0
  162. package/scripts/utils/docs/lock-cache-dir.md +6 -0
  163. package/scripts/utils/docs/pass.md +6 -0
  164. package/scripts/utils/docs/resolve-cargo-manifest.md +6 -0
  165. package/scripts/utils/docs/resolve-cmd.md +6 -0
  166. package/scripts/utils/docs/resolve-js-root.md +6 -0
  167. package/scripts/utils/docs/test-helpers.md +6 -0
  168. package/scripts/utils/docs/walk-cache.md +6 -0
  169. package/scripts/utils/docs/walkDir.md +6 -0
  170. package/scripts/utils/docs/worktree-fingerprint.md +6 -0
  171. package/scripts/utils/resolve-js-root.mjs +1 -1
  172. package/skills/doc-aggregate/SKILL.md +129 -0
  173. package/skills/doc-aggregate/js/docgen-ignore.mjs +9 -0
  174. package/skills/{docgen → doc-aggregate}/js/docgen-scan.mjs +22 -67
  175. package/skills/doc-aggregate/js/docs/docgen-ignore.md +21 -0
  176. package/skills/doc-files/SKILL.md +100 -0
  177. package/skills/doc-files/js/docgen-crc.mjs +164 -0
  178. package/skills/{docgen → doc-files}/js/docgen-extract-anchors.mjs +48 -13
  179. package/skills/{docgen → doc-files}/js/docgen-extract.mjs +39 -10
  180. package/skills/doc-files/js/docgen-files-batch.mjs +181 -0
  181. package/skills/doc-files/js/docgen-gen.mjs +336 -0
  182. package/skills/{docgen → doc-files}/js/docgen-prompts.mjs +65 -50
  183. package/skills/doc-files/js/docgen-scan.mjs +298 -0
  184. package/skills/doc-files/js/docs/docgen-crc.md +32 -0
  185. package/skills/doc-files/js/docs/docgen-extract-anchors.md +27 -0
  186. package/skills/doc-files/js/docs/docgen-extract.md +29 -0
  187. package/skills/doc-files/js/docs/docgen-files-batch.md +25 -0
  188. package/skills/doc-files/js/docs/docgen-gen.md +30 -0
  189. package/skills/doc-files/js/docs/docgen-prompts.md +32 -0
  190. package/skills/doc-files/js/docs/docgen-scan.md +25 -0
  191. package/skills/doc-files/js/units-js.mjs +139 -0
  192. package/skills/doc-files/js/units.mjs +19 -0
  193. package/skills/doc-files/meta.json +1 -0
  194. package/skills/fix/js/docs/llm-worker.md +6 -0
  195. package/skills/fix/js/docs/orchestrator.md +6 -0
  196. package/skills/fix/js/llm-worker.mjs +3 -3
  197. package/skills/fix/js/orchestrator.mjs +1 -1
  198. package/skills/start-check/js/check.mjs +5 -3
  199. package/skills/start-check/js/docs/check.md +6 -0
  200. package/skills/docgen/SKILL.md +0 -224
  201. package/skills/docgen/bench/etalon/firebase_hosting.md +0 -19
  202. package/skills/docgen/bench/etalon/k8s-tree.md +0 -24
  203. package/skills/docgen/bench/etalon/overlay-paths.md +0 -24
  204. package/skills/docgen/js/docgen-batch-omlx.mjs +0 -82
  205. package/skills/docgen/js/docgen-batch.mjs +0 -95
  206. package/skills/docgen/js/docgen-compare-pi-vs-direct.mjs +0 -95
  207. package/skills/docgen/js/docgen-gen.mjs +0 -306
  208. package/skills/docgen/js/docs/docgen-extract.md +0 -28
  209. package/skills/docgen/js/docs/docgen-gen.md +0 -41
  210. package/skills/docgen/js/docs/docgen-ignore.md +0 -24
  211. package/skills/docgen/js/docs/docgen-prompts.md +0 -24
  212. package/skills/docgen/js/docs/docgen-scan.md +0 -48
  213. /package/skills/{docgen → doc-aggregate}/meta.json +0 -0
  214. /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,336 @@
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, anchorTokens } from './docgen-extract-anchors.mjs'
11
+ import { QUALITY_THRESHOLD } from './docgen-crc.mjs'
12
+ import {
13
+ oneShotMessages,
14
+ sectionMessages,
15
+ overviewMessages,
16
+ criticMessages,
17
+ refineMessages,
18
+ guaranteesFromMarkers
19
+ } from './docgen-prompts.mjs'
20
+
21
+ const FENCE_OPEN_RE = /^```[a-z]*\n?/
22
+ const FENCE_CLOSE_RE = /\n?```\s*$/
23
+ const LEADING_HEADING_RE = /^#{1,6}[ \t]{1,8}[^\n]{0,400}\n{1,8}/
24
+ const SECTION_HEADING_RE = /^##\s+(.+)/
25
+ const SECTION_KEY_CLEAN_RE = /[^а-яіїєґa-z0-9]/gi
26
+ const CACHE_MENTION_RE = /кеш/i
27
+ const CACHE_NEGATION_RE = /(?:не|без)\s+(?:\S+\s+)?кеш|немає\s+кеш/i
28
+ const CRITIC_NONE_RE = /^\s*NONE\s*$/i
29
+ // R4: абстрактні «нічого-не-кажучі» формули, які обходять exact-blocklist і дають score=100
30
+ const GENERIC_RE =
31
+ /відповідност\S*\s+(?:даних\s+)?(?:визначеному\s+)?контракту|валідаці\S*\s+даних|перевірк\S*\s+(?:відповідності\s+)?даних|обробк\S*\s+даних|застосову\S*\s+логіку|інспекту\S*\s+та\s+збира\S*\s+дан/i
32
+ // R7: часті русизми/суржик (курований безпечний список — без false-positive на нормальній мові).
33
+ // Без \b: кирилиця не є ASCII-`\w`, тож межі слова в JS-regex не спрацьовують — терміни специфічні.
34
+ const SURZHIK_RE =
35
+ /пропуская|являється|в залежності|по замовчуванню|на протязі|відповідаюч|слідуюч|наступним разом|приймати участь|у відповідності/i
36
+ const ANCHOR_MISS_PENALTY = 5
37
+ const ANCHOR_MISS_CAP = 20
38
+
39
+ /**
40
+ * Прибирає код-фенс-обгортку (потрійні бектіки) й випадковий провідний
41
+ * `##`-заголовок із секції.
42
+ * @param {string} text сирий вихід моделі
43
+ * @returns {string} очищений текст секції
44
+ */
45
+ function stripSection(text) {
46
+ let t = text.trim()
47
+ if (t.startsWith('```')) {
48
+ t = t.replace(FENCE_OPEN_RE, '').replace(FENCE_CLOSE_RE, '').trim()
49
+ }
50
+ t = t.replace(LEADING_HEADING_RE, '') // зрізати випадковий заголовок
51
+ return t.trim()
52
+ }
53
+
54
+ /**
55
+ * Stage 2 (детермінований лінт, 0 токенів): зрізає сигнатури `name(args)` → `name`.
56
+ * Два проходи — щоб зняти вкладені виклики на кшталт `check(cwd = process.cwd())`.
57
+ * Не чіпає дужки без ідентифікатора перед ними (напр. `(abie.mdc)`, «(наприклад)»).
58
+ * @param {string} text текст секції
59
+ * @returns {string} текст без сигнатур у дужках
60
+ */
61
+ function stripSignatures(text) {
62
+ let t = text
63
+ for (let i = 0; i < 2; i++) t = t.replaceAll(/([`\w$.]{1,80})\([^()]{0,300}\)/g, '$1')
64
+ return t
65
+ }
66
+
67
+ /**
68
+ * Розбиває md на секції за ## заголовками.
69
+ * @param {string} md зібраний документ
70
+ * @returns {Record<string, string>} нормалізований ключ секції → її тіло
71
+ */
72
+ function parseSections(md) {
73
+ const result = {}
74
+ let cur = null
75
+ for (const line of md.split('\n')) {
76
+ const m = line.match(SECTION_HEADING_RE)
77
+ if (m) {
78
+ cur = m[1].toLowerCase().replaceAll(SECTION_KEY_CLEAN_RE, '')
79
+ result[cur] = ''
80
+ } else if (cur) result[cur] += line + '\n'
81
+ }
82
+ return result
83
+ }
84
+
85
+ /**
86
+ * Чи містить текст бектік-обгорнуте імʼя символу (`sym`) — уникає substring false positives.
87
+ * @param {string} text текст секції
88
+ * @param {string} sym імʼя символу без бектіків
89
+ * @returns {boolean} true — імʼя згадано
90
+ */
91
+ function hasName(text, sym) {
92
+ return text.includes('`' + sym + '`')
93
+ }
94
+
95
+ /**
96
+ * Stage 2.5 — детермінований скоринг (0 токенів): перевіряє вихід проти фактів.
97
+ * @param {string} md зібраний документ
98
+ * @param {object} facts факт-лист про файл
99
+ * @param {{ anchors?: object|null, src?: string }} [ctx] анкори й джерело для R5
100
+ * @returns {{ score: number, issues: string[] }} оцінка 0–100 і коди проблем
101
+ */
102
+ export function scoreDoc(md, facts, { anchors = null, src = '' } = {}) {
103
+ const s = parseSections(md)
104
+ let score = 100
105
+ const issues = []
106
+ const overview = s['огляд'] ?? ''
107
+
108
+ if (!s['огляд']) {
109
+ score -= 25
110
+ issues.push('no-overview')
111
+ }
112
+
113
+ // R4: generic-Огляд (парафрази, які обходять exact-blocklist) — як майже-відсутній.
114
+ if (GENERIC_RE.test(overview)) {
115
+ score -= 35
116
+ issues.push('generic-overview')
117
+ }
118
+
119
+ const behavior = s['поведінка'] ?? ''
120
+ if (behavior.length < 60) {
121
+ score -= 20
122
+ issues.push('short-behavior')
123
+ }
124
+
125
+ const guarantees = s['гарантіїповедінки'] ?? ''
126
+ // Будь-яка згадка "кеш" у Гарантіях коли файл не кешує — галюцинація
127
+ // Негація: "не кешує", "не має кешування", "без кешування", "немає кешу"
128
+ const cacheHit = CACHE_MENTION_RE.test(guarantees) && !CACHE_NEGATION_RE.test(guarantees)
129
+ if (!facts.markers?.caches && cacheHit) {
130
+ score -= 20
131
+ issues.push('cache-hallucination')
132
+ }
133
+
134
+ // R6: службові (неекспортовані) функції не мають фігурувати як публічні
135
+ const api = s['публічнийapi'] ?? ''
136
+ for (const sym of [...(facts.internalSymbols ?? []), ...(facts.localSymbols ?? [])]) {
137
+ const inDoc = hasName(guarantees, sym) || hasName(overview, sym) || hasName(behavior, sym) || hasName(api, sym)
138
+ if (inDoc) {
139
+ score -= 10
140
+ issues.push(`internal-name:${sym}`)
141
+ }
142
+ }
143
+
144
+ // R5: кожен валідний анкор (дослівний підрядок src) має зʼявитися в документі
145
+ if (anchors && src) {
146
+ let missPenalty = 0
147
+ for (const tok of anchorTokens(anchors)) {
148
+ if (!src.includes(tok)) continue // валідність: фейковий анкор не вимагаємо
149
+ if (!md.includes(tok) && missPenalty < ANCHOR_MISS_CAP) {
150
+ missPenalty += ANCHOR_MISS_PENALTY
151
+ issues.push(`anchor-miss:${tok}`)
152
+ }
153
+ }
154
+ score -= missPenalty
155
+ }
156
+
157
+ // R7: суржик/русизми
158
+ if (SURZHIK_RE.test(md)) {
159
+ score -= 10
160
+ issues.push('surzhik')
161
+ }
162
+
163
+ return { score: Math.max(0, score), issues }
164
+ }
165
+
166
+ /**
167
+ * E2 — один цикл critique→refine на секцію.
168
+ * Повертає або уточнену чорнетку, або оригінал якщо критик повідомив NONE.
169
+ * @param {'overview'|'behavior'|'api'} sectionKey ключ секції
170
+ * @param {string} draft чорнетка секції
171
+ * @param {object} facts факт-лист
172
+ * @param {object|null} anchors анкори файлу
173
+ * @param {string} model model-id
174
+ * @param {number} timeoutMs ліміт на один виклик
175
+ * @returns {string} фінальний текст секції
176
+ */
177
+ function critiqueRefineSection(sectionKey, draft, facts, anchors, model, timeoutMs) {
178
+ const critique = callLlm(criticMessages(sectionKey, draft, facts, anchors), model, { timeoutMs }).trim()
179
+ if (!critique || CRITIC_NONE_RE.test(critique) || critique.length < 12) return draft
180
+ const refined = callLlm(refineMessages(sectionKey, draft, critique, facts, anchors), model, { timeoutMs }).trim()
181
+ return stripSignatures(stripSection(refined)) || draft
182
+ }
183
+
184
+ /**
185
+ * Чи треба refine для секції API: тільки якщо є >1 експорту і всі desc-и порожні
186
+ * (саме там модель схильна писати «застосовує логіку до файлу»).
187
+ * @param {object} facts факт-лист
188
+ * @returns {boolean} true — секцію API варто прогнати через критика
189
+ */
190
+ function apiNeedsRefine(facts) {
191
+ const exps = facts.exports ?? []
192
+ if (exps.length <= 1) return false
193
+ return exps.every(e => !e.desc)
194
+ }
195
+
196
+ /**
197
+ * One-shot: один виклик LLM на весь документ (для unsupported-структур).
198
+ * @param {object} facts факт-лист
199
+ * @param {string} src вміст файлу
200
+ * @param {string} model model-id
201
+ * @param {number} [timeoutMs] ліміт на виклик
202
+ * @returns {{ md: string }} зібраний документ
203
+ */
204
+ function oneShotDoc(facts, src, model, timeoutMs = LOCAL_TIMEOUT_MS) {
205
+ const text = callLlm(oneShotMessages(facts, src), model, { timeoutMs })
206
+ let md = stripSignatures(stripSection(text))
207
+ if (!md.startsWith('#')) md = `# ${basename(facts.relPath)}\n\n${md}`
208
+ return { md: md + '\n' }
209
+ }
210
+
211
+ /**
212
+ * Stage 3: фіксовані заголовки у фіксованому порядку.
213
+ * @param {string} stem назва файлу для H1
214
+ * @param {Record<string, string>} sections тексти секцій за ключами
215
+ * @returns {string} зібраний md-документ
216
+ */
217
+ function assemble(stem, sections) {
218
+ const order = [
219
+ ['overview', '## Огляд'],
220
+ ['behavior', '## Поведінка'],
221
+ ['api', '## Публічний API'],
222
+ ['guarantees', '## Гарантії поведінки']
223
+ ]
224
+ const parts = [`# ${stem}`]
225
+ for (const [key, title] of order) {
226
+ const body = sections[key]
227
+ if (body && body.trim()) parts.push(`${title}\n\n${body.trim()}`)
228
+ }
229
+ return parts.join('\n\n') + '\n'
230
+ }
231
+
232
+ /**
233
+ * Orchestrated: N окремих LLM-викликів, по одному на секцію.
234
+ * Код потрапляє лише в `behavior`; решта секцій — на мінімальному факт-листі.
235
+ * @param {object} facts факт-лист
236
+ * @param {string} src вміст файлу
237
+ * @param {string} model model-id
238
+ * @param {number} timeoutMs ліміт на один виклик
239
+ * @param {{ anchors?: object|null, temperature?: number }} [opts] анкори й температура семплінгу
240
+ * @returns {{ md: string }} зібраний документ
241
+ */
242
+ function orchestratedDoc(facts, src, model, timeoutMs, { anchors = null, temperature = 0.2 } = {}) {
243
+ const sections = {}
244
+ const anc = anchors ?? extractAnchors(src)
245
+ // E3: «Гарантії» — детермінований шаблон з markers (0 LLM-запитів, 0 generic-фраз)
246
+ sections.guarantees = guaranteesFromMarkers(facts)
247
+ // Спершу Поведінка (+API) — секції з фактажем
248
+ for (const s of sectionMessages(facts, src, anc)) {
249
+ let draft = stripSignatures(stripSection(callLlm(s.messages, model, { timeoutMs, temperature })))
250
+ // E2: critique→refine для API, коли всі описи порожні (модель зриває на generic)
251
+ if (s.key === 'api' && apiNeedsRefine(facts)) {
252
+ draft = critiqueRefineSection(s.key, draft, facts, anc, model, timeoutMs)
253
+ }
254
+ sections[s.key] = draft
255
+ }
256
+ // R3: «Огляд» — ОСТАННІМ, узагальненням уже написаної Поведінки (не голого факт-листа)
257
+ let overview = stripSignatures(
258
+ stripSection(callLlm(overviewMessages(facts, sections.behavior ?? '', anc), model, { timeoutMs, temperature }))
259
+ )
260
+ overview = critiqueRefineSection('overview', overview, facts, anc, model, timeoutMs)
261
+ sections.overview = overview
262
+ return { md: assemble(basename(facts.relPath), sections) }
263
+ }
264
+
265
+ /** Максимальний час генерації одного LLM-виклику. */
266
+ const LOCAL_TIMEOUT_MS = 5 * 60 * 1000
267
+ /**
268
+ * Дефолтна модель: N_CURSOR_DOCGEN_MODEL → resolveModel('min') → omlx напряму.
269
+ * Останній fallback гарантує local-only шлях без жодних env (через pi CLI той
270
+ * самий локальний виклик виміряно повільніший на ~46%).
271
+ */
272
+ export const DEFAULT_LOCAL_MODEL = env.N_CURSOR_DOCGEN_MODEL ?? (resolveModel('min') || `omlx/${DEFAULT_OMLX_MODEL}`)
273
+
274
+ /**
275
+ * Головний API: файл → md-дока з det-оцінкою.
276
+ *
277
+ * Local-only (ADR 260610-2228): жодних cloud-ескалацій і pre-route — будь-який
278
+ * файл генерується локальною моделлю. Якщо det-score нижче порогу, один retry
279
+ * з вищою температурою (best-of-2); якщо й він не допоміг — результат
280
+ * позначається `degraded`, рішення про перегенерацію приймає batch/користувач.
281
+ * @param {string} file абсолютний шлях джерела
282
+ * @param {{ model?: string, threshold?: number }} [opts] model-id і поріг degraded
283
+ * @returns {{ md: string, ms: number, score: number|null, issues: string[], degraded: boolean, model: string }} документ і метадані генерації
284
+ */
285
+ export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUALITY_THRESHOLD } = {}) {
286
+ const src = readFileSync(file, 'utf8')
287
+ const facts = extractFacts(src, file)
288
+ const t0 = Date.now()
289
+
290
+ const anchors = facts.unsupported ? null : extractAnchors(src)
291
+ let r = facts.unsupported
292
+ ? oneShotDoc(facts, src, model)
293
+ : orchestratedDoc(facts, src, model, LOCAL_TIMEOUT_MS, { anchors })
294
+
295
+ // unsupported (vue/py до юніт-шару): скорер не застосовний — score=null, не degraded
296
+ if (facts.unsupported) {
297
+ return { ...r, ms: Date.now() - t0, score: null, issues: [], degraded: false, model }
298
+ }
299
+
300
+ // Stage 2.5: детермінований скоринг (0 токенів)
301
+ let { score, issues } = scoreDoc(r.md, facts, { anchors, src })
302
+
303
+ // E4: best-of-2 — один retry з вищою температурою, det-вибір кращого
304
+ if (score < threshold && env.N_CURSOR_DOCGEN_BEST_OF !== '0') {
305
+ try {
306
+ const r2 = orchestratedDoc(facts, src, model, LOCAL_TIMEOUT_MS, { anchors, temperature: 0.5 })
307
+ const s2 = scoreDoc(r2.md, facts, { anchors, src })
308
+ if (s2.score > score) {
309
+ r = r2
310
+ score = s2.score
311
+ issues = [...s2.issues, 'best-of-2:retry-won']
312
+ } else {
313
+ issues = [...issues, 'best-of-2:retry-lost']
314
+ }
315
+ } catch (error) {
316
+ issues = [...issues, `best-of-2:retry-error: ${error.message}`]
317
+ }
318
+ }
319
+
320
+ return { ...r, ms: Date.now() - t0, score, issues, degraded: score < threshold, model }
321
+ }
322
+
323
+ // CLI: node docgen-gen.mjs <file> [--model <m>]
324
+ if (isRunAsCli(import.meta.url)) {
325
+ const args = process.argv.slice(2)
326
+ const file = args.find(a => !a.startsWith('--'))
327
+ const mi = args.indexOf('--model')
328
+ const model = mi === -1 ? DEFAULT_LOCAL_MODEL : args[mi + 1]
329
+ if (!file) {
330
+ throw new Error('Usage: node docgen-gen.mjs <file> [--model <m>]')
331
+ }
332
+ const r = generateDoc(file, { model })
333
+ const issuesTxt = r.issues?.length ? ` issues=${r.issues.join(',')}` : ''
334
+ process.stderr.write(`[local ${r.model}] ${r.ms}ms / score=${r.score}${r.degraded ? ' DEGRADED' : ''}${issuesTxt}\n`)
335
+ process.stdout.write(r.md)
336
+ }