@nitra/cursor 5.1.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.claude-template/settings.template.json +22 -0
  2. package/.pi-template/extensions/n-cursor-adr/docs/index.md +15 -9
  3. package/CHANGELOG.md +18 -1
  4. package/bin/n-cursor.js +73 -16
  5. package/docs/stryker.config.md +6 -0
  6. package/docs/vitest.config.md +6 -0
  7. package/lib/docs/llm.md +29 -0
  8. package/lib/docs/models.md +24 -17
  9. package/lib/docs/omlx.md +32 -0
  10. package/lib/llm.mjs +137 -0
  11. package/lib/omlx.mjs +49 -4
  12. package/package.json +1 -1
  13. package/rules/abie/docs/fix.md +6 -0
  14. package/rules/abie/js/docs/applies.md +6 -0
  15. package/rules/abie/js/docs/env_dns.md +25 -22
  16. package/rules/abie/js/docs/firebase_hosting.md +6 -0
  17. package/rules/abie/js/docs/hc_pairing.md +21 -25
  18. package/rules/abie/js/docs/ua_http_route.md +27 -19
  19. package/rules/abie/js/docs/ua_node_selector.md +24 -19
  20. package/rules/abie/lib/docs/enabled.md +13 -7
  21. package/rules/abie/lib/docs/env-dns.md +9 -3
  22. package/rules/abie/lib/docs/hc-yaml.md +6 -0
  23. package/rules/abie/lib/docs/http-route.md +6 -0
  24. package/rules/abie/lib/docs/k8s-tree.md +6 -0
  25. package/rules/abie/lib/docs/kustomization-patches.md +6 -0
  26. package/rules/abie/lib/docs/overlay-paths.md +6 -0
  27. package/rules/abie/lib/docs/yaml.md +6 -0
  28. package/rules/adr/docs/fix.md +6 -0
  29. package/rules/adr/js/docs/hooks.md +29 -244
  30. package/rules/bun/docs/fix.md +6 -0
  31. package/rules/bun/js/docs/layout.md +37 -375
  32. package/rules/capacitor/docs/fix.md +22 -108
  33. package/rules/capacitor/js/docs/platforms.md +62 -268
  34. package/rules/changelog/docs/fix.md +6 -0
  35. package/rules/changelog/js/docs/consistency.md +36 -383
  36. package/rules/changelog/lib/docs/package-manifest.md +6 -0
  37. package/rules/ci4/docs/fix.md +23 -165
  38. package/rules/ci4/js/docs/marksman_config.md +9 -1
  39. package/rules/docker/docs/fix.md +6 -0
  40. package/rules/docker/js/docs/lint.md +55 -239
  41. package/rules/docker/lib/docs/docker-hadolint.md +6 -0
  42. package/rules/docker/lib/docs/docker-mirror.md +6 -0
  43. package/rules/docker/lib/docs/docker-native-addon.md +6 -0
  44. package/rules/docker/lib/docs/docker-nginx-user.md +6 -0
  45. package/rules/docker/lint/docs/lint.md +9 -1
  46. package/rules/efes/docs/fix.md +6 -0
  47. package/rules/feedback/docs/fix.md +21 -131
  48. package/rules/ga/docs/fix.md +14 -12
  49. package/rules/ga/js/docs/lint.md +12 -9
  50. package/rules/ga/js/docs/workflows.md +20 -19
  51. package/rules/ga/lint/docs/lint.md +6 -0
  52. package/rules/graphql/docs/fix.md +6 -0
  53. package/rules/graphql/js/docs/tooling.md +18 -253
  54. package/rules/graphql/lib/docs/graphql-gql-scan.md +6 -0
  55. package/rules/hasura/docs/fix.md +18 -111
  56. package/rules/image-avif/docs/fix.md +6 -0
  57. package/rules/image-avif/js/docs/avif_generation.md +6 -0
  58. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +9 -3
  59. package/rules/js-bun-redis/lib/docs/redis-imports.md +6 -0
  60. package/rules/js-lint/js/docs/utils_imports.md +6 -0
  61. package/rules/js-lint-ci/docs/fix.md +7 -1
  62. package/rules/js-mssql/docs/fix.md +6 -0
  63. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +6 -0
  64. package/rules/js-run/docs/fix.md +6 -0
  65. package/rules/js-run/lib/docs/bunyan-imports.md +6 -0
  66. package/rules/js-run/lib/docs/check-env-scan.md +6 -0
  67. package/rules/js-run/lib/docs/conn-file-rules.md +6 -0
  68. package/rules/js-run/lib/docs/conn-imports-scan.md +6 -0
  69. package/rules/js-run/lib/docs/promise-settimeout-scan.md +6 -0
  70. package/rules/js-run/lib/docs/temporal-scan.md +6 -0
  71. package/rules/k8s/docs/fix.md +6 -0
  72. package/rules/k8s/lint/docs/lint.md +6 -0
  73. package/rules/nginx-default-tpl/docs/fix.md +6 -0
  74. package/rules/npm-module/js/docs/header_doc_pointer.md +7 -0
  75. package/rules/npm-module/js/header_doc_pointer.mjs +2 -8
  76. package/rules/php/docs/fix.md +6 -0
  77. package/rules/php/lint/docs/lint.md +6 -0
  78. package/rules/python/docs/fix.md +6 -0
  79. package/rules/python/lint/docs/lint.md +6 -0
  80. package/rules/rego/lint/docs/lint.md +6 -0
  81. package/rules/release/docs/change.md +6 -0
  82. package/rules/release/docs/fix.md +6 -0
  83. package/rules/release/docs/release.md +6 -0
  84. package/rules/release/lib/docs/aggregate.md +6 -0
  85. package/rules/release/lib/docs/change-file.md +6 -0
  86. package/rules/release/lib/docs/fallback.md +6 -0
  87. package/rules/rust/lib/docs/has-cargo-toml.md +6 -0
  88. package/rules/security/docs/fix.md +7 -1
  89. package/rules/security/js/docs/lint.md +6 -0
  90. package/rules/style-lint/docs/fix.md +6 -0
  91. package/rules/tauri/docs/fix.md +6 -0
  92. package/rules/test/docs/fix.md +6 -0
  93. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +6 -0
  94. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +6 -0
  95. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +6 -0
  96. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +6 -0
  97. package/rules/text/docs/fix.md +6 -0
  98. package/rules/text/lint/docs/lint.md +6 -0
  99. package/rules/text/lint/docs/run-dotenv-linter.md +6 -0
  100. package/rules/text/lint/docs/run-shellcheck.md +6 -0
  101. package/rules/text/lint/docs/run-v8r.md +6 -0
  102. package/rules/vue/lib/docs/vue-forbidden-imports.md +6 -0
  103. package/scripts/coverage-classify/cache.mjs +1 -1
  104. package/scripts/coverage-classify/docs/apply.md +6 -0
  105. package/scripts/coverage-classify/docs/cache.md +6 -0
  106. package/scripts/coverage-classify/docs/prompt.md +6 -0
  107. package/scripts/coverage-classify/docs/verdict-schema.md +6 -0
  108. package/scripts/coverage-classify/prompt.mjs +1 -1
  109. package/scripts/coverage-fix-extract.mjs +1 -1
  110. package/scripts/coverage-fix.mjs +2 -1
  111. package/scripts/docs/auto-skills.md +6 -0
  112. package/scripts/docs/build-agents-commands.md +7 -1
  113. package/scripts/docs/cli-entry.md +6 -0
  114. package/scripts/docs/coverage-fix-extract.md +6 -0
  115. package/scripts/docs/coverage-fix.md +6 -0
  116. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +6 -0
  117. package/scripts/docs/lint-cli.md +6 -0
  118. package/scripts/docs/post-tool-use-fix.md +6 -0
  119. package/scripts/docs/rename-yaml-extensions.md +6 -0
  120. package/scripts/docs/skills-cli.md +6 -0
  121. package/scripts/docs/sync-setup-bun-deps-action.md +6 -0
  122. package/scripts/docs/upgrade-nitra-cursor-and-install.md +6 -0
  123. package/scripts/docs/worktree-cli.md +6 -0
  124. package/scripts/lib/docs/assert-project-root.md +6 -0
  125. package/scripts/lib/docs/check-mdc-template-refs.md +6 -0
  126. package/scripts/lib/docs/check-reporter.md +6 -0
  127. package/scripts/lib/docs/diff-added-lines.md +6 -0
  128. package/scripts/lib/docs/discover-check-rules-from-cursor.md +6 -0
  129. package/scripts/lib/docs/discover-checkable-rules.md +6 -0
  130. package/scripts/lib/docs/ensure-tool.md +6 -0
  131. package/scripts/lib/docs/generated-markdown.md +6 -0
  132. package/scripts/lib/docs/gha-workflow.md +6 -0
  133. package/scripts/lib/docs/inline-template-links.md +6 -0
  134. package/scripts/lib/docs/list-rule-ids.md +6 -0
  135. package/scripts/lib/docs/load-cursor-config.md +6 -0
  136. package/scripts/lib/docs/mirror-parity.md +6 -0
  137. package/scripts/lib/docs/read-n-cursor-config-lite.md +6 -0
  138. package/scripts/lib/docs/resolve-target-files.md +6 -0
  139. package/scripts/lib/docs/root-notice.md +6 -0
  140. package/scripts/lib/docs/rule-meta-helpers.md +6 -0
  141. package/scripts/lib/docs/rule-meta.md +6 -0
  142. package/scripts/lib/docs/run-conftest-batch.md +6 -0
  143. package/scripts/lib/docs/run-lint-step.md +6 -0
  144. package/scripts/lib/docs/run-rule-cli.md +6 -0
  145. package/scripts/lib/docs/run-rule.md +6 -0
  146. package/scripts/lib/docs/run-standard-lint.md +6 -0
  147. package/scripts/lib/docs/run-standard-rule.md +6 -0
  148. package/scripts/lib/docs/skill-meta.md +6 -0
  149. package/scripts/lib/docs/template.md +6 -0
  150. package/scripts/lib/docs/timing-summary.md +6 -0
  151. package/scripts/lib/docs/workspaces.md +6 -0
  152. package/scripts/lib/docs/worktree-notice.md +6 -0
  153. package/scripts/lib/docs/worktree.md +6 -0
  154. package/scripts/lib/mirror-parity.mjs +1 -1
  155. package/scripts/lib/root-notice.mjs +1 -1
  156. package/scripts/lib/worktree-notice.mjs +5 -5
  157. package/scripts/lib/worktree.mjs +1 -1
  158. package/scripts/sync-claude-config.mjs +3 -0
  159. package/scripts/utils/docs/ast-scan-utils.md +6 -0
  160. package/scripts/utils/docs/ensure-gitignore-entries.md +6 -0
  161. package/scripts/utils/docs/find-package-json-paths.md +6 -0
  162. package/scripts/utils/docs/lock-cache-dir.md +6 -0
  163. package/scripts/utils/docs/pass.md +6 -0
  164. package/scripts/utils/docs/resolve-cargo-manifest.md +6 -0
  165. package/scripts/utils/docs/resolve-cmd.md +6 -0
  166. package/scripts/utils/docs/resolve-js-root.md +6 -0
  167. package/scripts/utils/docs/test-helpers.md +6 -0
  168. package/scripts/utils/docs/walk-cache.md +6 -0
  169. package/scripts/utils/docs/walkDir.md +6 -0
  170. package/scripts/utils/docs/worktree-fingerprint.md +6 -0
  171. package/scripts/utils/resolve-js-root.mjs +1 -1
  172. package/skills/doc-aggregate/SKILL.md +129 -0
  173. package/skills/doc-aggregate/js/docgen-ignore.mjs +9 -0
  174. package/skills/{docgen → doc-aggregate}/js/docgen-scan.mjs +22 -67
  175. package/skills/doc-aggregate/js/docs/docgen-ignore.md +21 -0
  176. package/skills/doc-files/SKILL.md +100 -0
  177. package/skills/doc-files/js/docgen-crc.mjs +164 -0
  178. package/skills/{docgen → doc-files}/js/docgen-extract-anchors.mjs +48 -13
  179. package/skills/{docgen → doc-files}/js/docgen-extract.mjs +39 -10
  180. package/skills/doc-files/js/docgen-files-batch.mjs +181 -0
  181. package/skills/doc-files/js/docgen-gen.mjs +336 -0
  182. package/skills/{docgen → doc-files}/js/docgen-prompts.mjs +65 -50
  183. package/skills/doc-files/js/docgen-scan.mjs +298 -0
  184. package/skills/doc-files/js/docs/docgen-crc.md +32 -0
  185. package/skills/doc-files/js/docs/docgen-extract-anchors.md +27 -0
  186. package/skills/doc-files/js/docs/docgen-extract.md +29 -0
  187. package/skills/doc-files/js/docs/docgen-files-batch.md +25 -0
  188. package/skills/doc-files/js/docs/docgen-gen.md +30 -0
  189. package/skills/doc-files/js/docs/docgen-prompts.md +32 -0
  190. package/skills/doc-files/js/docs/docgen-scan.md +25 -0
  191. package/skills/doc-files/js/units-js.mjs +139 -0
  192. package/skills/doc-files/js/units.mjs +19 -0
  193. package/skills/doc-files/meta.json +1 -0
  194. package/skills/fix/js/docs/llm-worker.md +6 -0
  195. package/skills/fix/js/docs/orchestrator.md +6 -0
  196. package/skills/fix/js/llm-worker.mjs +3 -3
  197. package/skills/fix/js/orchestrator.mjs +1 -1
  198. package/skills/start-check/js/check.mjs +5 -3
  199. package/skills/start-check/js/docs/check.md +6 -0
  200. package/skills/docgen/SKILL.md +0 -224
  201. package/skills/docgen/bench/etalon/firebase_hosting.md +0 -19
  202. package/skills/docgen/bench/etalon/k8s-tree.md +0 -24
  203. package/skills/docgen/bench/etalon/overlay-paths.md +0 -24
  204. package/skills/docgen/js/docgen-batch-omlx.mjs +0 -82
  205. package/skills/docgen/js/docgen-batch.mjs +0 -95
  206. package/skills/docgen/js/docgen-compare-pi-vs-direct.mjs +0 -95
  207. package/skills/docgen/js/docgen-gen.mjs +0 -306
  208. package/skills/docgen/js/docs/docgen-extract.md +0 -28
  209. package/skills/docgen/js/docs/docgen-gen.md +0 -41
  210. package/skills/docgen/js/docs/docgen-ignore.md +0 -24
  211. package/skills/docgen/js/docs/docgen-prompts.md +0 -24
  212. package/skills/docgen/js/docs/docgen-scan.md +0 -48
  213. /package/skills/{docgen → doc-aggregate}/meta.json +0 -0
  214. /package/skills/{docgen → doc-files}/js/docgen-ignore.mjs +0 -0
