@nitra/cursor 5.3.2 → 5.3.3

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 (54) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +1 -1
  3. package/rules/abie/fix.mjs +1 -2
  4. package/rules/adr/fix.mjs +1 -2
  5. package/rules/bun/fix.mjs +1 -2
  6. package/rules/capacitor/fix.mjs +1 -2
  7. package/rules/capacitor/js/platforms.mjs +0 -2
  8. package/rules/changelog/fix.mjs +1 -2
  9. package/rules/ci4/fix.mjs +1 -2
  10. package/rules/docker/fix.mjs +1 -2
  11. package/rules/efes/fix.mjs +1 -2
  12. package/rules/feedback/fix.mjs +1 -2
  13. package/rules/ga/fix.mjs +1 -2
  14. package/rules/graphql/fix.mjs +1 -2
  15. package/rules/hasura/fix.mjs +1 -2
  16. package/rules/image-avif/fix.mjs +1 -2
  17. package/rules/image-compress/fix.mjs +1 -2
  18. package/rules/js-bun-db/fix.mjs +1 -2
  19. package/rules/js-bun-redis/fix.mjs +1 -2
  20. package/rules/js-lint/fix.mjs +1 -2
  21. package/rules/js-lint-ci/fix.mjs +1 -2
  22. package/rules/js-mssql/fix.mjs +1 -2
  23. package/rules/js-run/fix.mjs +1 -2
  24. package/rules/k8s/fix.mjs +1 -2
  25. package/rules/k8s/js/manifests.mjs +1 -5
  26. package/rules/nginx-default-tpl/fix.mjs +1 -2
  27. package/rules/npm-module/fix.mjs +1 -2
  28. package/rules/npm-module/js/package_structure.mjs +0 -1
  29. package/rules/php/fix.mjs +1 -2
  30. package/rules/python/fix.mjs +1 -2
  31. package/rules/rego/fix.mjs +1 -2
  32. package/rules/release/fix.mjs +1 -2
  33. package/rules/rust/fix.mjs +1 -2
  34. package/rules/security/fix.mjs +1 -2
  35. package/rules/style-lint/fix.mjs +1 -2
  36. package/rules/tauri/fix.mjs +1 -2
  37. package/rules/test/coverage/coverage.mjs +0 -2
  38. package/rules/test/fix.mjs +1 -2
  39. package/rules/text/fix.mjs +1 -2
  40. package/rules/vue/fix.mjs +1 -2
  41. package/rules/worktree/fix.mjs +1 -2
  42. package/scripts/lib/run-rule.mjs +0 -2
  43. package/scripts/lint-cli.mjs +0 -1
  44. package/scripts/utils/with-lock.mjs +0 -1
  45. package/skills/doc-aggregate/js/docgen-scan.mjs +17 -18
  46. package/skills/doc-files/.changes/260612-0002.md +5 -0
  47. package/skills/doc-files/.changes/260612-0006.md +5 -0
  48. package/skills/doc-files/.changes/260612-0008.md +5 -0
  49. package/skills/doc-files/js/docgen-extract.mjs +136 -0
  50. package/skills/doc-files/js/docgen-prompts.mjs +2 -2
  51. package/skills/doc-files/js/docgen-scan.mjs +21 -22
  52. package/skills/doc-files/js/docs/units-rs.md +35 -0
  53. package/skills/doc-files/js/units-rs.mjs +213 -0
  54. package/skills/doc-files/js/units.mjs +4 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.3.3] - 2026-06-11
4
+
5
+ ### Changed
6
+
7
+ - ✨ feat(npm/skills/doc-files): doc-files: підтримка Rust (.rs) — SOURCE_…
8
+
3
9
  ## [5.3.2] - 2026-06-11
4
10
 
5
11
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "5.3.2",
3
+ "version": "5.3.3",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
package/rules/adr/fix.mjs CHANGED
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
package/rules/bun/fix.mjs CHANGED
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -21,11 +21,9 @@ const IGNORED_DIRS_FOR_PACKAGE_JSON = new Set([
21
21
  ])
22
22
 
23
23
  /** `||` у діапазоні npm-версій */
24
- // eslint-disable-next-line sonarjs/slow-regex -- короткі **semver**-підрядки у **package.json**
25
24
  const NPM_OR_PARTS_RE = /\s*\|\|\s*/
26
25
 
27
26
  /** `a - b` (діапазон діапазонів) */
28
- // eslint-disable-next-line sonarjs/slow-regex -- форма **X - Y** у **npm**-range
29
27
  const NPM_HYPHEN_RANGE_RE = /^(.+?)\s+-\s+(.+)$/
30
28
 
31
29
  const FIRST_VERSION_NUM_RE = /^(?:v)?(\d+)/i
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
package/rules/ci4/fix.mjs CHANGED
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
package/rules/ga/fix.mjs CHANGED
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
package/rules/k8s/fix.mjs CHANGED
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -3984,11 +3984,8 @@ const noopFail = msg => msg
3984
3984
  * @type {readonly { ipBlock: { cidr: string } }[]}
3985
3985
  */
3986
3986
  const NETWORK_POLICY_GCLB_INGRESS_FROM = Object.freeze([
3987
- // eslint-disable-next-line sonarjs/no-hardcoded-ip
3988
3987
  { ipBlock: { cidr: '35.191.0.0/16' } },
3989
- // eslint-disable-next-line sonarjs/no-hardcoded-ip
3990
3988
  { ipBlock: { cidr: '130.211.0.0/22' } },
3991
- // eslint-disable-next-line sonarjs/no-hardcoded-ip
3992
3989
  { ipBlock: { cidr: '10.0.0.0/8' } }
3993
3990
  ])
3994
3991
 
@@ -4858,9 +4855,8 @@ function firstValidYamlJsonFromPatchText(patchText) {
4858
4855
  if (d.errors.length === 0) return d.toJSON()
4859
4856
  }
