@nitra/cursor 1.28.7 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.29.0] - 2026-05-28
8
+
9
+ ### Changed
10
+
11
+ - **`rules/ci4/ci4.mdc`** (`version` 2.0 → 2.1) — додано секцію **«Зв'язок із `.cursor/rules`»**: архітектурна документація у `docs/` не дублює зміст `.cursor/rules/*.mdc` (операційні правила лінту, тестів, CHANGELOG, версіонування), а посилається на потрібне правило через його ім'я у бектиках (наприклад, `див. **`changelog`**`). Дублікати розходяться з оригіналом і ламають automatic-перевірки `npx @nitra/cursor fix`/`check`; правки робляться в одному місці — у самому `.mdc`.
12
+
13
+
14
+ ## [1.28.8] - 2026-05-28
15
+
16
+ ### Added
17
+
18
+ - **`rules/test/js/data/stryker_config/stryker-vue-macros-ignorer.mjs`** — новий локальний Stryker `Ignore`-плагін `vue-macros`. `shouldIgnore(path)` повертає non-empty message для `CallExpression`, де `callee` — `Identifier` з ім'ям у наборі Vue `<script setup>`-макросів: `defineProps`, `defineEmits`, `defineModel`, `defineSlots`, `defineExpose`, `defineOptions`. Експортує `strykerPlugins: [{kind: 'Ignore', name: 'vue-macros', value: {shouldIgnore}}]` — це формат, який очікує stryker-core plugin-loader (`module.strykerPlugins`); без імпорту `@stryker-mutator/api`.
19
+ - **`rules/test/js/data/stryker_config/stryker.config.vue.baseline.mjs`** — vue-варіант baseline `stryker.config.mjs`: дефолтні поля + `plugins: ['@stryker-mutator/vitest-runner', './stryker-vue-macros-ignorer.mjs']` і `ignorers: ['vue-macros']` (поряд із vitest-runner тепер треба явно вказати runner, бо ручний `plugins` затирає Stryker default).
20
+ - **`rules/test/js/stryker_config.mjs`** — концерн `stryker_config` тепер детектить `.vue` файли під `<jsRoot>/src/**` (skip `node_modules`/`dist`/`reports`) і у JS-roots із SFC ставить vue-варіант baseline + копіює `stryker-vue-macros-ignorer.mjs` поряд із конфігом. Backward-compat: jsRoot без `.vue` отримує дефолтний baseline без `plugins`/`ignorers`. Обидва файли копіюються через `ensureBaselineFile` — idempotent, ручні модифікації не перетираються. Tests: `+5` сценаріїв у `rules/test/js/tests/stryker_config.test.mjs` (vue-detection happy path, no-vue → дефолт, mixed monorepo, `.vue` лише у node_modules — НЕ vue, idempotency для vue-файлів) + новий `rules/test/js/tests/stryker-vue-macros-ignorer.test.mjs` (всі 6 макросів, non-macro callee, non-CallExpression, MemberExpression callee, anonymous callee).
21
+ - **`rules/test/test.mdc`** (`version` 2.5 → 2.6) і дзеркало `.cursor/rules/n-test.mdc` — нова підсекція "Vue SFC (`<script setup>` macros)" у "Налаштування mutation-testing" з описом коли тригериться vue-варіант, які макроси скіпаються, чому без плагіна `@vue/compiler-sfc` падає на coverage-тернарнику Stryker. Мотивація — інакше boilerplate `// Stryker disable next-line` потрібен у кожному SFC.
22
+
7
23
  ## [1.28.7] - 2026-05-28
8
24
 
9
25
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.28.7",
3
+ "version": "1.29.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
package/rules/ci4/ci4.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Архітектурна документація продукту — Markdown як джерело істини; рекомендований стек arc42 + Diátaxis + ADR (MADR v4, формат описаний у правилі `adr`) + C4 як набір нотацій; гібридна модель manual + autogen-зон, що регенеруються з accepted ADR; MkDocs Material як viewer з collapsible engineer-блоками для змішаної аудиторії
3
3
  alwaysApply: true
4
- version: '2.0'
4
+ version: '2.1'
5
5
  ---
6
6
 
7
7
  Архітектурна документація проєкту живе у Markdown поряд із кодом. Це не довідник «для людей із порталу архітектора» — це **джерело істини**, з якого LLM-агент і людина читають намір системи перед будь-якою зміною коду. Тому правила нижче — не оформлення, а робочий процес: який стек використовуємо, як зберігаємо рішення, як автоматично перегенеровуємо проекції з ADR і як рендеримо для змішаної аудиторії (менеджери + інженери + ops).
@@ -319,6 +319,12 @@ ADR (`docs/adr/<slug>.md`) — джерело правди для autogen-про
319
319
 
