@nitra/cursor 3.27.0 → 3.29.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 (128) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +1 -3
  3. package/rules/abie/js/applies.mjs +1 -5
  4. package/rules/abie/js/env_dns.mjs +1 -9
  5. package/rules/abie/js/firebase_hosting.mjs +1 -5
  6. package/rules/abie/js/hc_pairing.mjs +1 -8
  7. package/rules/abie/js/ua_http_route.mjs +1 -10
  8. package/rules/abie/js/ua_node_selector.mjs +1 -8
  9. package/rules/adr/js/hooks.mjs +1 -20
  10. package/rules/bun/js/layout.mjs +1 -19
  11. package/rules/capacitor/js/platforms.mjs +1 -23
  12. package/rules/changelog/js/consistency.mjs +1 -29
  13. package/rules/ci4/js/marksman_config.mjs +1 -19
  14. package/rules/docker/js/lint.mjs +1 -34
  15. package/rules/ga/docs/fix.md +4 -4
  16. package/rules/ga/js/docs/lint.md +3 -3
  17. package/rules/ga/js/docs/workflows.md +14 -14
  18. package/rules/ga/js/workflows.mjs +1 -16
  19. package/rules/ga/lint/docs/lint.md +9 -9
  20. package/rules/graphql/js/tooling.mjs +1 -9
  21. package/rules/hasura/js/internal_urls.mjs +1 -24
  22. package/rules/image-avif/js/avif_generation.mjs +1 -27
  23. package/rules/image-compress/js/package_setup.mjs +1 -18
  24. package/rules/js-bun-db/js/safety.mjs +1 -31
  25. package/rules/js-bun-redis/js/imports.mjs +1 -12
  26. package/rules/js-lint/js/docs/lint-findings.md +30 -0
  27. package/rules/js-lint/js/lint-findings.mjs +1 -7
  28. package/rules/js-lint/js/lint.mjs +1 -10
  29. package/rules/js-lint/js/tooling.mjs +1 -13
  30. package/rules/js-lint/js/utils_imports.mjs +1 -18
  31. package/rules/js-lint-ci/js/lint.mjs +1 -6
  32. package/rules/js-mssql/js/deps.mjs +1 -10
  33. package/rules/js-run/js/runtime.mjs +1 -37
  34. package/rules/js-run/lib/docs/temporal-scan.md +25 -0
  35. package/rules/k8s/js/manifests.mjs +1 -137
  36. package/rules/nginx-default-tpl/js/template.mjs +1 -18
  37. package/rules/npm-module/js/docs/header_doc_pointer.md +25 -0
  38. package/rules/npm-module/js/header_doc_pointer.mjs +82 -0
  39. package/rules/npm-module/js/package_structure.mjs +1 -28
  40. package/rules/npm-module/js/rule_meta.mjs +1 -10
  41. package/rules/npm-module/js/skill_meta.mjs +1 -13
  42. package/rules/php/js/tooling.mjs +1 -11
  43. package/rules/python/js/applies.mjs +1 -8
  44. package/rules/python/js/tooling.mjs +1 -21
  45. package/rules/rego/js/applies.mjs +1 -11
  46. package/rules/rust/js/applies.mjs +1 -7
  47. package/rules/security/js/sample_secret.mjs +1 -28
  48. package/rules/security/js/trufflehog.mjs +1 -8
  49. package/rules/style-lint/js/lint.mjs +1 -5
  50. package/rules/style-lint/js/tooling.mjs +1 -19
  51. package/rules/tauri/js/cargo_mutants_config.mjs +1 -20
  52. package/rules/tauri/js/tooling.mjs +1 -21
  53. package/rules/test/js/cargo_mutants_config.mjs +1 -12
  54. package/rules/test/js/location.mjs +1 -9
  55. package/rules/test/js/no-process-chdir.mjs +1 -21
  56. package/rules/test/js/no-relative-fs-path.mjs +1 -23
  57. package/rules/test/js/stryker_config.mjs +4 -25
  58. package/rules/test/js/vitest-config-pool-forks.mjs +1 -17
  59. package/rules/text/js/forbidden-prettier.mjs +1 -10
  60. package/rules/text/js/formatting.mjs +1 -31
  61. package/rules/vue/js/packages.mjs +1 -24
  62. package/scripts/coverage-classify/index.mjs +60 -72
  63. package/scripts/coverage-fix.mjs +26 -23
  64. package/scripts/dispatcher/lib/subagent-runner.mjs +33 -102
  65. package/scripts/docs/coverage-fix-extract.md +32 -0
  66. package/scripts/docs/lint-cli.md +25 -0
  67. package/scripts/docs/post-tool-use-fix.md +27 -0
  68. package/scripts/docs/rename-yaml-extensions.md +36 -0
  69. package/scripts/docs/skills-cli.md +35 -0
  70. package/scripts/docs/sync-claude-config.md +52 -0
  71. package/scripts/docs/sync-setup-bun-deps-action.md +26 -0
  72. package/scripts/docs/upgrade-nitra-cursor-and-install.md +29 -0
  73. package/scripts/docs/worktree-cli.md +46 -0
  74. package/scripts/lib/docs/assert-project-root.md +28 -0
  75. package/scripts/lib/docs/diff-added-lines.md +34 -0
  76. package/scripts/lib/docs/read-n-cursor-config-lite.md +28 -0
  77. package/scripts/lib/docs/resolve-target-files.md +34 -0
  78. package/scripts/lib/docs/root-notice.md +28 -0
  79. package/scripts/lib/docs/rule-meta-helpers.md +34 -0
  80. package/scripts/lib/docs/rule-meta.md +34 -0
  81. package/scripts/lib/docs/rule-predicates.md +30 -0
  82. package/scripts/lib/docs/run-conftest-batch.md +26 -0
  83. package/scripts/lib/docs/run-lint-step.md +25 -0
  84. package/scripts/lib/docs/run-rule-cli.md +27 -0
  85. package/scripts/lib/docs/run-rule.md +32 -0
  86. package/scripts/lib/docs/run-standard-lint.md +22 -0
  87. package/scripts/lib/docs/run-standard-rule.md +24 -0
  88. package/scripts/lib/docs/skill-meta.md +31 -0
  89. package/scripts/lib/docs/sync-gitignore-worktree.md +31 -0
  90. package/scripts/lib/docs/template.md +40 -0
  91. package/scripts/lib/docs/timing-summary.md +24 -0
  92. package/scripts/lib/docs/workspaces.md +30 -0
  93. package/scripts/lib/docs/worktree-notice.md +27 -0
  94. package/scripts/lib/docs/worktree.md +38 -0
  95. package/scripts/utils/docs/ast-scan-utils.md +50 -0
  96. package/scripts/utils/docs/ensure-gitignore-entries.md +28 -0
  97. package/scripts/utils/docs/find-package-json-paths.md +26 -0
  98. package/scripts/utils/docs/lock-cache-dir.md +25 -0
  99. package/scripts/utils/docs/pass.md +25 -0
  100. package/scripts/utils/docs/resolve-cargo-manifest.md +23 -0
  101. package/scripts/utils/docs/resolve-cmd.md +29 -0
  102. package/scripts/utils/docs/resolve-js-root.md +25 -0
  103. package/scripts/utils/docs/test-helpers.md +36 -0
  104. package/scripts/utils/docs/walk-cache.md +27 -0
  105. package/scripts/utils/docs/walkDir.md +32 -0
  106. package/scripts/utils/docs/with-lock.md +25 -0
  107. package/scripts/utils/docs/worktree-fingerprint.md +27 -0
  108. package/skills/docgen/js/docgen-batch.mjs +95 -0
  109. package/skills/docgen/js/docgen-extract.mjs +33 -18
  110. package/skills/docgen/js/docgen-gen.mjs +140 -154
  111. package/skills/docgen/js/docgen-ignore.mjs +1 -6
  112. package/skills/docgen/js/docgen-prompts.mjs +33 -22
  113. package/skills/docgen/js/docgen-scan.mjs +1 -8
  114. package/skills/docgen/js/docs/docgen-extract.md +28 -0
  115. package/skills/docgen/js/docs/docgen-gen.md +41 -0
  116. package/skills/docgen/js/docs/docgen-ignore.md +24 -0
  117. package/skills/docgen/js/docs/docgen-prompts.md +24 -0
  118. package/skills/docgen/js/docs/docgen-scan.md +48 -0
  119. package/skills/fix/js/docs/llm-worker.md +27 -0
  120. package/skills/fix/js/docs/orchestrator.md +32 -0
  121. package/skills/fix/js/docs/t0.md +29 -0
  122. package/skills/fix/js/llm-worker.mjs +64 -29
  123. package/skills/fix/js/orchestrator.mjs +45 -54
  124. package/skills/fix/js/t0.mjs +16 -32
  125. package/skills/start-check/js/check.mjs +1 -16
  126. package/skills/start-check/js/docs/check.md +34 -0
  127. package/skills/taze/js/diff.mjs +1 -15
  128. package/skills/taze/js/docs/diff.md +33 -0
