@nitra/cursor 5.1.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.claude-template/settings.template.json +22 -0
  2. package/.pi-template/extensions/n-cursor-adr/docs/index.md +15 -9
  3. package/CHANGELOG.md +18 -1
  4. package/bin/n-cursor.js +73 -16
  5. package/docs/stryker.config.md +6 -0
  6. package/docs/vitest.config.md +6 -0
  7. package/lib/docs/llm.md +29 -0
  8. package/lib/docs/models.md +24 -17
  9. package/lib/docs/omlx.md +32 -0
  10. package/lib/llm.mjs +137 -0
  11. package/lib/omlx.mjs +49 -4
  12. package/package.json +1 -1
  13. package/rules/abie/docs/fix.md +6 -0
  14. package/rules/abie/js/docs/applies.md +6 -0
  15. package/rules/abie/js/docs/env_dns.md +25 -22
  16. package/rules/abie/js/docs/firebase_hosting.md +6 -0
  17. package/rules/abie/js/docs/hc_pairing.md +21 -25
  18. package/rules/abie/js/docs/ua_http_route.md +27 -19
  19. package/rules/abie/js/docs/ua_node_selector.md +24 -19
  20. package/rules/abie/lib/docs/enabled.md +13 -7
  21. package/rules/abie/lib/docs/env-dns.md +9 -3
  22. package/rules/abie/lib/docs/hc-yaml.md +6 -0
  23. package/rules/abie/lib/docs/http-route.md +6 -0
  24. package/rules/abie/lib/docs/k8s-tree.md +6 -0
  25. package/rules/abie/lib/docs/kustomization-patches.md +6 -0
  26. package/rules/abie/lib/docs/overlay-paths.md +6 -0
  27. package/rules/abie/lib/docs/yaml.md +6 -0
  28. package/rules/adr/docs/fix.md +6 -0
  29. package/rules/adr/js/docs/hooks.md +29 -244
  30. package/rules/bun/docs/fix.md +6 -0
  31. package/rules/bun/js/docs/layout.md +37 -375
  32. package/rules/capacitor/docs/fix.md +22 -108
  33. package/rules/capacitor/js/docs/platforms.md +62 -268
  34. package/rules/changelog/docs/fix.md +6 -0
  35. package/rules/changelog/js/docs/consistency.md +36 -383
  36. package/rules/changelog/lib/docs/package-manifest.md +6 -0
  37. package/rules/ci4/docs/fix.md +23 -165
  38. package/rules/ci4/js/docs/marksman_config.md +9 -1
  39. package/rules/docker/docs/fix.md +6 -0
  40. package/rules/docker/js/docs/lint.md +55 -239
  41. package/rules/docker/lib/docs/docker-hadolint.md +6 -0
  42. package/rules/docker/lib/docs/docker-mirror.md +6 -0
  43. package/rules/docker/lib/docs/docker-native-addon.md +6 -0
  44. package/rules/docker/lib/docs/docker-nginx-user.md +6 -0
  45. package/rules/docker/lint/docs/lint.md +9 -1
  46. package/rules/efes/docs/fix.md +6 -0
  47. package/rules/feedback/docs/fix.md +21 -131
  48. package/rules/ga/docs/fix.md +14 -12
  49. package/rules/ga/js/docs/lint.md +12 -9
  50. package/rules/ga/js/docs/workflows.md +20 -19
  51. package/rules/ga/lint/docs/lint.md +6 -0
  52. package/rules/graphql/docs/fix.md +6 -0
  53. package/rules/graphql/js/docs/tooling.md +18 -253
  54. package/rules/graphql/lib/docs/graphql-gql-scan.md +6 -0
  55. package/rules/hasura/docs/fix.md +18 -111
  56. package/rules/image-avif/docs/fix.md +6 -0
  57. package/rules/image-avif/js/docs/avif_generation.md +6 -0
  58. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +9 -3
  59. package/rules/js-bun-redis/lib/docs/redis-imports.md +6 -0
  60. package/rules/js-lint/js/docs/utils_imports.md +6 -0
  61. package/rules/js-lint-ci/docs/fix.md +7 -1
  62. package/rules/js-mssql/docs/fix.md +6 -0
  63. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +6 -0
  64. package/rules/js-run/docs/fix.md +6 -0
  65. package/rules/js-run/lib/docs/bunyan-imports.md +6 -0
  66. package/rules/js-run/lib/docs/check-env-scan.md +6 -0
  67. package/rules/js-run/lib/docs/conn-file-rules.md +6 -0
  68. package/rules/js-run/lib/docs/conn-imports-scan.md +6 -0
  69. package/rules/js-run/lib/docs/promise-settimeout-scan.md +6 -0
  70. package/rules/js-run/lib/docs/temporal-scan.md +6 -0
  71. package/rules/k8s/docs/fix.md +6 -0
  72. package/rules/k8s/lint/docs/lint.md +6 -0
  73. package/rules/nginx-default-tpl/docs/fix.md +6 -0
  74. package/rules/npm-module/js/docs/header_doc_pointer.md +7 -0
  75. package/rules/npm-module/js/header_doc_pointer.mjs +2 -8
  76. package/rules/php/docs/fix.md +6 -0
  77. package/rules/php/lint/docs/lint.md +6 -0
  78. package/rules/python/docs/fix.md +6 -0
  79. package/rules/python/lint/docs/lint.md +6 -0
  80. package/rules/rego/lint/docs/lint.md +6 -0
  81. package/rules/release/docs/change.md +6 -0
  82. package/rules/release/docs/fix.md +6 -0
  83. package/rules/release/docs/release.md +6 -0
  84. package/rules/release/lib/docs/aggregate.md +6 -0
  85. package/rules/release/lib/docs/change-file.md +6 -0
  86. package/rules/release/lib/docs/fallback.md +6 -0
  87. package/rules/rust/lib/docs/has-cargo-toml.md +6 -0
  88. package/rules/security/docs/fix.md +7 -1
  89. package/rules/security/js/docs/lint.md +6 -0
  90. package/rules/style-lint/docs/fix.md +6 -0
  91. package/rules/tauri/docs/fix.md +6 -0
  92. package/rules/test/docs/fix.md +6 -0
  93. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +6 -0
  94. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +6 -0
  95. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +6 -0
  96. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +6 -0
  97. package/rules/text/docs/fix.md +6 -0
  98. package/rules/text/lint/docs/lint.md +6 -0
  99. package/rules/text/lint/docs/run-dotenv-linter.md +6 -0
  100. package/rules/text/lint/docs/run-shellcheck.md +6 -0
  101. package/rules/text/lint/docs/run-v8r.md +6 -0
  102. package/rules/vue/lib/docs/vue-forbidden-imports.md +6 -0
  103. package/scripts/coverage-classify/cache.mjs +1 -1
  104. package/scripts/coverage-classify/docs/apply.md +6 -0
  105. package/scripts/coverage-classify/docs/cache.md +6 -0
  106. package/scripts/coverage-classify/docs/prompt.md +6 -0
  107. package/scripts/coverage-classify/docs/verdict-schema.md +6 -0
  108. package/scripts/coverage-classify/prompt.mjs +1 -1
  109. package/scripts/coverage-fix-extract.mjs +1 -1
  110. package/scripts/coverage-fix.mjs +2 -1
  111. package/scripts/docs/auto-skills.md +6 -0
  112. package/scripts/docs/build-agents-commands.md +7 -1
  113. package/scripts/docs/cli-entry.md +6 -0
  114. package/scripts/docs/coverage-fix-extract.md +6 -0
  115. package/scripts/docs/coverage-fix.md +6 -0
  116. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +6 -0
  117. package/scripts/docs/lint-cli.md +6 -0
  118. package/scripts/docs/post-tool-use-fix.md +6 -0
  119. package/scripts/docs/rename-yaml-extensions.md +6 -0
  120. package/scripts/docs/skills-cli.md +6 -0
  121. package/scripts/docs/sync-setup-bun-deps-action.md +6 -0
  122. package/scripts/docs/upgrade-nitra-cursor-and-install.md +6 -0
  123. package/scripts/docs/worktree-cli.md +6 -0
  124. package/scripts/lib/docs/assert-project-root.md +6 -0
  125. package/scripts/lib/docs/check-mdc-template-refs.md +6 -0
  126. package/scripts/lib/docs/check-reporter.md +6 -0
  127. package/scripts/lib/docs/diff-added-lines.md +6 -0
  128. package/scripts/lib/docs/discover-check-rules-from-cursor.md +6 -0
  129. package/scripts/lib/docs/discover-checkable-rules.md +6 -0
  130. package/scripts/lib/docs/ensure-tool.md +6 -0
  131. package/scripts/lib/docs/generated-markdown.md +6 -0
  132. package/scripts/lib/docs/gha-workflow.md +6 -0
  133. package/scripts/lib/docs/inline-template-links.md +6 -0
  134. package/scripts/lib/docs/list-rule-ids.md +6 -0
  135. package/scripts/lib/docs/load-cursor-config.md +6 -0
  136. package/scripts/lib/docs/mirror-parity.md +6 -0
  137. package/scripts/lib/docs/read-n-cursor-config-lite.md +6 -0
  138. package/scripts/lib/docs/resolve-target-files.md +6 -0
  139. package/scripts/lib/docs/root-notice.md +6 -0
  140. package/scripts/lib/docs/rule-meta-helpers.md +6 -0
  141. package/scripts/lib/docs/rule-meta.md +6 -0
  142. package/scripts/lib/docs/run-conftest-batch.md +6 -0
  143. package/scripts/lib/docs/run-lint-step.md +6 -0
  144. package/scripts/lib/docs/run-rule-cli.md +6 -0
  145. package/scripts/lib/docs/run-rule.md +6 -0
  146. package/scripts/lib/docs/run-standard-lint.md +6 -0
  147. package/scripts/lib/docs/run-standard-rule.md +6 -0
  148. package/scripts/lib/docs/skill-meta.md +6 -0
  149. package/scripts/lib/docs/template.md +6 -0
  150. package/scripts/lib/docs/timing-summary.md +6 -0
  151. package/scripts/lib/docs/workspaces.md +6 -0
  152. package/scripts/lib/docs/worktree-notice.md +6 -0
  153. package/scripts/lib/docs/worktree.md +6 -0
  154. package/scripts/lib/mirror-parity.mjs +1 -1
  155. package/scripts/lib/root-notice.mjs +1 -1
  156. package/scripts/lib/worktree-notice.mjs +5 -5
  157. package/scripts/lib/worktree.mjs +1 -1
  158. package/scripts/sync-claude-config.mjs +3 -0
  159. package/scripts/utils/docs/ast-scan-utils.md +6 -0
  160. package/scripts/utils/docs/ensure-gitignore-entries.md +6 -0
  161. package/scripts/utils/docs/find-package-json-paths.md +6 -0
  162. package/scripts/utils/docs/lock-cache-dir.md +6 -0
  163. package/scripts/utils/docs/pass.md +6 -0
  164. package/scripts/utils/docs/resolve-cargo-manifest.md +6 -0
  165. package/scripts/utils/docs/resolve-cmd.md +6 -0
  166. package/scripts/utils/docs/resolve-js-root.md +6 -0
  167. package/scripts/utils/docs/test-helpers.md +6 -0
  168. package/scripts/utils/docs/walk-cache.md +6 -0
  169. package/scripts/utils/docs/walkDir.md +6 -0
  170. package/scripts/utils/docs/worktree-fingerprint.md +6 -0
  171. package/scripts/utils/resolve-js-root.mjs +1 -1
  172. package/skills/doc-aggregate/SKILL.md +129 -0
  173. package/skills/doc-aggregate/js/docgen-ignore.mjs +9 -0
  174. package/skills/{docgen → doc-aggregate}/js/docgen-scan.mjs +22 -67
  175. package/skills/doc-aggregate/js/docs/docgen-ignore.md +21 -0
  176. package/skills/doc-files/SKILL.md +100 -0
  177. package/skills/doc-files/js/docgen-crc.mjs +164 -0
  178. package/skills/{docgen → doc-files}/js/docgen-extract-anchors.mjs +48 -13
  179. package/skills/{docgen → doc-files}/js/docgen-extract.mjs +39 -10
  180. package/skills/doc-files/js/docgen-files-batch.mjs +181 -0
  181. package/skills/doc-files/js/docgen-gen.mjs +336 -0
  182. package/skills/{docgen → doc-files}/js/docgen-prompts.mjs +65 -50
  183. package/skills/doc-files/js/docgen-scan.mjs +298 -0
  184. package/skills/doc-files/js/docs/docgen-crc.md +32 -0
  185. package/skills/doc-files/js/docs/docgen-extract-anchors.md +27 -0
  186. package/skills/doc-files/js/docs/docgen-extract.md +29 -0
  187. package/skills/doc-files/js/docs/docgen-files-batch.md +25 -0
  188. package/skills/doc-files/js/docs/docgen-gen.md +30 -0
  189. package/skills/doc-files/js/docs/docgen-prompts.md +32 -0
  190. package/skills/doc-files/js/docs/docgen-scan.md +25 -0
  191. package/skills/doc-files/js/units-js.mjs +139 -0
  192. package/skills/doc-files/js/units.mjs +19 -0
  193. package/skills/doc-files/meta.json +1 -0
  194. package/skills/fix/js/docs/llm-worker.md +6 -0
  195. package/skills/fix/js/docs/orchestrator.md +6 -0
  196. package/skills/fix/js/llm-worker.mjs +3 -3
  197. package/skills/fix/js/orchestrator.mjs +1 -1
  198. package/skills/start-check/js/check.mjs +5 -3
  199. package/skills/start-check/js/docs/check.md +6 -0
  200. package/skills/docgen/SKILL.md +0 -224
  201. package/skills/docgen/bench/etalon/firebase_hosting.md +0 -19
  202. package/skills/docgen/bench/etalon/k8s-tree.md +0 -24
  203. package/skills/docgen/bench/etalon/overlay-paths.md +0 -24
  204. package/skills/docgen/js/docgen-batch-omlx.mjs +0 -82
  205. package/skills/docgen/js/docgen-batch.mjs +0 -95
  206. package/skills/docgen/js/docgen-compare-pi-vs-direct.mjs +0 -95
  207. package/skills/docgen/js/docgen-gen.mjs +0 -306
  208. package/skills/docgen/js/docs/docgen-extract.md +0 -28
  209. package/skills/docgen/js/docs/docgen-gen.md +0 -41
  210. package/skills/docgen/js/docs/docgen-ignore.md +0 -24
  211. package/skills/docgen/js/docs/docgen-prompts.md +0 -24
  212. package/skills/docgen/js/docs/docgen-scan.md +0 -48
  213. /package/skills/{docgen → doc-aggregate}/meta.json +0 -0
  214. /package/skills/{docgen → doc-files}/js/docgen-ignore.mjs +0 -0