@@ -0,0 +1,32 @@
1
+ ---
2
+ docgen:
3
+ source: npm/skills/doc-files/js/docgen-prompts.mjs
4
+ crc: c454d2a6
5
+ ---
6
+
7
+ # docgen-prompts
8
+
9
+ ## Огляд
10
+
11
+ Промптовий шар local-only конвеєра файлових док: будує messages-набори для секційної генерації, критики й переписування, а секцію «Гарантії поведінки» формує детерміновано без LLM. Весь стиль документа (українська, поведінковість, заборона сигнатур) зашитий у спільний system-блок.
12
+
13
+ ## Поведінка
14
+
15
+ 1. Для кожної секції збирається мінімальний контекст: «Огляд» — лише людиночитний витяг фактів (без коду), «Поведінка» — єдина секція з повним кодом файлу у вікні, «Публічний API» — лише список експортів з їхніми описами. Анкори (URL, константи, маркери, приклади) додаються окремим блоком, коли вони є.
16
+ 2. Факт-лист перекладається в негативні й позитивні твердження для моделі: свідомі пропуски шляхів, read-only, перехоплення помилок, явне «Кешування: НЕМАЄ — не згадуй кеш» — щоб відсікти типові галюцинації ще в промпті.
17
+ 3. Критик отримує чорнетку секції і закритий список критеріїв дефектів (generic-фрази, пропущені анкори, граматика, підзаголовки, скопійовані сигнатури, вигадані факти); відповідь — список проблем або `NONE`. Переписувач отримує чорнетку разом зі списком проблем і повертає лише оновлений текст.
18
+ 4. «Гарантії поведінки» складаються шаблоном з маркерів факт-листа (read-only, fail-safe, кешування, пропуски шляхів, відсутність мережі) — нуль запитів і нуль generic-фраз; за відсутності маркерів — твердження про детермінованість.
19
+
20
+ ## Публічний API
21
+
22
+ - `sectionMessages` — секційні набори messages з мінімальним контекстом під кожну секцію.
23
+ - `criticMessages` / `refineMessages` — пара критика й переписувача для одного циклу уточнення секції.
24
+ - `guaranteesFromMarkers` — детермінований текст секції гарантій з маркерів.
25
+ - `oneShotMessages` — один запит на весь документ для нестандартних структур файлів.
26
+ - `STYLE` — спільний system-блок стилю.
27
+
28
+ ## Гарантії поведінки
29
+
30
+ - Код файлу потрапляє лише у промпт секції «Поведінка» й у one-shot — решта секцій працюють на факт-листі.
31
+ - Формування промптів детерміноване: однакові факти й код → однакові messages.
32
+ - Не звертається до мережі й не виконує LLM-викликів — лише будує тексти.
@@ -0,0 +1,25 @@
1
+ ---
2
+ docgen:
3
+ source: npm/skills/doc-files/js/docgen-scan.mjs
4
+ crc: b517ab25
5
+ ---
6
+
7
+ # docgen-scan
8
+
9
+ ## Огляд
10
+
11
+ Сканер кодових файлів і детектор стану файлових док: які джерела підлягають документуванню, де лежить їхня дока, що застаріло, що degraded. Працює і як бібліотека для batch-генерації, і як CLI (`doc-files scan` / `doc-files check`) для хуків.
12
+
13
+ ## Поведінка
14
+
15
+ 1. Рекурсивний обхід від кореня збирає кодові файли (`js`/`mjs`/`ts`/`vue`/`py`), пропускаючи тести, оголошення типів, ignore-дерева і теки `docs/`; для кореня з system-wide docs-layout файлові доки на верхньому рівні не плануються.
16
+ 2. Для кожного джерела обчислюється шлях доки (`<dir>/docs/<stem>.md`) і стан застарілості за контрольною сумою.
17
+ 3. `check` працює в режимах: `--hook` — один файл зі stdin-payload редакторського хука; `--git` — змінені відносно HEAD джерела як Stop-гейт із порогом великого прогону (`--max`, дефолт `50` або `N_CURSOR_DOC_FILES_GATE_MAX`: більше застарілих — попередження без блоку); явні шляхи — точкова перевірка.
18
+ 4. Застарілі доки → exit-код `2` зі списком і підказкою перегенерації; усе свіже або великий прогін — `0`.
19
+ 5. `check --degraded` — інформаційний звіт (exit `0`): свіжі за сумою доки з оцінкою нижче порогу, з кодами проблем і підказкою про `gen --retry-degraded`. Degraded — видимий борг, не гейт.
20
+
21
+ ## Гарантії поведінки
22
+
23
+ - Сканер read-only: жодних записів у дерево.
24
+ - Рішення «застаріла/свіжа» детерміноване і залежить лише від вмісту джерела й frontmatter доки.
25
+ - Stop-гейт ніколи не блокує масовий перший прогін понад поріг — захист від нескінченного блокування задач.
@@ -0,0 +1,139 @@
1
+ /** @see ./docs/units-js.md */
2
+
3
+ import { parseProgramOrNull, walkAstWithAncestors } from '../../../scripts/utils/ast-scan-utils.mjs'
4
+
5
+ // JSDoc-блок, що стоїть впритул перед позицією (лише пробіли між ними).
6
+ const JSDOC_BEFORE_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
7
+ const JSDOC_OPEN_RE = /^\s*\/\*\*?/
8
+ const JSDOC_CLOSE_RE = /\*\/\s*$/
9
+ const STAR_PREFIX_RE = /^\s*\*?\s?/
10
+
11
+ /**
12
+ * Очищає JSDoc від обрамлення `/** *​/` і `*`-префіксів.
13
+ * @param {string} raw сирий блок або порожній рядок
14
+ * @returns {string} текст опису без тегів-обрамлення
15
+ */
16
+ function cleanDoc(raw) {
17
+ if (!raw) return ''
18
+ return raw
19
+ .replace(JSDOC_OPEN_RE, '')
20
+ .replace(JSDOC_CLOSE_RE, '')
21
+ .split('\n')
22
+ .map(l => l.replace(STAR_PREFIX_RE, '').trimEnd())
23
+ .join('\n')
24
+ .trim()
25
+ }
26
+
27
+ /**
28
+ * JSDoc, що передує позиції `start` у джерелі (або порожній рядок).
29
+ * @param {string} src вміст файлу
30
+ * @param {number} start зміщення початку декларації
31
+ * @returns {string} очищений опис
32
+ */
33
+ function precedingDoc(src, start) {
34
+ const m = src.slice(0, start).match(JSDOC_BEFORE_RE)
35
+ return cleanDoc(m ? m[0] : '')
36
+ }
37
+
38
+ /**
39
+ * Імʼя функції, що викликається (проста Identifier або `obj.method`).
40
+ * @param {Record<string, unknown>} node CallExpression
41
+ * @returns {string|null} імʼя callee або null
42
+ */
43
+ function calleeName(node) {
44
+ const c = node.callee
45
+ if (!c || typeof c !== 'object') return null
46
+ if (c.type === 'Identifier') return c.name
47
+ if (c.type === 'MemberExpression' && !c.computed && c.property?.type === 'Identifier') return c.property.name
48
+ return null
49
+ }
50
+
51
+ /**
52
+ * Множина імен, що викликаються у тілі вузла (сирі callee — фільтрація на ребра
53
+ * call-graph робиться у `extractUnitsJs` після збору всіх імен юнітів).
54
+ * @param {unknown} node AST-вузол юніта
55
+ * @returns {Set<string>} імена викликів
56
+ */
57
+ function collectCalls(node) {
58
+ const names = new Set()
59
+ walkAstWithAncestors(node, [], n => {
60
+ if (n.type === 'CallExpression') {
61
+ const name = calleeName(n)
62
+ if (name) names.add(name)
63
+ }
64
+ })
65
+ return names
66
+ }
67
+
68
+ /**
69
+ * Будує юніт із декларації, додає у `units`. Розпізнає function/class та
70
+ * const-функції (`const x = () => {}` / `function expression`).
71
+ * @param {Record<string, unknown>} decl декларація (function/class/variable)
72
+ * @param {boolean} exported чи експортується
73
+ * @param {number} docStart зміщення для пошуку JSDoc (зовнішній export-вузол)
74
+ * @param {string} src вміст файлу
75
+ * @param {Array<object>} units акумулятор
76
+ * @returns {void}
77
+ */
78
+ function pushUnits(decl, exported, docStart, src, units) {
79
+ if (!decl || typeof decl !== 'object') return
80
+ const doc = precedingDoc(src, docStart)
81
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
82
+ const name = decl.id?.name
83
+ if (!name) return
84
+ units.push({
85
+ name,
86
+ kind: decl.type === 'ClassDeclaration' ? 'class' : 'function',
87
+ exported,
88
+ span: { start: decl.start, end: decl.end },
89
+ body: src.slice(decl.start, decl.end),
90
+ calls: collectCalls(decl),
91
+ doc
92
+ })
93
+ return
94
+ }
95
+ if (decl.type === 'VariableDeclaration') {
96
+ for (const d of decl.declarations ?? []) {
97
+ const init = d.init
98
+ const isFn = init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
99
+ if (!isFn || d.id?.type !== 'Identifier') continue
100
+ units.push({
101
+ name: d.id.name,
102
+ kind: 'const',
103
+ exported,
104
+ span: { start: init.start, end: init.end },
105
+ body: src.slice(init.start, init.end),
106
+ calls: collectCalls(init),
107
+ doc
108
+ })
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Юніт-шар для js/mjs/ts: top-level функції/класи/const-функції з тілом, JSDoc,
115
+ * прапором експорту і ребрами call-graph (виклики ІНШИХ юнітів у тілі).
116
+ * @param {string} src вміст файлу
117
+ * @param {string} [relPath] шлях (для вибору мови oxc)
118
+ * @returns {Array<{name:string, kind:string, exported:boolean, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null} юніти або null, якщо файл не парситься
119
+ */
120
+ export function extractUnitsJs(src, relPath = 'scan.ts') {
121
+ const program = parseProgramOrNull(src, relPath)
122
+ if (!program || !Array.isArray(program.body)) return null
123
+
124
+ const units = []
125
+ for (const node of program.body) {
126
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
127
+ pushUnits(node.declaration, true, node.start, src, units)
128
+ } else if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
129
+ pushUnits(node.declaration, true, node.start, src, units)
130
+ } else {
131
+ pushUnits(node, false, node.start, src, units)
132
+ }
133
+ }
134
+
135
+ // Ребра call-graph: лишаємо тільки виклики інших внутрішніх юнітів
136
+ const names = new Set(units.map(u => u.name))
137
+ for (const u of units) u.calls = [...u.calls].filter(n => names.has(n) && n !== u.name)
138
+ return units
139
+ }
@@ -0,0 +1,19 @@
1
+ /** @see ./docs/units.md */
2
+
3
+ import { extractUnitsJs } from './units-js.mjs'
4
+
5
+ const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
6
+
7
+ /**
8
+ * Мовно-агностичний фасад юніт-шару (Інкремент 1). Диспатчить за розширенням:
9
+ * js/mjs/ts → oxc; vue/py — додаються наступними кроками (поки `null` → виклик
10
+ * відкочується на whole-file шлях, як і раніше).
11
+ * @param {string} src вміст файлу
12
+ * @param {string} relPath шлях файлу
13
+ * @returns {Array<object>|null} юніти або null, якщо мова ще не підтримана / файл не парситься
14
+ */
15
+ export function extractUnits(src, relPath) {
16
+ const ext = (relPath.split('.').pop() || '').toLowerCase()
17
+ if (JS_EXT.has(ext)) return extractUnitsJs(src, relPath)
18
+ return null
19
+ }
@@ -0,0 +1 @@
1
+ { "auto": "завжди", "worktree": false, "requireRoot": true }
@@ -1,3 +1,9 @@
1
+ ---
2
+ docgen:
3
+ source: npm/skills/fix/js/llm-worker.mjs
4
+ crc: 8317f878
5
+ ---
6
+
1
7
  # llm-worker.mjs
2
8
 
3
9
  ## Огляд
@@ -1,3 +1,9 @@
1
+ ---
2
+ docgen:
3
+ source: npm/skills/fix/js/orchestrator.mjs
4
+ crc: 8b7a7de5
5
+ ---
6
+
1
7
  # orchestrator.mjs
2
8
 
3
9
  ## Огляд
@@ -12,7 +12,7 @@ import { callOmlx, isOmlxModel } from '../../../lib/omlx.mjs'
12
12
  export const MODEL = env.N_CURSOR_FIX_MODEL ?? resolveModel('min')
13
13
  export const MODEL_HEAVY = env.N_CURSOR_FIX_MODEL_HEAVY ?? resolveModel('avg')
14
14
 
15
- const JSON_CODE_BLOCK_RE = /```(?:json)?\s*([\s\S]*?)```/
15
+ const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
16
16
 
17
17
  /**
18
18
  * Витягує відносні шляхи файлів із violation output.
@@ -35,7 +35,7 @@ function extractFilePaths(output) {
35
35
  }
36
36
 
37
37
  // Патерн без workspace: просто path/to/file.ext або ./file.ext
38
- const re = /(?:^|\s)(\.?[\w][\w./-]*\.(?:json|js|mjs|ts|vue|yml|yaml|toml|mdc|md|sh|py))(?::\d+)?/gm
38
+ const re = /(?:^|\s)(\.?\w[\w./-]*\.(?:json|js|mjs|ts|vue|yml|yaml|toml|mdc|md|sh|py))(?::\d+)?/gm
39
39
  for (const m of output.matchAll(re)) {
40
40
  const p = m[1]
41
41
  if (!seen.has(p)) {
@@ -116,7 +116,7 @@ function callModel(prompt, model) {
116
116
  error: [
117
117
  `pi: немає ключа для ${provider}.`,
118
118
  `Встановіть N_CLOUD_MIN_MODEL=provider/model-id`,
119
- `(напр.: openai/gpt-5.4-mini, google/gemini-2.5-flash, ollama/gemma3:4b)`,
119
+ `(напр.: openai/gpt-5.4-mini, google/gemini-2.5-flash, ollama/gemma3:4b)`
120
120
  ].join(' ')
121
121
  }
122
122
  }
@@ -20,7 +20,7 @@ export async function runOrchestratorCli(args, cwd) {
20
20
 
21
21
  const maxIterIdx = args.indexOf('--max-iter')
22
22
  const maxIter =
23
- maxIterIdx === -1 ? DEFAULT_MAX_ITER : (Number(args[maxIterIdx + 1] ?? DEFAULT_MAX_ITER) || DEFAULT_MAX_ITER)
23
+ maxIterIdx === -1 ? DEFAULT_MAX_ITER : Number(args[maxIterIdx + 1] ?? DEFAULT_MAX_ITER) || DEFAULT_MAX_ITER
24
24
  const skipIdxs = new Set(maxIterIdx === -1 ? [] : [maxIterIdx, maxIterIdx + 1])
25
25
  const ruleFilter = args.filter((a, i) => !a.startsWith('-') && !skipIdxs.has(i))
26
26
 
@@ -67,8 +67,8 @@ export async function scanStartWorkspaces(cwd) {
67
67
  * @param {string} log обʼєднаний stdout+stderr
68
68
  * @returns {{ready:boolean, firstError:string|null, logTail:string}} витяг
69
69
  */
70
- export function parseStartLog(log) {
71
- const text = log ?? ''
70
+ export function parseStartLog(log = '') {
71
+ const text = log
72
72
  const lines = text.split('\n')
73
73
  const firstError = lines.find(l => ERROR_RE.test(l))?.trim() ?? null
74
74
  const logTail = lines
@@ -137,7 +137,9 @@ export async function runWorkspaceStart(cwd, workspace, opts = {}) {
137
137
 
138
138
  // server: успіх = дожив до кінця grace (timedOut) або встиг віддати рядок готовності.
139
139
  // cli: успіх = чистий вихід 0 у межах grace.
140
- const status = type === 'server' ? (timedOut || ready ? 'OK' : 'FAIL') : exitCode === 0 ? 'OK' : 'FAIL'
140
+ let status
141
+ if (type === 'server') status = timedOut || ready ? 'OK' : 'FAIL'
142
+ else status = exitCode === 0 ? 'OK' : 'FAIL'
141
143
 
142
144
  return {
143
145
  workspace,
@@ -1,3 +1,9 @@
1
+ ---
2
+ docgen:
3
+ source: npm/skills/start-check/js/check.mjs
4
+ crc: 1809127a
5
+ ---
6
+
1
7
  # check.mjs
2
8
 
3
9
  ## Огляд
@@ -1,224 +0,0 @@
1
- ---
2
- name: docgen
3
- description: >-
4
- Обходить проєкт і для кожного кодового файлу (js/mjs/ts/vue/py) пише лаконічну поведінкову українську md-документацію у теку docs/ поряд із кодом — диспатчить окремого субагента на кожен файл, за правилами adr/ci4
5
- ---
6
-
7
- # docgen — генерація документації по файлах
8
-
9
- ## Мета
10
-
11
- Для кожного кодового файлу проєкту створити лаконічну поведінкову `.md`-документацію у теці `docs/`
12
- **поряд із самим файлом** (`<dir>/docs/<stem>.md`). Документацію пише **окремий субагент**
13
- на кожен файл — не один прохід, а батч-диспатч. Джерело правди стилю — правила `adr` і
14
- `ci4` (`docs/explanation`/`docs/adr`-каталоги з тих правил **не застосовуємо** — доку
15
- кладемо локально поряд із кодом).
16
-
17
- Документація — **трирівнева**, рівні виконуються строго послідовно:
18
-
19
- 1. **Tier 1 — файли**: `<dir>/docs/<stem>.md`, субагент на файл (нижче).
20
- 2. **Tier 2 — module-summary**: `<module_root>/docs/ARCHITECTURE.md`, субагент на модуль.
21
- 3. **Tier 3 — доменні доки**: `docs/<домен>.md` у кореневій `docs/`, субагент-синтезатор
22
- виділяє бізнес-домени й пише файл на кожен домен.
23
-
24
- Агрегат ніколи не випереджає джерело: Tier 2 — лише після завершення всього Tier 1,
25
- Tier 3 — лише після завершення всього Tier 2.
26
-
27
- ## ⚠️ Паралелізм
28
-
29
- Диспатч субагентів — **батчами по 5 одночасно**. Не запускати весь список одразу
30
- (перевантаження). Кожен субагент пише свій окремий файл — спільного стану немає, гонок
31
- за файли немає.
32
-
33
- Рівні строго послідовні: Tier 2 стартує лише після завершення всього Tier 1, Tier 3 —
34
- лише після всього Tier 2. Усередині Tier 1 і Tier 2 — батчі по 5. Tier 3 — один субагент.
35
-
36
- ## Передумова
37
-
38
- - Поточна директорія — корінь проєкту, який документуємо.
39
- - Доступний `npx @nitra/cursor` (пакет `@nitra/cursor` встановлено або через npx).
40
-
41
- ## Workflow
42
-
43
- ### Крок 1: Зібрати список файлів
44
-
45
- ```bash
46
- npx @nitra/cursor docgen scan
47
- ```
48
-
49
- Команда друкує JSON-масив об'єктів. Усі шляхи в ньому — відносні до кореня проєкту:
50
-
51
- ```json
52
- [{ "sourcePath": "src/lib/foo.js", "docPath": "src/lib/docs/foo.md", "exists": false }]
53
- ```
54
-
55
- Розпарси JSON.
56
-
57
- ### Крок 2: Відфільтрувати вже описані
58
-
59
- За замовчуванням **пропусти** елементи з `"exists": true`. Перегенеровуй їх лише якщо
60
- користувач явно попросив `--overwrite` (тоді обробляй усі). `--overwrite` — **не** прапор
61
- `docgen scan`: scanner лише лістить файли, а рішення «пропустити чи перегенерувати» приймаєш
62
- ти тут, фільтруючи за полем `exists`.
63
-
64
- Якщо після фільтра список порожній — зупинись:
65
-
66
- ```
67
- ✓ Усі кодові файли вже мають документацію. Нічого робити.
68
- ```
69
-
70
- Запам'ятай `total = довжина відфільтрованого списку`.
71
-
72
- ### Крок 3: Диспатч субагентів батчами по 5
73
-
74
- Розбий список на батчі по 5 елементів. Для кожного батчу запусти **до 5 субагентів
75
- одночасно (в одному повідомленні)**, дочекайся завершення батчу, переходь до наступного.
76
-
77
- Промпт кожного субагента (підстав `sourcePath` і `docPath`):
78
-
79
- ```
80
- Напиши лаконічну технічну документацію для одного файлу коду — орієнтовану на поведінку, не реалізацію.
81
-
82
- ФАЙЛ-ДЖЕРЕЛО: <sourcePath>
83
- ЗАПИСАТИ В: <docPath>
84
-
85
- Кроки:
86
- 1. Прочитай файл <sourcePath> повністю.
87
- 2. Створи теку для <docPath>, якщо її немає.
88
- 3. Запиши markdown-документ у <docPath> за правилами нижче.
89
-
90
- Правила документа (за adr/ci4):
91
- - Мова — УКРАЇНСЬКА для всього тексту (заголовки, абзаци, таблиці). Code identifiers,
92
- шляхи, імена API, команди — лишай як у коді (зазвичай ASCII).
93
- - ЧИСТИЙ Markdown. Жодних HTML-обгорток (<div>/<span>/класів) — токен-ефективність.
94
- - ФОКУС НА ПОВЕДІНЦІ, не реалізації. Пиши ЩО і НАВІЩО, а не як саме це зроблено.
95
- - НЕ перелічуй модулі стандартної бібліотеки (node:fs, node:path, node:crypto, python stdlib
96
- тощо) — вони не несуть бізнес-значення. Зовнішні залежності (npm-пакети, внутрішні модулі)
97
- згадуй лише якщо їхня роль не очевидна з контексту.
98
- - НЕ перелічуй внутрішні назви допоміжних функцій/змінних — описуй їхню роль і поведінку.
99
- Імена публічних exports згадуй лише коли export — справжня точка інтеграції, яку кличуть
100
- ззовні. Для дрібних/листкових модулів з однією відповідальністю опиши роль поведінково,
101
- БЕЗ сигнатур, таблиць типів і переліку параметрів — це деталі реалізації.
102
- - Контекстна незалежність: кожна секція самодостатня. Уникай «як вище», «ця функція», «той сервіс».
103
- - Секції (включай лише доречні — порожніх не вигадуй):
104
- ## Огляд — 1-3 речення: що файл робить і навіщо він існує (роль у системі). Згадай
105
- ключову семантику, якщо вона визначає сенс файлу (opt-in/gate, кеш, ідемпотентність тощо).
106
- ## Поведінка — покроковий алгоритм у бізнес-термінах (не деталі реалізації).
107
- Нумерований список: що відбувається, умови, гілки логіки. Якщо файл керується
108
- конфігом чи форматом даних — наведи короткий приклад (тільки реальний з коду).
109
- ## Публічний API — ЛИШЕ якщо модуль має нетривіальну зовнішню поверхню, яку називають
110
- споживачі. Для кожного export: назва + що робить. Без сигнатур і таблиць типів.
111
- Не дублюй ## Поведінка. Для модуля з однією функцією-предикатом цю секцію пропусти.
112
- ## Де використовується — де в системі цей файл вживається (якщо відомо з коду).
113
- ## Гарантії поведінки — інваріанти й крайові випадки: що гарантовано (read-only,
114
- не кидає винятків, fail-safe-значення за замовчуванням, безпечна обробка поганих даних)
115
- і що стається при відсутніх ресурсах чи некоректному вводі. Пропусти, якщо таких гарантій немає.
116
- - Для .vue додай ## Інтерфейс компонента — props (типи, defaults), emits, slots, реактивний стан.
117
- - НЕ вигадуй деталей, яких немає в коді.
118
- - Мета — Behavior Test: читач розуміє, що робить файл і як він вписується в систему,
119
- без потреби читати реалізацію.
120
-
121
- Поверни лише підтвердження, що файл <docPath> записано.
122
- ```
123
-
124
- ### Крок 4: Tier 2 — module-summary
125
-
126
- Після завершення **всіх** батчів Tier 1 зібрати список модулів:
127
-
128
- ```bash
129
- npx @nitra/cursor docgen modules
130
- ```
131
-
132
- Команда друкує JSON-масив:
133
-
134
- ```json
135
- [
136
- {
137
- "moduleRoot": "/abs/npm/rules/adr",
138
- "relRoot": "npm/rules/adr",
139
- "slug": "npm-rules-adr",
140
- "docPath": "/abs/npm/rules/adr/docs/ARCHITECTURE.md",
141
- "members": ["npm/rules/adr/index.mjs"],
142
- "exists": false
143
- }
144
- ]
145
- ```
146
-
147
- module-summary **завжди регенерується** (це агрегат — поле `exists` ігноруй). Розбий модулі на батчі по 5 і диспатч субагентів. Промпт кожного (підстав `relRoot`, `docPath`, `members`):
148
-
149
- ```
150
- Напиши module-summary для одного логічного модуля.
151
-
152
- МОДУЛЬ: <relRoot>
153
- ЗАПИСАТИ В: <docPath>
154
- ФАЙЛИ МОДУЛЯ (members): <members>
155
-
156
- Кроки:
157
- 1. Прочитай файлові доки членів модуля. <member> — sourcePath відносно кореня проєкту
158
- (= поточний CWD); його файлова дока — <CWD>/<dir>/docs/<stem>.md. За потреби зазирни
159
- в самі файли.
160
- 2. Створи теку для <docPath>, якщо її немає.
161
- 3. Запиши markdown у <docPath> за тими ж правилами стилю, що й файлова дока
162
- (українська, чистий Markdown, контекстна незалежність, без HTML).
163
-
164
- Секції module-summary:
165
- ## Огляд модуля — призначення модуля <relRoot>, його роль у проєкті.
166
- ## Ключові файли — список із кліковими посиланнями (відносними до розташування цього
167
- ARCHITECTURE.md) на члени модуля та їхні файлові доки.
168
- ## Публічний API — що модуль експортує назовні.
169
- ## Внутрішній потік — як компоненти модуля взаємодіють.
170
- ## Підмодулі — вкладені модулі, якщо є.
171
-
172
- Поверни лише підтвердження, що файл <docPath> записано.
173
- ```
174
-
175
- ### Крок 5: Tier 3 — доменні доки
176
-
177
- Після завершення **всіх** module-summary диспатч **одного** субагента-синтезатора.
178
- У промпт підстав конкретний перелік шляхів module-summary (<module_root>/docs/ARCHITECTURE.md
179
- кожного модуля з виводу `docgen modules`), а не інструкцію їх шукати. Промпт:
180
-
181
- ```
182
- Синтезуй доменну документацію бізнес-процесів проєкту.
183
-
184
- ДЖЕРЕЛА (module-summary, читай усі): <перелік шляхів ARCHITECTURE.md, підставлений вище>
185
-
186
- Кроки:
187
- 1. Прочитай усі module-summary.
188
- 2. Виділи бізнес-домени та процеси (можуть перетинати межі модулів). Доменів може бути багато.
189
- 3. Для КОЖНОГО домену запиши окремий файл docs/<домен>.md у кореневій docs/:
190
- - назва файлу — короткий kebab-slug домену;
191
- - не перезаписуй файлові доки кореневих файлів у docs/ (напр. app.md, eslint.config.md):
192
- якщо слаґ домену збігається з іменем такого файлу — додай суфікс -domain
193
- (напр. app-domain.md). Інакше пиши docs/<домен>.md як є;
194
- - опиши бізнес-процес домену з кліковими відносними посиланнями на module-summary, конкретні файли й директорії.
195
-
196
- Правила стилю — ті ж (українська, чистий Markdown, контекстна незалежність, без HTML).
197
-
198
- Поверни перелік створених файлів docs/<домен>.md.
199
- ```
200
-
201
- ### Крок 6: Підсумок
202
-
203
- Після всіх батчів виведи:
204
-
205
- ```
206
- ✓ docgen завершено.
207
- Tier 1 (файли): описано <N>, пропущено <S>, помилок <E>.
208
- Tier 2 (модулі): <M> module-summary.
209
- Tier 3 (домени): <D> доменних доків у docs/.
210
- ```
211
-
212
- Перелічи файли з помилками (субагент впав або не записав `docPath`), якщо такі є.
213
- Помилка одного файлу не зупиняє решту — обробляй усі батчі до кінця.
214
-
215
- ## Нотатки
216
-
217
- - Не комітити автоматично — користувач вирішує, коли комітити згенеровану доку.
218
- - Scanner ігнорує `node_modules`, `dist`, `.git`, `__pycache__`, `coverage`, `.cursor`,
219
- `.claude`, усі теки `docs/`, а також `*.test.*` / `*.spec.*` / `*.d.ts`.
220
- Кореневий repo `docs/` — system-wide only: file-level docs туди не пишуться, і Tier 1
221
- має трактувати цей корінь як повністю нецільовий.
222
- - Список glob-ів для ignore живе в окремому snippet-модулі
223
- `npm/skills/docgen/js/docgen-ignore.mjs` (`DOCGEN_IGNORE_GLOBS`).
224
- Scanner лише читає цей список.
@@ -1,19 +0,0 @@
1
- # firebase_hosting.mjs
2
-
3
- ## Огляд
4
-
5
- Перевірка-концерн правила abie: у підкаталогах першого рівня репозиторію не повинно бути артефактів Firebase Hosting (`.firebaserc`, `firebase.json`, `.firebase/`), бо `abie.mdc` забороняє Firebase Hosting. Сам корінь репозиторію не перевіряється — там ці імена можуть належати суміжним проєктам.
6
-
7
- ## Поведінка
8
-
9
- 1. Прочитати список елементів кореня репозиторію. Якщо каталог не читається — зафіксувати помилку (fail) і завершитися.
10
- 2. Відібрати підкаталоги першого рівня, пропустивши `.git` і `node_modules`.
11
- 3. У кожному такому підкаталозі перевірити наявність заборонених імен: файлів `.firebaserc`, `firebase.json` і каталогу `.firebase/`. Кожна знахідка — окремий fail.
12
- 4. Якщо жодного порушення не знайдено — зафіксувати pass.
13
- 5. Повернути підсумковий exit-код.
14
-
15
- ## Гарантії поведінки
16
-
17
- - Read-only: лише перелічує й перевіряє існування шляхів.
18
- - Помилка читання кореня не валить процес винятком, а стає fail.
19
- - Перевіряється лише перший рівень; корінь і глибші рівні поза охопленням.
@@ -1,24 +0,0 @@
1
- # k8s-tree.mjs
2
-
3
- ## Огляд
4
-
5
- Обхід Kubernetes-дерева для перевірок abie з кешуванням на час одного прогону. Знаходить YAML під сегментом `k8s/` і визначає каталоги з `Deployment`. Перший виклик платить за обхід; наступні концерни прогону беруть із кешу.
6
-
7
- ## Поведінка
8
-
9
- 1. Пошук маніфестів: рекурсивно обійти дерево, відібравши `.yaml`/`.yml` під сегментом `k8s/`; `.github/` свідомо пропускається. Результат відсортований.
10
- 2. Каталоги з Deployment: розпарсити передані YAML, відібрати `kind: Deployment`, зібрати унікальні каталоги.
11
- 3. Кешування: обидві операції кешуються module-level singleton-ом за ключем із входів; повтор без I/O.
12
- 4. Пошкоджені YAML за замовчуванням мовчки пропускаються; репортер передає викликач.
13
-
14
- ## Публічний API
15
-
16
- - `findK8sYamlFiles` — відсортований список YAML під `k8s/` (з кешем, пропуск `.github/`).
17
- - `collectDeploymentDirs` — множина каталогів із Deployment (кеш, опц. репортер помилок).
18
-
19
- ## Гарантії поведінки
20
-
21
- - Read-only щодо проєкту.
22
- - Стійкість до пошкоджених YAML: помилкові документи пропускаються, обхід не переривається.
23
- - Детермінований вивід (стабільне сортування).
24
- - Кеш у межах прогону; повторні виклики безкоштовні.
@@ -1,24 +0,0 @@
1
- # overlay-paths.mjs
2
-
3
- ## Огляд
4
-
5
- Набір чистих path-хелперів для overlay-перевірок правила abie: класифікація шляхів (ua-overlay проти base-шару), виведення каталогу пакета з overlay-шляху, умовні питання правила (чи потрібен HTTPRoute, чи є Deployment). Уся логіка — над рядками/шляхами та перевіркою існування файлів; YAML не парситься.
6
-
7
- ## Поведінка
8
-
9
- - ua-overlay: шлях закінчується на `ua/kustomization.yaml`; base-шар — за сегментом `base/`.
10
- - Каталог пакета: з `…/k8s/ua/kustomization.yaml` виділяється батько `k8s/`; без збігу — немає результату.
11
- - HTTPRoute-gate: вимога лише для Vite-пакетів (є `vite.config.{js,mjs,ts}`).
12
- - Deployment: чи хоч один каталог із Deployment лежить у `k8s/` цього пакета.
13
- - base-шар: yaml під `<пакет>/k8s/` і не в `ua/`.
14
- - Шляхи нормалізуються до posix (`\`→`/`).
15
-
16
- ## Публічний API
17
-
18
- - `isUaKustomizationPath`, `abiePackageDirFromK8sOverlay`, `abieOverlayRequiresHttpRouteByVite`, `abieOverlayK8sTreeHasDeployment`, `isAbieK8sBaseYamlPath`, `isK8sYamlInAbiePackageExcludingUaOverlay`.
19
-
20
- ## Гарантії поведінки
21
-
22
- - Read-only, без побічних ефектів.
23
- - Невідповідність шаблону → негативний/порожній результат, не виняток.
24
- - Незалежність від ОС (розділювачі зводяться до `/`).