4860
4857
  } catch {
4861
- return undefined
4858
+ // ignore parse errors
4862
4859
  }
4863
- return undefined
4864
4860
  }
4865
4861
 
4866
4862
  /**
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -257,7 +257,6 @@ export function globToRegex(glob) {
257
257
  // Дозволено: уся функція існує саме для конструкції RegExp з glob-pattern
258
258
  // у `files` (значення з npm/package.json, не від кінцевого користувача), і
259
259
  // спецсимволи вже екрановано через `REGEX_SPECIAL_IN_GLOB` вище.
260
- // eslint-disable-next-line security/detect-non-literal-regexp
261
260
  return new RegExp(`^${re}$`, 'u')
262
261
  }
263
262
 
package/rules/php/fix.mjs CHANGED
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -12,6 +12,5 @@ export function run(ctx) {
12
12
  }
13
13
 
14
14
  if (isRunAsCli(import.meta.url)) {
15
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
16
- process.exit(await runRuleCli(import.meta.dirname))
15
+ process.exitCode = await runRuleCli(import.meta.dirname)
17
16
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -170,7 +170,6 @@ export function renderMarkdown(rows, allowedGaps = []) {
170
170
  async function loadProvider(rulesDir, ruleId) {
171
171
  const providerPath = join(rulesDir, ruleId, 'coverage', 'coverage.mjs')
172
172
  if (!existsSync(providerPath)) return null
173
- // eslint-disable-next-line no-unsanitized/method -- providerPath з join(rulesDir, ruleId, …), ruleId з конфігу
174
173
  const mod = await import(pathToFileURL(providerPath).href)
175
174
  if (typeof mod.detect !== 'function' || typeof mod.collect !== 'function') return null
176
175
  return mod
@@ -292,7 +291,6 @@ export async function runCoverageSteps(opts = {}) {
292
291
  console.log('✓ COVERAGE.md')
293
292
 
294
293
  if (opts.fix) {
295
- // eslint-disable-next-line no-unsanitized/method -- шлях відносний до пакету, не user-input
296
294
  const { fixSurvivedMutants } = await import(new URL('../../../scripts/coverage-fix.mjs', import.meta.url).href)
297
295
  await fixSurvivedMutants(allSurvived, cwd)
298
296
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
package/rules/vue/fix.mjs CHANGED
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -14,6 +14,5 @@ export function run(ctx) {
14
14
  if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
- // eslint-disable-next-line n/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
- process.exit(await runRuleCli(import.meta.dirname))
17
+ process.exitCode = await runRuleCli(import.meta.dirname)
19
18
  }
@@ -53,7 +53,6 @@ async function evaluateAppliesGate(bundledRulesDir, rule) {
53
53
  const concern = rule.jsConcerns.find(c => c.name === APPLIES_CONCERN_NAME)
54
54
  if (!concern) return true
55
55
  const path = resolveJsCheckPath(bundledRulesDir, rule.id, concern)
56
- // eslint-disable-next-line no-unsanitized/method -- path з discovered concern, файл з whitelist'у readdir
57
56
  const mod = await import(path)
58
57
  if (typeof mod.applies !== 'function') return true
59
58
  return Boolean(await mod.applies())
@@ -170,7 +169,6 @@ export async function runRule(rule, bundledRulesDir, walkCache) {
170
169
 
171
170
  for (const concern of rule.jsConcerns) {
172
171
  const path = resolveJsCheckPath(bundledRulesDir, rule.id, concern)
173
- // eslint-disable-next-line no-unsanitized/method -- path з discovered concern, файл з whitelist'у readdir
174
172
  const mod = await import(path)
175
173
  if (typeof mod.check === 'function') {
176
174
  const code = await mod.check()
@@ -74,7 +74,6 @@ export async function runLint(opts = {}) {
74
74
  log(`⚠️ lint: правило ${id} має lint-фазу, але немає js/lint.mjs — пропускаю.\n`)
75
75
  continue
76
76
  }
77
- // eslint-disable-next-line no-unsanitized/method -- шлях з discovered rule dir
78
77
  const mod = await import(lintPath)
79
78
  const code = await mod.lint(changed, cwd)
80
79
  if (code !== 0) return code
@@ -128,7 +128,6 @@ export async function withLock(key, runFn, opts = {}) {
128
128
 
129
129
  const onSignal = () => {
130
130
  release()
131
- // eslint-disable-next-line n/no-process-exit -- SIGINT/SIGTERM мають завершити процес із кодом 130
132
131
  process.exit(130)
133
132
  }
134
133
  process.once('SIGINT', onSignal)
@@ -1,6 +1,5 @@
1
1
  /** @see ./docs/docgen-scan.md */
2
- // eslint-disable-next-line unicorn/import-style
3
- import path from 'node:path'
2
+ import { join, relative, dirname, extname, sep, isAbsolute, resolve } from 'node:path'
4
3
  import { existsSync, readdirSync, statSync } from 'node:fs'
5
4
 
6
5
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
@@ -18,7 +17,7 @@ const TEST_FILE_RE = /\.(?:test|spec)\.[^.]+$/u
18
17
  * @returns {boolean} true — корінь system-wide docs
19
18
  */
20
19
  function isSystemWideDocsRoot(root) {
21
- return existsSync(path.join(root, 'docs', 'adr')) || existsSync(path.join(root, 'docs', 'explanation'))
20
+ return existsSync(join(root, 'docs', 'adr')) || existsSync(join(root, 'docs', 'explanation'))
22
21
  }
23
22
 