@@ -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
+ }
@@ -15,13 +15,23 @@
15
15
  */
16
16
 
17
17
  const URL_RE = /https?:\/\/[^\s'"`)<>]+/g
18
+ // Після обрізання template-частини URL має лишитися host (R10).
19
+ const STATIC_URL_RE = /^https?:\/\/[^/${]+/
18
20
  const EXPORT_CONST_RE = /export\s+const\s+([A-Z][A-Z0-9_]+)\s*=\s*(['"`])([^'"`]+)\2/g
19
21
  const ERROR_MARKER_RE = /\(([a-z][\w-]*\.mdc)\)/g
20
- const CONFIG_REF_RE = /\b(\.[a-z][\w.-]*\.json)\b/gi
22
+ // Повне ім'я json-конфіга (з опційним провідним дотом). Lookbehind `(?<![\w.])`
23
+ // не дає почати матч усередині складеного імені — інакше `settings.local.json`
24
+ // дало б хибний анкор `.local.json`, а `capacitor.config.json` → `.config.json`,
25
+ // і модель, маючи їх у «обов'язкових анкорах», писала б неіснуючий файл як факт.
26
+ const CONFIG_REF_RE = /(?<![\w.])(\.?[a-z][\w.-]*\.json)\b/gi
21
27
  const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
22
- const CODE_BLOCK_RE = /```[a-z]*\n([\s\S]*?)\n\s*\*?\s*```/g
28
+ const CODE_BLOCK_RE = /```[a-z]{0,12}\n([\s\S]*?)\n[ \t]{0,8}\*?[ \t]{0,8}```/g
23
29
 
24
- /** Dedup масив, зберігаючи порядок появи. */
30
+ /**
31
+ * Dedup масив, зберігаючи порядок появи.
32
+ * @param {Array<string>} arr вхідний масив
33
+ * @returns {Array<string>} масив без дублікатів у вихідному порядку
34
+ */
25
35
  function uniq(arr) {
26
36
  const seen = new Set()
27
37
  const out = []
@@ -36,17 +46,26 @@ function uniq(arr) {
36
46
 
37
47
  /**
38
48
  * Витягує анкори з вихідного коду файла.
39
- * @param {string} src
49
+ * @param {string} src вміст файлу
40
50
  * @returns {{
41
51
  * urls: string[],
42
52
  * magicStrings: Array<{name:string, value:string}>,
43
53
  * errorMarkers: string[],
44
54
  * configRefs: string[],
45
55
  * examples: string[]
46
- * }}
56
+ * }} категоризовані анкори файлу
47
57
  */
48
58
  export function extractAnchors(src) {
49
- const urls = uniq(Array.from(src.matchAll(URL_RE), m => m[0]))
59
+ // R10: template-literal URL (`https://h/${expr}/x`) обрізаємо на `${`, лишаючи
60
+ // статичний префікс. Інакше анкор тягне у доку сміття типу `…/${encodeURIComponent(name`.
61
+ const urls = uniq(
62
+ Array.from(src.matchAll(URL_RE), m => m[0])
63
+ .map(u => {
64
+ const i = u.indexOf('${')
65
+ return i === -1 ? u : u.slice(0, i)
66
+ })
67
+ .filter(u => STATIC_URL_RE.test(u))
68
+ )
50
69
 
51
70
  const magicStrings = []
52
71
  const seenNames = new Set()
@@ -69,24 +88,40 @@ export function extractAnchors(src) {
69
88
  return { urls, magicStrings, errorMarkers, configRefs, examples }
70
89
  }
71
90
 
91
+ /**
92
+ * Плоский список анкор-токенів, які мають дослівно зʼявитися в документі (R5):
93
+ * URLs, імена констант-рядків, маркери `(rule.mdc)`, конфіги. Приклади й
94
+ * code-блоки опускаються — їх багаторядковість не звіряється підрядком.
95
+ * @param {ReturnType<typeof extractAnchors>} a анкори файлу
96
+ * @returns {string[]} токени для перевірки покриття/валідності
97
+ */
98
+ export function anchorTokens(a) {
99
+ return [...a.urls, ...a.magicStrings.map(s => s.name), ...a.errorMarkers.map(m => `(${m})`), ...a.configRefs]
100
+ }
101
+
72
102
  /**
73
103
  * Форматує анкори у компактний текст для system-промпта.
74
104
  * Якщо анкорів немає взагалі — повертає порожній рядок (системний блок про
75
105
  * анкори не додається, щоб не вводити LLM в оману «обовʼязковими» полями).
76
- * @param {ReturnType<typeof extractAnchors>} a
77
- * @returns {string}
106
+ * @param {ReturnType<typeof extractAnchors>} a анкори файлу
107
+ * @returns {string} компактний текстовий блок або порожній рядок
78
108
  */
79
109
  export function anchorsToPrompt(a) {
80
110
  const blocks = []
81
111
  if (a.urls.length) blocks.push(`URLs (згадай у тексті): ${a.urls.join(', ')}`)
82
112
  if (a.magicStrings.length) {
83
- blocks.push(
84
- `Експортовані константи-рядки (наведи назву і призначення): ${a.magicStrings.map(s => `${s.name}=${JSON.stringify(s.value)}`).join('; ')}`
85
- )
113
+ const consts = a.magicStrings.map(s => s.name + '=' + JSON.stringify(s.value)).join('; ')
114
+ blocks.push(`Експортовані константи-рядки (наведи назву і призначення): ${consts}`)
115
+ }
116
+ if (a.errorMarkers.length) {
117
+ const markers = a.errorMarkers.map(m => '(' + m + ')').join(', ')
118
+ blocks.push(`Маркери повідомлень (згадай у Поведінці): ${markers}`)
86
119
  }
87
- if (a.errorMarkers.length) blocks.push(`Маркери повідомлень (згадай у Поведінці): ${a.errorMarkers.map(m => `(${m})`).join(', ')}`)
88
120
  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')}`)
121
+ if (a.examples.length) {
122
+ const fenced = a.examples.map(e => '```\n' + e + '\n```').join('\n')
123
+ blocks.push(`Приклади з документації автора (наведи дослівно у Поведінці):\n${fenced}`)
124
+ }
90
125
  if (!blocks.length) return ''
91
126
  return `АНКОРИ ДО ОБОВ'ЯЗКОВОГО ВКЛЮЧЕННЯ:\n${blocks.join('\n')}`
92
127
  }
@@ -25,21 +25,29 @@ 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
+ // Top-level function/class декларації (колонка 0) — для R6: службові функції,
34
+ // які не експортуються, не мають протікати у Поведінку/API як «публічні».
35
+ const TOP_FN_DECL_RE = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\*?|class)\s+(\w+)/gm
36
+ const IMPORT_FROM_RE = /^import[ \t]{1,8}[\s\S]{0,300}?from\s{1,8}['"]([^'"]+)['"]/gm
34
37
  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+.*/
38
+ const INTERNAL_IMPORT_RE = /import[ \t]{1,8}([^'"]{0,300}?)from[ \t]{1,8}['"]\.[^'"]{1,300}['"]/g
39
+ const NAMED_BRACES_RE = /\{([^}]{1,400})\}/
40
+ const IDENT_RE = /^[\w$]{1,80}$/
41
+ const IMPORT_AS_RE = /[ \t]{1,8}as[ \t]{1,8}.{0,200}/
37
42
  const WRITE_FS_RE = /\b(writeFile|mkdir|rmdir|unlink|appendFile|createWriteStream|rm\()/
38
43
  const CATCH_RE = /catch\s*\(/
39
44
  const TRY_RE = /\btry\s*\{/
40
45
  const FALSY_RETURN_RE = /return\s+(false|null|''|"")/
41
46
  const NETWORK_RE = /\bfetch\(|https?\.|axios|got\(/
42
- const CACHE_RE = /new Map\(\)|Cache|cache/
47
+ // Кеш лише за ІМЕНОВАНИМ маркером (`cache`/`Cache`/`memoize`), не за будь-яким
48
+ // `new Map()`: акумулятор (напр. `byPath = new Map()`) — не кеш, а хибна гарантія
49
+ // «Кешує результати» гірша за пропуск (фабрикація > мовчання).
50
+ const CACHE_RE = /cache|memoi[sz]e/i
43
51
 
44
52
  /**
45
53
  * Прибирає `/** *​/`-обрамлення й `*`-префікси, повертає чистий текст рядками.
@@ -152,12 +160,32 @@ function extractImports(src) {
152
160
  function extractInternalSymbols(src) {
153
161
  const out = new Set()
154
162
  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(',')) {
163
+ const clause = m[1]
164
+ const named = clause.match(NAMED_BRACES_RE)
165
+ if (named) {
166
+ for (const n of named[1].split(',')) {
158
167
  const name = n.replace(IMPORT_AS_RE, '').trim()
159
168
  if (name) out.add(name)
160
169
  }
170
+ }
171
+ const defName = clause.replace(NAMED_BRACES_RE, '').replaceAll(',', ' ').trim().split(' ')[0]
172
+ if (defName && IDENT_RE.test(defName)) out.add(defName)
173
+ }
174
+ return [...out]
175
+ }
176
+
177
+ /**
178
+ * Імена top-level функцій/класів, які НЕ експортуються (службові помічники).
179
+ * Модель не має подавати їх як «публічні функції» у Поведінці/API (R6).
180
+ * Const-стрілки свідомо не ловимо — менше false-positive на змістовних константах.
181
+ * @param {string} src вміст файлу
182
+ * @returns {Array<string>} список імен неекспортованих функцій/класів
183
+ */
184
+ function extractLocalSymbols(src) {
185
+ const exported = new Set(Array.from(src.matchAll(EXPORT_DECL_RE), m => m[2]))
186
+ const out = new Set()
187
+ for (const m of src.matchAll(TOP_FN_DECL_RE)) {
188
+ if (!exported.has(m[1])) out.add(m[1])
161
189
  }
162
190
  return [...out]
163
191
  }
@@ -201,6 +229,7 @@ export function extractFacts(src, relPath) {
201
229
  exports: extractExports(src),
202
230
  imports: extractImports(src),
203
231
  internalSymbols: extractInternalSymbols(src),
232
+ localSymbols: extractLocalSymbols(src),
204
233
  markers: extractMarkers(src)
205
234
  }
206
235
  }