@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 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.26.3",
3
+ "version": "1.27.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,7 +1,8 @@
1
1
  /**
2
- * JS-провайдер для `n-cursor coverage`: збирає метрики покриття (`bun test --coverage`)
3
- * і мутаційного тестування (Stryker) для JS/TS коду. Активується через `js-lint`
4
- * правило в `.n-cursor.json#rules`; реальна applies-логіка — у `detect(cwd)`.
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
- * Чи `scripts` містить coverage-сумісну команду.
21
- * @param {Record<string, string> | undefined} scripts секція scripts з package.json
22
- * @returns {boolean} true, якщо є test:coverage або test з --coverage
22
+ * Чи у пакеті встановлено vitest (через dependencies або devDependencies).
23
+ * @param {{dependencies?: Record<string,string>, devDependencies?: Record<string,string>}} pkg package.json
24
+ * @returns {boolean}
23
25
  */
24
- function hasCoverageScript(scripts) {
25
- if (!scripts || typeof scripts !== 'object') return false
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, якщо знайдено coverage-сумісний test-скрипт
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
- return hasCoverageScript(pkg.scripts)
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('bun', ['run', 'test:coverage', '--coverage-reporter=lcov', `--coverage-dir=${lcovDir}`], {
198
- cwd,
199
- stdio: 'inherit',
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 через bun test --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: 'command',
4
- commandRunner: { command: 'bun test' },
5
- // inPlace: уникає hoisted-node_modules issues у Bun monorepo (sandbox-копія втрачає resolution).
6
- // Також тести, що читають git/fs-state (integration checks), працюють тільки in-place.
7
- inPlace: true,
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 з bun test runner; mutate-патерни
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 BASELINE_PATH = join(HERE, 'data', 'stryker_config', 'stryker.config.baseline.mjs')
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
- if (!existsSync(BASELINE_PATH)) {
52
- reporter.fail(`stryker.config.mjs canonical baseline не знайдено (${BASELINE_PATH}) — перевстанови @nitra/cursor`)
53
- return reporter.getExitCode()
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
- const target = join(jsRoot, 'stryker.config.mjs')
58
- if (existsSync(target)) {
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
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "scripts": {
3
- "coverage": ["n-cursor coverage"]
3
+ "coverage": ["n-cursor coverage"],
4
+ "test": ["vitest"]
4
5
  }
5
6
  }
@@ -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.3'
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`: збирає метрики покриття (`bun test --coverage`, `cargo llvm-cov` тощо) і мутаційного тестування (Stryker, `cargo-mutants`) з усіх активних провайдерів у `.n-cursor.json#rules` і пише `COVERAGE.md` у корінь проєкту. Лок і дедуп — `withLock('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 (мінімум для роботи з `bun test`): [stryker.config.baseline.mjs](./js/data/stryker_config/stryker.config.baseline.mjs)
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