24
23
  /**
@@ -29,7 +28,7 @@ function isSystemWideDocsRoot(root) {
29
28
  export function isSourceFile(fileName) {
30
29
  if (fileName.endsWith('.d.ts')) return false
31
30
  if (TEST_FILE_RE.test(fileName)) return false
32
- return SOURCE_EXTENSIONS.has(path.extname(fileName))
31
+ return SOURCE_EXTENSIONS.has(extname(fileName))
33
32
  }
34
33
 
35
34
  /**
@@ -49,14 +48,14 @@ export function scanSourceFiles(root) {
49
48
  return
50
49
  }
51
50
  for (const entry of entries) {
52
- const fullPath = path.join(dir, entry.name)
53
- const relPath = path.relative(root, fullPath)
51
+ const fullPath = join(dir, entry.name)
52
+ const relPath = relative(root, fullPath)
54
53
  if (entry.isDirectory()) {
55
54
  if (isDocgenIgnored(relPath, 'dir')) continue
56
55
  walk(fullPath)
57
56
  } else if (entry.isFile() && isSourceFile(entry.name)) {
58
- if (isSystemWideDocsRoot(root) && path.dirname(relPath) === '.') continue
59
- const sourcePath = relPath.split(path.sep).join('/')
57
+ if (isSystemWideDocsRoot(root) && dirname(relPath) === '.') continue
58
+ const sourcePath = relPath.split(sep).join('/')
60
59
  if (isDocgenIgnored(sourcePath)) continue
61
60
  results.push(sourcePath)
62
61
  }
@@ -74,10 +73,10 @@ export function scanSourceFiles(root) {
74
73
  * @returns {string} slug: `npm/rules/adr` → `npm-rules-adr`, корінь → `root`
75
74
  */
76
75
  export function slugForModule(root, moduleRoot) {
77
- const rel = path.relative(root, moduleRoot)
76
+ const rel = relative(root, moduleRoot)
78
77
  if (rel === '') return 'root'
79
78
  return rel
80
- .split(path.sep)
79
+ .split(sep)
81
80
  .join('-')
82
81
  .replaceAll(/[^\w-]+/gu, '-')
83
82
  }
@@ -99,8 +98,8 @@ export function findModuleRoots(root) {
99
98
  return
100
99
  }
