@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.
- package/CHANGELOG.md +21 -0
- package/bin/n-cursor.js +16 -16
- package/package.json +2 -1
- package/rules/abie/fix.mjs +3 -3
- package/rules/adr/fix.mjs +3 -3
- package/rules/adr/js/hooks.mjs +6 -6
- package/rules/bun/fix.mjs +3 -3
- package/rules/capacitor/fix.mjs +3 -3
- package/rules/changelog/fix.mjs +3 -3
- package/rules/changelog/js/consistency.mjs +15 -15
- package/rules/ci4/fix.mjs +3 -3
- package/rules/docker/fix.mjs +3 -3
- package/rules/efes/fix.mjs +3 -3
- package/rules/feedback/fix.mjs +3 -3
- package/rules/ga/fix.mjs +3 -3
- package/rules/graphql/fix.mjs +3 -3
- package/rules/hasura/fix.mjs +3 -3
- package/rules/image-avif/fix.mjs +3 -3
- package/rules/image-compress/fix.mjs +3 -3
- package/rules/js-bun-db/fix.mjs +3 -3
- package/rules/js-bun-redis/fix.mjs +3 -3
- package/rules/js-lint/coverage/coverage.mjs +26 -36
- package/rules/js-lint/fix.mjs +3 -3
- package/rules/js-mssql/fix.mjs +3 -3
- package/rules/js-run/fix.mjs +3 -3
- package/rules/k8s/fix.mjs +3 -3
- package/rules/nginx-default-tpl/fix.mjs +3 -3
- package/rules/npm-module/fix.mjs +3 -3
- package/rules/php/fix.mjs +3 -3
- package/rules/rego/fix.mjs +3 -3
- package/rules/rust/coverage/coverage.mjs +22 -43
- package/rules/rust/fix.mjs +3 -3
- package/rules/rust/lib/has-cargo-toml.mjs +1 -3
- package/rules/security/fix.mjs +3 -3
- package/rules/style-lint/fix.mjs +3 -3
- package/rules/style-lint/js/tooling.mjs +1 -1
- package/rules/tauri/fix.mjs +3 -3
- package/rules/test/coverage/coverage.mjs +27 -25
- package/rules/test/fix.mjs +3 -3
- package/rules/test/js/cargo_mutants_config.mjs +65 -0
- package/rules/test/js/data/cargo_mutants_config/mutants.toml.baseline +4 -0
- package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +12 -0
- package/rules/test/js/location.mjs +1 -1
- package/rules/test/js/stryker_config.mjs +61 -0
- package/rules/test/test.mdc +16 -3
- package/rules/text/fix.mjs +3 -3
- package/rules/vue/fix.mjs +3 -3
- package/scripts/lib/run-rule-cli.mjs +11 -0
- package/scripts/lib/run-standard-rule.mjs +1 -1
- package/scripts/utils/resolve-cargo-manifest.mjs +62 -0
- package/scripts/utils/resolve-js-root.mjs +46 -0
- package/scripts/utils/with-lock.mjs +27 -16
- 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
|
-
|
|
100
|
-
const
|
|
85
|
+
runJsCoverage({ cwd, lcovDir }) {
|
|
86
|
+
const r = spawnSync('bun', ['run', 'test:coverage', '--coverage-reporter=lcov', `--coverage-dir=${lcovDir}`], {
|
|
101
87
|
cwd,
|
|
102
|
-
|
|
103
|
-
|
|
88
|
+
stdio: 'inherit',
|
|
89
|
+
env: process.env
|
|
104
90
|
})
|
|
105
|
-
return
|
|
91
|
+
return r.status ?? 1
|
|
106
92
|
},
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
return
|
|
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(
|
|
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
|
|
package/rules/js-lint/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 (
|
|
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
|
-
|
|
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/js-mssql/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 (
|
|
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
|
-
|
|
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/js-run/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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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/npm-module/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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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/rego/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 (
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
const exitCode = await proc.exited
|
|
60
|
-
return { exitCode, stdout }
|
|
36
|
+
return { exitCode: r.status ?? 1, stdout: r.stdout?.toString('utf8') ?? '' }
|
|
61
37
|
},
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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 })
|
package/rules/rust/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 (
|
|
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
|
-
|
|
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
|
}
|
package/rules/security/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 (
|
|
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
|
-
|
|
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/style-lint/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 (
|
|
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
|
-
|
|
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('
|
|
28
|
+
* @param {import('../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter репортер
|
|
29
29
|
*/
|
|
30
30
|
async function checkStylelintConfigPresence(reporter) {
|
|
31
31
|
const { pass, fail } = reporter
|
package/rules/tauri/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 (
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 = {}) {
|
package/rules/test/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 (
|
|
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
|
-
|
|
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
|
}
|