@@ -1,42 +1,45 @@
1
- /**
2
- * docgen-конвеєр (входна точка): код файлу → .md-документація.
3
- *
4
- * Інверсія керування: веде цей JS, а локальна модель — лише сервіс перефразування.
5
- * Stage 0 extractFacts — факти з коду (0 токенів)
6
- * Stage 1 sectionInstructions — точкові промпти на кожну секцію (спільний KV-cached префікс)
7
- * Stage 2 stripSignatures — детермінований зріз сигнатур (0 токенів)
8
- * Stage 2.5 scoreDoc — детермінований скоринг проти фактів (0 токенів)
9
- * Stage 3 assemble — фіксовані заголовки/порядок + зрізання fence
10
- * Tier 2 claudeOneShot — хмарний fallback якщо score < QUALITY_THRESHOLD
11
- *
12
- * Hybrid routing (sym-threshold):
13
- * sym < BORDERLINE_SYM_LOW → Tier 1 local (без хмарного рефері)
14
- * sym ∈ [BORDERLINE_SYM_LOW, sym<4) → Tier 1 + cloudScoreDoc (Haiku) → при низькому балі → Tier 2
15
- * sym >= DEFAULT_SYM_THRESHOLD → одразу Tier 2 (pre-routing, без local)
16
- */
1
+ /** @see ./docs/docgen-gen.md */
17
2
  import { readFileSync } from 'node:fs'
18
3
  import { basename } from 'node:path'
19
4
  import { request } from 'node:http'
5
+ import { spawnSync } from 'node:child_process'
20
6
  import { env } from 'node:process'
21
- import Anthropic from '@anthropic-ai/sdk'
7
+ import { LOCAL_MIN, resolveModel } from '../../../lib/models.mjs'
22
8
  import { extractFacts } from './docgen-extract.mjs'
23
9
  import { sectionMessages, oneShotMessages, STYLE, oneShotPromptText } from './docgen-prompts.mjs'
24
10
 
11
+ /** Strips provider prefix from tier string for direct ollama HTTP (ollama/gemma3:4b → gemma3:4b). */
12
+ function localModelId(tier) {
13
+ if (!tier) return 'gemma3:4b'
14
+ const i = tier.indexOf('/')
15
+ return i === -1 ? tier : tier.slice(i + 1)
16
+ }
17
+
25
18
  const QUALITY_THRESHOLD = 70
26
19
 
27
20
  /** Один виклик чату до ollama зі streaming (токени стримуються → socket активний, жодного timeout). */
