@nitra/cursor 1.26.2 → 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,37 @@
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
+
23
+ ## [1.26.3] - 2026-05-26
24
+
25
+ ### Added
26
+
27
+ - **`rules/tauri/js/cargo_mutants_config.mjs`**: новий концерн tauri-правила. Для кожного `<ws>/src-tauri/Cargo.toml` ідемпотентно гарантує наявність Tauri-канонічних ключів у `<ws>/src-tauri/.cargo/mutants.toml` — `additional_cargo_test_args = ["--lib", "--tests"]` та `exclude_globs` для `src/main.rs` (binary shell) і platform-bridge файлів (`*android.rs`, `*ios.rs`, `*mobile.rs`, `*desktop.rs`, `*macos.rs`, `*windows.rs`, `*linux.rs`). Семантика: ці файли — boundary, бізнес-логіка повинна жити у platform-neutral модулях. Файл відсутній → створює повний baseline; всі канонічні ключі є → `manual cargo-mutants config preserved`; частина ключів відсутня → додає лише відсутні в окремий блок у кінці, без зміни існуючих значень.
28
+ - **`rules/tauri/js/tests/cargo_mutants_config.test.mjs`**: 7 тестів — silent skip без Tauri, створення baseline, ідемпотентність (повторний прогон байт-в-байт), збереження ручних налаштувань, partial-merge (додаються лише відсутні ключі), кілька src-tauri у різних workspaces, augmentation поверх нейтрального test-rule baseline.
29
+ - **`rules/tauri/tauri.mdc` v1.3**: нові розділи «Виявлення проєкту Tauri» (опис маркерів і workspace-обходу) та «Mutation-testing: семантика app shell та platform bridge» з фіксованою семантикою boundary-файлів і ідемпотентністю взаємодії з `test`-rule.
30
+
31
+ ### Changed
32
+
33
+ - **`rules/test/js/data/cargo_mutants_config/mutants.toml.baseline`**: видалено Tauri-specific `additional_cargo_test_args = ["--lib", "--tests"]` — `test`-rule baseline тепер універсальний (тільки коментар, ніяких exclude'ів та framework-припущень). Customization-семантика framework-rules-ів описана в коментарі baseline'а.
34
+ - **`rules/test/test.mdc` v2.3**: додано розділ «Універсальний baseline і framework-specific tuning» — `test` володіє нейтральним baseline, framework-rules (tauri, capacitor) зобов'язані доповнювати ідемпотентно і не перетирати ручні налаштування.
35
+ - **`rules/tauri/js/tooling.mjs`**: виявлення Tauri тепер обходить усі workspace-пакети через `getMonorepoPackageRootDirs()` (раніше — тільки корінь). Маркером є будь-що з: `<ws>/src-tauri/`, `<ws>/src-tauri/Cargo.toml`, `<ws>/src-tauri/tauri.conf.json`, `<ws>/tauri.conf.json`, `<ws>/package.json#dependencies/devDependencies` з `@tauri-apps/*`. Дозволяє tauri-rule працювати в monorepo-проєктах, де Tauri живе в одному з пакетів, а не в корені.
36
+ - **`rules/test/js/tests/cargo_mutants_config.test.mjs`**: тест базлайну тепер перевіряє відсутність framework-specific ключів (`additional_cargo_test_args`, `exclude_globs`) у нейтральному baseline-файлі.
37
+
7
38
  ## [1.26.2] - 2026-05-26
8
39
 
9
40
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.26.2",
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 {
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Концерн `cargo_mutants_config` правила tauri (tauri.mdc): для кожного
3
+ * `<workspace>/src-tauri/Cargo.toml` ідемпотентно гарантує наявність
4
+ * Tauri-specific cargo-mutants налаштувань у `<workspace>/src-tauri/.cargo/mutants.toml`.
5
+ *
6
+ * Семантика (фіксована між Tauri-проєктами):
7
+ * - `src/main.rs` — binary shell entrypoint (smoke/e2e, не mutation unit);
8
+ * - `src/**\/{android,ios,mobile}.rs` — mobile plugin bridge / platform glue;
9
+ * - `src/**\/{macos,windows,linux,desktop}.rs` — desktop platform bridge / OS integration glue.
10
+ *
11
+ * Self-gating: silently skip, якщо в monorepo не знайдено жодного
12
+ * `<ws>/src-tauri/Cargo.toml` (test rule сам створить нейтральний baseline там,
13
+ * де потрібно).
14
+ *
15
+ * Ідемпотентність:
16
+ * - якщо файл відсутній — створює з Tauri-canonical baseline;
17
+ * - якщо файл існує і всі канонічні ключі вже є — `manual cargo-mutants config preserved`;
18
+ * - якщо файл існує, але якихось канонічних top-level ключів немає — додає
19
+ * лише відсутні ключі окремим блоком у кінці; існуючих значень не торкається.
20
+ */
21
+ import { existsSync } from 'node:fs'
22
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
23
+ import { dirname, join, relative } from 'node:path'
24
+
25
+ import { parse as parseToml } from 'smol-toml'
26
+
27
+ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
28
+ import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
29
+
30
+ const TAURI_BASELINE_HEADER = `# .cargo/mutants.toml — Tauri canonical cargo-mutants config (tauri.mdc).
31
+ # Виключаємо --bins і --doc щоб бінарник Tauri та doc-tests не перезбиралися
32
+ # з нуля під кожного мутанта (секунди → хвилини).
33
+ `
34
+
35
+ const TAURI_KEY_SNIPPETS = Object.freeze({
36
+ additional_cargo_test_args: 'additional_cargo_test_args = ["--lib", "--tests"]\n',
37
+ exclude_globs: `# Platform bridge / app shell — boundary-файли (тестуються smoke/e2e, не mutation unit).
38
+ # Якщо у bridge-файлі з'являється pure/business logic — винеси її у platform-neutral
39
+ # модуль (src/auth/oauth.rs, src/gmail/message.rs, ...) і тестуй mutation-testing там.
40
+ exclude_globs = [
41
+ "src/main.rs",
42
+ "src/**/android.rs",
43
+ "src/**/ios.rs",
44
+ "src/**/mobile.rs",
45
+ "src/**/desktop.rs",
46
+ "src/**/macos.rs",
47
+ "src/**/windows.rs",
48
+ "src/**/linux.rs"
49
+ ]
50
+ `
51
+ })
52
+
53
+ const TAURI_CANONICAL_KEYS = Object.freeze(Object.keys(TAURI_KEY_SNIPPETS))
54
+
55
+ /**
56
+ * Знаходить усі `<ws>/src-tauri/` каталоги з власним `Cargo.toml` у монорепо.
57
+ * Обходить workspace-пакети через `getMonorepoPackageRootDirs` (корінь + усі workspaces).
58
+ * @param {string} cwd корінь проєкту
59
+ * @returns {Promise<string[]>} абсолютні шляхи до знайдених `src-tauri/` каталогів
60
+ */
61
+ async function findSrcTauriDirs(cwd) {
62
+ const roots = await getMonorepoPackageRootDirs(cwd)
63
+ const result = []
64
+ for (const root of roots) {
65
+ const srcTauriCargo = join(cwd, root, 'src-tauri', 'Cargo.toml')
66
+ if (existsSync(srcTauriCargo)) {
67
+ result.push(join(cwd, root, 'src-tauri'))
68
+ }
69
+ }
70
+ return result
71
+ }
72
+
73
+ /**
74
+ * Зчитує існуючий `.cargo/mutants.toml` і повертає top-level ключі, яких ще немає.
75
+ * @param {string} targetPath абсолютний шлях до файла
76
+ * @returns {Promise<string[]>} список відсутніх канонічних ключів (зі збереженням порядку TAURI_CANONICAL_KEYS)
77
+ */
78
+ async function detectMissingKeys(targetPath) {
79
+ const existing = await readFile(targetPath, 'utf8')
80
+ const parsed = parseToml(existing)
81
+ return TAURI_CANONICAL_KEYS.filter(k => !(k in parsed))
82
+ }
83
+
84
+ /**
85
+ * Будує append-блок з відсутніх ключів. Існуючий вміст не торкається.
86
+ * @param {string} existing поточний вміст файла
87
+ * @param {string[]} missingKeys ключі, які треба додати
88
+ * @returns {string} новий вміст файла
89
+ */
90
+ function buildAppended(existing, missingKeys) {
91
+ const tail = existing.endsWith('\n') ? existing : `${existing}\n`
92
+ const block = ['\n# Tauri canonical cargo-mutants additions (tauri.mdc)\n']
93
+ for (const key of missingKeys) block.push(TAURI_KEY_SNIPPETS[key])
94
+ return tail + block.join('')
95
+ }
96
+
97
+ /**
98
+ * Будує повний Tauri-canonical baseline (для випадку, коли файла ще немає).
99
+ * @returns {string} вміст для нового `.cargo/mutants.toml`
100
+ */
101
+ function buildBaseline() {
102
+ return TAURI_BASELINE_HEADER + TAURI_CANONICAL_KEYS.map(k => TAURI_KEY_SNIPPETS[k]).join('\n')
103
+ }
104
+
105
+ /**
106
+ * Обробляє один `src-tauri/` каталог: створює або ідемпотентно доповнює `.cargo/mutants.toml`.
107
+ * @param {string} srcTauriDir абсолютний шлях до `src-tauri/`
108
+ * @param {string} cwd корінь проєкту (для relative-шляхів у репортах)
109
+ * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер концерну
110
+ * @returns {Promise<void>}
111
+ */
112
+ async function processOneSrcTauri(srcTauriDir, cwd, reporter) {
113
+ const target = join(srcTauriDir, '.cargo', 'mutants.toml')
114
+ const rel = relative(cwd, target)
115
+
116
+ if (!existsSync(target)) {
117
+ await mkdir(dirname(target), { recursive: true })
118
+ await writeFile(target, buildBaseline())
119
+ reporter.pass(`.cargo/mutants.toml створено з Tauri canonical baseline (${rel}) (tauri.mdc)`)
120
+ return
121
+ }
122
+
123
+ const missing = await detectMissingKeys(target)
124
+ if (missing.length === 0) {
125
+ reporter.pass(`.cargo/mutants.toml: manual cargo-mutants config preserved (${rel})`)
126
+ return
127
+ }
128
+
129
+ const existing = await readFile(target, 'utf8')
130
+ await writeFile(target, buildAppended(existing, missing))
131
+ reporter.pass(`.cargo/mutants.toml: додано відсутні Tauri-ключі [${missing.join(', ')}] (${rel}) (tauri.mdc)`)
132
+ }
133
+
134
+ /**
135
+ * @returns {Promise<number>} 0 — OK або silently skipped, 1 — порушення
136
+ */
137
+ export async function check() {
138
+ const reporter = createCheckReporter()
139
+ const cwd = process.cwd()
140
+ const srcTauriDirs = await findSrcTauriDirs(cwd)
141
+ if (srcTauriDirs.length === 0) {
142
+ return reporter.getExitCode()
143
+ }
144
+ for (const dir of srcTauriDirs) {
145
+ await processOneSrcTauri(dir, cwd, reporter)
146
+ }
147
+ return reporter.getExitCode()
148
+ }
@@ -3,11 +3,15 @@
3
3
  * проєктів, у яких є маркер Tauri.
4
4
  *
5
5
  * Cross-file gating (JS):
6
- * 1. Tauri-маркер визначаємо за **будь-яким** з:
7
- * - існує каталог `src-tauri/` у `cwd`;
8
- * - існує файл `tauri.conf.json` у `cwd` або в workspace-пакетах;
9
- * - кореневий `package.json#devDependencies` або `dependencies` містить
10
- * ключ з префіксом `@tauri-apps/`.
6
+ * 1. Tauri-маркер визначаємо обходом усіх workspace-пакетів через
7
+ * `getMonorepoPackageRootDirs()` (корінь + workspaces). Кожен workspace
8
+ * перевіряється за **будь-яким** з:
9
+ * - існує каталог `<ws>/src-tauri/`;
10
+ * - існує файл `<ws>/src-tauri/Cargo.toml`;
11
+ * - існує файл `<ws>/src-tauri/tauri.conf.json`;
12
+ * - існує файл `<ws>/tauri.conf.json` (legacy flat-layout);
13
+ * - `<ws>/package.json#dependencies` чи `devDependencies` містить ключ
14
+ * з префіксом `@tauri-apps/`.
11
15
  * 2. Якщо маркера немає — пропустити перевірку (tauri-tooling не вимагається).
12
16
  * 3. Інакше — для `.vscode/extensions.json` зробити FS-existence + делегувати
13
17
  * content `rego.tauri.vscode_extensions` через `runConftestBatch`.
@@ -17,9 +21,11 @@
17
21
  */
18
22
  import { existsSync, statSync } from 'node:fs'
19
23
  import { readFile } from 'node:fs/promises'
24
+ import { join } from 'node:path'
20
25
 
21
26
  import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
22
27
  import { runConftestBatch } from '../../../scripts/lib/run-conftest-batch.mjs'
28
+ import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
23
29
 
24
30
  /**
25
31
  * Чи є префікс `@tauri-apps/` у ключах `dependencies` або `devDependencies`.
@@ -39,16 +45,34 @@ function packageHasTauriDep(pkg) {
39
45
  }
40
46
 
41
47
  /**
42
- * Чи є у проєкті маркер Tauri: `src-tauri/`, `tauri.conf.json` (root або
43
- * workspace), або `@tauri-apps/*` у залежностях кореневого `package.json`.
48
+ * Чи має одиничний workspace-пакет маркер Tauri.
49
+ * @param {string} cwd корінь репо
50
+ * @param {string} ws відносний шлях workspace ('.' для root)
51
+ * @returns {Promise<boolean>} true, якщо в цьому workspace є Tauri
52
+ */
53
+ async function workspaceHasTauriMarker(cwd, ws) {
54
+ const base = ws === '.' ? cwd : join(cwd, ws)
55
+ const srcTauri = join(base, 'src-tauri')
56
+ if (existsSync(srcTauri) && statSync(srcTauri).isDirectory()) return true
57
+ if (existsSync(join(base, 'src-tauri', 'Cargo.toml'))) return true
58
+ if (existsSync(join(base, 'src-tauri', 'tauri.conf.json'))) return true
59
+ if (existsSync(join(base, 'tauri.conf.json'))) return true
60
+ const pkgPath = join(base, 'package.json')
61
+ if (!existsSync(pkgPath)) return false
62
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
63
+ return packageHasTauriDep(pkg)
64
+ }
65
+
66
+ /**
67
+ * Чи є у проєкті (root або будь-якому workspace-пакеті) маркер Tauri.
44
68
  * @returns {Promise<boolean>} true, якщо проєкт використовує Tauri
45
69
  */
46
70
  async function projectHasTauriMarker() {
47
- if (existsSync('src-tauri') && statSync('src-tauri').isDirectory()) return true
48
- if (existsSync('tauri.conf.json')) return true
49
- if (!existsSync('package.json')) return false
50
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
51
- if (packageHasTauriDep(pkg)) return true
71
+ const cwd = process.cwd()
72
+ const roots = await getMonorepoPackageRootDirs(cwd)
73
+ for (const ws of roots) {
74
+ if (await workspaceHasTauriMarker(cwd, ws)) return true
75
+ }
52
76
  return false
53
77
  }
54
78
 
@@ -2,7 +2,7 @@
2
2
  description: Tauri
3
3
  globs: "**/src-tauri/**,**/tauri.conf.json"
4
4
  alwaysApply: false
5
- version: '1.2'
5
+ version: '1.3'
6
6
  ---
7
7
 
8
8
  У `.vscode/extensions.json` `recommendations` має містити `tauri-apps.tauri-vscode`:
@@ -14,3 +14,53 @@ version: '1.2'
14
14
  ```
15
15
 
16
16
  Розширені Rust-вимоги (`rust-lang.rust-analyzer`, `tamasfe.even-better-toml`, скрипт `lint-rust`, CI `lint-rust.yml`) — у правилі **`rust`** (`n-rust.mdc`). Tauri-проєкт завжди має `src-tauri/Cargo.toml`, тому `rust` активується автоматично разом з `tauri`.
17
+
18
+ ## Виявлення проєкту Tauri
19
+
20
+ Правило активується, якщо хоч у одному workspace-пакеті (корінь або будь-який пакет з `package.json#workspaces`) виявлений маркер Tauri:
21
+
22
+ - існує каталог `<ws>/src-tauri/`;
23
+ - існує `<ws>/src-tauri/Cargo.toml`;
24
+ - існує `<ws>/src-tauri/tauri.conf.json`;
25
+ - існує `<ws>/tauri.conf.json` (legacy flat-layout);
26
+ - `<ws>/package.json#dependencies` чи `devDependencies` містить ключ з префіксом `@tauri-apps/`.
27
+
28
+ Виявлення виконує `js/tooling.mjs` через `getMonorepoPackageRootDirs()`. Це дозволяє Tauri-rule працювати у монорепо, де Tauri живе в одному з пакетів (а не в корені), не вимагаючи кореневих маркерів.
29
+
30
+ ## Mutation-testing: семантика app shell та platform bridge
31
+
32
+ `tauri` rule володіє Tauri-specific семантикою mutation-testing для каталога `src-tauri/`. Концерн `js/cargo_mutants_config.mjs` ідемпотентно додає у `<ws>/src-tauri/.cargo/mutants.toml` такі канонічні ключі:
33
+
34
+ ```toml title="<ws>/src-tauri/.cargo/mutants.toml"
35
+ additional_cargo_test_args = ["--lib", "--tests"]
36
+
37
+ exclude_globs = [
38
+ "src/main.rs",
39
+ "src/**/android.rs",
40
+ "src/**/ios.rs",
41
+ "src/**/mobile.rs",
42
+ "src/**/desktop.rs",
43
+ "src/**/macos.rs",
44
+ "src/**/windows.rs",
45
+ "src/**/linux.rs"
46
+ ]
47
+ ```
48
+
49
+ Семантика (фіксована між Tauri-проєктами):
50
+
51
+ - **`src/main.rs`** — binary shell entrypoint: запускає Tauri runtime, реєструє plugins/handlers і повертає управління циклу подій. Тестується smoke/e2e (запуск бінарника), не mutation unit;
52
+ - **`*android.rs`, `*ios.rs`, `*mobile.rs`** — mobile plugin bridge / platform glue: тонка обгортка над JNI/Objective-C виклики, mapping platform errors, виклики Tauri AppHandle і native API;
53
+ - **`*macos.rs`, `*windows.rs`, `*linux.rs`, `*desktop.rs`** — desktop platform bridge / OS integration glue: opener/window APIs, OS-specific I/O, mapping platform errors.
54
+
55
+ Ці файли мають містити **тільки platform boundary**: виклик plugin/native API, Tauri AppHandle, opener/window APIs, OS-specific I/O, mapping platform errors. Якщо у bridge-файлі з'являється pure/business logic — її потрібно винести у platform-neutral модуль (`src/auth/oauth.rs`, `src/gmail/message.rs`, …) і тестувати mutation-testing там.
56
+
57
+ Це створює фіксовану семантику: `*android.rs`/`*macos.rs` — boundary-файли, а не місце для бізнес-логіки.
58
+
59
+ ### Ідемпотентність і взаємодія з `test`-rule
60
+
61
+ - `test` rule створює універсальний нейтральний `.cargo/mutants.toml` (порожній з коментом) для кожного Cargo.toml-manifesta — без framework-specific exclude'ів. Це наш baseline.
62
+ - `tauri` rule додає Tauri-канонічні ключі **поверх** того, що вже є у `<ws>/src-tauri/.cargo/mutants.toml`:
63
+ - якщо файла немає — створює з повного Tauri-baseline;
64
+ - якщо обидва канонічні ключі (`additional_cargo_test_args`, `exclude_globs`) вже присутні — `manual cargo-mutants config preserved`, нічого не зміниться;
65
+ - якщо якийсь канонічний ключ відсутній — додається окремим блоком у кінці файла, без зміни існуючих значень.
66
+ - Послідовний `fix test` → `fix tauri` створює Tauri-config; повторний `fix tauri` не дублює секцій; повторний `fix test` не перетирає Tauri-tuning.
@@ -1,8 +1,7 @@
1
- # .cargo/mutants.toml — конфігурація cargo-mutants (опційно).
1
+ # .cargo/mutants.toml — universal cargo-mutants baseline (test.mdc).
2
+ # Цей baseline нейтральний: він не робить припущень про framework/app shell,
3
+ # не виключає platform glue, generated wrappers або binary entrypoints.
4
+ # Framework-specific tuning (Tauri, Capacitor тощо) належить відповідним
5
+ # правилам — вони ідемпотентно доповнюють цей файл, не перетирають його.
2
6
  # cargo-mutants має робочі defaults; цей файл — стартова точка для customization.
3
7
  # Документація: https://mutants.rs/
4
- # Канон постачає правило `test` (@nitra/cursor).
5
-
6
- # Виключаємо --bins і --doc: бінарник Tauri та doc-tests щоразу перезбираються
7
- # з нуля навіть з кешем, що збільшує час кожного мутанту з секунд до хвилин.
8
- additional_cargo_test_args = ["--lib", "--tests"]
@@ -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.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`: збирає метрики покриття (`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
 
@@ -82,4 +109,15 @@ Recursive globs ловлять файли всередині `tests/` так с
82
109
 
83
110
  Customization (mutate patterns, exclude rules, timeout) — відповідальність проєкту-споживача; концерни лише забезпечують наявність файлу як стартового baseline в кожному з виявлених workspace-каталогів.
84
111
 
112
+ ### Універсальний baseline і framework-specific tuning
113
+
114
+ Правило `test` відповідає лише за **універсальний baseline** mutation-testing:
115
+
116
+ - створює `.cargo/mutants.toml` для Rust crates (порожній, з коментом);
117
+ - не робить припущень про framework/app shell;
118
+ - не виключає platform glue, generated wrappers або binary entrypoints;
119
+ - framework-specific tuning (`--lib`/`--tests`, `exclude_globs` для app-shell і platform bridge файлів) належить відповідним правилам (`tauri`, `capacitor` тощо).
120
+
121
+ Якщо інше правило спеціалізує mutation-behavior — воно зобов'язане **доповнювати** існуючий `.cargo/mutants.toml` ідемпотентно (додавати лише відсутні ключі) і **не перетирати** ручні налаштування. Послідовний запуск `npx @nitra/cursor fix test` після `fix tauri` не має скидати tauri-tuning, і навпаки — повторний `fix tauri` не дублює секції.
122
+
85
123
  Додатково: коли `js-lint` enabled, концерн `stryker_config` ідемпотентно додає у кореневий `.gitignore` патерн `**/reports/stryker/` — увесь каталог Stryker-output-у (backup'и `tempDirName`, `mutation.json`, HTML/dashboard-репорти якщо додасте інші reporter-и). Це запобігає випадковому коміту build-артефактів.