320
320
  Архітектурні артефакти — частина **користувацької документації**, а не закритий артефакт для команди. Контекстна діаграма (C4 рівень 1) і контейнерна (рівень 2) живуть там, де читач шукає вступ у проєкт — у `explanation/architecture.md`, не у відокремленій теці «for-architects». MkDocs Material рендерить це з collapsible-блоками, тому менеджер бачить прозу, інженер провалюється в деталі.
321
321
 
322
+ ## Зв'язок із `.cursor/rules`
323
+
324
+ `.cursor/rules/*.mdc` — джерело правди для **операційних правил** проєкту: як запускати лінт і тести, як оформлювати CHANGELOG, як версіонувати модулі, які прапорці передавати інструментам тощо. Архітектурна документація у `docs/` **не дублює** ці правила і не переказує їх своїми словами. Дублікат миттєво розходиться з оригіналом, ламає механіку automatic-перевірок (`npx @nitra/cursor fix`/`check`) і вимагає двох правок при кожній зміні — одна з них неминуче пропускається.
325
+
326
+ Замість дублювання — **відсилка** на конкретне правило через його ім'я у бектиках: «див. правило **`changelog`**», «див. **`n-lint`**», «деталі формату — у правилі **`adr`**». Відсилка веде читача (людину чи LLM-агента) до єдиного джерела істини; коли правило еволюціонує, документація лишається коректною без правки. Якщо `.cursor/rule` потребує доповнення або уточнення — оновлюй сам `.mdc`, а не копіюй його зміст у `docs/`.
327
+
322
328
  ## Інструменти
323
329
 
324
330
  - **Claude Code** як runner — slash-команда `/regen-docs` або post-commit hook на зміни `docs/adr/**`
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Stryker `Ignore`-plugin: пропускає мутації виклику Vue `<script setup>`-макросів
3
+ * (`defineProps`, `defineEmits`, `defineModel`, `defineSlots`, `defineExpose`,
4
+ * `defineOptions`). Без цього Stryker обгортає аргументи макроса у тернарний
5
+ * coverage-вираз (`stryMutAct_9fa48(...) ? {} : (stryCov_9fa48(...), {...})`),
6
+ * а `@vue/compiler-sfc` падає з помилкою:
7
+ *
8
+ * defineProps() in <script setup> cannot reference locally declared variables
9
+ *
10
+ * бо макроси повинні бути статично-аналізованими на етапі compile-sfc.
11
+ *
12
+ * Стандартний Stryker plugin-loader (див. `@stryker-mutator/core/.../plugin-loader.js`)
13
+ * чекає експорт `strykerPlugins: Plugin[]`. У `stryker.config.mjs` файл цього
14
+ * плагіна додається у `plugins: ['./stryker-vue-macros-ignorer.mjs']`, а в
15
+ * `ignorers: ['vue-macros']` активується конкретно цей ignorer по імені.
16
+ */
17
+
18
+ const VUE_SETUP_MACROS = new Set([
19
+ 'defineProps',
20
+ 'defineEmits',
21
+ 'defineModel',
22
+ 'defineSlots',
23
+ 'defineExpose',
24
+ 'defineOptions'
25
+ ])
26
+
27
+ const IGNORE_MESSAGE = 'Vue <script setup> macro call cannot be mutated (defineProps/defineEmits/etc. must be statically analyzable for @vue/compiler-sfc).'
28
+
29
+ /**
30
+ * @param {{isCallExpression: () => boolean, node: {callee: {type: string, name?: string}}}} path babel NodePath, переданий Stryker-instrumenter
31
+ * @returns {string | undefined} non-empty message — пропустити мутацію піддерева; undefined — продовжити
32
+ */
33
+ export function shouldIgnore(path) {
34
+ if (!path.isCallExpression()) return
35
+ const callee = path.node.callee
36
+ if (callee.type !== 'Identifier') return
37
+ if (!VUE_SETUP_MACROS.has(callee.name)) return
38
+ return IGNORE_MESSAGE
39
+ }
40
+
41
+ export const strykerPlugins = [
42
+ {
43
+ kind: 'Ignore',
44
+ name: 'vue-macros',
45
+ value: { shouldIgnore }
46
+ }
47
+ ]
@@ -0,0 +1,23 @@
1
+ /** @type {import('@stryker-mutator/core').PartialStrykerOptions} */
2
+ export default {
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-patching,
10
+ // без копіювання node_modules у sandbox (стара проблема command runner у Bun monorepo).
11
+ tempDirName: 'reports/stryker/.tmp',
12
+ reporters: ['json', 'clear-text'],
13
+ jsonReporter: { fileName: 'reports/stryker/mutation.json' },
14
+ // incremental: зберігає результати між запусками, відновлює після краш/kill.
15
+ // Дає ~262× прискорення на noop-прогонах (див. benchmarks/runner-comparison/SPIKE.md).
16
+ incremental: true,
17
+ incrementalFile: 'reports/stryker/incremental.json',
18
+ // Local plugin: пропускає мутацію Vue <script setup>-макросів (defineProps/Emits/Model/
19
+ // Slots/Expose/Options). Інакше Stryker огортає аргументи у coverage-тернарник, який
20
+ // @vue/compiler-sfc не може статично проаналізувати і падає при компіляції SFC.
21
+ plugins: ['@stryker-mutator/vitest-runner', './stryker-vue-macros-ignorer.mjs'],
22
+ ignorers: ['vue-macros']
23
+ }
@@ -4,6 +4,12 @@
4
4
  * (всі workspaces з package.json, або cwd у single-package) і копіює canonical