101
100
  for (const entry of entries) {
102
- const fullPath = path.join(dir, entry.name)
103
- const relPath = path.relative(root, fullPath)
101
+ const fullPath = join(dir, entry.name)
102
+ const relPath = relative(root, fullPath)
104
103
  if (entry.isDirectory()) {
105
104
  if (isDocgenIgnored(relPath, 'dir')) continue
106
105
  walk(fullPath)
@@ -123,8 +122,8 @@ export function findModuleRoots(root) {
123
122
  export function nearestModuleRoot(filePath, moduleRoots) {
124
123
  let best = null
125
124
  for (const moduleRoot of moduleRoots) {
126
- const rel = path.relative(moduleRoot, filePath)
127
- if (rel.startsWith('..') || path.isAbsolute(rel)) continue
125
+ const rel = relative(moduleRoot, filePath)
126
+ if (rel.startsWith('..') || isAbsolute(rel)) continue
128
127
  if (best === null || moduleRoot.length > best.length) best = moduleRoot
129
128
  }
130
129
  return best
@@ -141,7 +140,7 @@ export function scanForModules(root) {
141
140
  const moduleRoots = findModuleRoots(root)
142
141
  const byRoot = new Map()
143
142
  for (const sourcePath of files) {
144
- const moduleRoot = nearestModuleRoot(path.join(root, sourcePath), moduleRoots)
143
+ const moduleRoot = nearestModuleRoot(join(root, sourcePath), moduleRoots)
145
144
  if (moduleRoot === null) continue
146
145
  if (!byRoot.has(moduleRoot)) byRoot.set(moduleRoot, [])
147
146
  byRoot.get(moduleRoot).push(sourcePath)
@@ -151,10 +150,10 @@ export function scanForModules(root) {
151
150
  for (const moduleRoot of moduleRoots) {
152
151
  const members = byRoot.get(moduleRoot)
153
152
  if (!members || members.length === 0) continue
154
- const docPath = path.join(moduleRoot, 'docs', 'ARCHITECTURE.md')
153
+ const docPath = join(moduleRoot, 'docs', 'ARCHITECTURE.md')
155
154
  results.push({
156
155
  moduleRoot,
157
- relRoot: path.relative(root, moduleRoot) || '.',
156
+ relRoot: relative(root, moduleRoot) || '.',
158
157
  slug: slugForModule(root, moduleRoot),
159
158
  docPath,
160
159
  members: members.toSorted(),
@@ -171,7 +170,7 @@ export function scanForModules(root) {
171
170
  */
172
171
  export function resolveRoot(argv) {
173
172
  const i = argv.indexOf('--root')
174
- return i !== -1 && argv[i + 1] ? path.resolve(argv[i + 1]) : process.cwd()
173
+ return i !== -1 && argv[i + 1] ? resolve(argv[i + 1]) : process.cwd()
175
174
  }
176
175
 
177
176
  /**
@@ -0,0 +1,5 @@
1
+ ---
2
+ bump: minor
3
+ section: Added
4
+ ---
5
+ doc-files: підтримка Rust (.rs) — SOURCE_EXTENSIONS, target/-ignore, extractFactsRust, units-rs
@@ -0,0 +1,5 @@
1
+ ---
2
+ bump: patch
3
+ section: Fixed
4
+ ---
5
+ мовно-нейтральний текст returnsFalsyOnFail (false/null/Err); units-rs: тести + docs
@@ -0,0 +1,5 @@
1
+ ---
2
+ bump: patch
3
+ section: Added
4
+ ---
5
+ Rust .rs: тести units-rs, doc units-rs.md, мовно-нейтральний текст гарантії returnsFalsyOnFail
@@ -211,6 +211,141 @@ function extractMarkers(src) {
211
211
  }
212
212
  }
213
213
 
214
+ // ── Rust-екстрактор ──────────────────────────────────────────────────────────
215
+
216
+ // Модульний //! doc-коментар (inner doc)
217
+ const RS_MODULE_DOC_RE = /^(?:[ \t]*\/\/![ \t]?(.*)\n)*/m
218
+
219
+ // pub fn / pub struct / pub enum / pub trait (та fn із exposure-атрибутом)
220
+ const RS_PUB_ITEM_RE = /^[ \t]*(pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/gm
221
+
222
+ // Exposure-атрибути (#[tauri::command] тощо)
223
+ const RS_EXPOSURE_ATTR_RE = /#\[(?:tauri::command|wasm_bindgen|uniffi::export|pyo3::pyfunction|napi)/gm
224
+
225
+ // /// line-doc перед елементом
226
+ const RS_LINE_DOC_RE = /(?:[ \t]*\/\/\/[ \t]?.*\n)*/
227
+
228
+ // use crate::module::{A, B} або use std::..;
229
+ const RS_USE_RE = /^[ \t]*use\s+([\w:]+(?:::\{[^}]+\})?(?:::\*)?(?:::\w+)?)\s*;/gm
230
+
231
+ // Файловий запис: fs::write / File::create / remove_file / create_dir / write_all
232
+ const RS_WRITE_RE = /fs::write|File::create|remove_file|create_dir|BufWriter::new|OpenOptions[^;]*\.write\s*\(\s*true/
233
+
234
+ // Обробка помилок (але не просто `?`)
235
+ const RS_CATCH_RE = /\.unwrap_or(?:_else|_default)?|if\s+let\s+Err\s*\(|match\s+\S+.*\{\s*[\s\S]*?Err\s*\(|\.map_err\s*\(|\.ok\s*\(\)/
236
+
237
+ // Функції, що повертають Result або Option
238
+ const RS_RESULT_RE = /->\s*(?:Result|Option)\s*</
239
+
240
+ // Мережа
241
+ const RS_NETWORK_RE = /reqwest|hyper::|TcpStream|UdpSocket|tokio::net/
242
+
243
+ // Кешування
244
+ const RS_CACHE_RE = /\bcache\b|\bCache\b|lazy_static!|OnceCell|OnceLock|DashMap/i
245
+
246
+ /**
247
+ * Видобуває `///` doc-рядки перед рядком `lineIdx` (назад через `#[...]` та пусті рядки).
248
+ * @param {string[]} lines рядки файлу
249
+ * @param {number} lineIdx індекс рядка декларації
250
+ * @returns {string} опис або ''
251
+ */
252
+ function rsDocBefore(lines, lineIdx) {
253
+ const doc = []
254
+ for (let i = lineIdx - 1; i >= 0; i--) {
255
+ const t = lines[i].trim()
256
+ if (t.startsWith('///')) doc.unshift(t.slice(3).trim())
257
+ else if (t.startsWith('#[') || t.startsWith('#![') || t === '') { /* skip */ }
258
+ else break
259
+ }
260
+ return doc.join(' ').trim()
261
+ }
262
+
263
+ /**
264
+ * Витягує факт-лист для `.rs` файлу.
265
+ * @param {string} src вміст файлу
266
+ * @param {string} relPath відносний шлях
267
+ * @returns {object} факт-лист без `unsupported`
268
+ */
269
+ function extractFactsRust(src, relPath) {
270
+ // header — //! module-level doc
271
+ const headerLines = []
272
+ for (const line of src.split('\n')) {
273
+ const t = line.trim()
274
+ if (t.startsWith('//!')) headerLines.push(t.slice(3).trim())
275
+ else if (t === '' || t.startsWith('//')) continue
276
+ else break
277
+ }
278
+ const header = headerLines.join(' ').trim()
279
+
280
+ // Exposure-атрибути: рядки, після яких fn стає фактично pub
281
+ const srcLines = src.split('\n')
282
+ const exposedLineSet = new Set()
283
+ for (const m of src.matchAll(RS_EXPOSURE_ATTR_RE)) {
284
+ // Знаходимо, який рядок містить цей атрибут
285
+ let pos = 0
286
+ for (let li = 0; li < srcLines.length; li++) {
287
+ if (pos + srcLines[li].length >= m.index) {
288
+ // Шукаємо наступний не-атрибутний рядок з fn
289
+ for (let nli = li + 1; nli < Math.min(li + 5, srcLines.length); nli++) {
290
+ const t = srcLines[nli].trim()
291
+ if (t.startsWith('#[') || t === '') continue
292
+ if (/^(?:pub\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+/.test(t)) exposedLineSet.add(nli)
293
+ break
294
+ }
295
+ break
296
+ }
297
+ pos += srcLines[li].length + 1
298
+ }
299
+ }
300
+
301
+ // exports — pub items + exposure-exposed fns
302
+ const exports = []
303
+ let lineOffset = 0
304
+ for (let li = 0; li < srcLines.length; li++) {
305
+ const line = srcLines[li]
306
+ const m = line.match(/^[ \t]*(pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/)
307
+ if (m) {
308
+ const isPub = Boolean(m[1]) || exposedLineSet.has(li)
309
+ if (isPub) {
310
+ const desc = rsDocBefore(srcLines, li)
311
+ exports.push({ name: m[3], kind: m[2], desc })
312
+ }
313
+ }
314
+ lineOffset += line.length + 1
315
+ }
316
+
317
+ // localSymbols — приватні fn (не pub і не exposed) — не документуємо як публічний API
318
+ const localSymbols = []
319
+ for (let li = 0; li < srcLines.length; li++) {
320
+ const line = srcLines[li]
321
+ const m = line.match(/^[ \t]*(?:async\s+)?(?:unsafe\s+)?fn\s+(\w+)/)
322
+ if (m && !exports.some(e => e.name === m[1])) localSymbols.push(m[1])
323
+ }
324
+
325
+ // imports — use-рядки, класифіковані на std / external / internal
326
+ const stdlib = new Set()
327
+ const external = new Set()
328
+ for (const m of src.matchAll(RS_USE_RE)) {
329
+ const path = m[1]
330
+ const root = path.split('::')[0]
331
+ if (root === 'std' || root === 'core' || root === 'alloc') stdlib.add(path)
332
+ else external.add(path)
333
+ }
334
+ const imports = { stdlib: [...stdlib], external: [...external], internal: [] }
335
+
336
+ // markers
337
+ const markers = {
338
+ readOnly: !RS_WRITE_RE.test(src),
339
+ catchesErrors: RS_CATCH_RE.test(src),
340
+ returnsFalsyOnFail: RS_RESULT_RE.test(src),
341
+ network: RS_NETWORK_RE.test(src),
342
+ caches: RS_CACHE_RE.test(src),
343
+ skips: []
344
+ }
345
+
346
+ return { relPath, lang: 'rs', header, exports, imports, internalSymbols: [], localSymbols, markers }
347
+ }
348
+
214
349
  /**
215
350
  * Головний екстрактор: код файлу → факт-лист.
216
351
  * @param {string} src вміст файлу
@@ -219,6 +354,7 @@ function extractMarkers(src) {
219
354
  */
220
355
  export function extractFacts(src, relPath) {
221
356
  const lang = relPath.split('.').pop()
357
+ if (lang === 'rs') return extractFactsRust(src, relPath)
222
358
  if (!['js', 'mjs', 'ts'].includes(lang)) {
223
359
  return { relPath, lang, unsupported: true, header: '', exports: [], imports: {}, markers: {} }
224
360
  }
@@ -32,7 +32,7 @@ function factsSummary(facts) {
32
32
  if (m.skips?.length) lines.push(`Свідомо пропускає шляхи: ${m.skips.join(', ')}`)
33
33
  lines.push(`Read-only: ${m.readOnly ? 'так' : 'ні'}`)
34
34
  if (m.catchesErrors) lines.push('Перехоплює помилки (fail-safe), не кидає винятків назовні')
35
- if (m.returnsFalsyOnFail) lines.push('За невдачі повертає false/null замість винятку')
35
+ if (m.returnsFalsyOnFail) lines.push('За невдачі повертає значення помилки (false/null/Err) замість винятку чи паніки')
36
36
  lines.push(m.caches ? 'Кешування: так, у межах прогону' : 'Кешування: НЕМАЄ — не згадуй кеш у гарантіях')
37
37
  if (m.network) lines.push('Звертається до мережі')
38
38
  else lines.push('Робота з мережею: немає')
@@ -195,7 +195,7 @@ export function guaranteesFromMarkers(facts) {
195
195
  const lines = []
196
196
  if (m.readOnly) lines.push('- Read-only: файл не виконує операцій запису у файлову систему.')
197
197
  if (m.catchesErrors) lines.push('- Перехоплює помилки і не пропускає винятків назовні (fail-safe).')
198
- if (m.returnsFalsyOnFail) lines.push('- За невдалої перевірки повертає `false`/`null` замість винятку.')
198
+ if (m.returnsFalsyOnFail) lines.push('- За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.')
199
199
  if (m.caches) lines.push('- Кешує результати в межах одного прогону.')
200
200
  if (m.skips?.length) {
201
201
  lines.push(`- Свідомо пропускає шляхи: ${m.skips.map(s => '`' + s + '`').join(', ')}.`)
@@ -1,6 +1,5 @@
1
1
  /** @see ./docs/docgen-scan.md */
2
- // eslint-disable-next-line unicorn/import-style
3
- import path from 'node:path'
2
+ import { join, dirname, basename, extname, relative, resolve, sep, isAbsolute, posix } from 'node:path'
4
3
  import { existsSync, readdirSync, statSync } from 'node:fs'
5
4
  import { execFileSync } from 'node:child_process'
6
5
  import { once } from 'node:events'
@@ -27,7 +26,7 @@ const DEFAULT_GATE_MAX = Number(env.N_CURSOR_DOC_FILES_GATE_MAX ?? 50) || 50
27
26
  * @returns {boolean} true — корінь system-wide docs
28
27
  */
29
28
  function isSystemWideDocsRoot(root) {
30
- return existsSync(path.join(root, 'docs', 'adr')) || existsSync(path.join(root, 'docs', 'explanation'))
29
+ return existsSync(join(root, 'docs', 'adr')) || existsSync(join(root, 'docs', 'explanation'))
31
30
  }
32
31
 
33
32
  /**
@@ -38,7 +37,7 @@ function isSystemWideDocsRoot(root) {
38
37
  export function isSourceFile(fileName) {
39
38
  if (fileName.endsWith('.d.ts')) return false
40
39
  if (TEST_FILE_RE.test(fileName)) return false
41
- return SOURCE_EXTENSIONS.has(path.extname(fileName))
40
+ return SOURCE_EXTENSIONS.has(extname(fileName))
42
41
  }
43
42
 
44
43
  /**
@@ -48,9 +47,9 @@ export function isSourceFile(fileName) {
48
47
  * @returns {string} шлях до `<dir>/docs/<stem>.md` у тому ж просторі шляхів
49
48
  */
50
49
  export function docPathForSource(sourcePath) {
51
- const dir = path.dirname(sourcePath)
52
- const stem = path.basename(sourcePath, path.extname(sourcePath))
53
- return path.join(dir, 'docs', `${stem}.md`)
50
+ const dir = dirname(sourcePath)
51
+ const stem = basename(sourcePath, extname(sourcePath))
52
+ return join(dir, 'docs', `${stem}.md`)
54
53
  }
55
54
 
56
55
  /**
@@ -61,9 +60,9 @@ export function docPathForSource(sourcePath) {
61
60
  * @returns {boolean} true — кандидат на доку
62
61
  */
63
62
  export function isDocCandidate(root, relPath) {
64
- const fileName = path.posix.basename(relPath)
63
+ const fileName = posix.basename(relPath)
65
64
  if (!isSourceFile(fileName)) return false
66
- if (isSystemWideDocsRoot(root) && path.posix.dirname(relPath) === '.') return false
65
+ if (isSystemWideDocsRoot(root) && posix.dirname(relPath) === '.') return false
67
66
  return !isDocgenIgnored(relPath)
68
67
  }
69
68
 
@@ -75,7 +74,7 @@ export function isDocCandidate(root, relPath) {
75
74
  */
76
75
  export function describeFile(root, sourcePath) {
77
76
  const docPath = docPathForSource(sourcePath)
78
- const { stale, reason } = staleness(path.join(root, sourcePath), path.join(root, docPath))
77
+ const { stale, reason } = staleness(join(root, sourcePath), join(root, docPath))
79
78
  return { sourcePath, docPath, stale, reason }
80
79
  }
81
80
 
@@ -97,14 +96,14 @@ export function scanForDocFiles(root) {
97
96
  return
98
97
  }
99
98
  for (const entry of entries) {
100
- const fullPath = path.join(dir, entry.name)
101
- const relPath = path.relative(root, fullPath)
99
+ const fullPath = join(dir, entry.name)
100
+ const relPath = relative(root, fullPath)
102
101
  if (entry.isDirectory()) {
103
102
  if (isDocgenIgnored(relPath, 'dir')) continue
104
103
  walk(fullPath)
105
104
  } else if (entry.isFile() && isSourceFile(entry.name)) {
106
- if (isSystemWideDocsRoot(root) && path.dirname(relPath) === '.') continue
107
- const sourcePath = relPath.split(path.sep).join('/')
105
+ if (isSystemWideDocsRoot(root) && dirname(relPath) === '.') continue
106
+ const sourcePath = relPath.split(sep).join('/')
108
107
  if (isDocgenIgnored(sourcePath)) continue
109
108
  results.push(describeFile(root, sourcePath))
110
109
  }
@@ -122,7 +121,7 @@ export function scanForDocFiles(root) {
122
121
  */
123
122
  export function resolveRoot(argv) {
124
123
  const i = argv.indexOf('--root')
125
- return i !== -1 && argv[i + 1] ? path.resolve(argv[i + 1]) : process.cwd()
124
+ return i !== -1 && argv[i + 1] ? resolve(argv[i + 1]) : process.cwd()
126
125
  }
127
126
 
128
127
  /**
@@ -190,7 +189,7 @@ function gitChangedSources(root) {
190
189
  return out
191
190
  .split('\n')
192
191
  .map(s => s.trim())
193
- .filter(rel => rel && isDocCandidate(root, rel) && existsSync(path.join(root, rel)))
192
+ .filter(rel => rel && isDocCandidate(root, rel) && existsSync(join(root, rel)))
194
193
  }
195
194
 
196
195
  /**
@@ -200,9 +199,9 @@ function gitChangedSources(root) {
200
199
  * @returns {string|null} posix-шлях від кореня
201
200
  */
202
201
  function toRelSource(root, candidate) {
203
- const rel = path.relative(root, path.resolve(root, candidate))
204
- if (rel.startsWith('..') || path.isAbsolute(rel)) return null
205
- return rel.split(path.sep).join('/')
202
+ const rel = relative(root, resolve(root, candidate))
203
+ if (rel.startsWith('..') || isAbsolute(rel)) return null
204
+ return rel.split(sep).join('/')
206
205
  }
207
206
 
208
207
  /**
@@ -216,7 +215,7 @@ function runDegradedReport(root) {
216
215
  const degraded = []
217
216
  for (const f of scanForDocFiles(root)) {
218
217
  if (f.stale) continue
219
- const { score, issues } = readDocQuality(path.join(root, f.docPath))
218
+ const { score, issues } = readDocQuality(join(root, f.docPath))
220
219
  if (score !== null && score < QUALITY_THRESHOLD) degraded.push({ ...f, score, issues })
221
220
  }
222
221
  if (degraded.length === 0) {
@@ -262,14 +261,14 @@ export async function runDocFilesCheckCli(argv) {
262
261
  if (hookMode) {
263
262
  const fp = extractHookFilePath(await readStdin())
264
263
  const rel = fp ? toRelSource(root, fp) : null
265
- sources = rel && isDocCandidate(root, rel) && existsSync(path.join(root, rel)) ? [rel] : []
264
+ sources = rel && isDocCandidate(root, rel) && existsSync(join(root, rel)) ? [rel] : []
266
265
  } else if (gitMode) {
267
266
  sources = gitChangedSources(root)
268
267
  } else {
269
268
  sources = argv
270
269
  .filter(a => !a.startsWith('--') && a !== argv[maxIdx + 1])
271
270
  .map(a => toRelSource(root, a))
272
- .filter(rel => rel && isDocCandidate(root, rel) && existsSync(path.join(root, rel)))
271
+ .filter(rel => rel && isDocCandidate(root, rel) && existsSync(join(root, rel)))
273
272
  }
274
273
 
275
274
  const stale = sources.map(src => describeFile(root, src)).filter(f => f.stale)
@@ -0,0 +1,35 @@
1
+ ---
2
+ docgen:
3
+ source: npm/skills/doc-files/js/units-rs.mjs
4
+ crc: 11099381
5
+ score: 100
6
+ ---
7
+
8
+ # units-rs.mjs
9
+
10
+ ## Огляд
11
+
12
+ Файл аналізує рядки для визначення структури та елементів модуля. Він ітерує по рядках для визначення глибини, обробляє рядкові літерали для визначення зміщення, змінює глибину при зустрічі відкриваючої чи закриваючої фігу, видаляє відкриті блоки зі стека, перевіряє наявність атрибутів експонування, визначає декларації на верхньому рівні чи в модулях, а також витягує тіло функції та збирає виклики інших юнітів у тілі функції.
13
+
14
+ ## Поведінка
15
+
16
+ 1. Скан файлу по рядках.
17
+ 2. Ітерація по рядках для визначення глибини.
18
+ 3. Обробка рядкових літералів для визначення зміщення.
19
+ 4. Зміна глибини при зустрічі відкриваючої фігу.
20
+ 5. Зміна глибини при зустрічі закриваючої фігу.
21
+ 6. Видалення відкритих блоків з стека.
22
+ 7. Перевірка наявності атрибутів експонування.
23
+ 8. Визначення декларацій на верхньому рівні або в модулях.
24
+ 9. Визначення публічних функцій, структур, перерахувань, трейтів, типів.
25
+ 10. Витягування тіла функції через пошук закриваючої фігу.
26
+ 11. Збір викликів інших юнітів у тілі функції.
27
+
28
+ ## Публічний API
29
+
30
+ extractUnitsRs — Визначає top-level і impl-методи через підрахунок дужок по рядках. Обмеження: рядкові літерали з `{`/`}` всередині `{}` можуть дати хибну глибину.
31
+
32
+ ## Гарантії поведінки
33
+
34
+ - Read-only: файл не виконує операцій запису у файлову систему.
35
+ - Не звертається до мережі.
@@ -0,0 +1,213 @@
1
+ /** @see ./docs/units-rs.md */
2
+
3
+ /**
4
+ * Пропускає рядковий літерал `"..."` (з escape-послідовностями).
5
+ * @param {string} src вміст файлу
6
+ * @param {number} i позиція відкриваючого `"`
7
+ * @returns {number} позиція ПІСЛЯ закриваючого `"`
8
+ */
9
+ function skipString(src, i) {
10
+ i++ // відкриваючий "
11
+ while (i < src.length) {
12
+ if (src[i] === '\\') { i += 2; continue }
13
+ if (src[i] === '"') return i + 1
14
+ i++
15
+ }
16
+ return i
17
+ }
18
+
19
+ /**
20
+ * Знаходить індекс закриваючої `}` для відкриваючої `{` на позиції `start`.
21
+ * Правильно пропускає рядки/блочні коментарі та рядкові літерали.
22
+ * @param {string} src вміст файлу
23
+ * @param {number} start позиція відкриваючої `{`
24
+ * @returns {number} індекс `}` або -1, якщо не знайдено
25
+ */
26
+ function findClosingBrace(src, start) {
27
+ let depth = 0
28
+ let i = start
29
+ while (i < src.length) {
30
+ const ch = src[i]
31
+ if (ch === '/' && src[i + 1] === '/') {
32
+ const nl = src.indexOf('\n', i)
33
+ i = nl === -1 ? src.length : nl + 1
34
+ continue
35
+ }
36
+ if (ch === '/' && src[i + 1] === '*') {
37
+ const end = src.indexOf('*/', i + 2)
38
+ i = end === -1 ? src.length : end + 2
39
+ continue
40
+ }
41
+ if (ch === '"') { i = skipString(src, i); continue }
42
+ if (ch === '{') { depth++; i++; continue }
43
+ if (ch === '}') {
44
+ depth--
45
+ if (depth === 0) return i
46
+ i++
47
+ continue
48
+ }
49
+ i++
50
+ }
51
+ return -1
52
+ }
53
+
54
+ /**
55
+ * Видобуває `///` doc-рядки безпосередньо перед рядком `lineIdx`.
56
+ * Сканує назад через `///`, `#[...]` та пусті рядки.
57
+ * @param {string[]} lines рядки файлу
58
+ * @param {number} lineIdx рядок декларації
59
+ * @returns {string} склеєний опис або ''
60
+ */
61
+ function docBefore(lines, lineIdx) {
62
+ const doc = []
63
+ for (let i = lineIdx - 1; i >= 0; i--) {
64
+ const t = lines[i].trim()
65
+ if (t.startsWith('///')) {
66
+ doc.unshift(t.slice(3).trim())
67
+ } else if (t.startsWith('#[') || t.startsWith('#![') || t === '') {
68
+ // пропустити атрибути та пусті рядки
69
+ } else {
70
+ break
71
+ }
72
+ }
73
+ return doc.join(' ').trim()
74
+ }
75
+
76
+ // Pub-items: pub fn / pub struct / pub enum / pub trait / pub type
77
+ // Також ловить fn без pub (для localSymbols і impl-методів)
78
+ const ITEM_RE = /^[ \t]*(pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/
79
+
80
+ // impl Type { або impl<T> Trait for Type {
81
+ const IMPL_RE = /^[ \t]*(?:pub\s+)?impl(?:<[^>]*>)?\s+(?:(?:\w[\w:<>, ]*\s+for\s+))?(\w+)/
82
+
83
+ // Підозрілі exposure-атрибути, що роблять непуб-fn фактично публічними
84
+ const EXPOSURE_ATTR_RE = /#\[(?:tauri::command|wasm_bindgen|uniffi::export|pyo3::pyfunction|napi)/
85
+
86
+ // Базовий виклик fn-імені (для call-graph всередині юніта)
87
+ const CALL_RE = /\b([a-z_]\w*)\s*\(/g
88
+
89
+ /**
90
+ * Юніт-екстрактор для `.rs` файлів.
91
+ * Визначає top-level і impl-методи через підрахунок дужок по рядках.
92
+ * Відомі обмеження: рядкові літерали з `{`/`}` всередині `{}` можуть дати
93
+ * хибну глибину (рідкісно в реальному Rust-коді з rustfmt).
94
+ * @param {string} src вміст файлу
95
+ * @param {string} [_relPath] резервний (не використовується)
96
+ * @returns {Array<{name:string, kind:string, exported:boolean, implName:string|null, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null}
97
+ */
98
+ export function extractUnitsRs(src, _relPath) {
99
+ const lines = src.split('\n')
100
+ const units = []
101
+ let depth = 0
102
+ let lineOffset = 0
103
+ // Стек відкритих impl: { typeName, openDepth }
104
+ const implStack = []
105
+ // Флаг: наступний fn отримує exposure (через #[tauri::command] тощо)
106
+ let nextFnExposed = false
107
+
108
+ for (let li = 0; li < lines.length; li++) {
109
+ const line = lines[li]
110
+ const depthAtStart = depth
111
+
112
+ // Підраховуємо `{` і `}` в рядку (пропускаємо рядкові коментарі та рядки)
113
+ let j = 0
114
+ while (j < line.length) {
115
+ const ch = line[j]
116
+ if (ch === '/' && line[j + 1] === '/') break
117
+ if (ch === '"') {
118
+ j++
119
+ while (j < line.length && line[j] !== '"') {
120
+ if (line[j] === '\\') j++
121
+ j++
122
+ }
123
+ j++
124
+ continue
125
+ }
126
+ if (ch === '{') depth++
127
+ else if (ch === '}') depth--
128
+ j++
129
+ }
130
+
131
+ // Закриті impl-блоки прибираємо зі стека
132
+ while (implStack.length > 0 && implStack[implStack.length - 1].openDepth > depth) {
133
+ implStack.pop()
134
+ }
135
+
136
+ const currentImpl = implStack.at(-1)?.typeName ?? null
137
+
138
+ // Перевіряємо exposure-атрибути
139
+ if (EXPOSURE_ATTR_RE.test(line)) {
140
+ nextFnExposed = true
141
+ }
142
+
143
+ // Impl-декларація (зазвичай глибина 0, але може бути в mod)
144
+ if (depthAtStart <= 1) {
145
+ const implM = line.match(IMPL_RE)
146
+ if (implM && line.includes('{')) {
147
+ implStack.push({ typeName: implM[1], openDepth: depth })
148
+ }
149
+ }
150
+
151
+ // Елементи на глибині 0 (top-level) і 1 (всередині impl)
152
+ if (depthAtStart <= 1) {
153
+ const m = line.match(ITEM_RE)
154
+ if (m) {
155
+ const isPub = Boolean(m[1]) || (m[2] === 'fn' && nextFnExposed)
156
+ if (m[2] === 'fn') nextFnExposed = false
157
+ const kind = m[2]
158
+ const name = m[3]
159
+ const doc = docBefore(lines, li)
160
+
161
+ // Витягуємо тіло через findClosingBrace для fn/struct/enum/trait
162
+ let body = ''
163
+ let itemEnd = lineOffset + line.length
164
+ if (kind !== 'type') {
165
+ const openBraceIdx = src.indexOf('{', lineOffset)
166
+ // Шукаємо `{` не далі ніж через 3 рядки від початку декларації
167
+ const threeLines = lines.slice(li, li + 3).join('\n').length
168
+ if (openBraceIdx !== -1 && openBraceIdx - lineOffset <= threeLines) {
169
+ const closeIdx = findClosingBrace(src, openBraceIdx)
170
+ if (closeIdx !== -1) {
171
+ itemEnd = closeIdx + 1
172
+ body = src.slice(lineOffset, itemEnd)
173
+ }
174
+ }
175
+ }
176
+
177
+ units.push({
178
+ name,
179
+ kind,
180
+ exported: isPub,
181
+ implName: depthAtStart === 1 ? currentImpl : null,
182
+ span: { start: lineOffset, end: itemEnd },
183
+ body,
184
+ calls: [],
185
+ doc
186
+ })
187
+ } else {
188
+ // Рядок не є item — скидаємо exposure-флаг якщо не атрибут
189
+ const t = line.trim()
190
+ if (!t.startsWith('#[') && !t.startsWith('#![') && !t.startsWith('///') && t !== '') {
191
+ nextFnExposed = false
192
+ }
193
+ }
194
+ }
195
+
196
+ lineOffset += line.length + 1 // +1 для '\n'
197
+ }
198
+
199
+ // Базовий call-graph: виклики інших юнітів цього файлу
200
+ const unitNames = new Set(units.map(u => u.name))
201
+ for (const u of units) {
202
+ if (!u.body) continue
203
+ const calls = new Set()
204
+ let cm
205
+ const re = new RegExp(CALL_RE.source, 'g')
206
+ while ((cm = re.exec(u.body)) !== null) {
207
+ if (unitNames.has(cm[1]) && cm[1] !== u.name) calls.add(cm[1])
208
+ }
209
+ u.calls = [...calls]
210
+ }
211
+
212
+ return units.length > 0 ? units : null
213
+ }
@@ -1,13 +1,13 @@
1
1
  /** @see ./docs/units.md */
2
2
 
3
3
  import { extractUnitsJs } from './units-js.mjs'
4
+ import { extractUnitsRs } from './units-rs.mjs'
4
5
 
5
6
  const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
6
7
 
7
8
  /**
8
- * Мовно-агностичний фасад юніт-шару (Інкремент 1). Диспатчить за розширенням:
9
- * js/mjs/ts → oxc; vue/py додаються наступними кроками (поки `null` → виклик
10
- * відкочується на whole-file шлях, як і раніше).
9
+ * Мовно-агностичний фасад юніт-шару. Диспатчить за розширенням:
10
+ * js/mjs/ts → oxc AST; rs → regex+brace-counting; vue/py null (whole-file шлях).
11
11
  * @param {string} src вміст файлу
12
12
  * @param {string} relPath шлях файлу
13
13
  * @returns {Array<object>|null} юніти або null, якщо мова ще не підтримана / файл не парситься
@@ -15,5 +15,6 @@ const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
15
15
  export function extractUnits(src, relPath) {
16
16
  const ext = (relPath.split('.').pop() || '').toLowerCase()
17
17
  if (JS_EXT.has(ext)) return extractUnitsJs(src, relPath)
18
+ if (ext === 'rs') return extractUnitsRs(src, relPath)
18
19
  return null
19
20
  }