@nitra/cursor 1.17.0 → 1.17.2

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 (53) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/n-cursor.js +16 -16
  3. package/package.json +2 -1
  4. package/rules/abie/fix.mjs +3 -3
  5. package/rules/adr/fix.mjs +3 -3
  6. package/rules/adr/js/hooks.mjs +6 -6
  7. package/rules/bun/fix.mjs +3 -3
  8. package/rules/capacitor/fix.mjs +3 -3
  9. package/rules/changelog/fix.mjs +3 -3
  10. package/rules/changelog/js/consistency.mjs +15 -15
  11. package/rules/ci4/fix.mjs +3 -3
  12. package/rules/docker/fix.mjs +3 -3
  13. package/rules/efes/fix.mjs +3 -3
  14. package/rules/feedback/fix.mjs +3 -3
  15. package/rules/ga/fix.mjs +3 -3
  16. package/rules/graphql/fix.mjs +3 -3
  17. package/rules/hasura/fix.mjs +3 -3
  18. package/rules/image-avif/fix.mjs +3 -3
  19. package/rules/image-compress/fix.mjs +3 -3
  20. package/rules/js-bun-db/fix.mjs +3 -3
  21. package/rules/js-bun-redis/fix.mjs +3 -3
  22. package/rules/js-lint/coverage/coverage.mjs +26 -36
  23. package/rules/js-lint/fix.mjs +3 -3
  24. package/rules/js-mssql/fix.mjs +3 -3
  25. package/rules/js-run/fix.mjs +3 -3
  26. package/rules/k8s/fix.mjs +3 -3
  27. package/rules/nginx-default-tpl/fix.mjs +3 -3
  28. package/rules/npm-module/fix.mjs +3 -3
  29. package/rules/php/fix.mjs +3 -3
  30. package/rules/rego/fix.mjs +3 -3
  31. package/rules/rust/coverage/coverage.mjs +22 -43
  32. package/rules/rust/fix.mjs +3 -3
  33. package/rules/rust/lib/has-cargo-toml.mjs +1 -3
  34. package/rules/security/fix.mjs +3 -3
  35. package/rules/style-lint/fix.mjs +3 -3
  36. package/rules/style-lint/js/tooling.mjs +1 -1
  37. package/rules/tauri/fix.mjs +3 -3
  38. package/rules/test/coverage/coverage.mjs +27 -25
  39. package/rules/test/fix.mjs +3 -3
  40. package/rules/test/js/cargo_mutants_config.mjs +65 -0
  41. package/rules/test/js/data/cargo_mutants_config/mutants.toml.baseline +4 -0
  42. package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +12 -0
  43. package/rules/test/js/location.mjs +1 -1
  44. package/rules/test/js/stryker_config.mjs +61 -0
  45. package/rules/test/test.mdc +16 -3
  46. package/rules/text/fix.mjs +3 -3
  47. package/rules/vue/fix.mjs +3 -3
  48. package/scripts/lib/run-rule-cli.mjs +11 -0
  49. package/scripts/lib/run-standard-rule.mjs +1 -1
  50. package/scripts/utils/resolve-cargo-manifest.mjs +62 -0
  51. package/scripts/utils/resolve-js-root.mjs +46 -0
  52. package/scripts/utils/with-lock.mjs +27 -16
  53. package/scripts/utils/worktree-fingerprint.mjs +10 -7
@@ -5,33 +5,18 @@
5
5
  *
6
6
  * Контракт провайдера — у docs/superpowers/specs/2026-05-24-coverage-rule-design.md.
7
7
  */
8
+ import { spawnSync } from 'node:child_process'
8
9
  import { existsSync } from 'node:fs'
9
10
  import { mkdtemp, readFile, rm } from 'node:fs/promises'
10
11
  import { tmpdir } from 'node:os'
11
12
  import { join } from 'node:path'
12
13
 