5
5
  * baseline `stryker.config.mjs` + `vitest.config.js` у кожен root, де файлу немає.
6
6
  *
7
+ * Для JS-roots із `.vue` файлами (Vue 3 + `<script setup>`) копіюється vue-варіант
8
+ * baseline, який реєструє локальний Ignore-плагін `vue-macros` — інакше Stryker
9
+ * огортає виклики `defineProps`/`defineEmits`/... у coverage-тернарник і
10
+ * `@vue/compiler-sfc` падає при компіляції SFC. Плагін копіюється у той самий
11
+ * jsRoot як `stryker-vue-macros-ignorer.mjs`.
12
+ *
7
13
  * Self-gating: концерн silently skips коли `js-lint` не enabled — це навмисно,
8
14
  * щоб не шуміти у single-language проєктах без JS coverage tooling.
9
15
  *
@@ -11,7 +17,7 @@
11
17
  * лишаються на Stryker defaults (`src/**\/*.{js,mjs,ts,jsx,tsx,cjs}`).
12
18
  */
13
19
  import { existsSync } from 'node:fs'
14
- import { copyFile } from 'node:fs/promises'
20
+ import { copyFile, glob } from 'node:fs/promises'
15
21
  import { dirname, join, relative } from 'node:path'
16
22
  import { fileURLToPath } from 'node:url'
17
23
 
@@ -22,6 +28,9 @@ import { resolveAllJsRoots } from '../../../scripts/utils/resolve-js-root.mjs'
22
28
 
23
29
  const HERE = dirname(fileURLToPath(import.meta.url))
24
30
  const STRYKER_BASELINE_PATH = join(HERE, 'data', 'stryker_config', 'stryker.config.baseline.mjs')
31
+ const STRYKER_VUE_BASELINE_PATH = join(HERE, 'data', 'stryker_config', 'stryker.config.vue.baseline.mjs')
32
+ const STRYKER_VUE_PLUGIN_PATH = join(HERE, 'data', 'stryker_config', 'stryker-vue-macros-ignorer.mjs')
33
+ const STRYKER_VUE_PLUGIN_FILENAME = 'stryker-vue-macros-ignorer.mjs'
25
34
  const VITEST_BASELINE_PATH = join(HERE, 'data', 'vitest_config', 'vitest.config.baseline.js')
26
35
 
27
36
  // Stryker-output патерн для .gitignore: увесь каталог reports/stryker/ — це
