@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 +31 -0
- package/package.json +1 -1
- package/rules/js-lint/coverage/coverage.mjs +27 -20
- package/rules/tauri/js/cargo_mutants_config.mjs +148 -0
- package/rules/tauri/js/tooling.mjs +36 -12
- package/rules/tauri/tauri.mdc +51 -1
- package/rules/test/js/data/cargo_mutants_config/mutants.toml.baseline +5 -6
- 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 +44 -6
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,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 {
|
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
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
|
-
* Чи
|
|
43
|
-
*
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
package/rules/tauri/tauri.mdc
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Tauri
|
|
3
3
|
globs: "**/src-tauri/**,**/tauri.conf.json"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
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 —
|
|
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: '
|
|
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
|
|
|
@@ -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-артефактів.
|