28
21
  async function ollamaChat(messages, { model, numPredict = 600 }) {
29
22
  const body = JSON.stringify({
30
- model, messages, stream: true, think: false,
23
+ model,
24
+ messages,
25
+ stream: true,
26
+ think: false,
31
27
  options: { num_ctx: 8192, temperature: 0.2, num_predict: numPredict },
32
28
  keep_alive: '15m'
33
29
  })
34
30
  return new Promise((resolve, reject) => {
35
31
  const req = request(
36
- { hostname: 'localhost', port: 11434, path: '/api/chat', method: 'POST',
37
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
38
- (res) => {
39
- let text = '', genTok = 0, buf = ''
32
+ {
33
+ hostname: 'localhost',
34
+ port: 11434,
35
+ path: '/api/chat',
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
38
+ },
39
+ res => {
40
+ let text = '',
41
+ genTok = 0,
42
+ buf = ''
40
43
  res.on('data', chunk => {
41
44
  buf += chunk.toString()
42
45
  const lines = buf.split('\n')
@@ -47,7 +50,9 @@ async function ollamaChat(messages, { model, numPredict = 600 }) {
47
50
  const j = JSON.parse(line)
48
51
  text += j.message?.content ?? ''
49
52
  if (j.done) genTok = j.eval_count ?? 0
50
- } catch { /* partial line */ }
53
+ } catch {
54
+ /* partial line */
55
+ }
51
56
  }
52
57
  })
53
58
  res.on('end', () => resolve({ text, genTok }))
@@ -64,7 +69,10 @@ async function ollamaChat(messages, { model, numPredict = 600 }) {
64
69
  function stripSection(text) {
65
70
  let t = text.trim()
66
71
  if (t.startsWith('```')) {
67
- t = t.replace(/^```[a-z]*\n?/, '').replace(/\n?```\s*$/, '').trim()
72
+ t = t
73
+ .replace(/^```[a-z]*\n?/, '')
74
+ .replace(/\n?```\s*$/, '')
75
+ .trim()
68
76
  }
69
77
  t = t.replace(/^#{1,6}\s+.*\n+/, '') // зрізати випадковий заголовок
70
78
  return t.trim()
@@ -87,8 +95,10 @@ function parseSections(md) {
87
95
  let cur = null
88
96
  for (const line of md.split('\n')) {
89
97
  const m = line.match(/^##\s+(.+)/)
90
- if (m) { cur = m[1].toLowerCase().replace(/[^а-яіїєґa-z0-9]/gi, ''); result[cur] = '' }
91
- else if (cur) result[cur] += line + '\n'
98
+ if (m) {
99
+ cur = m[1].toLowerCase().replace(/[^а-яіїєґa-z0-9]/gi, '')
100
+ result[cur] = ''
101
+ } else if (cur) result[cur] += line + '\n'
92
102
  }
93
103
  return result
94
104
  }
@@ -102,93 +112,53 @@ function scoreDoc(md, facts) {
102
112
  let score = 100
103
113
  const issues = []
104
114
 
105
- if (!s['огляд'])
106
- { score -= 25; issues.push('no-overview') }
115
+ if (!s['огляд']) {
116
+ score -= 25
117
+ issues.push('no-overview')
118
+ }
107
119
 
108
120
  const behavior = s['поведінка'] ?? ''
109
- if (behavior.length < 60)
110
- { score -= 20; issues.push('short-behavior') }
121
+ if (behavior.length < 60) {
122
+ score -= 20
123
+ issues.push('short-behavior')
124
+ }
111
125
 
112
126
  const guarantees = s['гарантіїповедінки'] ?? ''
113
127
  // Будь-яка згадка "кеш" у Гарантіях коли файл не кешує — галюцинація
114
128
  // Негація: "не кешує", "не має кешування", "без кешування", "немає кешу"
115
129
  const cacheHit = /кеш/i.test(guarantees) && !/(?:не|без)\s+(?:\S+\s+)?кеш|немає\s+кеш/i.test(guarantees)
116
- if (!facts.markers?.caches && cacheHit)
117
- { score -= 20; issues.push('cache-hallucination') }
130
+ if (!facts.markers?.caches && cacheHit) {
131
+ score -= 20
132
+ issues.push('cache-hallucination')
133
+ }
118
134
 
119
135
  // Перевіряємо лише бектік-обгорнуті імена (`sym`) — уникаємо substring false positives
120
136
  const hasName = (text, sym) => text.includes('`' + sym + '`')
121
137
  for (const sym of facts.internalSymbols ?? []) {
122
138
  const inDoc = hasName(guarantees, sym) || hasName(s['огляд'] ?? '', sym) || hasName(s['поведінка'] ?? '', sym)
123
- if (inDoc) { score -= 10; issues.push(`internal-name:${sym}`) }
139
+ if (inDoc) {
140
+ score -= 10
141
+ issues.push(`internal-name:${sym}`)
142
+ }
124
143
  }
125
144
 
126
145
  return { score: Math.max(0, score), issues }
127
146
  }
128
147
 
129
- const SCORE_RUBRIC = `Оціни якість документації для JavaScript-модуля за 4 критеріями (1-3 кожен):
130
-
131
- - огляд: 3=описує роль модуля в системі (ЩО і НАВІЩО); 2=частково розмитий; 1=відсутній або перераховує функції
132
- - поведінка: 3=бізнес-терміни, без деталей реалізації; 2=деякі impl-деталі; 1=переважно реалізація або відсутня
133
- - гарантії: 3=лише реальні інваріанти підтверджені кодом, без галюцинацій; 2=частково правильні; 1=вигадані або відсутні
134
- - стиль: 3=без сигнатур/internal-імен, правильна markdown-структура; 2=дрібні порушення; 1=сигнатури/internal-імена/відсутні заголовки
135
-
136
- Відповідай ТІЛЬКИ JSON без пояснень:
137
- {"огляд":N,"поведінка":N,"гарантії":N,"стиль":N,"issues":["коротко про кожен мінус 1-5 слів"]}`
138
-
139
- /**
140
- * Stage 2.5 cloud: Claude Haiku оцінює якість доку проти коду + фактів.
141
- * Використовує найдешевшу хмарну модель — haiku — для мінімальної вартості судді.
142
- * @returns {{ score: number, scores: object, issues: string[], tok: number }}
143
- */
144
- async function cloudScoreDoc(md, facts, src, model = 'claude-haiku-4-5-20251001') {
145
- const client = new Anthropic()
146
- const factsTxt = [
147
- facts.exports?.length ? `Публічні функції: ${facts.exports.map(e => e.name).join(', ')}` : '',
148
- facts.internalSymbols?.length ? `Внутрішні (не публічні): ${facts.internalSymbols.join(', ')}` : '',
149
- facts.markers?.caches ? 'Кешування: є' : 'Кешування: немає',
150
- facts.markers?.network ? 'Мережа: є' : 'Мережа: немає',
151
- facts.markers?.readOnly ? 'Read-only (не змінює файли/стан)' : ''
152
- ].filter(Boolean).join('\n')
153
-
154
- const msg = await client.messages.create({
155
- model,
156
- max_tokens: 256,
157
- system: SCORE_RUBRIC,
158
- messages: [{
159
- role: 'user',
160
- content: [
161
- { type: 'text', text: `ФАКТИ:\n${factsTxt}`, cache_control: { type: 'ephemeral' } },
162
- { type: 'text', text: `КОД:\n\`\`\`\n${src.slice(0, 4000)}\n\`\`\``, cache_control: { type: 'ephemeral' } },
163
- { type: 'text', text: `ДОКУМЕНТАЦІЯ:\n${md}` }
164
- ]
165
- }]
166
- })
167
- const tok = (msg.usage?.input_tokens ?? 0) + (msg.usage?.output_tokens ?? 0)
168
- try {
169
- const j = JSON.parse(msg.content[0]?.text ?? '{}')
170
- const total = ((j.огляд ?? 0) + (j.поведінка ?? 0) + (j.гарантії ?? 0) + (j.стиль ?? 0)) / 12 * 100
171
- return { score: Math.round(total), scores: j, issues: j.issues ?? [], tok }
172
- } catch {
173
- return { score: 50, scores: {}, issues: ['parse-error'], tok }
174
- }
175
- }
176
-
177
- /** Tier 2: хмарний fallback через Claude коли local-score < QUALITY_THRESHOLD. */
178
- async function claudeOneShot(facts, src, model = 'claude-sonnet-4-6') {
179
- const client = new Anthropic()
180
- const prompt = oneShotPromptText(facts, src)
181
- const msg = await client.messages.create({
182
- model,
183
- max_tokens: 1500,
184
- system: STYLE,
185
- messages: [{ role: 'user', content: prompt }]
148
+ /** Tier 2: виклик через pi (провайдер-нейтрально). model рядок `provider/model-id`. */
149
+ function piOneShot(facts, src, model) {
150
+ const fullPrompt = `${STYLE}\n\n${oneShotPromptText(facts, src)}`
151
+ const modelArgs = model ? ['--model', model] : []
152
+ const r = spawnSync('pi', ['-p', fullPrompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {
153
+ encoding: 'utf8',
154
+ timeout: 120_000
186
155
  })
187
- const text = msg.content[0]?.text ?? ''
188
- const genTok = msg.usage?.output_tokens ?? 0
156
+ if (r.error) throw new Error(`pi Tier 2 error: ${r.error.message}`)
157
+ if (r.status !== 0) throw new Error(`pi Tier 2 exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
158
+ const text = r.stdout?.trim() ?? ''
189
159
  let md = stripSignatures(stripSection(text))
190
160
  if (!md.startsWith('#')) md = `# ${basename(facts.relPath)}\n\n${md}`
191
- return { md: md + '\n', genTok }
161
+ return { md: md + '\n', genTok: 0 }
192
162
  }
193
163
 
194
164
  /** Stage 3: фіксовані заголовки у фіксованому порядку. */
@@ -229,106 +199,122 @@ async function generateOneShot(facts, src, model) {
229
199
 
230
200
  /** Файли з sym ≥ цього значення одразу йдуть у Tier 2 (без локального проходу). */
231
201
  const DEFAULT_SYM_THRESHOLD = 4
232
- /** Файли з sym цього значення отримують хмарного рефері (Haiku) після локального проходу. */
233
- const BORDERLINE_SYM_LOW = 2
202
+ /** Максимальний час локальної генерації на один файл перед ескалацією у Tier 2. */
203
+ const LOCAL_TIMEOUT_MS = 5 * 60 * 1000
204
+ /** Дефолтна Tier 1 модель: N_CURSOR_DOCGEN_MODEL → LOCAL_MIN → ollama gemma3:4b. */
205
+ const DEFAULT_LOCAL_MODEL = localModelId(env.N_CURSOR_DOCGEN_MODEL ?? LOCAL_MIN)
206
+ /** Дефолтна Tier 2 модель (provider/model-id для pi): N_CURSOR_DOCGEN_CLOUD_MODEL → resolveModel('avg'). */
207
+ const DEFAULT_CLOUD_MODEL = env.N_CURSOR_DOCGEN_CLOUD_MODEL ?? resolveModel('avg')
208
+
209
+ /** Повертає promise, що відхиляється через `ms` мс з повідомленням про timeout. */
210
+ function withTimeout(promise, ms) {
211
+ return Promise.race([
212
+ promise,
213
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`local timeout after ${ms / 1000}s`)), ms))
214
+ ])
215
+ }
234
216
 
235
217
  /**
236
218
  * Головний API: файл → { md, genTok, ms, score, issues, tier }.
237
219
  *
238
220
  * Routing (sym-threshold):
239
- * sym < BORDERLINE_SYM_LOW → Tier 1 (без хмарного рефері)
240
- * sym [BORDERLINE_SYM_LOW, symThreshold) → Tier 1 + cloudScoreDoc (Haiku) як рефері
241
- * sym >= symThreshold → Pre-routing одразу Tier 2
242
- * scoreCloud=true → примусово запускає cloudScoreDoc для всіх Tier 1
221
+ * sym < symThreshold → Tier 1 local (timeout: LOCAL_TIMEOUT_MS) + det-scorer
222
+ * timeout або det-score < threshold Tier 2
223
+ * sym >= symThreshold → Pre-routing одразу Tier 2
243
224
  *
244
- * @param {string} scoreModel — модель для хмарного рефері (Haiku за замовч.)
245
225
  * @param {string} cloudModel — модель для Tier 2 генерації (Sonnet за замовч.)
246
- * @param {boolean} scoreCloud — якщо true, cloudScoreDoc запускається для всіх Tier 1 файлів
247
226
  */
248
- export async function generateDoc(file, {
249
- model = 'gemma3:4b',
250
- mode = 'orchestrated',
251
- scoreModel = 'claude-haiku-4-5-20251001',
252
- cloudModel = 'claude-sonnet-4-6',
253
- threshold = QUALITY_THRESHOLD,
254
- scoreCloud = false,
255
- symThreshold = DEFAULT_SYM_THRESHOLD
256
- } = {}) {
227
+ export async function generateDoc(
228
+ file,
229
+ {
230
+ model = DEFAULT_LOCAL_MODEL,
231
+ mode = 'orchestrated',
232
+ cloudModel = DEFAULT_CLOUD_MODEL,
233
+ threshold = QUALITY_THRESHOLD,
234
+ symThreshold = DEFAULT_SYM_THRESHOLD
235
+ } = {}
236
+ ) {
257
237
  const src = readFileSync(file, 'utf8')
258
238
  const facts = extractFacts(src, file)
259
239
  const t0 = Date.now()
260
240
 
261
241
  // Pre-routing: складні файли (sym ≥ symThreshold) → одразу Tier 2, не витрачаємо local-час
262
242
  const complexity = facts.internalSymbols?.length ?? 0
263
- if (complexity >= symThreshold && env.ANTHROPIC_API_KEY) {
264
- const r2 = await claudeOneShot(facts, src, cloudModel)
265
- return { ...r2, ms: Date.now() - t0, score: null, issues: [`pre-routed:sym=${complexity}`], tier: 2 }
243
+ if (complexity >= symThreshold && cloudModel) {
244
+ const r2 = piOneShot(facts, src, cloudModel)
245
+ return {
246
+ ...r2,
247
+ ms: Date.now() - t0,
248
+ score: null,
249
+ issues: [`pre-routed:sym=${complexity}`],
250
+ tier: 2,
251
+ model: cloudModel
252
+ }
266
253
  }
267
254
 
268
- let r = facts.unsupported
269
- ? await generateOneShot(facts, src, model)
270
- : mode === 'oneshot'
271
- ? await generateOneShot(facts, src, model)
272
- : await generateOrchestrated(facts, src, model)
273
-
274
- // Stage 2.5a: детермінований скоринг (0 токенів)
275
- const { score: detScore, issues: detIssues } = scoreDoc(r.md, facts)
276
-
277
- // Stage 2.5b: cloudScoreDoc (Haiku) як рефері для borderline-файлів або при scoreCloud=true
278
- const isBorderline = complexity >= BORDERLINE_SYM_LOW && complexity < symThreshold
279
- if ((isBorderline || scoreCloud) && env.ANTHROPIC_API_KEY) {
280
- const cs = await cloudScoreDoc(r.md, facts, src, scoreModel)
281
- if (cs.score < threshold) {
282
- const r2 = await claudeOneShot(facts, src, cloudModel)
283
- return { ...r2, ms: Date.now() - t0, score: cs.score, cloudScores: cs.scores,
284
- issues: cs.issues, detScore, detIssues, tier: 2 }
255
+ // Tier 1: локальна генерація з timeout 5 хв — при перевищенні одразу Tier 2
256
+ let r
257
+ try {
258
+ const localPromise =
259
+ facts.unsupported || mode === 'oneshot'
260
+ ? generateOneShot(facts, src, model)
261
+ : generateOrchestrated(facts, src, model)
262
+ r = await withTimeout(localPromise, LOCAL_TIMEOUT_MS)
263
+ } catch (e) {
264
+ if (cloudModel) {
265
+ const r2 = piOneShot(facts, src, cloudModel)
266
+ return {
267
+ ...r2,
268
+ ms: Date.now() - t0,
269
+ score: null,
270
+ issues: [`local-timeout: ${e.message}`],
271
+ tier: 2,
272
+ model: cloudModel
273
+ }
285
274
  }
286
- return { ...r, ms: Date.now() - t0, score: cs.score, cloudScores: cs.scores,
287
- issues: cs.issues, detScore, detIssues, tier: 1 }
275
+ throw e
288
276
  }
289
277
 
290
- // Детермінований fallback (без хмарного рефері)
291
- if (detScore < threshold && env.ANTHROPIC_API_KEY) {
292
- const r2 = await claudeOneShot(facts, src, cloudModel)
293
- return { ...r2, ms: Date.now() - t0, score: detScore, issues: detIssues, tier: 2 }
278
+ // Stage 2.5: детермінований скоринг (0 токенів) — gate перед Tier 2
279
+ const { score: detScore, issues: detIssues } = scoreDoc(r.md, facts)
280
+
281
+ if (detScore < threshold && cloudModel) {
282
+ const r2 = piOneShot(facts, src, cloudModel)
283
+ return { ...r2, ms: Date.now() - t0, score: detScore, issues: detIssues, tier: 2, model: cloudModel }
294
284
  }
295
285
 
296
- return { ...r, ms: Date.now() - t0, score: detScore, issues: detIssues, tier: 1 }
286
+ return { ...r, ms: Date.now() - t0, score: detScore, issues: detIssues, tier: 1, model }
297
287
  }
298
288
 
299
- // CLI: node docgen-gen.mjs <file> [--oneshot] [--score-cloud] [--model <m>] [--score-model <m>] [--sym-threshold N] [--tier-only]
289
+ // CLI: node docgen-gen.mjs <file> [--oneshot] [--model <m>] [--sym-threshold N] [--tier-only]
300
290
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
301
291
  if (isRunAsCli(import.meta.url)) {
302
292
  const args = process.argv.slice(2)
303
293
  const file = args.find(a => !a.startsWith('--'))
304
294
  const mode = args.includes('--oneshot') ? 'oneshot' : 'orchestrated'
305
- const scoreCloud = args.includes('--score-cloud')
306
295
  const tierOnly = args.includes('--tier-only')
307
- const mi = args.indexOf('--model'); const model = mi >= 0 ? args[mi + 1] : 'gemma3:4b'
308
- const smi = args.indexOf('--score-model'); const scoreModel = smi >= 0 ? args[smi + 1] : 'claude-haiku-4-5-20251001'
309
- const si = args.indexOf('--sym-threshold'); const symThreshold = si >= 0 ? Number(args[si + 1]) : DEFAULT_SYM_THRESHOLD
296
+ const mi = args.indexOf('--model')
297
+ const model = mi >= 0 ? args[mi + 1] : DEFAULT_LOCAL_MODEL
298
+ const si = args.indexOf('--sym-threshold')
299
+ const symThreshold = si >= 0 ? Number(args[si + 1]) : DEFAULT_SYM_THRESHOLD
310
300
  if (!file) {
311
- console.error('Usage: node docgen-gen.mjs <file> [--oneshot] [--score-cloud] [--model <m>] [--score-model <m>] [--sym-threshold N] [--tier-only]')
301
+ console.error('Usage: node docgen-gen.mjs <file> [--oneshot] [--model <m>] [--sym-threshold N] [--tier-only]')
312
302
  process.exit(1)
313
303
  }
314
304
  if (tierOnly) {
315
305
  const src = readFileSync(file, 'utf8')
316
306
  const facts = extractFacts(src, file)
317
307
  const sym = facts.internalSymbols?.length ?? 0
318
- let label, icon
319
- if (sym >= symThreshold) {
320
- icon = '☁️ '; label = `Tier 2 cloud (sym=${sym} ${symThreshold}, pre-routed)`
321
- } else if (sym >= BORDERLINE_SYM_LOW) {
322
- icon = '🔀'; label = `Tier 1+judge (sym=${sym} [${BORDERLINE_SYM_LOW},${symThreshold}), Haiku рефері)`
323
- } else {
324
- icon = '💻'; label = `Tier 1 local (sym=${sym} < ${BORDERLINE_SYM_LOW})`
325
- }
308
+ const icon = sym >= symThreshold ? '☁️ ' : '💻'
309
+ const label =
310
+ sym >= symThreshold
311
+ ? `Tier 2 cloud (sym=${sym} ${symThreshold}, pre-routed)`
312
+ : `Tier 1 local (sym=${sym} < ${symThreshold})`
326
313
  process.stdout.write(`${icon} ${label} | ${file}\n`)
327
314
  process.exit(0)
328
315
  }
329
- const r = await generateDoc(file, { model, mode, scoreCloud, scoreModel, symThreshold })
316
+ const r = await generateDoc(file, { model, mode, symThreshold })
330
317
  const issuesTxt = r.issues?.length ? ` issues=${r.issues.join(',')}` : ''
331
- const cloudTxt = r.cloudScores ? ` cloud-scores=${JSON.stringify(r.cloudScores)}` : ''
332
- process.stderr.write(`[tier${r.tier} ${mode}] ${r.ms}ms / ${r.genTok} tok / score=${r.score}${issuesTxt}${cloudTxt}\n`)
318
+ process.stderr.write(`[tier${r.tier} ${mode}] ${r.ms}ms / ${r.genTok} tok / score=${r.score}${issuesTxt}\n`)
333
319
  process.stdout.write(r.md)
334
320
  }
@@ -1,9 +1,4 @@
1
- /**
2
- * Глоби, які `docgen` завжди ігнорує.
3
- *
4
- * Це окремий snippet-модуль: список правиться тут, scanner лише читає його
5
- * через predicate. Патерни пишуться в posix-формі відносно кореня проєкту.
6
- */
1
+ /** @see ./docs/docgen-ignore.md */
7
2
  import picomatch from 'picomatch'
8
3
 
9
4
  /** Базовий список glob-ів для `docgen` ignore. */
@@ -1,10 +1,4 @@
1
- /**
2
- * Stage 1 docgen-конвеєра: факт-лист + код → точкові промпти на кожну секцію.
3
- *
4
- * v2 — СЕКЦІЙНО-МІНІМАЛЬНИЙ контекст: код іде ЛИШЕ у `Поведінку`. `Огляд` бере тільки
5
- * header, `API` — лише список експортів, `Гарантії` — лише markers. Так інгест коду
6
- * оплачується один раз (а не на кожну секцію), і оркестрація перестає програвати в часі.
7
- */
1
+ /** @see ./docs/docgen-prompts.md */
8
2
 
9
3
  export const STYLE = [
10
4
  'Ти технічний письменник. Пишеш лаконічну ПОВЕДІНКОВУ документацію до коду українською, чистим Markdown.',
@@ -28,7 +22,10 @@ function factsSummary(facts) {
28
22
  return lines.join('\n')
29
23
  }
30
24
 
31
- const msgs = (system, user) => [{ role: 'system', content: system }, { role: 'user', content: user }]
25
+ const msgs = (system, user) => [
26
+ { role: 'system', content: system },
27
+ { role: 'user', content: user }
28
+ ]
32
29
 
33
30
  /**
34
31
  * Секційні набори messages з МІНІМАЛЬНИМ контекстом під кожну секцію.
@@ -42,33 +39,45 @@ export function sectionMessages(facts, src) {
42
39
 
43
40
  // Огляд — лише факти (без коду)
44
41
  out.push({
45
- key: 'overview', numPredict: 220,
46
- messages: msgs(`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
47
- 'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій.')
42
+ key: 'overview',
43
+ numPredict: 220,
44
+ messages: msgs(
45
+ `${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
46
+ 'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій.'
47
+ )
48
48
  })
49
49
 
50
50
  // Поведінка — ЄДИНА секція, якій потрібен код
51
51
  out.push({
52
- key: 'behavior', numPredict: 500,
53
- messages: msgs(`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
54
- `Напиши вміст секції «Поведінка»: ${multi ? 'для кожної публічної функції — один короткий пункт «що вона робить»' : 'нумерований алгоритм у бізнес-термінах'}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${facts.internalSymbols?.length ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.` : ''} Без заголовка.`)
52
+ key: 'behavior',
53
+ numPredict: 500,
54
+ messages: msgs(
55
+ `${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
56
+ `Напиши вміст секції «Поведінка»: ${multi ? 'для кожної публічної функції — один короткий пункт «що вона робить»' : 'нумерований алгоритм у бізнес-термінах'}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${facts.internalSymbols?.length ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.` : ''} Без заголовка.`
57
+ )
55
58
  })
56
59
 
57
60
  // API — лише список експортів (без коду)
58
61
  if (multi || facts.exports?.some(e => e.desc)) {
59
62
  const list = facts.exports.map(e => `- ${e.name}: ${e.desc || '(сформулюй стисло з наміру файлу)'}`).join('\n')
60
63
  out.push({
61
- key: 'api', numPredict: 320,
62
- messages: msgs(STYLE,
63
- `Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка.`)
64
+ key: 'api',
65
+ numPredict: 320,
66
+ messages: msgs(
67
+ STYLE,
68
+ `Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка.`
69
+ )
64
70
  })
65
71
  }
66
72
 
67
73
  // Гарантії — лише markers (без коду)
68
74
  out.push({
69
- key: 'guarantees', numPredict: 300,
70
- messages: msgs(`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
71
- 'Напиши вміст секції «Гарантії поведінки» як маркери-інваріанти СУВОРО на основі ВІДОМИХ ФАКТІВ (read-only, fail-safe, пропуски). Згадуй кеш ЛИШЕ якщо у фактах прямо є «Кешує». Без сигнатур у дужках і без імен внутрішніх структур/Map-ів/кешів. Не вигадуй гарантій, яких немає у фактах. Без заголовка.')
75
+ key: 'guarantees',
76
+ numPredict: 300,
77
+ messages: msgs(
78
+ `${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
79
+ 'Напиши вміст секції «Гарантії поведінки» як маркери-інваріанти СУВОРО на основі ВІДОМИХ ФАКТІВ (read-only, fail-safe, пропуски). Згадуй кеш ЛИШЕ якщо у фактах прямо є «Кешує». Без сигнатур у дужках і без імен внутрішніх структур/Map-ів/кешів. Не вигадуй гарантій, яких немає у фактах. Без заголовка.'
80
+ )
72
81
  })
73
82
 
74
83
  return out
@@ -77,8 +86,10 @@ export function sectionMessages(facts, src) {
77
86
  /** One-shot messages (база для порівняння). */
78
87
  export function oneShotMessages(facts, src) {
79
88
  const multi = (facts.exports?.length || 0) > 1
80
- return msgs(STYLE,
81
- `Напиши документацію для файлу. Секції: ## Огляд (1-3 речення), ## Поведінка (нумерований/маркований алгоритм), ${multi ? '## Публічний API (назва + що робить), ' : ''}## Гарантії поведінки.\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\``)
89
+ return msgs(
90
+ STYLE,
91
+ `Напиши документацію для файлу. Секції: ## Огляд (1-3 речення), ## Поведінка (нумерований/маркований алгоритм), ${multi ? '## Публічний API (назва + що робить), ' : ''}## Гарантії поведінки.\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\``
92
+ )
82
93
  }
83
94
 
84
95
  /** Лише текст user-промпту для one-shot (для хмарного fallback через Anthropic SDK). */
@@ -1,11 +1,4 @@
1
- /**
2
- * docgen scanner — детермінований обхід проєкту для скілу `docgen`.
3
- *
4
- * Друкує JSON-список кодових файлів із відносними `sourcePath`/`docPath`
5
- * (тека `docs/` поряд із джерелом). Рішення про overwrite/skip приймає скіл —
6
- * scanner лише лістить і ставить прапор `exists`. LLM/мережі тут немає: уся
7
- * генерація доки — у субагентах скілу.
8
- */
1
+ /** @see ./docs/docgen-scan.md */
9
2
  // eslint-disable-next-line unicorn/import-style
10
3
  import path from 'node:path'
11
4
  import { existsSync, readdirSync, statSync } from 'node:fs'
@@ -0,0 +1,28 @@
1
+ # docgen-extract.mjs
2
+
3
+ ## Огляд
4
+
5
+ Файл `extractFacts` витягує інформацію про поведінку коду, формуючи об'єкт "факт-лист". Цей факт-лист використовується Stage 1 docgen-конвеєра для створення точкових промптів. Функція є ключовою частиною детермінованого процесу генерації документації, виключаючи використання великих мовних моделей.
6
+
7
+ ## Поведінка
8
+
9
+ 1. **Обробка даних:** Система аналізує код, витягуючи інформацію про експортовані функції, імпорти та їхні описи.
10
+ 2. **Ідентифікація відхилень:** Система визначає, чи використовується код для виконання операцій, які можуть призвести до помилок, наприклад, запис у файли, створення директорій, видалення файлів, обробка мережевих запитів або використання try/catch блоків.
11
+ 3. **Виявлення пропущених шляхів:** Система виявляє, чи використовується код для обробки певних шляхів файлів, таких як `.github`, `.git`, `node_modules` або `base/`, `ua/`, `.firebase`.
12
+ 4. **Визначення кешування:** Система визначає, чи використовується код для кешування даних, наприклад, за допомогою Map або Cache.
13
+ 5. **Визначення відсутності обробки помилок:** Система визначає, чи використовується код для обробки помилок, наприклад, за допомогою try/catch блоків.
14
+ 6. **Визначення мережевих операцій:** Система визначає, чи використовується код для виконання мережевих операцій, наприклад, за допомогою fetch або axios.
15
+ 7. **Вихідні дані:** Система надає вихідні дані у вигляді об'єкта, що містить витягнуту інформацію про код, а також виявлені відхилення та пропущені шляхи.
16
+
17
+ ## Публічний API
18
+
19
+ extractFacts — Витягує факти з коду файлу, представляючи їх у вигляді списку.
20
+
21
+ ## Гарантії поведінки
22
+
23
+ - Екстрагує з коду список фактів.
24
+ - Повертає об'єкт з витягнутими фактами.
25
+ - Не викликає винятків при невдачі.
26
+ - Кешує результати для одного прогону.
27
+ - Не використовує мережу.
28
+ - Не обробляє файли/каталоги .github, .git, node_modules, base/, ua/, .firebase.
@@ -0,0 +1,41 @@
1
+ # docgen-gen.mjs
2
+
3
+ ## Огляд
4
+
5
+ Цей файл генерує документацію на основі коду, використовуючи конвеєр обробки. Він автоматично створює Markdown-файли, що містять опис коду, використовуючи декілька етапів, включаючи вилучення фактів та створення текстових описів. Ця функція є ключовою частиною системи документування, забезпечуючи автоматизований та послідовний процес створення документації.
6
+
7
+ ## Поведінка
8
+
9
+ 1. Модуль визначає основну функцію `generateDoc`, яка приймає код джерела та налаштовує параметри для генерації документації.
10
+ 2. Вхідний код обробляється для вилучення ключових фактів про функціональність модуля, включаючи назви функцій та їх призначення.
11
+ 3. Вилучені факти використовуються для оцінки якості документації, зокрема, перевіряється, чи опис відповідає реальній поведінці модуля.
12
+ 4. Оцінка якості здійснюється за допомогою декількох критеріїв: опис має бути чітким та описовим, а поведінка – узгодженою з кодом.
13
+ 5. Якщо оцінка якості низька, використовується хмарний сервіс Claude для перевірки документації та надання рекомендацій щодо покращення.
14
+ 6. У випадку, коли оцінка якості висока, генерується документ, який містить огляд модуля, його поведінку, API та гарантії поведінки.
15
+ 7. Документація форматується у Markdown, з використанням фіксованого набору заголовків та розділів.
16
+ 8. Під час генерації документації враховуються можливі пропуски в інформації про шляхи, що може вплинути на оцінку якості.
17
+ 9. Використовується хмарний сервіс Claude для оцінки якості документації та надання рекомендацій щодо покращення.
18
+ 10. Генерується документ, який містить огляд модуля, його поведінку, API та гарантії поведінки.
19
+
20
+ ## Публічний API
21
+
22
+ - generateDoc — Генерує документацію з файлу, включаючи метадані, токени, оцінки, проблеми та рівень.
23
+ - Routing (sym-threshold) — Розподіляє обробку на основі значення символу, визначаючи рівень (Tier) та тип рефері (Haiku або без хмарного рефері).
24
+ - sym < BORDERLINE_SYM_LOW — Tier 1 без хмарного рефері.
25
+ - sym ∈ [BORDERLINE_SYM_LOW, symThreshold) — Tier 1 з хмарним рефері (Haiku).
26
+ - sym >= symThreshold — Відразу Tier 2.
27
+ - scoreCloud=true — Автоматично запускає хмарний рефері для всіх Tier 1.
28
+
29
+ ## Гарантії поведінки
30
+
31
+ - Конвеєр завжди генерує .md-документацію на основі вхідного коду.
32
+ - Конвеєр використовує інверсію керування, де JS-код визначає весь процес.
33
+ - Stage 0 (extractFacts) не впливає на вихідну документацію.
34
+ - Stage 1 (sectionInstructions) генерує точкові промпти для кожної секції коду.
35
+ - Stage 2 (stripSignatures) завжди видаляє інформацію про сигнатури.
36
+ - Stage 2.5 (scoreDoc) оцінює документацію за фактами.
37
+ - Stage 3 (assemble) збирає документацію з фіксованими заголовками та порядком.
38
+ - При низькому балі скорингу (claudeOneShot) використовується хмарний сервіс для перефразування.
39
+ - При сим-значенні нижче BORDERLINE_SYM_LOW, документація генерується локально без використання хмарного рефері.
40
+ - При сим-значенні між BORDERLINE_SYM_LOW та DEFAULT_SYM_THRESHOLD, документація генерується локально та оцінюється за допомогою cloudScoreDoc (Haiku).
41
+ - При сим-значенні, що перевищує DEFAULT_SYM_THRESHOLD, документація генерується безпосередньо, без локальної оцінки
@@ -0,0 +1,24 @@
1
+ # docgen-ignore.mjs
2
+
3
+ ## Огляд
4
+
5
+ Цей файл містить список глобальних файлів, які `docgen` ігнорує під час генерації документації. Він використовується для визначення, які файли не потрібно аналізувати, забезпечуючи ефективність процесу генерації та запобігаючи включенню нерелевантної інформації. Цей список є основою для predicate, який scanner використовує для визначення, чи потрібно читати певний файл.
6
+
7
+ ## Поведінка
8
+
9
+ DOCGEN_IGNORE_GLOBS: визначає список глобів, які `docgen` ігнорує.
10
+ isDocgenIgnored: перевіряє, чи шлях має бути пропущений `docgen`, використовуючи глоби.
11
+
12
+ ## Публічний API
13
+
14
+ - DOCGEN_IGNORE_GLOBS — Список glob-ів, які ігнорує `docgen`.
15
+ - isDocgenIgnored — Перевіряє, чи потрібно пропустити шлях під час генерації документації.
16
+
17
+ ## Гарантії поведінки
18
+
19
+ - `DOCGEN_IGNORE_GLOBS` завжди ігнорує певний набір шляхів.
20
+ - `isDocgenIgnored` повертає `true` якщо шлях ігнорується.
21
+ - Шляхи `.git` та `node_modules` завжди ігноруються.
22
+ - Файл є read-only.
23
+ - У разі невдачі повертає `false` або `null`.
24
+ - Результат прогону кешується для подальшого використання.