@nitra/cursor 5.3.2 → 5.3.4

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 (61) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/docs/llm.md +23 -12
  3. package/package.json +1 -1
  4. package/rules/abie/fix.mjs +1 -2
  5. package/rules/adr/fix.mjs +1 -2
  6. package/rules/bun/fix.mjs +1 -2
  7. package/rules/capacitor/fix.mjs +1 -2
  8. package/rules/capacitor/js/platforms.mjs +0 -2
  9. package/rules/changelog/fix.mjs +1 -2
  10. package/rules/ci4/fix.mjs +1 -2
  11. package/rules/docker/fix.mjs +1 -2
  12. package/rules/efes/fix.mjs +1 -2
  13. package/rules/feedback/fix.mjs +1 -2
  14. package/rules/ga/fix.mjs +1 -2
  15. package/rules/graphql/fix.mjs +1 -2
  16. package/rules/hasura/fix.mjs +1 -2
  17. package/rules/image-avif/fix.mjs +1 -2
  18. package/rules/image-compress/fix.mjs +1 -2
  19. package/rules/js-bun-db/fix.mjs +1 -2
  20. package/rules/js-bun-redis/fix.mjs +1 -2
  21. package/rules/js-lint/fix.mjs +1 -2
  22. package/rules/js-lint-ci/fix.mjs +1 -2
  23. package/rules/js-mssql/fix.mjs +1 -2
  24. package/rules/js-run/fix.mjs +1 -2
  25. package/rules/k8s/fix.mjs +1 -2
  26. package/rules/k8s/js/manifests.mjs +1 -5
  27. package/rules/nginx-default-tpl/fix.mjs +1 -2
  28. package/rules/npm-module/fix.mjs +1 -2
  29. package/rules/npm-module/js/package_structure.mjs +0 -1
  30. package/rules/php/fix.mjs +1 -2
  31. package/rules/python/fix.mjs +1 -2
  32. package/rules/rego/fix.mjs +1 -2
  33. package/rules/release/fix.mjs +1 -2
  34. package/rules/rust/fix.mjs +1 -2
  35. package/rules/security/fix.mjs +1 -2
  36. package/rules/style-lint/fix.mjs +1 -2
  37. package/rules/tauri/fix.mjs +1 -2
  38. package/rules/test/coverage/coverage.mjs +0 -2
  39. package/rules/test/fix.mjs +1 -2
  40. package/rules/text/fix.mjs +1 -2
  41. package/rules/vue/fix.mjs +1 -2
  42. package/rules/worktree/fix.mjs +1 -2
  43. package/scripts/lib/run-rule.mjs +0 -2
  44. package/scripts/lint-cli.mjs +0 -1
  45. package/scripts/utils/with-lock.mjs +0 -1
  46. package/skills/doc-aggregate/js/docgen-scan.mjs +17 -18
  47. package/skills/doc-files/.changes/260612-0002.md +5 -0
  48. package/skills/doc-files/.changes/260612-0006.md +5 -0
  49. package/skills/doc-files/.changes/260612-0008.md +5 -0
  50. package/skills/doc-files/.changes/260612-0012.md +5 -0
  51. package/skills/doc-files/js/docgen-extract.mjs +136 -0
  52. package/skills/doc-files/js/docgen-prompts.mjs +2 -2
  53. package/skills/doc-files/js/docgen-scan.mjs +21 -22
  54. package/skills/doc-files/js/docs/docgen-extract-anchors.md +28 -10
  55. package/skills/doc-files/js/docs/docgen-extract.md +22 -12
  56. package/skills/doc-files/js/docs/docgen-files-batch.md +21 -11
  57. package/skills/doc-files/js/docs/docgen-gen.md +29 -13
  58. package/skills/doc-files/js/docs/docgen-ignore.md +37 -0
  59. package/skills/doc-files/js/docs/units-rs.md +35 -0
  60. package/skills/doc-files/js/units-rs.mjs +213 -0
  61. package/skills/doc-files/js/units.mjs +4 -3
@@ -1,6 +1,5 @@
1
1
  /** @see ./docs/docgen-scan.md */
2
- // eslint-disable-next-line unicorn/import-style
3
- import path from 'node:path'
2
+ import { join, relative, dirname, extname, sep, isAbsolute, resolve } from 'node:path'
4
3
  import { existsSync, readdirSync, statSync } from 'node:fs'
5
4
 
6
5
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
@@ -18,7 +17,7 @@ const TEST_FILE_RE = /\.(?:test|spec)\.[^.]+$/u
18
17
  * @returns {boolean} true — корінь system-wide docs
19
18
  */
20
19
  function isSystemWideDocsRoot(root) {
21
- return existsSync(path.join(root, 'docs', 'adr')) || existsSync(path.join(root, 'docs', 'explanation'))
20
+ return existsSync(join(root, 'docs', 'adr')) || existsSync(join(root, 'docs', 'explanation'))
22
21
  }
