@nitra/cursor 1.26.3 → 1.27.0
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 +16 -0
- package/package.json +1 -1
- package/rules/js-lint/coverage/coverage.mjs +27 -20
- package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +9 -6
- package/rules/test/js/data/vitest_config/vitest.config.baseline.js +11 -0
- package/rules/test/js/stryker_config.mjs +29 -13
- package/rules/test/policy/package_json/template/package.json.contains.json +2 -1
- package/rules/test/test.mdc +33 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.27.0] - 2026-05-26
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`rules/test/js/data/stryker_config/stryker.config.baseline.mjs`**: канон Stryker перейшов з `command` runner (`bun test`, `concurrency: 1`, `inPlace: true`, `coverageAnalysis: 'off'`) на `vitest` runner з `coverageAnalysis: 'perTest'`. У verify-first spike (158 мутантів, `benchmarks/runner-comparison/SPIKE.md`) це дало 31×–57× прискорення повного прогону і ≈262× для incremental noop-прогону. `inPlace` більше не потрібен — vitest-runner ізолює мутантів через AST-патчінг у пам'яті, без копіювання node_modules у sandbox (стара проблема command runner у Bun monorepo).
|
|
12
|
+
- **`rules/test/js/stryker_config.mjs`**: концерн тепер копіює два canonical baseline-и у кожен JS-root: `stryker.config.mjs` + `vitest.config.js`. Ідемпотентність збережена — обидва файли копіюються лише якщо ще немає.
|
|
13
|
+
- **`rules/js-lint/coverage/coverage.mjs`**: `detect()` тепер шукає `vitest` у `dependencies`/`devDependencies` (раніше — `scripts.test:coverage` або `scripts.test` з `--coverage`). `runJsCoverage` спавнить `bunx vitest run --coverage --coverage.reporter=lcov --coverage.reportsDirectory=…` замість `bun run test:coverage --coverage-reporter=lcov`. `parseLcov` без змін — формат lcov у Vitest v8-coverage співпадає з тим, що віддавало `bun test --coverage`. Якщо vitest відсутній — `detect` повертає `false` із одноразовим hint у stderr.
|
|
14
|
+
- **`rules/test/policy/package_json/template/package.json.contains.json`**: канон scripts тепер містить додатково `"test": ["vitest"]` (substring-вимога). `coverage` як було — `["n-cursor coverage"]`.
|
|
15
|
+
- **`rules/test/test.mdc` v2.4**: нові розділи «Vitest baseline та `package.json#scripts`» і «Frontend-варіант (Vue/Vite + happy-dom)». Текст про purpose `bun test --coverage` оновлено на `vitest run --coverage`. `globs` додатково ловить `vitest.config.js`.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`rules/test/js/data/vitest_config/vitest.config.baseline.js`**: новий canonical baseline для Vitest (`environment: 'node'`, `coverage.provider: 'v8'` із lcov+text-summary, `include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}']`). Концерн `stryker_config` копіює його як `vitest.config.js` у кожен JS-root.
|
|
20
|
+
- **`rules/test/js/tests/stryker_config.test.mjs`**: нові кейси — копіювання `vitest.config.js`, перевірка вмісту нового Stryker baseline (`testRunner: 'vitest'`, `coverageAnalysis: 'perTest'`), ідемпотентність `vitest.config.js`.
|
|
21
|
+
- **`rules/test/policy/package_json/package_json_test.rego`**: нові кейси для `scripts.test` — deny при відсутності/некоректному значенні, allow при substring-розширенні.
|
|
22
|
+
|
|
7
23
|
## [1.26.3] - 2026-05-26
|
|
8
24
|
|
|
9
25
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JS-провайдер для `n-cursor coverage`: збирає метрики покриття (`
|
|
3
|
-
* і мутаційного тестування (Stryker) для JS/TS коду.
|
|
4
|
-
* правило в `.n-cursor.json#rules`; реальна applies-логіка
|
|
2
|
+
* JS-провайдер для `n-cursor coverage`: збирає метрики покриття (`vitest run --coverage`)
|
|
3
|
+
* і мутаційного тестування (Stryker з vitest-runner + perTest) для JS/TS коду.
|
|
4
|
+
* Активується через `js-lint` правило в `.n-cursor.json#rules`; реальна applies-логіка
|
|
5
|
+
* — у `detect(cwd)`.
|
|
5
6
|
*
|
|
6
7
|
* Контракт провайдера — у docs/superpowers/specs/2026-05-24-coverage-rule-design.md.
|
|
7
8
|
*/
|
|
@@ -15,23 +16,22 @@ import { resolveJsRoot } from '../../../scripts/utils/resolve-js-root.mjs'
|
|
|
15
16
|
|
|
16
17
|
const TEST_BLOCK_START = /^\s*(it|test)\(/
|
|
17
18
|
const FILE_EXTENSION = /\.[^.]+$/
|
|
19
|
+
const VITEST_HINT = 'js-lint coverage: vitest відсутній у package.json — додай `vitest`, `@vitest/coverage-v8` та `@stryker-mutator/vitest-runner` у devDependencies (див. test.mdc)'
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
|
-
* Чи
|
|
21
|
-
* @param {Record<string, string>
|
|
22
|
-
* @returns {boolean}
|
|
22
|
+
* Чи у пакеті встановлено vitest (через dependencies або devDependencies).
|
|
23
|
+
* @param {{dependencies?: Record<string,string>, devDependencies?: Record<string,string>}} pkg package.json
|
|
24
|
+
* @returns {boolean}
|
|
23
25
|
*/
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
if (typeof scripts['test:coverage'] === 'string' && scripts['test:coverage'].length > 0) return true
|
|
27
|
-
if (typeof scripts.test === 'string' && scripts.test.includes('--coverage')) return true
|
|
28
|
-
return false
|
|
26
|
+
function hasVitestDep(pkg) {
|
|
27
|
+
return Boolean(pkg.devDependencies?.vitest) || Boolean(pkg.dependencies?.vitest)
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
|
-
* Чи провайдер застосовний у поточному cwd.
|
|
31
|
+
* Чи провайдер застосовний у поточному cwd. Активується, коли у JS-root знайдено
|
|
32
|
+
* `vitest` як залежність — інакше silent skip із hint у stderr (одноразово).
|
|
33
33
|
* @param {string} cwd корінь проєкту
|
|
34
|
-
* @returns {Promise<boolean>} true, якщо
|
|
34
|
+
* @returns {Promise<boolean>} true, якщо проєкт сумісний з vitest-based coverage
|
|
35
35
|
*/
|
|
36
36
|
export async function detect(cwd) {
|
|
37
37
|
const jsRoot = await resolveJsRoot(cwd)
|
|
@@ -39,7 +39,14 @@ export async function detect(cwd) {
|
|
|
39
39
|
const pkgPath = join(jsRoot, 'package.json')
|
|
40
40
|
if (!existsSync(pkgPath)) return false
|
|
41
41
|
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
42
|
-
|
|
42
|
+
if (!hasVitestDep(pkg)) {
|
|
43
|
+
if (!detect._hinted) {
|
|
44
|
+
console.error(VITEST_HINT)
|
|
45
|
+
detect._hinted = true
|
|
46
|
+
}
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
return true
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
/**
|
|
@@ -194,11 +201,11 @@ export function parseStrykerReport(report, jsRoot) {
|
|
|
194
201
|
*/
|
|
195
202
|
const defaultRunner = {
|
|
196
203
|
runJsCoverage({ cwd, lcovDir }) {
|
|
197
|
-
const r = spawnSync(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
env: process.env
|
|
201
|
-
|
|
204
|
+
const r = spawnSync(
|
|
205
|
+
'bunx',
|
|
206
|
+
['vitest', 'run', '--coverage', '--coverage.reporter=lcov', `--coverage.reportsDirectory=${lcovDir}`],
|
|
207
|
+
{ cwd, stdio: 'inherit', env: process.env }
|
|
208
|
+
)
|
|
202
209
|
return r.status ?? 1
|
|
203
210
|
},
|
|
204
211
|
runStryker({ cwd }) {
|
|
@@ -218,7 +225,7 @@ export async function collect(cwd, opts = {}) {
|
|
|
218
225
|
const jsRoot = await resolveJsRoot(cwd)
|
|
219
226
|
if (jsRoot === null) throw new Error('js-lint coverage: package.json не знайдено')
|
|
220
227
|
|
|
221
|
-
// 1. Coverage через
|
|
228
|
+
// 1. Coverage через vitest run --coverage (v8 provider пише lcov.info у lcovDir)
|
|
222
229
|
const lcovDir = await mkdtemp(join(tmpdir(), 'js-lint-cov-'))
|
|
223
230
|
let coverage
|
|
224
231
|
try {
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/** @type {import('@stryker-mutator/core').PartialStrykerOptions} */
|
|
2
2
|
export default {
|
|
3
|
-
testRunner: '
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
3
|
+
testRunner: 'vitest',
|
|
4
|
+
vitest: { configFile: 'vitest.config.js' },
|
|
5
|
+
// perTest: Stryker запускає лише тести, що покривають мутовану лінію — головний приріст
|
|
6
|
+
// швидкості проти command runner (де треба було б ганяти ввесь test-suite на кожен мутант).
|
|
7
|
+
coverageAnalysis: 'perTest',
|
|
8
|
+
// concurrency: за замовч. Stryker обирає os.cpus().length - 1.
|
|
9
|
+
// inPlace більше не потрібен — vitest-runner ізолює мутантів у пам'яті через AST-патчінг,
|
|
10
|
+
// без копіювання node_modules у sandbox (стара проблема command runner у Bun monorepo).
|
|
8
11
|
tempDirName: 'reports/stryker/.tmp',
|
|
9
12
|
reporters: ['json', 'clear-text'],
|
|
10
13
|
jsonReporter: { fileName: 'reports/stryker/mutation.json' },
|
|
11
|
-
coverageAnalysis: 'off',
|
|
12
14
|
// incremental: зберігає результати між запусками, відновлює після краш/kill.
|
|
15
|
+
// Дає ~262× прискорення на noop-прогонах (див. benchmarks/runner-comparison/SPIKE.md).
|
|
13
16
|
incremental: true,
|
|
14
17
|
incrementalFile: 'reports/stryker/incremental.json'
|
|
15
18
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
// Підхоплюються обидві основні розкладки: тести поряд із кодом (rule `test`-конвенція —
|
|
6
|
+
// у піддиректоріях `tests/`) і top-level integration suites у `<root>/tests/`.
|
|
7
|
+
include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}'],
|
|
8
|
+
environment: 'node',
|
|
9
|
+
coverage: { provider: 'v8', reporter: ['lcov', 'text-summary'] }
|
|
10
|
+
}
|
|
11
|
+
})
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Концерн `stryker_config` правила test (test.mdc): якщо `js-lint` присутнє в
|
|
3
3
|
* `.n-cursor.json#rules` і не у `disable-rules` — резолвить ВСІ JS-roots
|
|
4
4
|
* (всі workspaces з package.json, або cwd у single-package) і копіює canonical
|
|
5
|
-
* baseline `stryker.config.mjs` у кожен root, де файлу немає.
|
|
5
|
+
* baseline `stryker.config.mjs` + `vitest.config.js` у кожен root, де файлу немає.
|
|
6
6
|
*
|
|
7
7
|
* Self-gating: концерн silently skips коли `js-lint` не enabled — це навмисно,
|
|
8
8
|
* щоб не шуміти у single-language проєктах без JS coverage tooling.
|
|
9
9
|
*
|
|
10
|
-
* Baseline — мінімум для запуску Stryker з
|
|
10
|
+
* Baseline — мінімум для запуску Stryker з vitest-runner + perTest; mutate-патерни
|
|
11
11
|
* лишаються на Stryker defaults (`src/**\/*.{js,mjs,ts,jsx,tsx,cjs}`).
|
|
12
12
|
*/
|
|
13
13
|
import { existsSync } from 'node:fs'
|
|
@@ -21,7 +21,8 @@ import { ensureGitignoreEntries } from '../../../scripts/utils/ensure-gitignore-
|
|
|
21
21
|
import { resolveAllJsRoots } from '../../../scripts/utils/resolve-js-root.mjs'
|
|
22
22
|
|
|
23
23
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
24
|
-
const
|
|
24
|
+
const STRYKER_BASELINE_PATH = join(HERE, 'data', 'stryker_config', 'stryker.config.baseline.mjs')
|
|
25
|
+
const VITEST_BASELINE_PATH = join(HERE, 'data', 'vitest_config', 'vitest.config.baseline.js')
|
|
25
26
|
|
|
26
27
|
// Stryker-output патерн для .gitignore: увесь каталог reports/stryker/ — це
|
|
27
28
|
// build-артефакти (`tempDirName` backup'и, mutation.json, HTML/dashboard-репорти
|
|
@@ -29,6 +30,24 @@ const BASELINE_PATH = join(HERE, 'data', 'stryker_config', 'stryker.config.basel
|
|
|
29
30
|
// перелічування під-патернів. Подвійний-зірочка-префікс — для monorepo workspaces.
|
|
30
31
|
const STRYKER_GITIGNORE_ENTRIES = ['**/reports/stryker/']
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Копіює baseline у target, якщо target ще не існує. Idempotent.
|
|
35
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter check-reporter для логу pass/fail
|
|
36
|
+
* @param {string} cwd корінь проєкту (для relative-шляхів у логах)
|
|
37
|
+
* @param {string} baselinePath абсолютний шлях до canonical baseline
|
|
38
|
+
* @param {string} target абсолютний шлях, куди копіювати
|
|
39
|
+
* @param {string} label людиночитна мітка ("stryker.config.mjs" / "vitest.config.js")
|
|
40
|
+
* @returns {Promise<void>}
|
|
41
|
+
*/
|
|
42
|
+
async function ensureBaselineFile(reporter, cwd, baselinePath, target, label) {
|
|
43
|
+
if (existsSync(target)) {
|
|
44
|
+
reporter.pass(`${label} існує (${relative(cwd, target)})`)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
await copyFile(baselinePath, target)
|
|
48
|
+
reporter.pass(`${label} створено з canonical baseline (${relative(cwd, target)}) (test.mdc)`)
|
|
49
|
+
}
|
|
50
|
+
|
|
32
51
|
/**
|
|
33
52
|
* @returns {Promise<number>} 0 — OK або silently skipped, 1 — порушення
|
|
34
53
|
*/
|
|
@@ -48,19 +67,16 @@ export async function check() {
|
|
|
48
67
|
return reporter.getExitCode()
|
|
49
68
|
}
|
|
50
69
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
for (const baselinePath of [STRYKER_BASELINE_PATH, VITEST_BASELINE_PATH]) {
|
|
71
|
+
if (!existsSync(baselinePath)) {
|
|
72
|
+
reporter.fail(`canonical baseline не знайдено (${baselinePath}) — перевстанови @nitra/cursor`)
|
|
73
|
+
return reporter.getExitCode()
|
|
74
|
+
}
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
for (const jsRoot of jsRoots) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
reporter.pass(`stryker.config.mjs існує (${relative(cwd, target)})`)
|
|
60
|
-
continue
|
|
61
|
-
}
|
|
62
|
-
await copyFile(BASELINE_PATH, target)
|
|
63
|
-
reporter.pass(`stryker.config.mjs створено з canonical baseline (${relative(cwd, target)}) (test.mdc)`)
|
|
78
|
+
await ensureBaselineFile(reporter, cwd, STRYKER_BASELINE_PATH, join(jsRoot, 'stryker.config.mjs'), 'stryker.config.mjs')
|
|
79
|
+
await ensureBaselineFile(reporter, cwd, VITEST_BASELINE_PATH, join(jsRoot, 'vitest.config.js'), 'vitest.config.js')
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
// Гарантуємо що Stryker temp/output ніколи не комітяться. Patterns
|
package/rules/test/test.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: JS-тести (*.test.mjs) живуть у tests/. Правило `test` керує stryker.config.mjs (якщо js-lint enabled) і .cargo/mutants.toml (якщо rust enabled).
|
|
3
|
-
version: '2.
|
|
4
|
-
globs: "**/{.n-cursor.json,package.json,Cargo.toml,stryker.config.mjs,.cargo/mutants.toml},**/*.test.mjs"
|
|
2
|
+
description: JS-тести (*.test.mjs) живуть у tests/. Правило `test` керує stryker.config.mjs + vitest.config.js (якщо js-lint enabled) і .cargo/mutants.toml (якщо rust enabled).
|
|
3
|
+
version: '2.4'
|
|
4
|
+
globs: "**/{.n-cursor.json,package.json,Cargo.toml,stryker.config.mjs,vitest.config.js,.cargo/mutants.toml},**/*.test.mjs"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -62,7 +62,7 @@ Recursive globs ловлять файли всередині `tests/` так с
|
|
|
62
62
|
|
|
63
63
|
## Покриття + мутаційне тестування
|
|
64
64
|
|
|
65
|
-
Канонічна команда — `n-cursor coverage`: збирає метрики покриття (`
|
|
65
|
+
Канонічна команда — `n-cursor coverage`: збирає метрики покриття (`vitest run --coverage`, `cargo llvm-cov` тощо) і мутаційного тестування (Stryker з vitest-runner + `coverageAnalysis: 'perTest'`, `cargo-mutants`) з усіх активних провайдерів у `.n-cursor.json#rules` і пише `COVERAGE.md` у корінь проєкту. Лок і дедуп — `withLock('coverage', ...)`.
|
|
66
66
|
|
|
67
67
|
Провайдери живуть у `npm/rules/<rule>/coverage/coverage.mjs` (постачаються правилами мови/рантайму: `js-lint`, `rust`, у майбутньому `python` тощо). Оркестратор — у `npm/rules/test/coverage/coverage.mjs`.
|
|
68
68
|
|
|
@@ -72,9 +72,36 @@ Recursive globs ловлять файли всередині `tests/` так с
|
|
|
72
72
|
|
|
73
73
|
## Налаштування mutation-testing
|
|
74
74
|
|
|
75
|
-
Якщо у `.n-cursor.json#rules` присутнє правило `js-lint` — правило `test` створює canonical baseline `stryker.config.mjs` у **кожному** JS-root проєкту: у кожному workspace з власним `package.json` (або в корені для single-package). У monorepo з `workspaces: ['app', 'scripts']` отримаєте `app/stryker.config.mjs` і `scripts/stryker.config.mjs`.
|
|
75
|
+
Якщо у `.n-cursor.json#rules` присутнє правило `js-lint` — правило `test` створює canonical baseline `stryker.config.mjs` + `vitest.config.js` у **кожному** JS-root проєкту: у кожному workspace з власним `package.json` (або в корені для single-package). У monorepo з `workspaces: ['app', 'scripts']` отримаєте `app/stryker.config.mjs` + `app/vitest.config.js` і `scripts/stryker.config.mjs` + `scripts/vitest.config.js`.
|
|
76
76
|
|
|
77
|
-
Канон Stryker config (
|
|
77
|
+
Канон Stryker config (Vitest runner + perTest): [stryker.config.baseline.mjs](./js/data/stryker_config/stryker.config.baseline.mjs)
|
|
78
|
+
|
|
79
|
+
### Vitest baseline та `package.json#scripts`
|
|
80
|
+
|
|
81
|
+
Поряд зі Stryker концерн `stryker_config` ідемпотентно копіює `vitest.config.js` (теж тільки якщо файлу немає). Canonical: [vitest.config.baseline.js](./js/data/vitest_config/vitest.config.baseline.js) — `environment: 'node'`, `coverage.provider: 'v8'` з lcov+text-summary репортами, `include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}']` (підхоплює обидві розкладки — тести у `tests/`-піддиректоріях і top-level integration suites у `<root>/tests/`).
|
|
82
|
+
|
|
83
|
+
У `package.json#scripts` має бути `"test": "vitest run"` (canonical contains-substring `vitest` — допустимо `vitest run` та інші локальні розширення); опційно — `"test:watch": "vitest"`.
|
|
84
|
+
|
|
85
|
+
Канон scripts: [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
|
|
86
|
+
|
|
87
|
+
### Frontend-варіант (Vue/Vite + happy-dom)
|
|
88
|
+
|
|
89
|
+
Для проєктів зі своїм `vite.config.js` `vitest.config.js` має реюзати vite-плагіни та aliases і перемкнути `environment` на `'happy-dom'` (або `'jsdom'`):
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { defineConfig, mergeConfig } from 'vitest/config'
|
|
93
|
+
import viteConfig from './vite.config.js'
|
|
94
|
+
|
|
95
|
+
export default mergeConfig(viteConfig, defineConfig({
|
|
96
|
+
test: {
|
|
97
|
+
include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}'],
|
|
98
|
+
environment: 'happy-dom',
|
|
99
|
+
coverage: { provider: 'v8', reporter: ['lcov', 'text-summary'] }
|
|
100
|
+
}
|
|
101
|
+
}))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Концерн ставить **node-варіант** baseline; перехід на frontend — ручна модифікація, після якої концерн уже не перетирає (idempotent).
|
|
78
105
|
|
|
79
106
|
Аналогічно, якщо `rust` присутнє в `rules` — створюється `.cargo/mutants.toml` у каталозі **кожного** Cargo.toml-маніфесту: кореневий `Cargo.toml`, `<workspace>/src-tauri/Cargo.toml` (Tauri-патерн) і `<workspace>/Cargo.toml` (flat workspace).
|
|
80
107
|
|