@nitra/cursor 1.16.1 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/README.md +2 -2
- package/bin/n-cursor.js +28 -18
- 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-bun-redis/js/imports.mjs +1 -5
- package/rules/js-lint/coverage/coverage.mjs +148 -0
- package/rules/js-lint/fix.mjs +3 -3
- package/rules/js-lint/js-lint.mdc +5 -1
- package/rules/js-mssql/fix.mjs +3 -3
- package/rules/js-run/fix.mjs +3 -3
- package/rules/js-run/js/runtime.mjs +2 -9
- 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 +111 -0
- package/rules/rust/fix.mjs +3 -3
- package/rules/rust/lib/has-cargo-toml.mjs +1 -3
- package/rules/rust/rust.mdc +5 -1
- 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 +165 -0
- package/rules/test/fix.mjs +3 -3
- package/rules/test/js/location.mjs +1 -1
- package/rules/test/policy/package_json/package_json.rego +17 -0
- package/rules/test/policy/package_json/target.json +5 -0
- package/rules/test/policy/package_json/template/package.json.contains.json +5 -0
- package/rules/test/test.mdc +11 -1
- 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/with-lock.mjs +27 -16
- package/scripts/utils/worktree-fingerprint.mjs +10 -9
|
@@ -16,11 +16,7 @@ import { join, relative } from 'node:path'
|
|
|
16
16
|
|
|
17
17
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
18
18
|
import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
|
|
19
|
-
import {
|
|
20
|
-
findRedisImportsInText,
|
|
21
|
-
isRedisScanSourceFile,
|
|
22
|
-
shouldSkipFileForRedisScan
|
|
23
|
-
} from '../lib/redis-imports.mjs'
|
|
19
|
+
import { findRedisImportsInText, isRedisScanSourceFile, shouldSkipFileForRedisScan } from '../lib/redis-imports.mjs'
|
|
24
20
|
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
25
21
|
|
|
26
22
|
/**
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JS-провайдер для `n-cursor coverage`: збирає метрики покриття (`bun test --coverage`)
|
|
3
|
+
* і мутаційного тестування (Stryker) для JS/TS коду. Активується через `js-lint`
|
|
4
|
+
* правило в `.n-cursor.json#rules`; реальна applies-логіка — у `detect(cwd)`.
|
|
5
|
+
*
|
|
6
|
+
* Контракт провайдера — у docs/superpowers/specs/2026-05-24-coverage-rule-design.md.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from 'node:child_process'
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
11
|
+
import { tmpdir } from 'node:os'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Резолвить cwd, у якому стоять JS-тести. Workspace-проєкти — перший workspace
|
|
16
|
+
* (наприклад: app/), single-package — корінь.
|
|
17
|
+
* @param {string} cwd корінь проєкту
|
|
18
|
+
* @returns {Promise<string|null>} абсолютний шлях до JS-root або null без package.json
|
|
19
|
+
*/
|
|
20
|
+
async function resolveJsRoot(cwd) {
|
|
21
|
+
const rootPkgPath = join(cwd, 'package.json')
|
|
22
|
+
if (!existsSync(rootPkgPath)) return null
|
|
23
|
+
const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
24
|
+
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
|
|
25
|
+
if (workspaces.length > 0) {
|
|
26
|
+
const wsPath = join(cwd, workspaces[0])
|
|
27
|
+
if (existsSync(join(wsPath, 'package.json'))) return wsPath
|
|
28
|
+
}
|
|
29
|
+
return cwd
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Чи `scripts` містить coverage-сумісну команду.
|
|
34
|
+
* @param {Record<string, string> | undefined} scripts секція scripts з package.json
|
|
35
|
+
* @returns {boolean} true, якщо є test:coverage або test з --coverage
|
|
36
|
+
*/
|
|
37
|
+
function hasCoverageScript(scripts) {
|
|
38
|
+
if (!scripts || typeof scripts !== 'object') return false
|
|
39
|
+
if (typeof scripts['test:coverage'] === 'string' && scripts['test:coverage'].length > 0) return true
|
|
40
|
+
if (typeof scripts.test === 'string' && scripts.test.includes('--coverage')) return true
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Чи провайдер застосовний у поточному cwd.
|
|
46
|
+
* @param {string} cwd корінь проєкту
|
|
47
|
+
* @returns {Promise<boolean>} true, якщо знайдено coverage-сумісний test-скрипт
|
|
48
|
+
*/
|
|
49
|
+
export async function detect(cwd) {
|
|
50
|
+
const jsRoot = await resolveJsRoot(cwd)
|
|
51
|
+
if (jsRoot === null) return false
|
|
52
|
+
const pkgPath = join(jsRoot, 'package.json')
|
|
53
|
+
if (!existsSync(pkgPath)) return false
|
|
54
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
55
|
+
return hasCoverageScript(pkg.scripts)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Парс lcov.info: сумує LF/LH (рядки) і FNF/FNH (функції) по всіх records.
|
|
60
|
+
* @param {string} text вміст lcov.info
|
|
61
|
+
* @returns {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} агреговані totals
|
|
62
|
+
*/
|
|
63
|
+
function parseLcov(text) {
|
|
64
|
+
const acc = { lines: { covered: 0, total: 0 }, functions: { covered: 0, total: 0 } }
|
|
65
|
+
for (const line of text.split('\n')) {
|
|
66
|
+
if (line.startsWith('LF:')) acc.lines.total += Number(line.slice(3))
|
|
67
|
+
else if (line.startsWith('LH:')) acc.lines.covered += Number(line.slice(3))
|
|
68
|
+
else if (line.startsWith('FNF:')) acc.functions.total += Number(line.slice(4))
|
|
69
|
+
else if (line.startsWith('FNH:')) acc.functions.covered += Number(line.slice(4))
|
|
70
|
+
}
|
|
71
|
+
return acc
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Парс Stryker mutation.json: Killed+Timeout → caught; Survived+NoCoverage → до total.
|
|
76
|
+
* Compile/Runtime errors виключаються з total.
|
|
77
|
+
* @param {{files:Record<string,{mutants:Array<{status:string}>}>}} report розпарсений mutation.json
|
|
78
|
+
* @returns {{caught:number,total:number}} агрегований mutation score
|
|
79
|
+
*/
|
|
80
|
+
function parseStrykerReport(report) {
|
|
81
|
+
let caught = 0
|
|
82
|
+
let total = 0
|
|
83
|
+
for (const file of Object.values(report.files)) {
|
|
84
|
+
for (const mutant of file.mutants) {
|
|
85
|
+
if (mutant.status === 'Killed' || mutant.status === 'Timeout') {
|
|
86
|
+
caught += 1
|
|
87
|
+
total += 1
|
|
88
|
+
} else if (mutant.status === 'Survived' || mutant.status === 'NoCoverage') {
|
|
89
|
+
total += 1
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { caught, total }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Default runner — спавнить реальні bun-команди через `node:child_process.spawnSync`
|
|
98
|
+
* (працює і в Node-runtime через shebang `n-cursor`, і в Bun). Замінюється у тестах.
|
|
99
|
+
*/
|
|
100
|
+
const defaultRunner = {
|
|
101
|
+
runJsCoverage({ cwd, lcovDir }) {
|
|
102
|
+
const r = spawnSync('bun', ['run', 'test:coverage', '--coverage-reporter=lcov', `--coverage-dir=${lcovDir}`], {
|
|
103
|
+
cwd,
|
|
104
|
+
stdio: 'inherit',
|
|
105
|
+
env: process.env
|
|
106
|
+
})
|
|
107
|
+
return r.status ?? 1
|
|
108
|
+
},
|
|
109
|
+
runStryker({ cwd }) {
|
|
110
|
+
const r = spawnSync('bunx', ['stryker', 'run'], { cwd, stdio: 'inherit', env: process.env })
|
|
111
|
+
return r.status ?? 1
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Збирає JS-метрики покриття + мутаційного тестування.
|
|
117
|
+
* @param {string} cwd корінь проєкту
|
|
118
|
+
* @param {{runner?: typeof defaultRunner}} [opts] runner-ін'єкція для тестів
|
|
119
|
+
* @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>} рядки для COVERAGE.md
|
|
120
|
+
*/
|
|
121
|
+
export async function collect(cwd, opts = {}) {
|
|
122
|
+
const runner = opts.runner ?? defaultRunner
|
|
123
|
+
const jsRoot = await resolveJsRoot(cwd)
|
|
124
|
+
if (jsRoot === null) throw new Error('js-lint coverage: package.json не знайдено')
|
|
125
|
+
|
|
126
|
+
// 1. Coverage через bun test --coverage
|
|
127
|
+
const lcovDir = await mkdtemp(join(tmpdir(), 'js-lint-cov-'))
|
|
128
|
+
let coverage
|
|
129
|
+
try {
|
|
130
|
+
const code = await runner.runJsCoverage({ cwd: jsRoot, lcovDir })
|
|
131
|
+
if (code !== 0) throw new Error(`JS coverage exit ${code}`)
|
|
132
|
+
coverage = parseLcov(await readFile(join(lcovDir, 'lcov.info'), 'utf8'))
|
|
133
|
+
} finally {
|
|
134
|
+
await rm(lcovDir, { recursive: true, force: true })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2. Mutation через Stryker
|
|
138
|
+
await runner.runStryker({ cwd: jsRoot })
|
|
139
|
+
let mutationReport
|
|
140
|
+
try {
|
|
141
|
+
mutationReport = JSON.parse(await readFile(join(jsRoot, 'reports', 'stryker', 'mutation.json'), 'utf8'))
|
|
142
|
+
} catch {
|
|
143
|
+
throw new Error('js-lint coverage: stryker не залишив mutation.json — перевір stryker.config.mjs у проєкті')
|
|
144
|
+
}
|
|
145
|
+
const mutation = parseStrykerReport(mutationReport)
|
|
146
|
+
|
|
147
|
+
return [{ area: 'JS', coverage, mutation }]
|
|
148
|
+
}
|
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
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
globs: "**/{.oxlintrc.json,eslint.config.js,.jscpd.json,knip.json,package.json},**/*.{js,mjs,cjs,jsx,ts,tsx}"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
5
|
+
version: '1.25'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
**oxlint**, **ESLint**, **jscpd**, **knip**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`**, **`bunx knip`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.9.2`** (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint); пакет **`@e18e/eslint-plugin`** окремо не додавай. Пакети oxlint/eslint/jscpd/knip не додавай без потреби монорепо.
|
|
@@ -200,3 +200,7 @@ for (const item of arr) {
|
|
|
200
200
|
## Тести
|
|
201
201
|
|
|
202
202
|
Проєкт має бути покритий unit-тестами (**Bun test**). Код: синтаксис Node **24+**, **top level await** (узгоджено з `engines.node` у `package.json`).
|
|
203
|
+
|
|
204
|
+
## Покриття + мутаційне тестування JS
|
|
205
|
+
|
|
206
|
+
Покриття + мутаційне тестування JS постачаються через `n-cursor coverage` (правило `test.mdc`). Реалізація провайдера — у `npm/rules/js-lint/coverage/coverage.mjs`: `bun test --coverage --coverage-reporter=lcov` + `bunx stryker run`. Stryker конфігурується в `stryker.config.mjs` у JS-корені (single-package або `workspaces[0]`).
|
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
|
}
|
|
@@ -36,11 +36,7 @@ import { existsSync, statSync } from 'node:fs'
|
|
|
36
36
|
import { readFile } from 'node:fs/promises'
|
|
37
37
|
import { join, relative } from 'node:path'
|
|
38
38
|
|
|
39
|
-
import {
|
|
40
|
-
findBunyanImportsInText,
|
|
41
|
-
isBunyanScanSourceFile,
|
|
42
|
-
shouldSkipFileForBunyanScan
|
|
43
|
-
} from '../lib/bunyan-imports.mjs'
|
|
39
|
+
import { findBunyanImportsInText, isBunyanScanSourceFile, shouldSkipFileForBunyanScan } from '../lib/bunyan-imports.mjs'
|
|
44
40
|
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from '../lib/check-env-scan.mjs'
|
|
45
41
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
46
42
|
import { runConftestBatch } from '../../../scripts/lib/run-conftest-batch.mjs'
|
|
@@ -52,10 +48,7 @@ import {
|
|
|
52
48
|
resolveConnDirFromPackageJson
|
|
53
49
|
} from '../lib/conn-imports-scan.mjs'
|
|
54
50
|
import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
|
|
55
|
-
import {
|
|
56
|
-
findPromiseSetTimeoutInText,
|
|
57
|
-
isPromiseSetTimeoutScanSourceFile
|
|
58
|
-
} from '../lib/promise-settimeout-scan.mjs'
|
|
51
|
+
import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from '../lib/promise-settimeout-scan.mjs'
|
|
59
52
|
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
60
53
|
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
61
54
|
|
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
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rust-провайдер для `n-cursor coverage`: збирає метрики покриття (`cargo llvm-cov`)
|
|
3
|
+
* і мутаційного тестування (`cargo-mutants`) для Rust-коду. Активується через
|
|
4
|
+
* правило `rust` у `.n-cursor.json#rules`; applies-логіка — у `detect(cwd)`
|
|
5
|
+
* (наявність Cargo.toml у cwd або workspace-підкаталозі).
|
|
6
|
+
*
|
|
7
|
+
* Контракт провайдера — у docs/superpowers/specs/2026-05-24-coverage-rule-design.md.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
12
|
+
import { tmpdir } from 'node:os'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
import { hasCargoTomlInTree } from '../lib/has-cargo-toml.mjs'
|
|
16
|
+
|
|
17
|
+
const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo', 'target'])
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Чи провайдер застосовний у поточному cwd.
|
|
21
|
+
* @param {string} cwd корінь проєкту
|
|
22
|
+
* @returns {Promise<boolean>} true, якщо знайдено Cargo.toml у cwd або workspace-піддереві
|
|
23
|
+
*/
|
|
24
|
+
export function detect(cwd) {
|
|
25
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) return Promise.resolve(true)
|
|
26
|
+
return Promise.resolve(hasCargoTomlInTree(cwd, IGNORED_DIR_NAMES))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Знайти Cargo.toml: cwd/Cargo.toml або в одному з workspace-підкаталогів.
|
|
31
|
+
* @param {string} cwd корінь проєкту
|
|
32
|
+
* @returns {Promise<string>} абсолютний шлях до Cargo.toml
|
|
33
|
+
*/
|
|
34
|
+
async function resolveCargoManifest(cwd) {
|
|
35
|
+
const rootManifest = join(cwd, 'Cargo.toml')
|
|
36
|
+
if (existsSync(rootManifest)) return rootManifest
|
|
37
|
+
|
|
38
|
+
const rootPkgPath = join(cwd, 'package.json')
|
|
39
|
+
if (existsSync(rootPkgPath)) {
|
|
40
|
+
const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
41
|
+
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
|
|
42
|
+
for (const ws of workspaces) {
|
|
43
|
+
const tauriManifest = join(cwd, ws, 'src-tauri', 'Cargo.toml')
|
|
44
|
+
if (existsSync(tauriManifest)) return tauriManifest
|
|
45
|
+
const flatManifest = join(cwd, ws, 'Cargo.toml')
|
|
46
|
+
if (existsSync(flatManifest)) return flatManifest
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new Error('rust coverage: Cargo.toml не знайдено (cwd + workspaces)')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const defaultRunner = {
|
|
54
|
+
runLlvmCov({ manifestPath }) {
|
|
55
|
+
const r = spawnSync('cargo', ['llvm-cov', '--manifest-path', manifestPath, '--json', '--summary-only'], {
|
|
56
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
57
|
+
env: process.env
|
|
58
|
+
})
|
|
59
|
+
return { exitCode: r.status ?? 1, stdout: r.stdout?.toString('utf8') ?? '' }
|
|
60
|
+
},
|
|
61
|
+
runCargoMutants({ manifestPath, outDir }) {
|
|
62
|
+
const r = spawnSync('cargo', ['mutants', '--in-place', '-o', outDir, '--manifest-path', manifestPath], {
|
|
63
|
+
stdio: 'inherit',
|
|
64
|
+
env: process.env
|
|
65
|
+
})
|
|
66
|
+
return r.status ?? 1
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Збирає Rust-метрики покриття + мутаційного тестування.
|
|
72
|
+
* @param {string} cwd корінь проєкту
|
|
73
|
+
* @param {{runner?: typeof defaultRunner}} [opts] ін'єкція runner-а для тестів
|
|
74
|
+
* @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>} рядки для COVERAGE.md
|
|
75
|
+
*/
|
|
76
|
+
export async function collect(cwd, opts = {}) {
|
|
77
|
+
const runner = opts.runner ?? defaultRunner
|
|
78
|
+
const manifestPath = await resolveCargoManifest(cwd)
|
|
79
|
+
|
|
80
|
+
// 1. Coverage через cargo llvm-cov
|
|
81
|
+
const { exitCode: llvmCode, stdout: llvmJson } = await runner.runLlvmCov({ manifestPath })
|
|
82
|
+
if (llvmCode !== 0) {
|
|
83
|
+
throw new Error('rust coverage: cargo llvm-cov упав — встанови: cargo install cargo-llvm-cov')
|
|
84
|
+
}
|
|
85
|
+
const totals = JSON.parse(llvmJson).data[0].totals
|
|
86
|
+
const coverage = {
|
|
87
|
+
lines: { covered: totals.lines.covered, total: totals.lines.count },
|
|
88
|
+
functions: { covered: totals.functions.covered, total: totals.functions.count }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Mutation через cargo mutants
|
|
92
|
+
const outDir = await mkdtemp(join(tmpdir(), 'rust-mutants-'))
|
|
93
|
+
let mutation
|
|
94
|
+
try {
|
|
95
|
+
// cargo-mutants exit ≠ 0 коли є missed — це нормально, не помилка.
|
|
96
|
+
// Реальний крах — відсутній outcomes.json.
|
|
97
|
+
await runner.runCargoMutants({ manifestPath, outDir })
|
|
98
|
+
let outcomes
|
|
99
|
+
try {
|
|
100
|
+
outcomes = JSON.parse(await readFile(join(outDir, 'mutants.out', 'outcomes.json'), 'utf8'))
|
|
101
|
+
} catch {
|
|
102
|
+
throw new Error('rust coverage: cargo mutants не залишив outcomes.json — встанови: cargo install cargo-mutants')
|
|
103
|
+
}
|
|
104
|
+
const caught = (outcomes.caught ?? 0) + (outcomes.timeout ?? 0)
|
|
105
|
+
mutation = { caught, total: caught + (outcomes.missed ?? 0) }
|
|
106
|
+
} finally {
|
|
107
|
+
await rm(outDir, { recursive: true, force: true })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [{ area: 'Rust', coverage, mutation }]
|
|
111
|
+
}
|
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/rust/rust.mdc
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Перевірка Rust коду
|
|
3
3
|
globs: "**/{Cargo.toml,Cargo.lock,rustfmt.toml,clippy.toml,.vscode/extensions.json,package.json},**/*.rs"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
5
|
+
version: '1.1'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
**rustfmt** ([rust-lang/rustfmt](https://github.com/rust-lang/rustfmt)) — форматер; **clippy** ([rust-lang/rust-clippy](https://github.com/rust-lang/rust-clippy)) — лінтер. У скрипті **`lint-rust`** локально йдуть три кроки в одному рядку: `cargo fmt --all` → `cargo clippy --fix --allow-staged --allow-dirty --all-targets --all-features` → фінальний `cargo clippy --all-targets --all-features -- -D warnings`. У CI — без `--fix`: `cargo fmt --all -- --check` і `cargo clippy ... -- -D warnings` (див. `lint-rust.yml`).
|
|
@@ -25,3 +25,7 @@ Tauri-проєкт завжди має `src-tauri/Cargo.toml`, тому прав
|
|
|
25
25
|
- `tauri` — `tauri-apps.tauri-vscode` (див. **tauri.mdc**).
|
|
26
26
|
|
|
27
27
|
Обидва правила перевіряють `.vscode/extensions.json` за `contains`-семантикою; конкурентного запису немає.
|
|
28
|
+
|
|
29
|
+
## Покриття + мутаційне тестування Rust
|
|
30
|
+
|
|
31
|
+
Покриття + мутаційне тестування Rust постачаються через `n-cursor coverage` (правило `test.mdc`). Реалізація провайдера — у `npm/rules/rust/coverage/coverage.mjs`: `cargo llvm-cov --json --summary-only` + `cargo mutants --in-place`. Бінарники: `cargo install cargo-llvm-cov && cargo install cargo-mutants`.
|
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
|
}
|