23
22
 
24
23
  /**
@@ -29,7 +28,7 @@ function isSystemWideDocsRoot(root) {
29
28
  export function isSourceFile(fileName) {
30
29
  if (fileName.endsWith('.d.ts')) return false
31
30
  if (TEST_FILE_RE.test(fileName)) return false
32
- return SOURCE_EXTENSIONS.has(path.extname(fileName))
31
+ return SOURCE_EXTENSIONS.has(extname(fileName))
33
32
  }
34
33
 
35
34
  /**
@@ -49,14 +48,14 @@ export function scanSourceFiles(root) {
49
48
  return
50
49
  }
51
50
  for (const entry of entries) {
52
- const fullPath = path.join(dir, entry.name)
53
- const relPath = path.relative(root, fullPath)
51
+ const fullPath = join(dir, entry.name)
52
+ const relPath = relative(root, fullPath)
54
53
  if (entry.isDirectory()) {
55
54
  if (isDocgenIgnored(relPath, 'dir')) continue
56
55
  walk(fullPath)
57
56
  } else if (entry.isFile() && isSourceFile(entry.name)) {
58
- if (isSystemWideDocsRoot(root) && path.dirname(relPath) === '.') continue
59
- const sourcePath = relPath.split(path.sep).join('/')
57
+ if (isSystemWideDocsRoot(root) && dirname(relPath) === '.') continue
58
+ const sourcePath = relPath.split(sep).join('/')
60
59
  if (isDocgenIgnored(sourcePath)) continue
61
60
  results.push(sourcePath)
62
61
  }
@@ -74,10 +73,10 @@ export function scanSourceFiles(root) {
74
73
  * @returns {string} slug: `npm/rules/adr` → `npm-rules-adr`, корінь → `root`
75
74
  */
76
75
  export function slugForModule(root, moduleRoot) {
77
- const rel = path.relative(root, moduleRoot)
76
+ const rel = relative(root, moduleRoot)
78
77
  if (rel === '') return 'root'
79
78
  return rel
80
- .split(path.sep)
79
+ .split(sep)
81
80
  .join('-')
82
81
  .replaceAll(/[^\w-]+/gu, '-')
83
82
  }
@@ -99,8 +98,8 @@ export function findModuleRoots(root) {
99
98
  return
100
99
  }