13
- /**
14
- * Резолвить cwd, у якому стоять JS-тести. Workspace-проєкти — перший workspace
15
- * (mlmail: app/), single-package — корінь.
16
- * @param {string} cwd корінь проєкту
17
- * @returns {Promise<string|null>} абсолютний шлях або null якщо package.json відсутній
18
- */
19
- async function resolveJsRoot(cwd) {
20
- const rootPkgPath = join(cwd, 'package.json')
21
- if (!existsSync(rootPkgPath)) return null
22
- const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
23
- const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
24
- if (workspaces.length > 0) {
25
- const wsPath = join(cwd, workspaces[0])
26
- if (existsSync(join(wsPath, 'package.json'))) return wsPath
27
- }
28
- return cwd
29
- }
14
+ import { resolveJsRoot } from '../../../scripts/utils/resolve-js-root.mjs'
30
15
 
31
16
  /**
32
17
  * Чи `scripts` містить coverage-сумісну команду.
33
- * @param {Record<string, string> | undefined} scripts
34
- * @returns {boolean}
18
+ * @param {Record<string, string> | undefined} scripts секція scripts з package.json
19
+ * @returns {boolean} true, якщо є test:coverage або test з --coverage
35
20
  */
36
21
  function hasCoverageScript(scripts) {
37
22
  if (!scripts || typeof scripts !== 'object') return false
@@ -42,8 +27,8 @@ function hasCoverageScript(scripts) {
42
27
 
43
28
  /**
44
29
  * Чи провайдер застосовний у поточному cwd.
45
- * @param {string} cwd
46
- * @returns {Promise<boolean>}
30
+ * @param {string} cwd корінь проєкту
31
+ * @returns {Promise<boolean>} true, якщо знайдено coverage-сумісний test-скрипт
47
32
  */
48
33
  export async function detect(cwd) {
49
34
  const jsRoot = await resolveJsRoot(cwd)
@@ -56,8 +41,8 @@ export async function detect(cwd) {
56
41
 
57
42
  /**
58
43
  * Парс lcov.info: сумує LF/LH (рядки) і FNF/FNH (функції) по всіх records.
59
- * @param {string} text
60
- * @returns {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}}
44
+ * @param {string} text вміст lcov.info
45
+ * @returns {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} агреговані totals
61
46
  */
62
47
  function parseLcov(text) {
63
48
  const acc = { lines: { covered: 0, total: 0 }, functions: { covered: 0, total: 0 } }
@@ -73,8 +58,8 @@ function parseLcov(text) {
73
58
  /**
74
59
  * Парс Stryker mutation.json: Killed+Timeout → caught; Survived+NoCoverage → до total.
75
60
  * Compile/Runtime errors виключаються з total.
76
- * @param {{files:Record<string,{mutants:Array<{status:string}>}>}} report
77
- * @returns {{caught:number,total:number}}
61
+ * @param {{files:Record<string,{mutants:Array<{status:string}>}>}} report розпарсений mutation.json
62
+ * @returns {{caught:number,total:number}} агрегований mutation score
78
63
  */
79
64
  function parseStrykerReport(report) {
80
65
  let caught = 0
@@ -93,20 +78,21 @@ function parseStrykerReport(report) {
93
78
  }
94
79
 
95
80
  /**
96
- * Default runner — спавнить реальні bun-команди. Замінюється у тестах.
81
+ * Default runner — спавнить реальні bun-команди через `node:child_process.spawnSync`
82
+ * (працює і в Node-runtime через shebang `n-cursor`, і в Bun). Замінюється у тестах.
97
83
  */
98
84
  const defaultRunner = {
99
- async runJsCoverage({ cwd, lcovDir }) {
100
- const proc = Bun.spawn(['bun', 'run', 'test:coverage', '--coverage-reporter=lcov', `--coverage-dir=${lcovDir}`], {
85
+ runJsCoverage({ cwd, lcovDir }) {
86
+ const r = spawnSync('bun', ['run', 'test:coverage', '--coverage-reporter=lcov', `--coverage-dir=${lcovDir}`], {
101
87
  cwd,
102
- stdout: 'inherit',
103
- stderr: 'inherit'
88
+ stdio: 'inherit',
89
+ env: process.env
104
90
  })
105
- return proc.exited
91
+ return r.status ?? 1
106
92
  },
107
- async runStryker({ cwd }) {
108
- const proc = Bun.spawn(['bunx', 'stryker', 'run'], { cwd, stdout: 'inherit', stderr: 'inherit' })
109
- return proc.exited
93
+ runStryker({ cwd }) {
94
+ const r = spawnSync('bunx', ['stryker', 'run'], { cwd, stdio: 'inherit', env: process.env })
95
+ return r.status ?? 1
110
96
  }
111
97
  }
112
98
 
@@ -114,7 +100,7 @@ const defaultRunner = {
114
100
  * Збирає JS-метрики покриття + мутаційного тестування.
115
101
  * @param {string} cwd корінь проєкту
116
102
  * @param {{runner?: typeof defaultRunner}} [opts] runner-ін'єкція для тестів
117
- * @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>}
103
+ * @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>} рядки для COVERAGE.md
118
104
  */
119
105
  export async function collect(cwd, opts = {}) {
120
106
  const runner = opts.runner ?? defaultRunner
@@ -138,7 +124,11 @@ export async function collect(cwd, opts = {}) {
138
124
  try {
139
125
  mutationReport = JSON.parse(await readFile(join(jsRoot, 'reports', 'stryker', 'mutation.json'), 'utf8'))
140
126
  } catch {
141
- throw new Error('js-lint coverage: stryker не залишив mutation.json — перевір stryker.config.mjs у проєкті')
127
+ throw new Error(
128
+ 'js-lint coverage: stryker не залишив mutation.json — ' +
129
+ 'запусти `npx @nitra/cursor fix test` для встановлення canonical stryker.config.mjs, ' +
130
+ 'або налаштуй його вручну'
131
+ )
142
132
  }
143
133
  const mutation = parseStrykerReport(mutationReport)
144
134
 
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
package/rules/k8s/fix.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
package/rules/php/fix.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -6,77 +6,56 @@
6
6
  *
7
7
  * Контракт провайдера — у docs/superpowers/specs/2026-05-24-coverage-rule-design.md.
8
8
  */
9
+ import { spawnSync } from 'node:child_process'
9
10
  import { existsSync } from 'node:fs'
10
11
  import { mkdtemp, readFile, rm } from 'node:fs/promises'
11
12
  import { tmpdir } from 'node:os'
12
13
  import { join } from 'node:path'
13
14
 
14
15
  import { hasCargoTomlInTree } from '../lib/has-cargo-toml.mjs'
16
+ import { resolveCargoManifest } from '../../../scripts/utils/resolve-cargo-manifest.mjs'
15
17
 
16
18
  const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo', 'target'])
17
19
 
18
20
  /**
19
21
  * Чи провайдер застосовний у поточному cwd.
20
- * @param {string} cwd
21
- * @returns {Promise<boolean>}
22
- */
23
- export async function detect(cwd) {
24
- if (existsSync(join(cwd, 'Cargo.toml'))) return true
25
- return hasCargoTomlInTree(cwd, IGNORED_DIR_NAMES)
26
- }
27
-
28
- /**
29
- * Знайти Cargo.toml: cwd/Cargo.toml або в одному з workspace-підкаталогів.
30
- * @param {string} cwd
31
- * @returns {Promise<string>} абсолютний шлях до Cargo.toml
22
+ * @param {string} cwd корінь проєкту
23
+ * @returns {Promise<boolean>} true, якщо знайдено Cargo.toml у cwd або workspace-піддереві
32
24
  */
33
- async function resolveCargoManifest(cwd) {
34
- const rootManifest = join(cwd, 'Cargo.toml')
35
- if (existsSync(rootManifest)) return rootManifest
36
-
37
- const rootPkgPath = join(cwd, 'package.json')
38
- if (existsSync(rootPkgPath)) {
39
- const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
40
- const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
41
- for (const ws of workspaces) {
42
- const tauriManifest = join(cwd, ws, 'src-tauri', 'Cargo.toml')
43
- if (existsSync(tauriManifest)) return tauriManifest
44
- const flatManifest = join(cwd, ws, 'Cargo.toml')
45
- if (existsSync(flatManifest)) return flatManifest
46
- }
47
- }
48
-
49
- throw new Error('rust coverage: Cargo.toml не знайдено (cwd + workspaces)')
25
+ export function detect(cwd) {
26
+ if (existsSync(join(cwd, 'Cargo.toml'))) return Promise.resolve(true)
27
+ return Promise.resolve(hasCargoTomlInTree(cwd, IGNORED_DIR_NAMES))
50
28
  }
51
29
 
52
30
  const defaultRunner = {
53
- async runLlvmCov({ manifestPath }) {
54
- const proc = Bun.spawn(['cargo', 'llvm-cov', '--manifest-path', manifestPath, '--json', '--summary-only'], {
55
- stdout: 'pipe',
56
- stderr: 'inherit'
31
+ runLlvmCov({ manifestPath }) {
32
+ const r = spawnSync('cargo', ['llvm-cov', '--manifest-path', manifestPath, '--json', '--summary-only'], {
33
+ stdio: ['inherit', 'pipe', 'inherit'],
34
+ env: process.env
57
35
  })
58
- const stdout = await new Response(proc.stdout).text()
59
- const exitCode = await proc.exited
60
- return { exitCode, stdout }
36
+ return { exitCode: r.status ?? 1, stdout: r.stdout?.toString('utf8') ?? '' }
61
37
  },
62
- async runCargoMutants({ manifestPath, outDir }) {
63
- const proc = Bun.spawn(['cargo', 'mutants', '--in-place', '-o', outDir, '--manifest-path', manifestPath], {
64
- stdout: 'inherit',
65
- stderr: 'inherit'
38
+ runCargoMutants({ manifestPath, outDir }) {
39
+ const r = spawnSync('cargo', ['mutants', '--in-place', '-o', outDir, '--manifest-path', manifestPath], {
40
+ stdio: 'inherit',
41
+ env: process.env
66
42
  })
67
- return proc.exited
43
+ return r.status ?? 1
68
44
  }
69
45
  }
70
46
 
71
47
  /**
72
48
  * Збирає Rust-метрики покриття + мутаційного тестування.
73
49
  * @param {string} cwd корінь проєкту
74
- * @param {{runner?: typeof defaultRunner}} [opts]
75
- * @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>}
50
+ * @param {{runner?: typeof defaultRunner}} [opts] ін'єкція runner-а для тестів
51
+ * @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>} рядки для COVERAGE.md
76
52
  */
77
53
  export async function collect(cwd, opts = {}) {
78
54
  const runner = opts.runner ?? defaultRunner
79
55
  const manifestPath = await resolveCargoManifest(cwd)
56
+ if (manifestPath === null) {
57
+ throw new Error('rust coverage: Cargo.toml не знайдено (cwd + workspaces)')
58
+ }
80
59
 
81
60
  // 1. Coverage через cargo llvm-cov
82
61
  const { exitCode: llvmCode, stdout: llvmJson } = await runner.runLlvmCov({ manifestPath })
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -29,9 +29,7 @@ export function hasCargoTomlInTree(root, ignoredDirNames) {
29
29
  }
30
30
  for (const entry of entries) {
31
31
  if (entry.isFile() && entry.name === 'Cargo.toml') return true
32
- if (entry.isDirectory() && !ignoredDirNames.has(entry.name)) {
33
- if (walk(join(dir, entry.name))) return true
34
- }
32
+ if (entry.isDirectory() && !ignoredDirNames.has(entry.name) && walk(join(dir, entry.name))) return true
35
33
  }
36
34
  return false
37
35
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -25,7 +25,7 @@ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
25
25
  * Альтернатива полю `stylelint` у `package.json` — зовнішній файл конфігу. Якщо
26
26
  * поля немає і файлу немає, фейлимося; якщо є хоч щось — пропускаємо. Поле
27
27
  * `stylelint.extends == "@nitra/stylelint-config"` сам формат — у Rego.
28
- * @param {import('../../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter репортер
28
+ * @param {import('../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter репортер
29
29
  */
30
30
  async function checkStylelintConfigPresence(reporter) {
31
31
  const { pass, fail } = reporter
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -15,7 +15,7 @@
15
15
  import { existsSync } from 'node:fs'
16
16
  import { writeFile } from 'node:fs/promises'
17
17
  import { dirname, join } from 'node:path'
18
- import { fileURLToPath } from 'node:url'
18
+ import { fileURLToPath, pathToFileURL } from 'node:url'
19
19
 
20
20
  import { readNCursorConfigLite } from '../../../scripts/lib/read-n-cursor-config-lite.mjs'
21
21
  import { withLock } from '../../../scripts/utils/with-lock.mjs'
@@ -25,9 +25,9 @@ const RULES_DIR = dirname(dirname(dirname(fileURLToPath(import.meta.url))))
25
25
 
26
26
  /**
27
27
  * Сума двох coverage-totals.
28
- * @param {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} a
29
- * @param {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} b
30
- * @returns {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}}
28
+ * @param {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} a перший subtotal
29
+ * @param {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} b другий subtotal
30
+ * @returns {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} сумарні lines/functions
31
31
  */
32
32
  export function addCoverage(a, b) {
33
33
  return {
@@ -41,9 +41,9 @@ export function addCoverage(a, b) {
41
41
 
42
42
  /**
43
43
  * Сума двох mutation-counts.
44
- * @param {{caught:number,total:number}} a
45
- * @param {{caught:number,total:number}} b
46
- * @returns {{caught:number,total:number}}
44
+ * @param {{caught:number,total:number}} a перший subtotal
45
+ * @param {{caught:number,total:number}} b другий subtotal
46
+ * @returns {{caught:number,total:number}} сумарні caught/total
47
47
  */
48
48
  export function addMutation(a, b) {
49
49
  return { caught: a.caught + b.caught, total: a.total + b.total }
@@ -51,8 +51,8 @@ export function addMutation(a, b) {
51
51
 
52
52
  /**
53
53
  * Форматує covered/total як `XX.XX% (covered/total)`.
54
- * @param {{covered:number,total:number}} metric
55
- * @returns {string}
54
+ * @param {{covered:number,total:number}} metric метрика lines або functions
55
+ * @returns {string} відформатований рядок для таблиці COVERAGE.md
56
56
  */
57
57
  export function formatCoverage({ covered, total }) {
58
58
  const percent = total === 0 ? '—' : `${((covered / total) * 100).toFixed(2)}%`
@@ -61,8 +61,8 @@ export function formatCoverage({ covered, total }) {
61
61
 
62
62
  /**
63
63
  * Форматує мутаційний score як `XX.XX%`.
64
- * @param {{caught:number,total:number}} metric
65
- * @returns {string}
64
+ * @param {{caught:number,total:number}} metric агрегований mutation score
65
+ * @returns {string} відформатований score або прочерк
66
66
  */
67
67
  export function formatScore({ caught, total }) {
68
68
  return total === 0 ? '—' : `${((caught / total) * 100).toFixed(2)}%`
@@ -71,8 +71,8 @@ export function formatScore({ caught, total }) {
71
71
  /**
72
72
  * Рендерить таблицю покриття + мутаційного тестування як Markdown.
73
73
  * Без timestamp, щоб git diff рухався лише при зміні метрик.
74
- * @param {Array<{area:string, coverage:{lines:{covered:number,total:number},functions:{covered:number,total:number}}, mutation:{caught:number,total:number}}>} rows
75
- * @returns {string}
74
+ * @param {Array<{area:string, coverage:{lines:{covered:number,total:number},functions:{covered:number,total:number}}, mutation:{caught:number,total:number}}>} rows рядки провайдерів
75
+ * @returns {string} Markdown-таблиця з заголовком `# Coverage`
76
76
  */
77
77
  export function renderMarkdown(rows) {
78
78
  const lines = [
@@ -96,36 +96,38 @@ export function renderMarkdown(rows) {
96
96
  * - файлу немає (rule без coverage-провайдера),
97
97
  * - файл існує, але не експортує `detect` + `collect` як функції (наприклад,
98
98
  * `rules/test/coverage/coverage.mjs` — сам оркестратор, не провайдер).
99
- * @param {string} rulesDir
100
- * @param {string} ruleId
101
- * @returns {Promise<{detect:Function, collect:Function}|null>}
99
+ * @param {string} rulesDir корінь `npm/rules/`
100
+ * @param {string} ruleId id правила з `.n-cursor.json#rules`
101
+ * @returns {Promise<{detect:(cwd:string)=>Promise<boolean>, collect:(cwd:string)=>Promise<Array<object>>}|null>} provider-модуль або null
102
102
  */
103
103
  async function loadProvider(rulesDir, ruleId) {
104
104
  const providerPath = join(rulesDir, ruleId, 'coverage', 'coverage.mjs')
105
105
  if (!existsSync(providerPath)) return null
106
- const mod = await import(providerPath)
106
+ // eslint-disable-next-line no-unsanitized/method -- providerPath з join(rulesDir, ruleId, …), ruleId з конфігу
107
+ const mod = await import(pathToFileURL(providerPath).href)
107
108
  if (typeof mod.detect !== 'function' || typeof mod.collect !== 'function') return null
108
109
  return mod
109
110
  }
110
111
 
111
112
  /**
112
113
  * Будує підсумковий рядок «Разом» через сумування всіх coverage/mutation.
113
- * @param {Array<{area:string, coverage:object, mutation:object}>} rows
114
- * @returns {{area:string, coverage:object, mutation:{caught:number,total:number}}}
114
+ * @param {Array<{area:string, coverage:object, mutation:object}>} rows рядки провайдерів без totals
115
+ * @returns {{area:string, coverage:object, mutation:{caught:number,total:number}}} агрегований рядок «Разом»
115
116
  */
116
117
  function buildTotalsRow(rows) {
117
- const totalCoverage = rows.reduce((acc, row) => addCoverage(acc, row.coverage), {
118
- lines: { covered: 0, total: 0 },
119
- functions: { covered: 0, total: 0 }
120
- })
121
- const totalMutation = rows.reduce((acc, row) => addMutation(acc, row.mutation), { caught: 0, total: 0 })
118
+ let totalCoverage = { lines: { covered: 0, total: 0 }, functions: { covered: 0, total: 0 } }
119
+ let totalMutation = { caught: 0, total: 0 }
120
+ for (const row of rows) {
121
+ totalCoverage = addCoverage(totalCoverage, row.coverage)
122
+ totalMutation = addMutation(totalMutation, row.mutation)
123
+ }
122
124
  return { area: '**Разом**', coverage: totalCoverage, mutation: totalMutation }
123
125
  }
124
126
 
125
127
  /**
126
128
  * Виконує coverage-pipeline: discovery провайдерів за `.n-cursor.json#rules`,
127
129
  * detect+collect для кожного, агрегація, запис COVERAGE.md.
128
- * @param {{cwd?:string, rulesDir?:string}} [opts] ін'єкція для тестів
130
+ * @param {{cwd?:string, rulesDir?:string}} [opts] ін'єкція cwd/rulesDir для тестів
129
131
  * @returns {Promise<number>} exit code (0 OK, 1 коли жоден провайдер не дав даних)
130
132
  */
131
133
  export async function runCoverageSteps(opts = {}) {
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }