@nitra/cursor 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/.claude-template/settings.template.json +22 -0
  2. package/.pi-template/extensions/n-cursor-adr/docs/index.md +15 -9
  3. package/CHANGELOG.md +12 -1
  4. package/bin/n-cursor.js +73 -16
  5. package/docs/stryker.config.md +6 -0
  6. package/docs/vitest.config.md +6 -0
  7. package/lib/docs/llm.md +29 -0
  8. package/lib/docs/omlx.md +32 -0
  9. package/lib/llm.mjs +137 -0
  10. package/lib/omlx.mjs +49 -4
  11. package/package.json +1 -1
  12. package/rules/abie/docs/fix.md +6 -0
  13. package/rules/abie/js/docs/applies.md +6 -0
  14. package/rules/abie/js/docs/env_dns.md +25 -22
  15. package/rules/abie/js/docs/firebase_hosting.md +6 -0
  16. package/rules/abie/js/docs/hc_pairing.md +21 -25
  17. package/rules/abie/js/docs/ua_http_route.md +27 -19
  18. package/rules/abie/js/docs/ua_node_selector.md +24 -19
  19. package/rules/abie/lib/docs/enabled.md +13 -7
  20. package/rules/abie/lib/docs/env-dns.md +9 -3
  21. package/rules/abie/lib/docs/hc-yaml.md +6 -0
  22. package/rules/abie/lib/docs/http-route.md +6 -0
  23. package/rules/abie/lib/docs/k8s-tree.md +6 -0
  24. package/rules/abie/lib/docs/kustomization-patches.md +6 -0
  25. package/rules/abie/lib/docs/overlay-paths.md +6 -0
  26. package/rules/abie/lib/docs/yaml.md +6 -0
  27. package/rules/adr/docs/fix.md +6 -0
  28. package/rules/adr/js/docs/hooks.md +29 -244
  29. package/rules/bun/docs/fix.md +6 -0
  30. package/rules/bun/js/docs/layout.md +37 -375
  31. package/rules/capacitor/docs/fix.md +22 -108
  32. package/rules/capacitor/js/docs/platforms.md +62 -268
  33. package/rules/changelog/docs/fix.md +6 -0
  34. package/rules/changelog/lib/docs/package-manifest.md +6 -0
  35. package/rules/ci4/docs/fix.md +23 -165
  36. package/rules/ci4/js/docs/marksman_config.md +9 -1
  37. package/rules/docker/docs/fix.md +6 -0
  38. package/rules/docker/js/docs/lint.md +55 -239
  39. package/rules/docker/lib/docs/docker-hadolint.md +6 -0
  40. package/rules/docker/lib/docs/docker-mirror.md +6 -0
  41. package/rules/docker/lib/docs/docker-native-addon.md +6 -0
  42. package/rules/docker/lib/docs/docker-nginx-user.md +6 -0
  43. package/rules/docker/lint/docs/lint.md +9 -1
  44. package/rules/efes/docs/fix.md +6 -0
  45. package/rules/ga/lint/docs/lint.md +6 -0
  46. package/rules/graphql/docs/fix.md +6 -0
  47. package/rules/graphql/lib/docs/graphql-gql-scan.md +6 -0
  48. package/rules/image-avif/docs/fix.md +6 -0
  49. package/rules/image-avif/js/docs/avif_generation.md +6 -0
  50. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +9 -3
  51. package/rules/js-bun-redis/lib/docs/redis-imports.md +6 -0
  52. package/rules/js-lint/js/docs/utils_imports.md +6 -0
  53. package/rules/js-lint-ci/docs/fix.md +7 -1
  54. package/rules/js-mssql/docs/fix.md +6 -0
  55. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +6 -0
  56. package/rules/js-run/docs/fix.md +6 -0
  57. package/rules/js-run/lib/docs/bunyan-imports.md +6 -0
  58. package/rules/js-run/lib/docs/check-env-scan.md +6 -0
  59. package/rules/js-run/lib/docs/conn-file-rules.md +6 -0
  60. package/rules/js-run/lib/docs/conn-imports-scan.md +6 -0
  61. package/rules/js-run/lib/docs/promise-settimeout-scan.md +6 -0
  62. package/rules/js-run/lib/docs/temporal-scan.md +6 -0
  63. package/rules/k8s/docs/fix.md +6 -0
  64. package/rules/k8s/lint/docs/lint.md +6 -0
  65. package/rules/nginx-default-tpl/docs/fix.md +6 -0
  66. package/rules/npm-module/js/docs/header_doc_pointer.md +7 -0
  67. package/rules/npm-module/js/header_doc_pointer.mjs +2 -8
  68. package/rules/php/docs/fix.md +6 -0
  69. package/rules/php/lint/docs/lint.md +6 -0
  70. package/rules/python/docs/fix.md +6 -0
  71. package/rules/python/lint/docs/lint.md +6 -0
  72. package/rules/rego/lint/docs/lint.md +6 -0
  73. package/rules/release/docs/change.md +6 -0
  74. package/rules/release/docs/fix.md +6 -0
  75. package/rules/release/docs/release.md +6 -0
  76. package/rules/release/lib/docs/aggregate.md +6 -0
  77. package/rules/release/lib/docs/change-file.md +6 -0
  78. package/rules/release/lib/docs/fallback.md +6 -0
  79. package/rules/rust/lib/docs/has-cargo-toml.md +6 -0
  80. package/rules/security/docs/fix.md +7 -1
  81. package/rules/security/js/docs/lint.md +6 -0
  82. package/rules/style-lint/docs/fix.md +6 -0
  83. package/rules/tauri/docs/fix.md +6 -0
  84. package/rules/test/docs/fix.md +6 -0
  85. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +6 -0
  86. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +6 -0
  87. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +6 -0
  88. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +6 -0
  89. package/rules/text/docs/fix.md +6 -0
  90. package/rules/text/lint/docs/lint.md +6 -0
  91. package/rules/text/lint/docs/run-dotenv-linter.md +6 -0
  92. package/rules/text/lint/docs/run-shellcheck.md +6 -0
  93. package/rules/text/lint/docs/run-v8r.md +6 -0
  94. package/rules/vue/lib/docs/vue-forbidden-imports.md +6 -0
  95. package/scripts/coverage-classify/cache.mjs +1 -1
  96. package/scripts/coverage-classify/docs/apply.md +6 -0
  97. package/scripts/coverage-classify/docs/cache.md +6 -0
  98. package/scripts/coverage-classify/docs/prompt.md +6 -0
  99. package/scripts/coverage-classify/docs/verdict-schema.md +6 -0
  100. package/scripts/coverage-classify/prompt.mjs +1 -1
  101. package/scripts/coverage-fix-extract.mjs +1 -1
  102. package/scripts/coverage-fix.mjs +2 -1
  103. package/scripts/docs/auto-skills.md +6 -0
  104. package/scripts/docs/build-agents-commands.md +7 -1
  105. package/scripts/docs/cli-entry.md +6 -0
  106. package/scripts/docs/coverage-fix-extract.md +6 -0
  107. package/scripts/docs/coverage-fix.md +6 -0
  108. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +6 -0
  109. package/scripts/docs/lint-cli.md +6 -0
  110. package/scripts/docs/post-tool-use-fix.md +6 -0
  111. package/scripts/docs/rename-yaml-extensions.md +6 -0
  112. package/scripts/docs/skills-cli.md +6 -0
  113. package/scripts/docs/sync-setup-bun-deps-action.md +6 -0
  114. package/scripts/docs/upgrade-nitra-cursor-and-install.md +6 -0
  115. package/scripts/docs/worktree-cli.md +6 -0
  116. package/scripts/lib/docs/assert-project-root.md +6 -0
  117. package/scripts/lib/docs/check-mdc-template-refs.md +6 -0
  118. package/scripts/lib/docs/check-reporter.md +6 -0
  119. package/scripts/lib/docs/diff-added-lines.md +6 -0
  120. package/scripts/lib/docs/discover-check-rules-from-cursor.md +6 -0
  121. package/scripts/lib/docs/discover-checkable-rules.md +6 -0
  122. package/scripts/lib/docs/ensure-tool.md +6 -0
  123. package/scripts/lib/docs/generated-markdown.md +6 -0
  124. package/scripts/lib/docs/gha-workflow.md +6 -0
  125. package/scripts/lib/docs/inline-template-links.md +6 -0
  126. package/scripts/lib/docs/list-rule-ids.md +6 -0
  127. package/scripts/lib/docs/load-cursor-config.md +6 -0
  128. package/scripts/lib/docs/mirror-parity.md +6 -0
  129. package/scripts/lib/docs/read-n-cursor-config-lite.md +6 -0
  130. package/scripts/lib/docs/resolve-target-files.md +6 -0
  131. package/scripts/lib/docs/root-notice.md +6 -0
  132. package/scripts/lib/docs/rule-meta-helpers.md +6 -0
  133. package/scripts/lib/docs/rule-meta.md +6 -0
  134. package/scripts/lib/docs/run-conftest-batch.md +6 -0
  135. package/scripts/lib/docs/run-lint-step.md +6 -0
  136. package/scripts/lib/docs/run-rule-cli.md +6 -0
  137. package/scripts/lib/docs/run-rule.md +6 -0
  138. package/scripts/lib/docs/run-standard-lint.md +6 -0
  139. package/scripts/lib/docs/run-standard-rule.md +6 -0
  140. package/scripts/lib/docs/skill-meta.md +6 -0
  141. package/scripts/lib/docs/template.md +6 -0
  142. package/scripts/lib/docs/timing-summary.md +6 -0
  143. package/scripts/lib/docs/workspaces.md +6 -0
  144. package/scripts/lib/docs/worktree-notice.md +6 -0
  145. package/scripts/lib/docs/worktree.md +6 -0
  146. package/scripts/lib/mirror-parity.mjs +1 -1
  147. package/scripts/lib/root-notice.mjs +1 -1
  148. package/scripts/lib/worktree-notice.mjs +5 -5
  149. package/scripts/lib/worktree.mjs +1 -1
  150. package/scripts/sync-claude-config.mjs +3 -0
  151. package/scripts/utils/docs/ast-scan-utils.md +6 -0
  152. package/scripts/utils/docs/ensure-gitignore-entries.md +6 -0
  153. package/scripts/utils/docs/find-package-json-paths.md +6 -0
  154. package/scripts/utils/docs/lock-cache-dir.md +6 -0
  155. package/scripts/utils/docs/pass.md +6 -0
  156. package/scripts/utils/docs/resolve-cargo-manifest.md +6 -0
  157. package/scripts/utils/docs/resolve-cmd.md +6 -0
  158. package/scripts/utils/docs/resolve-js-root.md +6 -0
  159. package/scripts/utils/docs/test-helpers.md +6 -0
  160. package/scripts/utils/docs/walk-cache.md +6 -0
  161. package/scripts/utils/docs/walkDir.md +6 -0
  162. package/scripts/utils/docs/worktree-fingerprint.md +6 -0
  163. package/scripts/utils/resolve-js-root.mjs +1 -1
  164. package/skills/doc-aggregate/SKILL.md +129 -0
  165. package/skills/doc-aggregate/js/docgen-ignore.mjs +9 -0
  166. package/skills/{docgen → doc-aggregate}/js/docgen-scan.mjs +22 -67
  167. package/skills/doc-aggregate/js/docs/docgen-ignore.md +21 -0
  168. package/skills/doc-files/SKILL.md +100 -0
  169. package/skills/doc-files/js/docgen-crc.mjs +164 -0
  170. package/skills/{docgen → doc-files}/js/docgen-extract-anchors.mjs +20 -11
  171. package/skills/{docgen → doc-files}/js/docgen-extract.mjs +15 -9
  172. package/skills/doc-files/js/docgen-files-batch.mjs +181 -0
  173. package/skills/doc-files/js/docgen-gen.mjs +291 -0
  174. package/skills/{docgen → doc-files}/js/docgen-prompts.mjs +43 -40
  175. package/skills/doc-files/js/docgen-scan.mjs +298 -0
  176. package/skills/doc-files/js/docs/docgen-crc.md +32 -0
  177. package/skills/doc-files/js/docs/docgen-extract-anchors.md +27 -0
  178. package/skills/doc-files/js/docs/docgen-extract.md +29 -0
  179. package/skills/doc-files/js/docs/docgen-files-batch.md +25 -0
  180. package/skills/doc-files/js/docs/docgen-gen.md +30 -0
  181. package/skills/doc-files/js/docs/docgen-prompts.md +32 -0
  182. package/skills/doc-files/js/docs/docgen-scan.md +25 -0
  183. package/skills/doc-files/meta.json +1 -0
  184. package/skills/fix/js/docs/llm-worker.md +6 -0
  185. package/skills/fix/js/docs/orchestrator.md +6 -0
  186. package/skills/fix/js/llm-worker.mjs +3 -3
  187. package/skills/fix/js/orchestrator.mjs +1 -1
  188. package/skills/start-check/js/check.mjs +5 -3
  189. package/skills/start-check/js/docs/check.md +6 -0
  190. package/skills/docgen/SKILL.md +0 -224
  191. package/skills/docgen/bench/etalon/firebase_hosting.md +0 -19
  192. package/skills/docgen/bench/etalon/k8s-tree.md +0 -24
  193. package/skills/docgen/bench/etalon/overlay-paths.md +0 -24
  194. package/skills/docgen/js/docgen-batch-omlx.mjs +0 -82
  195. package/skills/docgen/js/docgen-batch.mjs +0 -95
  196. package/skills/docgen/js/docgen-compare-pi-vs-direct.mjs +0 -95
  197. package/skills/docgen/js/docgen-gen.mjs +0 -306
  198. package/skills/docgen/js/docs/docgen-extract.md +0 -28
  199. package/skills/docgen/js/docs/docgen-gen.md +0 -41
  200. package/skills/docgen/js/docs/docgen-ignore.md +0 -24
  201. package/skills/docgen/js/docs/docgen-prompts.md +0 -24
  202. package/skills/docgen/js/docs/docgen-scan.md +0 -48
  203. /package/skills/{docgen → doc-aggregate}/meta.json +0 -0
  204. /package/skills/{docgen → doc-files}/js/docgen-ignore.mjs +0 -0
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Re-export спільного списку ignore-глобів зі скіла doc-files.
3
+ *
4
+ * Канонічне джерело — `npm/skills/doc-files/js/docgen-ignore.mjs`: обидва скіли
5
+ * (file-level доки і агрегати) мусять бачити однакове дерево кодових файлів,
6
+ * інакше агрегат посилатиметься на файли без док (або навпаки). Залежність
7
+ * спрямована doc-aggregate → doc-files за ADR про розбиття docgen.
8
+ */
9
+ export * from '../../doc-files/js/docgen-ignore.mjs'
@@ -13,9 +13,7 @@ const SOURCE_EXTENSIONS = new Set(['.js', '.mjs', '.ts', '.vue', '.py'])
13
13
  const TEST_FILE_RE = /\.(?:test|spec)\.[^.]+$/u
