@nitra/cursor 1.26.3 → 1.27.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 CHANGED
@@ -4,6 +4,28 @@
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.1] - 2026-05-26
8
+
9
+ ### Changed
10
+
11
+ - Додано workspace-level `vitest.config.js` baseline і follow-up правки для Vitest/Stryker coverage-концернів після переходу тестового правила на Vitest runner.
12
+
13
+ ## [1.27.0] - 2026-05-26
14
+
15
+ ### Changed
16
+
17
+ - **`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).
18
+ - **`rules/test/js/stryker_config.mjs`**: концерн тепер копіює два canonical baseline-и у кожен JS-root: `stryker.config.mjs` + `vitest.config.js`. Ідемпотентність збережена — обидва файли копіюються лише якщо ще немає.
19
+ - **`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.
20
+ - **`rules/test/policy/package_json/template/package.json.contains.json`**: канон scripts тепер містить додатково `"test": ["vitest"]` (substring-вимога). `coverage` як було — `["n-cursor coverage"]`.
21
+ - **`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`.
22
+
23
+ ### Added
24
+
25
+ - **`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.
26
+ - **`rules/test/js/tests/stryker_config.test.mjs`**: нові кейси — копіювання `vitest.config.js`, перевірка вмісту нового Stryker baseline (`testRunner: 'vitest'`, `coverageAnalysis: 'perTest'`), ідемпотентність `vitest.config.js`.
27
+ - **`rules/test/policy/package_json/package_json_test.rego`**: нові кейси для `scripts.test` — deny при відсутності/некоректному значенні, allow при substring-розширенні.
28
+
7
29
  ## [1.26.3] - 2026-05-26
8
30
 
9
31
  ### 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.1",
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,23 @@ 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 =
20
+ 'js-lint coverage: vitest відсутній у package.json — додай `vitest`, `@vitest/coverage-v8` та `@stryker-mutator/vitest-runner` у devDependencies (див. test.mdc)'
18
21
 
19
22
  /**
20
- * Чи `scripts` містить coverage-сумісну команду.
21
- * @param {Record<string, string> | undefined} scripts секція scripts з package.json
22
- * @returns {boolean} true, якщо є test:coverage або test з --coverage
23
+ * Чи у пакеті встановлено vitest (через dependencies або devDependencies).
24
+ * @param {{dependencies?: Record<string,string>, devDependencies?: Record<string,string>}} pkg package.json
25
+ * @returns {boolean}
23
26
  */
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
27
+ function hasVitestDep(pkg) {
28
+ return Boolean(pkg.devDependencies?.vitest) || Boolean(pkg.dependencies?.vitest)
29
29
  }
30
30
 
31
31
  /**
32
- * Чи провайдер застосовний у поточному cwd.
32
+ * Чи провайдер застосовний у поточному cwd. Активується, коли у JS-root знайдено
33
+ * `vitest` як залежність — інакше silent skip із hint у stderr (одноразово).
33
34
  * @param {string} cwd корінь проєкту
34
- * @returns {Promise<boolean>} true, якщо знайдено coverage-сумісний test-скрипт
35
+ * @returns {Promise<boolean>} true, якщо проєкт сумісний з vitest-based coverage
35
36
  */
36
37
  export async function detect(cwd) {
37
38
  const jsRoot = await resolveJsRoot(cwd)
@@ -39,7 +40,14 @@ export async function detect(cwd) {
39
40
  const pkgPath = join(jsRoot, 'package.json')
40
41
  if (!existsSync(pkgPath)) return false
41
42
  const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
42
- return hasCoverageScript(pkg.scripts)
43
+ if (!hasVitestDep(pkg)) {
44
+ if (!detect._hinted) {
45
+ console.error(VITEST_HINT)
46
+ detect._hinted = true
47
+ }
48
+ return false
49
+ }
50
+ return true
43
51
  }
44
52
 
45
53
  /**
@@ -194,11 +202,11 @@ export function parseStrykerReport(report, jsRoot) {
194
202
  */
195
203
  const defaultRunner = {
196
204
  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
- })
205
+ const r = spawnSync(
206
+ 'bunx',
207
+ ['vitest', 'run', '--coverage', '--coverage.reporter=lcov', `--coverage.reportsDirectory=${lcovDir}`],
208
+ { cwd, stdio: 'inherit', env: process.env }
209
+ )
202
210
  return r.status ?? 1
203
211
  },
204
212
  runStryker({ cwd }) {
@@ -218,7 +226,7 @@ export async function collect(cwd, opts = {}) {
218
226
  const jsRoot = await resolveJsRoot(cwd)
219
227
  if (jsRoot === null) throw new Error('js-lint coverage: package.json не знайдено')
220
228
 
221
- // 1. Coverage через bun test --coverage
229
+ // 1. Coverage через vitest run --coverage (v8 provider пише lcov.info у lcovDir)
222
230
  const lcovDir = await mkdtemp(join(tmpdir(), 'js-lint-cov-'))
223
231
  let coverage
224
232
  try {
@@ -4451,7 +4451,8 @@ export async function collectHttpRouteIngressForWorkload(dir, appLabel, fail) {
4451
4451
  function serviceSelectorAppLabel(spec) {
4452
4452
  if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) return null
4453
4453
  const selector = /** @type {Record<string, unknown>} */ (spec).selector
4454
- if (selector === null || selector === undefined || typeof selector !== 'object' || Array.isArray(selector)) return null
4454
+ if (selector === null || selector === undefined || typeof selector !== 'object' || Array.isArray(selector))
4455
+ return null
4455
4456
  const app = /** @type {Record<string, unknown>} */ (selector).app
4456
4457
  return typeof app === 'string' && app.trim() !== '' ? app : null
4457
4458
  }
@@ -100,7 +100,14 @@ export function renderMarkdown(rows) {
100
100
  lines.push(`| ${m.line} | \`${m.original}\` | \`${m.replacement}\` | ${m.mutantType} |`)
101
101
  }
102
102
  if (group.exampleTest) {
103
- lines.push('', `**Приклад тесту** (\`${group.exampleTest.testFile}\`):`, '', '```js', group.exampleTest.code ?? '', '```')
103
+ lines.push(
104
+ '',
105
+ `**Приклад тесту** (\`${group.exampleTest.testFile}\`):`,
106
+ '',
107
+ '```js',
108
+ group.exampleTest.code ?? '',
109
+ '```'
110
+ )
104
111
  }
105
112
  if (group.recommendationText) {
106
113
  lines.push('', '**Що треба протестувати:**', '', group.recommendationText)
@@ -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,22 @@ 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(
79
+ reporter,
80
+ cwd,
81
+ STRYKER_BASELINE_PATH,
82
+ join(jsRoot, 'stryker.config.mjs'),
83
+ 'stryker.config.mjs'
84
+ )
85
+ await ensureBaselineFile(reporter, cwd, VITEST_BASELINE_PATH, join(jsRoot, 'vitest.config.js'), 'vitest.config.js')
64
86
  }
65
87
 
66
88
  // Гарантуємо що 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