101
100
  for (const entry of entries) {
102
- const fullPath = path.join(dir, entry.name)
103
- const relPath = path.relative(root, fullPath)
101
+ const fullPath = join(dir, entry.name)
102
+ const relPath = relative(root, fullPath)
104
103
  if (entry.isDirectory()) {
105
104
  if (isDocgenIgnored(relPath, 'dir')) continue
106
105
  walk(fullPath)
@@ -123,8 +122,8 @@ export function findModuleRoots(root) {
123
122
  export function nearestModuleRoot(filePath, moduleRoots) {
124
123
  let best = null
125
124
  for (const moduleRoot of moduleRoots) {
126
- const rel = path.relative(moduleRoot, filePath)
127
- if (rel.startsWith('..') || path.isAbsolute(rel)) continue
125
+ const rel = relative(moduleRoot, filePath)
126
+ if (rel.startsWith('..') || isAbsolute(rel)) continue
128
127
  if (best === null || moduleRoot.length > best.length) best = moduleRoot
129
128
  }
130
129
  return best
@@ -141,7 +140,7 @@ export function scanForModules(root) {
141
140
  const moduleRoots = findModuleRoots(root)
142
141
  const byRoot = new Map()
143
142
  for (const sourcePath of files) {
144
- const moduleRoot = nearestModuleRoot(path.join(root, sourcePath), moduleRoots)
143
+ const moduleRoot = nearestModuleRoot(join(root, sourcePath), moduleRoots)
145
144
  if (moduleRoot === null) continue
146
145
  if (!byRoot.has(moduleRoot)) byRoot.set(moduleRoot, [])
147
146
  byRoot.get(moduleRoot).push(sourcePath)
@@ -151,10 +150,10 @@ export function scanForModules(root) {
151
150
  for (const moduleRoot of moduleRoots) {
152
151
  const members = byRoot.get(moduleRoot)
153
152
  if (!members || members.length === 0) continue
154
- const docPath = path.join(moduleRoot, 'docs', 'ARCHITECTURE.md')
153
+ const docPath = join(moduleRoot, 'docs', 'ARCHITECTURE.md')
155
154
  results.push({
156
155
  moduleRoot,
157
- relRoot: path.relative(root, moduleRoot) || '.',
156
+ relRoot: relative(root, moduleRoot) || '.',
158
157
  slug: slugForModule(root, moduleRoot),
159
158
  docPath,
160
159
  members: members.toSorted(),
@@ -171,7 +170,7 @@ export function scanForModules(root) {
171
170
  */
172
171
  export function resolveRoot(argv) {
173
172
  const i = argv.indexOf('--root')
174
- return i !== -1 && argv[i + 1] ? path.resolve(argv[i + 1]) : process.cwd()
173
+ return i !== -1 && argv[i + 1] ? resolve(argv[i + 1]) : process.cwd()
175
174
  }
176
175
 
177
176
  /**
@@ -0,0 +1,5 @@
1
+ ---
2
+ bump: minor
3
+ section: Added
4
+ ---
5
+ doc-files: підтримка Rust (.rs) — SOURCE_EXTENSIONS, target/-ignore, extractFactsRust, units-rs
@@ -0,0 +1,5 @@
1
+ ---
2
+ bump: patch
3
+ section: Fixed
4
+ ---
5
+ мовно-нейтральний текст returnsFalsyOnFail (false/null/Err); units-rs: тести + docs
@@ -0,0 +1,5 @@
1
+ ---
2
+ bump: patch
3
+ section: Added
4
+ ---
5
+ Rust .rs: тести units-rs, doc units-rs.md, мовно-нейтральний текст гарантії returnsFalsyOnFail
@@ -0,0 +1,5 @@
1
+ ---
2
+ bump: patch
3
+ section: Added
4
+ ---
5
+ Rust (.rs): тести extractFactsRust у docgen-extract.test.mjs — exports, tauri::command, struct/enum, markers, header, desc
@@ -211,6 +211,141 @@ function extractMarkers(src) {
211
211
  }
212
212
  }
213
213
 
214
+ // ── Rust-екстрактор ──────────────────────────────────────────────────────────
215
+
216
+ // Модульний //! doc-коментар (inner doc)
217
+ const RS_MODULE_DOC_RE = /^(?:[ \t]*\/\/![ \t]?(.*)\n)*/m
218
+
219
+ // pub fn / pub struct / pub enum / pub trait (та fn із exposure-атрибутом)
220
+ const RS_PUB_ITEM_RE = /^[ \t]*(pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/gm
221
+
222
+ // Exposure-атрибути (#[tauri::command] тощо)
223
+ const RS_EXPOSURE_ATTR_RE = /#\[(?:tauri::command|wasm_bindgen|uniffi::export|pyo3::pyfunction|napi)/gm
224
+
225
+ // /// line-doc перед елементом
226
+ const RS_LINE_DOC_RE = /(?:[ \t]*\/\/\/[ \t]?.*\n)*/
227
+
228
+ // use crate::module::{A, B} або use std::..;
229
+ const RS_USE_RE = /^[ \t]*use\s+([\w:]+(?:::\{[^}]+\})?(?:::\*)?(?:::\w+)?)\s*;/gm
230
+
231
+ // Файловий запис: fs::write / File::create / remove_file / create_dir / write_all
232
+ const RS_WRITE_RE = /fs::write|File::create|remove_file|create_dir|BufWriter::new|OpenOptions[^;]*\.write\s*\(\s*true/
233
+
234
+ // Обробка помилок (але не просто `?`)
235
+ const RS_CATCH_RE = /\.unwrap_or(?:_else|_default)?|if\s+let\s+Err\s*\(|match\s+\S+.*\{\s*[\s\S]*?Err\s*\(|\.map_err\s*\(|\.ok\s*\(\)/
236
+
237
+ // Функції, що повертають Result або Option
238
+ const RS_RESULT_RE = /->\s*(?:Result|Option)\s*</
239
+
240
+ // Мережа
241
+ const RS_NETWORK_RE = /reqwest|hyper::|TcpStream|UdpSocket|tokio::net/
242
+
243
+ // Кешування
244
+ const RS_CACHE_RE = /\bcache\b|\bCache\b|lazy_static!|OnceCell|OnceLock|DashMap/i
245
+
246
+ /**
247
+ * Видобуває `///` doc-рядки перед рядком `lineIdx` (назад через `#[...]` та пусті рядки).
248
+ * @param {string[]} lines рядки файлу
249
+ * @param {number} lineIdx індекс рядка декларації
250
+ * @returns {string} опис або ''
251
+ */
252
+ function rsDocBefore(lines, lineIdx) {
253
+ const doc = []
254
+ for (let i = lineIdx - 1; i >= 0; i--) {
255
+ const t = lines[i].trim()
256
+ if (t.startsWith('///')) doc.unshift(t.slice(3).trim())
257
+ else if (t.startsWith('#[') || t.startsWith('#![') || t === '') { /* skip */ }
258
+ else break
259
+ }
260
+ return doc.join(' ').trim()
261
+ }
262
+
263
+ /**
264
+ * Витягує факт-лист для `.rs` файлу.
265
+ * @param {string} src вміст файлу
266
+ * @param {string} relPath відносний шлях
267
+ * @returns {object} факт-лист без `unsupported`
268
+ */
269
+ function extractFactsRust(src, relPath) {
270
+ // header — //! module-level doc
271
+ const headerLines = []
272
+ for (const line of src.split('\n')) {
273
+ const t = line.trim()
274
+ if (t.startsWith('//!')) headerLines.push(t.slice(3).trim())
275
+ else if (t === '' || t.startsWith('//')) continue
276
+ else break
277
+ }
278
+ const header = headerLines.join(' ').trim()
279
+
280
+ // Exposure-атрибути: рядки, після яких fn стає фактично pub
281
+ const srcLines = src.split('\n')
282
+ const exposedLineSet = new Set()
283
+ for (const m of src.matchAll(RS_EXPOSURE_ATTR_RE)) {
284
+ // Знаходимо, який рядок містить цей атрибут
285
+ let pos = 0
286
+ for (let li = 0; li < srcLines.length; li++) {
287
+ if (pos + srcLines[li].length >= m.index) {
288
+ // Шукаємо наступний не-атрибутний рядок з fn
289
+ for (let nli = li + 1; nli < Math.min(li + 5, srcLines.length); nli++) {
290
+ const t = srcLines[nli].trim()
291
+ if (t.startsWith('#[') || t === '') continue
292
+ if (/^(?:pub\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+/.test(t)) exposedLineSet.add(nli)
293
+ break
294
+ }
295
+ break
296
+ }
297
+ pos += srcLines[li].length + 1
298
+ }
299
+ }
300
+
301
+ // exports — pub items + exposure-exposed fns
302
+ const exports = []
303
+ let lineOffset = 0
304
+ for (let li = 0; li < srcLines.length; li++) {
305
+ const line = srcLines[li]
306
+ const m = line.match(/^[ \t]*(pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/)
307
+ if (m) {
308
+ const isPub = Boolean(m[1]) || exposedLineSet.has(li)
309
+ if (isPub) {
310
+ const desc = rsDocBefore(srcLines, li)
311
+ exports.push({ name: m[3], kind: m[2], desc })
312
+ }
313
+ }
314
+ lineOffset += line.length + 1
315
+ }
316
+
317
+ // localSymbols — приватні fn (не pub і не exposed) — не документуємо як публічний API
318
+ const localSymbols = []
319
+ for (let li = 0; li < srcLines.length; li++) {
320
+ const line = srcLines[li]
321
+ const m = line.match(/^[ \t]*(?:async\s+)?(?:unsafe\s+)?fn\s+(\w+)/)
322
+ if (m && !exports.some(e => e.name === m[1])) localSymbols.push(m[1])
323
+ }
324
+
325
+ // imports — use-рядки, класифіковані на std / external / internal
326
+ const stdlib = new Set()
327
+ const external = new Set()
328
+ for (const m of src.matchAll(RS_USE_RE)) {
329
+ const path = m[1]
330
+ const root = path.split('::')[0]
331
+ if (root === 'std' || root === 'core' || root === 'alloc') stdlib.add(path)
332
+ else external.add(path)
333
+ }
334
+ const imports = { stdlib: [...stdlib], external: [...external], internal: [] }
335
+
336
+ // markers
337
+ const markers = {
338
+ readOnly: !RS_WRITE_RE.test(src),
339
+ catchesErrors: RS_CATCH_RE.test(src),
340
+ returnsFalsyOnFail: RS_RESULT_RE.test(src),
341
+ network: RS_NETWORK_RE.test(src),
342
+ caches: RS_CACHE_RE.test(src),
343
+ skips: []
344
+ }
345
+
346
+ return { relPath, lang: 'rs', header, exports, imports, internalSymbols: [], localSymbols, markers }
347
+ }
348
+
214
349
  /**
215
350
  * Головний екстрактор: код файлу → факт-лист.
216
351
  * @param {string} src вміст файлу
@@ -219,6 +354,7 @@ function extractMarkers(src) {
219
354
  */
220
355
  export function extractFacts(src, relPath) {
221
356
  const lang = relPath.split('.').pop()
357
+ if (lang === 'rs') return extractFactsRust(src, relPath)
222
358
  if (!['js', 'mjs', 'ts'].includes(lang)) {
223
359
  return { relPath, lang, unsupported: true, header: '', exports: [], imports: {}, markers: {} }
224
360
  }
@@ -32,7 +32,7 @@ function factsSummary(facts) {
32
32
  if (m.skips?.length) lines.push(`Свідомо пропускає шляхи: ${m.skips.join(', ')}`)
33
33
  lines.push(`Read-only: ${m.readOnly ? 'так' : 'ні'}`)
34
34
  if (m.catchesErrors) lines.push('Перехоплює помилки (fail-safe), не кидає винятків назовні')
35
- if (m.returnsFalsyOnFail) lines.push('За невдачі повертає false/null замість винятку')
35
+ if (m.returnsFalsyOnFail) lines.push('За невдачі повертає значення помилки (false/null/Err) замість винятку чи паніки')
36
36
  lines.push(m.caches ? 'Кешування: так, у межах прогону' : 'Кешування: НЕМАЄ — не згадуй кеш у гарантіях')
37
37
  if (m.network) lines.push('Звертається до мережі')
38
38
  else lines.push('Робота з мережею: немає')
@@ -195,7 +195,7 @@ export function guaranteesFromMarkers(facts) {
195
195
  const lines = []
196
196
  if (m.readOnly) lines.push('- Read-only: файл не виконує операцій запису у файлову систему.')
197
197
  if (m.catchesErrors) lines.push('- Перехоплює помилки і не пропускає винятків назовні (fail-safe).')
198
- if (m.returnsFalsyOnFail) lines.push('- За невдалої перевірки повертає `false`/`null` замість винятку.')
198
+ if (m.returnsFalsyOnFail) lines.push('- За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.')
199
199
  if (m.caches) lines.push('- Кешує результати в межах одного прогону.')
200
200
  if (m.skips?.length) {
201
201
  lines.push(`- Свідомо пропускає шляхи: ${m.skips.map(s => '`' + s + '`').join(', ')}.`)
@@ -1,6 +1,5 @@
1
1
  /** @see ./docs/docgen-scan.md */
2
- // eslint-disable-next-line unicorn/import-style
3
- import path from 'node:path'
2
+ import { join, dirname, basename, extname, relative, resolve, sep, isAbsolute, posix } from 'node:path'
4
3
  import { existsSync, readdirSync, statSync } from 'node:fs'
5
4
  import { execFileSync } from 'node:child_process'
6
5
  import { once } from 'node:events'
@@ -27,7 +26,7 @@ const DEFAULT_GATE_MAX = Number(env.N_CURSOR_DOC_FILES_GATE_MAX ?? 50) || 50
27
26
  * @returns {boolean} true — корінь system-wide docs
28
27
  */
29
28
  function isSystemWideDocsRoot(root) {
30
- return existsSync(path.join(root, 'docs', 'adr')) || existsSync(path.join(root, 'docs', 'explanation'))
29
+ return existsSync(join(root, 'docs', 'adr')) || existsSync(join(root, 'docs', 'explanation'))
31
30
  }
32
31
 
33
32
  /**
@@ -38,7 +37,7 @@ function isSystemWideDocsRoot(root) {
38
37
  export function isSourceFile(fileName) {
39
38
  if (fileName.endsWith('.d.ts')) return false
40
39
  if (TEST_FILE_RE.test(fileName)) return false
41
- return SOURCE_EXTENSIONS.has(path.extname(fileName))
40
+ return SOURCE_EXTENSIONS.has(extname(fileName))
42
41
  }
43
42
 
44
43
  /**
@@ -48,9 +47,9 @@ export function isSourceFile(fileName) {
48
47
  * @returns {string} шлях до `<dir>/docs/<stem>.md` у тому ж просторі шляхів
49
48
  */
50
49
  export function docPathForSource(sourcePath) {
51
- const dir = path.dirname(sourcePath)
52
- const stem = path.basename(sourcePath, path.extname(sourcePath))
53
- return path.join(dir, 'docs', `${stem}.md`)
50
+ const dir = dirname(sourcePath)
51
+ const stem = basename(sourcePath, extname(sourcePath))
52
+ return join(dir, 'docs', `${stem}.md`)
54
53
  }
55
54
 
56
55
  /**
@@ -61,9 +60,9 @@ export function docPathForSource(sourcePath) {
61
60
  * @returns {boolean} true — кандидат на доку
62
61
  */
63
62
  export function isDocCandidate(root, relPath) {
64
- const fileName = path.posix.basename(relPath)
63
+ const fileName = posix.basename(relPath)
65
64
  if (!isSourceFile(fileName)) return false
66
- if (isSystemWideDocsRoot(root) && path.posix.dirname(relPath) === '.') return false
65
+ if (isSystemWideDocsRoot(root) && posix.dirname(relPath) === '.') return false
67
66
  return !isDocgenIgnored(relPath)
68
67
  }
69
68
 
@@ -75,7 +74,7 @@ export function isDocCandidate(root, relPath) {
75
74
  */
76
75
  export function describeFile(root, sourcePath) {
77
76
  const docPath = docPathForSource(sourcePath)
78
- const { stale, reason } = staleness(path.join(root, sourcePath), path.join(root, docPath))
77
+ const { stale, reason } = staleness(join(root, sourcePath), join(root, docPath))
79
78
  return { sourcePath, docPath, stale, reason }
80
79
  }
81
80
 
@@ -97,14 +96,14 @@ export function scanForDocFiles(root) {
97
96
  return
98
97
  }
99
98
  for (const entry of entries) {
100
- const fullPath = path.join(dir, entry.name)
101
- const relPath = path.relative(root, fullPath)
99
+ const fullPath = join(dir, entry.name)
100
+ const relPath = relative(root, fullPath)
102
101
  if (entry.isDirectory()) {
103
102
  if (isDocgenIgnored(relPath, 'dir')) continue
104
103
  walk(fullPath)
105
104
  } else if (entry.isFile() && isSourceFile(entry.name)) {
106
- if (isSystemWideDocsRoot(root) && path.dirname(relPath) === '.') continue
107
- const sourcePath = relPath.split(path.sep).join('/')
105
+ if (isSystemWideDocsRoot(root) && dirname(relPath) === '.') continue
106
+ const sourcePath = relPath.split(sep).join('/')
108
107
  if (isDocgenIgnored(sourcePath)) continue
109
108
  results.push(describeFile(root, sourcePath))
110
109
  }
@@ -122,7 +121,7 @@ export function scanForDocFiles(root) {
122
121
  */
123
122
  export function resolveRoot(argv) {
124
123
  const i = argv.indexOf('--root')
125
- return i !== -1 && argv[i + 1] ? path.resolve(argv[i + 1]) : process.cwd()
124
+ return i !== -1 && argv[i + 1] ? resolve(argv[i + 1]) : process.cwd()
126
125
  }
127
126
 
128
127
  /**
@@ -190,7 +189,7 @@ function gitChangedSources(root) {
190
189
  return out
191
190
  .split('\n')
192
191
  .map(s => s.trim())
193
- .filter(rel => rel && isDocCandidate(root, rel) && existsSync(path.join(root, rel)))
192
+ .filter(rel => rel && isDocCandidate(root, rel) && existsSync(join(root, rel)))
194
193
  }
195
194
 
196
195
  /**
@@ -200,9 +199,9 @@ function gitChangedSources(root) {
200
199
  * @returns {string|null} posix-шлях від кореня
201
200
  */
202
201
  function toRelSource(root, candidate) {
203
- const rel = path.relative(root, path.resolve(root, candidate))
204
- if (rel.startsWith('..') || path.isAbsolute(rel)) return null
205
- return rel.split(path.sep).join('/')
202
+ const rel = relative(root, resolve(root, candidate))
203
+ if (rel.startsWith('..') || isAbsolute(rel)) return null
204
+ return rel.split(sep).join('/')
206
205
  }
207
206
 
208
207
  /**
@@ -216,7 +215,7 @@ function runDegradedReport(root) {
216
215
  const degraded = []
217
216
  for (const f of scanForDocFiles(root)) {
218
217
  if (f.stale) continue
219
- const { score, issues } = readDocQuality(path.join(root, f.docPath))
218
+ const { score, issues } = readDocQuality(join(root, f.docPath))
220
219
  if (score !== null && score < QUALITY_THRESHOLD) degraded.push({ ...f, score, issues })
221
220
  }
222
221
  if (degraded.length === 0) {
@@ -262,14 +261,14 @@ export async function runDocFilesCheckCli(argv) {
262
261
  if (hookMode) {
263
262
  const fp = extractHookFilePath(await readStdin())
264
263
  const rel = fp ? toRelSource(root, fp) : null
265
- sources = rel && isDocCandidate(root, rel) && existsSync(path.join(root, rel)) ? [rel] : []
264
+ sources = rel && isDocCandidate(root, rel) && existsSync(join(root, rel)) ? [rel] : []
266
265
  } else if (gitMode) {
267
266
  sources = gitChangedSources(root)
268
267
  } else {
269
268
  sources = argv
270
269
  .filter(a => !a.startsWith('--') && a !== argv[maxIdx + 1])
271
270
  .map(a => toRelSource(root, a))
272
- .filter(rel => rel && isDocCandidate(root, rel) && existsSync(path.join(root, rel)))
271
+ .filter(rel => rel && isDocCandidate(root, rel) && existsSync(join(root, rel)))
273
272
  }
274
273
 
275
274
  const stale = sources.map(src => describeFile(root, src)).filter(f => f.stale)
@@ -1,27 +1,45 @@
1
1
  ---
2
2
  docgen:
3
3
  source: npm/skills/doc-files/js/docgen-extract-anchors.mjs
4
- crc: e80e0827
4
+ crc: 49749e9c
5
+ score: 80
5
6
  ---
6
7
 
7
- # docgen-extract-anchors
8
+ # docgen-extract-anchors.mjs
8
9
 
9
10
  ## Огляд
10
11
 
11
- Детермінований витяг «анкорів» конкретних фрагментів коду, які модель зобов'язана згадати в документації, щоб не зісковзнути на generic-фрази. Анкори підставляються у промпти окремим блоком обов'язкового включення, а їх покриття перевіряється скорером.
12
+ Витягує анкори з вихідного коду файлу.
13
+
14
+ Витягує анкори з вихідного коду файлу.
15
+
16
+ Формує плоский список анкор-токенів для перевірки покриття.
17
+
18
+ Форматує анкори у компактний текст для system-промпта.
12
19
 
13
20
  ## Поведінка
14
21
 
15
- 1. З тексту джерела збираються п'ять категорій анкорів: усі URL; експортовані константи-рядки з непорожнім значенням; маркери повідомлень про помилки виду `(rule.mdc)`; посилання на json-конфіги проєкту; code-block-приклади з провідного коментаря файлу (де автор зазвичай показує контракт).
16
- 2. Кожна категорія дедуплікується зі збереженням порядку появи.
17
- 3. Для промпта анкори форматуються в компактний текстовий блок з інструкціями, де саме їх згадати; якщо анкорів немає взагалі — блок не додається, щоб не вводити модель в оману «обов'язковими» полями.
22
+ X
23
+ Витягує анкори з вихідного коду файлу
24
+
25
+ extractAnchors
26
+ Витягує анкори з вихідного коду файлу
27
+
28
+ anchorTokens
29
+ Формує плоский список анкор-токенів для перевірки покриття
30
+
31
+ anchorsToPrompt
32
+ Форматує анкори у компактний текст для system-промпта
18
33
 
19
34
  ## Публічний API
20
35
 
21
- - `extractAnchors` текст джерела категоризовані анкори.
22
- - `anchorsToPrompt` — анкори текстовий блок для system-промпта або порожній рядок.
36
+ Xвитягує анкори з вихідного коду файлу.
37
+ extractAnchorsвитягує анкори з вихідного коду файлу.
38
+ anchorTokens — плоский список анкор-токенів, які мають з'явитися в документі.
39
+ anchorsToPrompt — форматує анкори у компактний текст для system-промпта.
23
40
 
24
41
  ## Гарантії поведінки
25
42
 
26
- - Повністю детермінований і read-only; жодних LLM-викликів і мережі.
27
- - Працює по сирому тексту без AST: дешево і свідомо толерує надлишок (зайвий анкор менша проблема, ніж пропущений).
43
+ - Read-only: файл не виконує операцій запису у файлову систему.
44
+ - За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.
45
+ - Не звертається до мережі.
@@ -1,29 +1,39 @@
1
1
  ---
2
2
  docgen:
3
3
  source: npm/skills/doc-files/js/docgen-extract.mjs
4
- crc: 26bb2901
4
+ crc: 1592076b
5
+ score: 100
5
6
  ---
6
7
 
7
- # docgen-extract
8
+ # docgen-extract.mjs
8
9
 
9
10
  ## Огляд
10
11
 
11
- Детермінований екстрактор фактів про кодовий файл (нуль токенів): з тексту джерела збирається факт-лист, на якому конвеєр будує промпти, маркери поведінки й список заборонених до згадки внутрішніх імен.
12
+ Файл витягує факти про конфігурацію та структуру проекту. Визначає мову та заголовок файлу. Витягує публічні експорти та імпорти з різних джерел. Витягує внутрішні та локальні символи. Документація перевіряє наявність операцій запису, обробки помилок, повернення хибних значень при невдачах, мережевих викликів, механізмів кешування та пропуск певних шляхів.
12
13
 
13
14
  ## Поведінка
14
15
 
15
- 1. Провідний блок-коментар файлу (до першого коду) стає «наміром файлу»; з документувальних коментарів перед кожним експортом витягуються опис, параметри та опис повернення, відкидаючи беззмістовні заглушки.
16
- 2. Збираються всі експортовані оголошення з безпосередньо передуючими їм коментарями.
17
- 3. Імпорти класифікуються на stdlib / npm / внутрішні; імена символів, імпортованих із внутрішніх модулів, складають список internalSymbols — модель не має згадувати їх у доці, а скорер штрафує за витік.
18
- 4. Маркери поведінки визначаються евристиками по тексту: read-only (немає запису у файлову систему), перехоплення помилок, повернення false/null при невдачі, звертання до мережі, кешування, свідомі пропуски шляхів.
19
- 5. Для розширень поза js/mjs/ts повертається позначка unsupported — конвеєр переходить на one-shot-шлях.
16
+ 1. Витягується факт про файл.
17
+ 2. Визначається мова файлу.
18
+ 3. Витягується заголовок файлу.
19
+ 4. Витягуються публічні експорти.
20
+ 5. Витягуються імпорти, класифіковані за джерелом.
21
+ 6. Витягуються внутрішні символи, імпортовані з внутрішніх модулів.
22
+ 7. Витягуються локальні символи, неекспортовані функції.
23
+ 8. Визначаються евристичні маркери.
24
+ * readOnly: перевірка на наявність операцій запису.
25
+ * catchesErrors: перевірка на наявність обробки помилок.
26
+ * returnsFalsyOnFail: перевірка на повернення хибних значень при невдачах.
27
+ * network: перевірка на наявність мережевих викликів.
28
+ * caches: перевірка на наявність механізмів кешування.
29
+ * skips: пропуск шляхів .github, .git, node_modules, base/, ua/, .firebase.
20
30
 
21
31
  ## Публічний API
22
32
 
23
- - `extractFacts`головна точка: текст джерела + шлях → факт-лист `{header, exports, imports, internalSymbols, markers}` або `{unsupported: true}`.
33
+ extractFacts — код файлу перетворює на список фактів
24
34
 
25
35
  ## Гарантії поведінки
26
36
 
27
- - Повністю детермінований: однаковий вхід однаковий факт-лист; жодних LLM-викликів і мережі.
28
- - Read-only: файл не виконує операцій запису у файлову систему.
29
- - Евристики свідомо толерують надлишок (зайвий маркер менша проблема, ніж пропущений).
37
+ - За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.
38
+ - Кешує результати в межах одного прогону.
39
+ - Свідомо пропускає шляхи: `.github`, `.git`, `node_modules`, `base/`, `ua/`, `.firebase`.
@@ -1,25 +1,35 @@
1
1
  ---
2
2
  docgen:
3
3
  source: npm/skills/doc-files/js/docgen-files-batch.mjs
4
- crc: 20a14675
4
+ crc: 5c9b8d72
5
+ score: 95
5
6
  ---
6
7
 
7
- # docgen-files-batch
8
+ # docgen-files-batch.mjs
8
9
 
9
10
  ## Огляд
10
11
 
11
- CLI-оркестратор масової генерації файлових док (`doc-files gen` / `doc-files stamp`): черга, вибір цілей, preflight локального сервера, запис док зі свіжою контрольною сумою і degraded-маркером. Уся важка робота живе тут, а не в контексті агента.
12
+ runDocFilesGenCli
13
+ Запускає генерацію документації для застарілих або відсутніх файлів.
14
+
15
+ runDocFilesStampCli
16
+ Перештампує frontmatter джерело та CRC у наявних документах без виклику LLM.
12
17
 
13
18
  ## Поведінка
14
19
 
15
- 1. Дерево проєкту сканується, цілі обираються за режимом: за замовчуванням — застарілі доки; `--overwrite` — усі; `--retry-degraded` — свіжі за сумою, але з оцінкою нижче порогу. Зріз великого прогону — `--from N --limit M`.
16
- 2. Перед генерацією preflight локального сервера: «сервер лежить», «модель не влазить у пам'ять зайнятої машини» чи «потрібен API-ключ» зупиняють прогін одним зрозумілим повідомленням замість лавини помилок по файлах.
17
- 3. Кожна ціль генерується локальним конвеєром; дока пишеться поряд із джерелом у `docs/` зі свіжою сумою. Якщо оцінка нижча за поріг — у frontmatter додаються оцінка й коди проблем, файл рахується як degraded.
18
- 4. Підсумок: кількість успішних, degraded і помилкових файлів; за наявності degraded — підказка про `--retry-degraded`. Помилка хоча б одного файлу → exit-код `1`.
19
- 5. `stamp` детерміновано перештамповує frontmatter у наявних доках без LLM (міграція док без суми), зберігаючи наявні поля якості.
20
+ runDocFilesGenCli
21
+ Запускає генерацію документації для застарілих/відсутніх док.
22
+
23
+ runDocFilesStampCli
24
+ Перештампує frontmatter source+crc у наявних доках без виклику LLM.
25
+
26
+ ## Публічний API
27
+
28
+ - runDocFilesGenCli — згенерувати документацію для застарілих/відсутніх док.
29
+ - runDocFilesStampCli — детерміновано (пере)штампувати frontmatter `source`+`crc` у наявних доках без виклику LLM. Для міграції док, які ще не мають CRC. Поля якості (`score`/`issues`) зберігаються з наявного frontmatter.
20
30
 
21
31
  ## Гарантії поведінки
22
32
 
23
- - Жодних хмарних викликів: збій локальної генерації стає помилкою чи degraded-маркером, а не ескалацією.
24
- - Доки пишуться атомарно по файлу: успішні цілі не відкочуються через подальші збої.
25
- - Прогін ніколи не комітить — рішення про фіксацію приймає користувач.
33
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
34
+ - За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.
35
+ - Не звертається до мережі.