14
14
 
15
15
  /**
16
- * Чи корінь має system-wide docs layout.
17
- * Такий корінь зарезервований під репозиторні docs/adr, docs/explanation тощо,
18
- * тому file-level docs у нього не пишемо.
16
+ * Чи корінь має system-wide docs layout (зарезервований під repo docs/adr тощо).
19
17
  * @param {string} root абсолютний корінь обходу
20
18
  * @returns {boolean} true — корінь system-wide docs
21
19
  */
@@ -26,7 +24,7 @@ function isSystemWideDocsRoot(root) {
26
24
  /**
27
25
  * Чи є файл кодовим джерелом для документування.
28
26
  * @param {string} fileName базове ім'я файлу
29
- * @returns {boolean} true — документуємо; false — пропускаємо
27
+ * @returns {boolean} true — документуємо
30
28
  */
31
29
  export function isSourceFile(fileName) {
32
30
  if (fileName.endsWith('.d.ts')) return false
@@ -35,30 +33,14 @@ export function isSourceFile(fileName) {
35
33
  }
36
34
 
37
35
  /**
38
- * Обчислює шлях md-документа для кодового файлу: тека `docs/` поряд із джерелом.
39
- * Якщо `sourcePath` відносний, `docPath` теж відносний; якщо абсолютний — абсолютний.
40
- * @param {string} sourcePath шлях до джерела (відносний або абсолютний)
41
- * @returns {string} шлях до `<dir>/docs/<stem>.md` у тому ж просторі шляхів
42
- */
43
- export function docPathForSource(sourcePath) {
44
- const dir = path.dirname(sourcePath)
45
- const stem = path.basename(sourcePath, path.extname(sourcePath))
46
- return path.join(dir, 'docs', `${stem}.md`)
47
- }
48
-
49
- /**
50
- * Рекурсивно обходить дерево від `root`, повертає кодові файли для документування.
51
- * Синхронний `readdirSync` — детермінований порядок і простий рекурсивний обхід без
52
- * гонок; обсяг дерева проєкту це дозволяє.
36
+ * Рекурсивно збирає кодові файли проєкту (posix-шляхи від кореня).
53
37
  * @param {string} root абсолютний корінь обходу
54
- * @returns {Array<{sourcePath:string, docPath:string, exists:boolean}>} список кандидатів з відносними шляхами
38
+ * @returns {string[]} sourcePath-и
55
39
  */
56
- export function scanForDocgen(root) {
40
+ export function scanSourceFiles(root) {
57
41
  const results = []
58
42
 
59
- /**
60
- * @param {string} dir поточний каталог обходу
61
- */
43
+ /** @param {string} dir поточний каталог обходу */
62
44
  function walk(dir) {
63
45
  let entries
64
46
  try {
@@ -76,12 +58,7 @@ export function scanForDocgen(root) {
76
58
  if (isSystemWideDocsRoot(root) && path.dirname(relPath) === '.') continue
77
59
  const sourcePath = relPath.split(path.sep).join('/')
78
60
  if (isDocgenIgnored(sourcePath)) continue
79
- const docPath = docPathForSource(sourcePath)
80
- results.push({
81
- sourcePath,
82
- docPath,
83
- exists: existsSync(path.join(root, docPath))
84
- })
61
+ results.push(sourcePath)
85
62
  }
86
63
  }
87
64
  }
@@ -98,7 +75,6 @@ export function scanForDocgen(root) {
98
75
  */
99
76
  export function slugForModule(root, moduleRoot) {
100
77
  const rel = path.relative(root, moduleRoot)
101
- // корінь репо: фіксований sentinel 'root'
102
78
  if (rel === '') return 'root'
103
79
  return rel
104
80
  .split(path.sep)
@@ -108,7 +84,6 @@ export function slugForModule(root, moduleRoot) {
108
84
 
109
85
  /**
110
86
  * Знаходить корені модулів — теки з `package.json` (корінь завжди модуль).
111
- * Ті ж ignore-glob правила, тож `package.json` у службових деревах не враховується.
112
87
  * @param {string} root абсолютний корінь обходу
113
88
  * @returns {string[]} абсолютні шляхи коренів модулів
114
89
  */
@@ -159,17 +134,17 @@ export function nearestModuleRoot(filePath, moduleRoots) {
159
134
  * Лістить логічні модулі проєкту з членами-файлами і docPath module-summary.
160
135
  * Модулі без кодових файлів пропускаються.
161
136
  * @param {string} root абсолютний корінь обходу
162
- * @returns {Array<{moduleRoot:string, relRoot:string, slug:string, docPath:string, members:string[], exists:boolean}>} модулі (members — sourcePath-и, відносні від root)
137
+ * @returns {Array<{moduleRoot:string, relRoot:string, slug:string, docPath:string, members:string[], exists:boolean}>} модулі (members — sourcePath від root)
163
138
  */
164
139
  export function scanForModules(root) {
165
- const files = scanForDocgen(root)
140
+ const files = scanSourceFiles(root)
166
141
  const moduleRoots = findModuleRoots(root)
167
142
  const byRoot = new Map()
168
- for (const file of files) {
169
- const moduleRoot = nearestModuleRoot(path.join(root, file.sourcePath), moduleRoots)
143
+ for (const sourcePath of files) {
144
+ const moduleRoot = nearestModuleRoot(path.join(root, sourcePath), moduleRoots)
170
145
  if (moduleRoot === null) continue
171
146
  if (!byRoot.has(moduleRoot)) byRoot.set(moduleRoot, [])
172
- byRoot.get(moduleRoot).push(file.sourcePath)
147
+ byRoot.get(moduleRoot).push(sourcePath)
173
148
  }
174
149
 
175
150
  const results = []
@@ -190,7 +165,7 @@ export function scanForModules(root) {
190
165
  }
191
166
 
192
167
  /**
193
- * Парсить `--root <dir>` з argv; default — cwd.
168
+ * Парсить `--root <dir>`; default — cwd.
194
169
  * @param {string[]} argv аргументи після підкоманди
195
170
  * @returns {string} абсолютний корінь
196
171
  */
@@ -200,42 +175,22 @@ export function resolveRoot(argv) {
200
175
  }
201
176
 
202
177
  /**
203
- * Парсить `--root <dir>` (default cwd), сканує і друкує JSON-масив у stdout.
204
- * @param {string[]} argv аргументи після назви субкоманди (наприклад ['--root', '<dir>'])
205
- * @returns {Promise<number>} exit-код: 0 — успіх, 1 — корінь не існує
206
- */
207
- export async function runDocgenScanCli(argv) {
208
- const root = resolveRoot(argv)
209
-
210
- if (!existsSync(root) || !statSync(root).isDirectory()) {
211
- console.error(`docgen scan: корінь не існує або не є директорією: ${root}`)
212
- return 1
213
- }
214
-
215
- const items = await scanForDocgen(root)
216
- console.log(JSON.stringify(items, null, 2))
217
- return 0
218
- }
219
-
220
- /**
221
- * Парсить `--root`, сканує модулі і друкує JSON-масив у stdout.
222
- * @param {string[]} argv аргументи після назви субкоманди (наприклад ['--root', '<dir>'])
223
- * @returns {Promise<number>} exit-код: 0 — успіх, 1 — корінь не існує
178
+ * `doc-aggregate modules` — сканує модулі і друкує JSON-масив у stdout.
179
+ * @param {string[]} argv аргументи після назви субкоманди
180
+ * @returns {number} exit-код: 0 — успіх, 1 — корінь не існує
224
181
  */
225
- export async function runDocgenModulesCli(argv) {
182
+ export function runDocAggregateModulesCli(argv) {
226
183
  const root = resolveRoot(argv)
227
-
228
184
  if (!existsSync(root) || !statSync(root).isDirectory()) {
229
- console.error(`docgen modules: корінь не існує або не є директорією: ${root}`)
185
+ console.error(`doc-aggregate modules: корінь не існує або не є директорією: ${root}`)
230
186
  return 1
231
187
  }
232
-
233
- const items = await scanForModules(root)
234
- console.log(JSON.stringify(items, null, 2))
188
+ console.log(JSON.stringify(scanForModules(root), null, 2))
235
189
  return 0
236
190
  }
237
191
 
238
192
  if (isRunAsCli(import.meta.url)) {
239
- // Прямий запуск: `node skills/docgen/js/docgen-scan.mjs --root <dir>`
240
- process.exitCode = await runDocgenScanCli(process.argv.slice(2))
193
+ // Прямий запуск: `node skills/doc-aggregate/js/docgen-scan.mjs modules --root <dir>`
194
+ const [sub, ...rest] = process.argv.slice(2)
195
+ process.exitCode = runDocAggregateModulesCli(sub === 'modules' ? rest : process.argv.slice(2))
241
196
  }
@@ -0,0 +1,21 @@
1
+ ---
2
+ docgen:
3
+ source: npm/skills/doc-aggregate/js/docgen-ignore.mjs
4
+ crc: 8821af65
5
+ ---
6
+
7
+ # docgen-ignore
8
+
9
+ ## Огляд
10
+
11
+ Re-export спільного списку ignore-глобів зі скіла doc-files: обидва скіли документації (пофайлові доки й агрегати) мусять бачити однакове дерево кодових файлів, інакше агрегат посилатиметься на файли без док або навпаки.
12
+
13
+ ## Поведінка
14
+
15
+ 1. Модуль не має власної логіки: повністю делегує канонічному джерелу в doc-files і повторно експортує його API (список глобів і перевірку належності шляху до ігнорованих).
16
+ 2. Напрям залежності — doc-aggregate → doc-files, відповідно до рішення про розбиття docgen на два скіли.
17
+
18
+ ## Гарантії поведінки
19
+
20
+ - Списки ignore-глобів двох скілів не можуть розійтися — джерело одне.
21
+ - Read-only, без мережі й побічних ефектів.
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: doc-files
3
+ description: >-
4
+ Обовʼязковий крок задачі (як lint): для кожного зміненого/нового кодового файлу (js/mjs/ts/vue/py) JS-оркестрована генерація лаконічної поведінкової української md-документації у теку docs/ поряд із кодом, зі звіркою застарілості за CRC у frontmatter
5
+ ---
6
+
7
+ # doc-files — файлова документація (обовʼязковий крок)
8
+
9
+ ## Мета
10
+
11
+ Для кожного кодового файлу проєкту тримати **актуальну** лаконічну поведінкову `.md`-документацію
12
+ у теці `docs/` **поряд із самим файлом** (`<dir>/docs/<stem>.md`). Це **обовʼязковий крок кожної
13
+ задачі** — як `lint`: після зміни коду його дока має бути перегенерована.
14
+
15
+ Застарілість визначається **детерміновано за CRC**: кожна дока несе у frontmatter контрольну
16
+ суму байтів джерела на момент генерації. Дока **застаріла**, якщо її немає або
17
+ `crc(поточне джерело) ≠ crc у frontmatter`.
18
+
19
+ ```markdown
20
+ ---
21
+ docgen:
22
+ source: src/lib/foo.js
23
+ crc: a3f1c9e0
24
+ ---
25
+
26
+ ## Огляд
27
+
28
+
29
+ ```
30
+
31
+ ## Оркестрацію веде JS, не модель; конвеєр — local-only
32
+
33
+ Уся важка робота — черга, батчинг, виклики LLM і штамп CRC — живе в JS-команді
34
+ `doc-files gen`. **Ти не диспатчиш субагентів і не тримаєш сотні файлів у контексті**
35
+ — тому навіть масовий перший прогін усього репо не «заморює». Цей скіл **тонкий**: твоє завдання —
36
+ запустити генерацію і прочитати підсумок.
37
+
38
+ Конвеєр **суто локальний** (ADR `260610-2228`): будь-який файл генерується локальною
39
+ моделлю (`omlx/…` напряму), хмарних ескалацій немає. Якщо det-оцінка нижча за поріг —
40
+ дока все одно пишеться з **degraded-маркером** (`score`/`issues` у frontmatter, CRC свіжий),
41
+ а перегенерація таких док — окремою командою пізніше.
42
+
43
+ ## Передумова
44
+
45
+ - Поточна директорія — корінь проєкту (`requireRoot`), не worktree.
46
+ - Доступний `npx @nitra/cursor`.
47
+
48
+ ## Workflow
49
+
50
+ ### Крок 1: Генерація застарілих/відсутніх док
51
+
52
+ ```bash
53
+ npx @nitra/cursor doc-files gen
54
+ ```
55
+
56
+ Команда сама: перевіряє omlx (preflight: «сервер лежить» / «модель не влазить у пам'ять
57
+ зайнятої машини» / «потрібен API-ключ» → одна зрозуміла зупинка замість лавини «✗») →
58
+ сканує проєкт → фільтрує застарілі (`stale`) → генерує локальною моделлю → пише доку зі
59
+ **свіжим CRC** (і degraded-маркером, якщо не дотягнула) → друкує прогрес і підсумок.
60
+
61
+ - Для дуже великого прогону можна порціями: `--from N --limit M`.
62
+ - Перегенерувати **всі** доки (не лише застарілі): `--overwrite`.
63
+ - Перегенерувати лише degraded (свіжі за CRC, score < порогу): `--retry-degraded`.
64
+
65
+ ### Крок 2: Підтвердження
66
+
67
+ Дочекайся підсумку `✓ OK: <N> ⚠ degraded: <D> ✗ Err: <E>`. Якщо є помилки — перелічи
68
+ проблемні файли. Exit-код `1` означає, що хоча б один файл не згенерувався (або не пройшов
69
+ preflight). Degraded — не помилка: дока існує, борг видно у `check --degraded`.
70
+
71
+ ### Крок 3 (за потреби): перевірка перед завершенням
72
+
73
+ ```bash
74
+ npx @nitra/cursor doc-files check --git
75
+ ```
76
+
77
+ Перевіряє змінені у задачі джерела (`git diff --name-only HEAD`) проти CRC їхніх док.
78
+ Цю ж перевірку виконує **Stop-hook** як твердий гейт: завершити задачу зі застарілими
79
+ доками не можна (виняток — масовий прогін понад поріг `N_CURSOR_DOC_FILES_GATE_MAX`, дефолт 50).
80
+ Degraded-доки гейт **не** блокує (CRC свіжий); їх список — `doc-files check --degraded`.
81
+
82
+ ## Правила стилю документа (за adr/ci4)
83
+
84
+ - Мова — **УКРАЇНСЬКА** для всього тексту. Code identifiers, шляхи, імена API, команди — як у коді.
85
+ - **Чистий Markdown.** Жодних HTML-обгорток. Єдиний виняток — машинний `docgen:`-frontmatter із CRC.
86
+ - **Фокус на ПОВЕДІНЦІ, не реалізації.** ЩО і НАВІЩО, а не як саме зроблено.
87
+ - Не перелічуй модулі стандартної бібліотеки і внутрішні назви допоміжних функцій.
88
+ - Кожна секція самодостатня (без «як вище», «ця функція»).
89
+ - Секції (лише доречні): `## Огляд`, `## Поведінка`, `## Публічний API`, `## Де використовується`,
90
+ `## Гарантії поведінки`; для `.vue` — `## Інтерфейс компонента`.
91
+ - Не вигадуй деталей, яких немає в коді.
92
+
93
+ ## Нотатки
94
+
95
+ - Не комітити автоматично — користувач вирішує, коли комітити згенеровану доку.
96
+ - Scanner ігнорує `node_modules`, `dist`, `.git`, `__pycache__`, `coverage`, `.cursor`, `.claude`,
97
+ усі теки `docs/`, а також `*.test.*` / `*.spec.*` / `*.d.ts`. Кореневий repo `docs/` —
98
+ system-wide only: file-level docs туди не пишуться. Список glob-ів — `docgen-ignore.mjs`.
99
+ - Агрегуюча документація (module-summary, доменні доки) — окремий скіл `doc-aggregate`, за запитом.
100
+ - Для наявних док без CRC одноразово: `npx @nitra/cursor doc-files stamp` (штампує frontmatter без LLM).
@@ -0,0 +1,164 @@
1
+ /**
2
+ * CRC32 джерела + YAML-frontmatter файлової документації.
3
+ *
4
+ * Кожна файлова дока несе у frontmatter контрольну суму байтів джерела на момент
5
+ * генерації. Це детермінований маркер застарілості: `crc32(поточне джерело)` звіряється
6
+ * з `crc` у доці — розбіжність (або відсутня дока) означає, що дока відстала від коду.
7
+ * CRC не залежить від git-стану (rebase, незакомічене, гілки), тож придатний і для
8
+ * per-edit hook (бачить лише змінений файл), і для повного сканування.
9
+ *
10
+ * Degraded-маркер (ADR 260610-2228): якщо локальний конвеєр не дотягнув до порогу
11
+ * якості, дока все одно пишеться, а frontmatter додатково несе `score` (det-оцінка)
12
+ * та `issues` (коди проблем). CRC при цьому свіжий — Stop-гейт не блокує задачі через
13
+ * слабкість моделі; борг видимий через `check --degraded` і адресно перегенеровується
14
+ * через `gen --retry-degraded`.
15
+ *
16
+ * Frontmatter — єдиний дозволений виняток із правила «чистий Markdown без HTML»:
17
+ * це машинні метадані, не контент. Формат:
18
+ *
19
+ * ---
20
+ * docgen:
21
+ * source: src/lib/foo.js
22
+ * crc: a3f1c9e0
23
+ * score: 55
24
+ * issues: short-behavior,internal-name:bar
25
+ * ---
26
+ */
27
+ import { existsSync, readFileSync } from 'node:fs'
28
+ import { crc32 as zlibCrc32 } from 'node:zlib'
29
+ import { env } from 'node:process'
30
+
31
+ /** Поріг degraded: дока зі `score` нижче вважається неякісною. */
32
+ export const QUALITY_THRESHOLD = Number(env.N_CURSOR_DOC_FILES_THRESHOLD ?? 70) || 70
33
+
34
+ /**
35
+ * CRC32 вмісту у hex (8 символів, з провідними нулями). Делегує у нативний
36
+ * `node:zlib.crc32` — без ручної бітової арифметики.
37
+ * @param {string|Buffer} input текст або байти джерела
38
+ * @returns {string} CRC32 у hex
39
+ */
40
+ export function crc32(input) {
41
+ const buf = typeof input === 'string' ? Buffer.from(input, 'utf8') : input
42
+ return zlibCrc32(buf).toString(16).padStart(8, '0')
43
+ }
44
+
45
+ /** Провідний YAML-frontmatter-блок `---\n…\n---`. */
46
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/u
47
+ const SOURCE_RE = /^[ \t]{0,8}source:[ \t]{0,8}(.+)$/mu
48
+ const CRC_RE = /^[ \t]{0,8}crc:[ \t]{0,8}(.+)$/mu
49
+ const SCORE_RE = /^[ \t]{0,8}score:[ \t]{0,8}(\d+)$/mu
50
+ const ISSUES_RE = /^[ \t]{0,8}issues:[ \t]{0,8}(.+)$/mu
51
+ const LEADING_NEWLINES_RE = /^\n+/u
52
+ const ISSUE_CODE_TAIL_RE = /[,:]$/u
53
+
54
+ /**
55
+ * Парсить frontmatter файлової доки. Без блоку — `data:null` і `body` дорівнює входу.
56
+ * Поля `score`/`issues` опційні (back-compat зі старими доками): без них —
57
+ * `score:null`, `issues:[]`.
58
+ * @param {string} md вміст md-файлу
59
+ * @returns {{ data: { source: string|null, crc: string|null, score: number|null, issues: string[] }|null, body: string }} метадані + тіло без frontmatter
60
+ */
61
+ export function parseDocFrontmatter(md) {
62
+ const match = md.match(FRONTMATTER_RE)
63
+ if (!match) return { data: null, body: md }
64
+ const block = match[1]
65
+ const scoreRaw = block.match(SCORE_RE)?.[1]
66
+ const issuesRaw = block.match(ISSUES_RE)?.[1]
67
+ return {
68
+ data: {
69
+ source: block.match(SOURCE_RE)?.[1].trim() ?? null,
70
+ crc: block.match(CRC_RE)?.[1].trim() ?? null,
71
+ score: scoreRaw === undefined ? null : Number(scoreRaw),
72
+ issues: issuesRaw
73
+ ? issuesRaw
74
+ .split(',')
75
+ .map(s => s.trim())
76
+ .filter(Boolean)
77
+ : []
78
+ },
79
+ body: md.slice(match[0].length)
80
+ }
81
+ }
82
+
83
+ /** Максимум кодів issues у frontmatter — це маркер, а не повний лог. */
84
+ const MAX_ISSUE_CODES = 8
85
+
86
+ /**
87
+ * Нормалізує issues до YAML-безпечних кодів: бере фрагмент до першого пробілу
88
+ * (зрізає людиночитні хвости помилок), відкидає порожні, обмежує кількість.
89
+ * @param {string[]} issues сирі issue-рядки від скорера
90
+ * @returns {string[]} коди без пробілів
91
+ */
92
+ function issueCodes(issues) {
93
+ return issues
94
+ .map(i => String(i).split(' ')[0].replace(ISSUE_CODE_TAIL_RE, ''))
95
+ .filter(Boolean)
96
+ .slice(0, MAX_ISSUE_CODES)
97
+ }
98
+
99
+ /**
100
+ * Будує frontmatter-блок із шляхом джерела, CRC і (опційно) якістю.
101
+ * @param {string} source відносний шлях джерела
102
+ * @param {string} crc CRC32 джерела у hex
103
+ * @param {{ score: number, issues?: string[] }|null} [quality] det-оцінка доки; null — без полів якості
104
+ * @returns {string} рядок `---\ndocgen:\n source: …\n crc: …[\n score: …][\n issues: …]\n---\n`
105
+ */
106
+ export function buildDocFrontmatter(source, crc, quality = null) {
107
+ const lines = [`source: ${source}`, `crc: ${crc}`]
108
+ if (quality && typeof quality.score === 'number') {
109
+ lines.push(`score: ${quality.score}`)
110
+ const codes = issueCodes(quality.issues ?? [])
111
+ if (codes.length > 0) lines.push(`issues: ${codes.join(',')}`)
112
+ }
113
+ const indented = lines.map(l => ' ' + l).join('\n')
114
+ return `---\ndocgen:\n${indented}\n---\n`
115
+ }
116
+
117
+ /**
118
+ * (Пере)штампує frontmatter у md-доку: знімає наявний блок і додає свіжий.
119
+ * @param {string} md тіло доки (з frontmatter або без)
120
+ * @param {string} source відносний шлях джерела
121
+ * @param {string} crc CRC32 джерела у hex
122
+ * @param {{ score: number, issues?: string[] }|null} [quality] det-оцінка доки
123
+ * @returns {string} md зі свіжим frontmatter
124
+ */
125
+ export function stampDoc(md, source, crc, quality = null) {
126
+ const { body } = parseDocFrontmatter(md)
127
+ return `${buildDocFrontmatter(source, crc, quality)}\n${body.replace(LEADING_NEWLINES_RE, '')}`
128
+ }
129
+
130
+ /**
131
+ * CRC, збережений у frontmatter доки; `null` — доки немає або CRC відсутній.
132
+ * @param {string} docAbsPath абсолютний шлях md-доки
133
+ * @returns {string|null} CRC32 з frontmatter або null
134
+ */
135
+ export function readDocCrc(docAbsPath) {
136
+ if (!existsSync(docAbsPath)) return null
137
+ return parseDocFrontmatter(readFileSync(docAbsPath, 'utf8')).data?.crc ?? null
138
+ }
139
+
140
+ /**
141
+ * Якість, збережена у frontmatter доки.
142
+ * @param {string} docAbsPath абсолютний шлях md-доки
143
+ * @returns {{ score: number|null, issues: string[] }} `score:null` — доки немає або поле відсутнє
144
+ */
145
+ export function readDocQuality(docAbsPath) {
146
+ if (!existsSync(docAbsPath)) return { score: null, issues: [] }
147
+ const data = parseDocFrontmatter(readFileSync(docAbsPath, 'utf8')).data
148
+ return { score: data?.score ?? null, issues: data?.issues ?? [] }
149
+ }
150
+
151
+ /**
152
+ * Стан застарілості доки відносно її джерела.
153
+ * `missing` — доки немає; `crc-mismatch` — CRC джерела ≠ CRC у доці; інакше свіжа.
154
+ * @param {string} sourceAbsPath абсолютний шлях джерела
155
+ * @param {string} docAbsPath абсолютний шлях md-доки
156
+ * @returns {{ stale: boolean, reason: 'missing'|'crc-mismatch'|null }} стан застарілості
157
+ */
158
+ export function staleness(sourceAbsPath, docAbsPath) {
159
+ const docCrc = readDocCrc(docAbsPath)
160
+ if (docCrc === null) return { stale: true, reason: 'missing' }
161
+ const srcCrc = crc32(readFileSync(sourceAbsPath))
162
+ if (srcCrc !== docCrc) return { stale: true, reason: 'crc-mismatch' }
163
+ return { stale: false, reason: null }
164
+ }
@@ -19,9 +19,13 @@ const EXPORT_CONST_RE = /export\s+const\s+([A-Z][A-Z0-9_]+)\s*=\s*(['"`])([^'"`]
19
19
  const ERROR_MARKER_RE = /\(([a-z][\w-]*\.mdc)\)/g
20
20
  const CONFIG_REF_RE = /\b(\.[a-z][\w.-]*\.json)\b/gi
21
21
  const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
22
- const CODE_BLOCK_RE = /```[a-z]*\n([\s\S]*?)\n\s*\*?\s*```/g
22
+ const CODE_BLOCK_RE = /```[a-z]{0,12}\n([\s\S]*?)\n[ \t]{0,8}\*?[ \t]{0,8}```/g
23
23
 
24
- /** Dedup масив, зберігаючи порядок появи. */
24
+ /**
25
+ * Dedup масив, зберігаючи порядок появи.
26
+ * @param {Array<string>} arr вхідний масив
27
+ * @returns {Array<string>} масив без дублікатів у вихідному порядку
28
+ */
25
29
  function uniq(arr) {
26
30
  const seen = new Set()
27
31
  const out = []
@@ -36,14 +40,14 @@ function uniq(arr) {
36
40
 
37
41
  /**
38
42
  * Витягує анкори з вихідного коду файла.
39
- * @param {string} src
43
+ * @param {string} src вміст файлу
40
44
  * @returns {{
41
45
  * urls: string[],
42
46
  * magicStrings: Array<{name:string, value:string}>,
43
47
  * errorMarkers: string[],
44
48
  * configRefs: string[],
45
49
  * examples: string[]
46
- * }}
50
+ * }} категоризовані анкори файлу
47
51
  */
48
52
  export function extractAnchors(src) {
49
53
  const urls = uniq(Array.from(src.matchAll(URL_RE), m => m[0]))
@@ -73,20 +77,25 @@ export function extractAnchors(src) {
73
77
  * Форматує анкори у компактний текст для system-промпта.
74
78
  * Якщо анкорів немає взагалі — повертає порожній рядок (системний блок про
75
79
  * анкори не додається, щоб не вводити LLM в оману «обовʼязковими» полями).
76
- * @param {ReturnType<typeof extractAnchors>} a
77
- * @returns {string}
80
+ * @param {ReturnType<typeof extractAnchors>} a анкори файлу
81
+ * @returns {string} компактний текстовий блок або порожній рядок
78
82
  */
79
83
  export function anchorsToPrompt(a) {
80
84
  const blocks = []
81
85
  if (a.urls.length) blocks.push(`URLs (згадай у тексті): ${a.urls.join(', ')}`)
82
86
  if (a.magicStrings.length) {
83
- blocks.push(
84
- `Експортовані константи-рядки (наведи назву і призначення): ${a.magicStrings.map(s => `${s.name}=${JSON.stringify(s.value)}`).join('; ')}`
85
- )
87
+ const consts = a.magicStrings.map(s => s.name + '=' + JSON.stringify(s.value)).join('; ')
88
+ blocks.push(`Експортовані константи-рядки (наведи назву і призначення): ${consts}`)
89
+ }
90
+ if (a.errorMarkers.length) {
91
+ const markers = a.errorMarkers.map(m => '(' + m + ')').join(', ')
92
+ blocks.push(`Маркери повідомлень (згадай у Поведінці): ${markers}`)
86
93
  }
87
- if (a.errorMarkers.length) blocks.push(`Маркери повідомлень (згадай у Поведінці): ${a.errorMarkers.map(m => `(${m})`).join(', ')}`)
88
94
  if (a.configRefs.length) blocks.push(`Конфіги, на які спирається код: ${a.configRefs.join(', ')}`)
89
- if (a.examples.length) blocks.push(`Приклади з документації автора (наведи дослівно у Поведінці):\n${a.examples.map(e => '```\n' + e + '\n```').join('\n')}`)
95
+ if (a.examples.length) {
96
+ const fenced = a.examples.map(e => '```\n' + e + '\n```').join('\n')
97
+ blocks.push(`Приклади з документації автора (наведи дослівно у Поведінці):\n${fenced}`)
98
+ }
90
99
  if (!blocks.length) return ''
91
100
  return `АНКОРИ ДО ОБОВ'ЯЗКОВОГО ВКЛЮЧЕННЯ:\n${blocks.join('\n')}`
92
101
  }
@@ -25,15 +25,17 @@ const BUILTIN_MODULES = new Set([
25
25
  const JSDOC_OPEN_RE = /^\s*\/\*\*?/
26
26
  const JSDOC_CLOSE_RE = /\*\/\s*$/
27
27
  const STAR_PREFIX_RE = /^\s*\*?\s?/
28
- const PARAM_LINE_RE = /^@param\s+(?:\{[^}]*\}\s+)?\[?([A-Za-z0-9_.]+)\]?\s*(.*)$/
29
- const RETURNS_LINE_RE = /^@returns?\s+(?:\{[^}]*\}\s+)?(.*)$/
28
+ const PARAM_LINE_RE = /^@param[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?\[?([\w.]{1,80})\]?[ \t]{0,8}(.{0,400})$/
29
+ const RETURNS_LINE_RE = /^@returns?[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?(.{0,400})$/
30
30
  const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
31
31
  const PRECEDING_JSDOC_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
32
- const EXPORT_DECL_RE = /export\s+(?:async\s+)?(function|const|class)\s+([A-Za-z0-9_]+)/g
33
- const IMPORT_FROM_RE = /^import\s+[\s\S]*?from\s+['"]([^'"]+)['"]/gm
32
+ const EXPORT_DECL_RE = /export\s+(?:async\s+)?(function|const|class)\s+(\w+)/g
33
+ const IMPORT_FROM_RE = /^import[ \t]{1,8}[\s\S]{0,300}?from\s{1,8}['"]([^'"]+)['"]/gm
34
34
  const NODE_PREFIX_RE = /^node:/
35
- const INTERNAL_IMPORT_RE = /import\s+(?:([A-Za-z0-9_$]+)\s*,?\s*)?(?:\{([^}]+)\})?\s+from\s+['"](\.[^'"]+)['"]/g
36
- const IMPORT_AS_RE = /\s+as\s+.*/
35
+ const INTERNAL_IMPORT_RE = /import[ \t]{1,8}([^'"]{0,300}?)from[ \t]{1,8}['"]\.[^'"]{1,300}['"]/g
36
+ const NAMED_BRACES_RE = /\{([^}]{1,400})\}/
37
+ const IDENT_RE = /^[\w$]{1,80}$/
38
+ const IMPORT_AS_RE = /[ \t]{1,8}as[ \t]{1,8}.{0,200}/
37
39
  const WRITE_FS_RE = /\b(writeFile|mkdir|rmdir|unlink|appendFile|createWriteStream|rm\()/
38
40
  const CATCH_RE = /catch\s*\(/
39
41
  const TRY_RE = /\btry\s*\{/
@@ -152,12 +154,16 @@ function extractImports(src) {
152
154
  function extractInternalSymbols(src) {
153
155
  const out = new Set()
154
156
  for (const m of src.matchAll(INTERNAL_IMPORT_RE)) {
155
- if (m[1]) out.add(m[1].trim())
156
- if (m[2])
157
- for (const n of m[2].split(',')) {
157
+ const clause = m[1]
158
+ const named = clause.match(NAMED_BRACES_RE)
159
+ if (named) {
160
+ for (const n of named[1].split(',')) {
158
161
  const name = n.replace(IMPORT_AS_RE, '').trim()
159
162
  if (name) out.add(name)
160
163
  }
164
+ }
165
+ const defName = clause.replace(NAMED_BRACES_RE, '').replaceAll(',', ' ').trim().split(' ')[0]
166
+ if (defName && IDENT_RE.test(defName)) out.add(defName)
161
167
  }
162
168
  return [...out]
163
169
  }