@@ -30,6 +39,23 @@ const VITEST_BASELINE_PATH = join(HERE, 'data', 'vitest_config', 'vitest.config.
30
39
  // перелічування під-патернів. Подвійний-зірочка-префікс — для monorepo workspaces.
31
40
  const STRYKER_GITIGNORE_ENTRIES = ['**/reports/stryker/']
32
41
 
42
+ // .vue detection: scope — `<jsRoot>/src/**/*.vue` (як і Stryker mutate defaults для src/);
43
+ // skip build-артефактів і чужих node_modules, щоб не вмикати vue-варіант через transitive deps.
44
+ const VUE_GLOB_PATTERN = 'src/**/*.vue'
45
+ const VUE_GLOB_IGNORE = ['**/node_modules/**', '**/dist/**', '**/reports/**']
46
+
47
+ /**
48
+ * Чи містить jsRoot хоч один `.vue` файл під `src/` (skipping node_modules/dist/reports).
49
+ * @param {string} jsRoot абсолютний шлях до workspace-каталогу
50
+ * @returns {Promise<boolean>} true якщо знайдено хоча б один `.vue`
51
+ */
52
+ async function hasVueFiles(jsRoot) {
53
+ for await (const _rel of glob(VUE_GLOB_PATTERN, { cwd: jsRoot, exclude: VUE_GLOB_IGNORE })) {
54
+ return true
55
+ }
56
+ return false
57
+ }
58
+
33
59
  /**
34
60
  * Копіює baseline у target, якщо target ще не існує. Idempotent.
35
61
  * @param {ReturnType<typeof createCheckReporter>} reporter check-reporter для логу pass/fail
@@ -67,7 +93,12 @@ export async function check() {
67
93
  return reporter.getExitCode()
68
94
  }
69
95
 
70
- for (const baselinePath of [STRYKER_BASELINE_PATH, VITEST_BASELINE_PATH]) {
96
+ for (const baselinePath of [
97
+ STRYKER_BASELINE_PATH,
98
+ STRYKER_VUE_BASELINE_PATH,
99
+ STRYKER_VUE_PLUGIN_PATH,
100
+ VITEST_BASELINE_PATH
101
+ ]) {
71
102
  if (!existsSync(baselinePath)) {
72
103
  reporter.fail(`canonical baseline не знайдено (${baselinePath}) — перевстанови @nitra/cursor`)
73
104
  return reporter.getExitCode()
@@ -75,13 +106,24 @@ export async function check() {
75
106
  }
76
107
 
77
108
  for (const jsRoot of jsRoots) {
109
+ const isVueRoot = await hasVueFiles(jsRoot)
110
+ const strykerBaseline = isVueRoot ? STRYKER_VUE_BASELINE_PATH : STRYKER_BASELINE_PATH
78
111
  await ensureBaselineFile(
79
112
  reporter,
80
113
  cwd,
81
- STRYKER_BASELINE_PATH,
114
+ strykerBaseline,
82
115
  join(jsRoot, 'stryker.config.mjs'),
83
116
  'stryker.config.mjs'
84
117
  )
118
+ if (isVueRoot) {
119
+ await ensureBaselineFile(
120
+ reporter,
121
+ cwd,
122
+ STRYKER_VUE_PLUGIN_PATH,
123
+ join(jsRoot, STRYKER_VUE_PLUGIN_FILENAME),
124
+ STRYKER_VUE_PLUGIN_FILENAME
125
+ )
126
+ }
85
127
  await ensureBaselineFile(reporter, cwd, VITEST_BASELINE_PATH, join(jsRoot, 'vitest.config.js'), 'vitest.config.js')
86
128
  }
87
129
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: JS-тести (*.test.mjs) живуть у tests/. Правило `test` керує stryker.config.mjs + vitest.config.js (якщо js-lint enabled) і .cargo/mutants.toml (якщо rust enabled).
3
- version: '2.5'
3
+ version: '2.6'
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
  ---
@@ -101,6 +101,14 @@ Canonical `vitest.config.js` (для довідки — `pool: 'forks'` + `inclu
101
101
 
102
102
  Канон Stryker config (Vitest runner + perTest): [stryker.config.baseline.mjs](./js/data/stryker_config/stryker.config.baseline.mjs)
103
103
 
104
+ ### Vue SFC (`<script setup>` macros)
105
+
106
+ Якщо у JS-root знайдено бодай один `.vue` під `src/` (skip `node_modules`/`dist`/`reports`) — концерн ставить **vue-варіант** baseline ([`stryker.config.vue.baseline.mjs`](./js/data/stryker_config/stryker.config.vue.baseline.mjs)) замість звичайного і додатково копіює локальний Stryker `Ignore`-плагін [`stryker-vue-macros-ignorer.mjs`](./js/data/stryker_config/stryker-vue-macros-ignorer.mjs) поряд із конфігом.
107
+
108
+ Плагін реєструється як `plugins: ['@stryker-mutator/vitest-runner', './stryker-vue-macros-ignorer.mjs']` + `ignorers: ['vue-macros']` і виключає з мутацій виклики `<script setup>`-макросів: **`defineProps`**, **`defineEmits`**, **`defineModel`**, **`defineSlots`**, **`defineExpose`**, **`defineOptions`**. Без плагіна Stryker огортає аргументи макроса у coverage-тернарник (`stryMutAct_9fa48(...) ? {} : (stryCov_9fa48(...), {...})`), а `@vue/compiler-sfc` падає з `defineProps() in <script setup> cannot reference locally declared variables` — макроси мають бути статично-аналізованими на етапі compile-sfc. Альтернатива з boilerplate `// Stryker disable next-line` у кожному SFC не масштабується.
109
+
110
+ JS-root без `.vue` отримує дефолтний baseline без `plugins`/`ignorers` (backward-compatible). Обидва файли копіюються idempotent — наявний `stryker.config.mjs` / `stryker-vue-macros-ignorer.mjs` не перетирається.
111
+
104
112
  ### Vitest baseline та `package.json#scripts`
105
113
 
106
114
  Поряд зі 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/`).