@nitra/cursor 1.27.2 → 1.27.5

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.
Files changed (44) hide show
  1. package/.claude-template/hooks/capture-decisions.sh +1 -1
  2. package/.claude-template/hooks/normalize-decisions.sh +1 -1
  3. package/.pi-template/extensions/n-cursor-adr/tsconfig.json +1 -0
  4. package/CHANGELOG.md +65 -31
  5. package/bin/n-cursor.js +2 -2
  6. package/package.json +4 -3
  7. package/rules/bun/bun.mdc +1 -1
  8. package/rules/bun/policy/package_json/package_json.rego +14 -4
  9. package/rules/changelog/js/consistency.mjs +1 -1
  10. package/rules/image-avif/js/avif_generation.mjs +2 -2
  11. package/rules/js-lint/coverage/coverage.mjs +17 -9
  12. package/rules/js-lint/js-lint.mdc +1 -1
  13. package/rules/js-mssql/js/deps.mjs +1 -1
  14. package/rules/k8s/js/manifests.mjs +19 -19
  15. package/rules/k8s/k8s.mdc +9 -9
  16. package/rules/k8s/policy/network_policy/network_policy.rego +5 -5
  17. package/rules/tauri/js/cargo_mutants_config.mjs +3 -3
  18. package/rules/tauri/tauri.mdc +2 -2
  19. package/rules/test/coverage/coverage.mjs +4 -4
  20. package/rules/test/js/cargo_mutants_config.mjs +2 -2
  21. package/rules/test/js/data/cargo_mutants_config/mutants.toml.baseline +1 -1
  22. package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +1 -1
  23. package/rules/test/js/stryker_config.mjs +3 -3
  24. package/rules/test/test.mdc +5 -5
  25. package/rules/text/js/forbidden-prettier.mjs +59 -0
  26. package/rules/text/js/formatting.mjs +1 -4
  27. package/rules/text/policy/package_json/package_json.rego +16 -0
  28. package/rules/text/text.mdc +1 -1
  29. package/rules/vue/vue.mdc +14 -10
  30. package/schemas/v8r-catalog.json +6 -0
  31. package/scripts/coverage-fix.mjs +12 -12
  32. package/scripts/lib/run-lint-cli.mjs +5 -5
  33. package/scripts/lib/run-lint-step.mjs +1 -1
  34. package/scripts/lib/run-rule.mjs +1 -1
  35. package/scripts/lib/timing-summary.mjs +3 -3
  36. package/scripts/post-tool-use-fix.mjs +4 -4
  37. package/scripts/sync-claude-config.mjs +2 -2
  38. package/scripts/utils/ensure-gitignore-entries.mjs +2 -2
  39. package/scripts/utils/resolve-cargo-manifest.mjs +1 -1
  40. package/scripts/utils/resolve-js-root.mjs +1 -1
  41. package/scripts/utils/walkDir.mjs +2 -2
  42. package/skills/coverage-fix/SKILL.md +15 -12
  43. package/skills/fix-tests/SKILL.md +13 -13
  44. /package/rules/k8s/policy/network_policy/template/{statefulset.snippet.yaml → stateful-set.snippet.yaml} +0 -0
@@ -36,7 +36,7 @@ log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG"; }
36
36
 
37
37
  # Підвантажуємо спільний helper (sourcing — не sub-shell, функції видимі поточному скрипту).
38
38
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
39
- # shellcheck source=lib/tooling-only.sh
39
+ # shellcheck source=npm/.claude-template/hooks/lib/tooling-only.sh
40
40
  . "$SCRIPT_DIR/lib/tooling-only.sh"
41
41
 
42
42
  log "fired: $SESSION_ID"
@@ -41,7 +41,7 @@ log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG"; }
41
41
 
42
42
  # Підвантажуємо спільний helper (sourcing — не sub-shell, функції видимі поточному скрипту).
43
43
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
44
- # shellcheck source=lib/tooling-only.sh
44
+ # shellcheck source=npm/.claude-template/hooks/lib/tooling-only.sh
45
45
  . "$SCRIPT_DIR/lib/tooling-only.sh"
46
46
 
47
47
  # Витягає поле `transcript:` з YAML frontmatter ADR-чернетки.
@@ -1,4 +1,5 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/tsconfig.json",
2
3
  "$comment": "TS-конфіг для pi.dev extension. Не компілюється сам пакетом (синкається як є у .pi/extensions/<name>/), потрібен лише для IDE/TS-сервера у проєкті-споживачі, щоб резолвити node:* модулі. Споживачу треба мати @types/node у devDependencies (зазвичай уже є транзитивно).",
3
4
  "compilerOptions": {
4
5
  "module": "NodeNext",
package/CHANGELOG.md CHANGED
@@ -4,16 +4,50 @@
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.5] - 2026-05-26
8
+
9
+ ### Added
10
+
11
+ - **`text` rule — strict programmatic checks для Prettier-артефактів**: новий JS concern `rules/text/js/forbidden-prettier.mjs` падає (exit 1) при наявності у корені будь-якого з `.prettierignore`, `.prettierrc`, `.prettierrc.{json,jsonc,json5,yaml,yml,toml,js,cjs,mjs,ts,cts,mts}`, `prettier.config.{js,cjs,mjs,ts,cts,mts}`. Раніше `npx @nitra/cursor fix text` пропускав `.prettierignore` і нові 3.x-формати, бо у `formatting.mjs` мав hardcoded старий короткий список.
12
+ - **`text.package_json` Rego — token-based deny для `scripts.*`**: `bunx prettier --write .`, `npx prettier --check .`, `prettier --write src`, `./node_modules/.bin/prettier …` тепер ловляться у `deny` через regex `(^|[\s/"'])prettier($|[\s'"@])`. Покривається unit-тестами (`rules/text/policy/package_json/package_json_test.rego` + `js/tests/forbidden-prettier.test.mjs`).
13
+
14
+ ### Changed
15
+
16
+ - **`rules/text/js/formatting.mjs`**: inline стародавній цикл по `['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']` видалено — Prettier-FS-сторону тепер цілком покриває окремий concern `text.forbidden-prettier`.
17
+ - **`rules/text/text.mdc` (`## Перевірка`)**: явно зафіксовано, що `npx @nitra/cursor fix text` падає на `.prettierignore`, `.prettierrc*`, `prettier.config.*` і будь-який `package.json#scripts` із токеном `prettier`.
18
+
19
+ ## [1.27.4] - 2026-05-26
20
+
21
+ ### Fixed
22
+
23
+ - **`lint-text` / v8r**: локальний catalog тепер явно матчить `tsconfig.json` у `.pi/extensions/*/` та `npm/.pi-template/extensions/*/`, щоб schema validation не падала на hidden/template шляхах.
24
+ - **`text` rule**: додано Rego-перевірку, що забороняє `prettier` у `package.json#scripts`; canonical formatter лишається `oxfmt`.
25
+
26
+ ### Changed
27
+
28
+ - **Dog food dependencies**: bun-rule явно дозволяє root-only Vitest/Stryker peer/tools (`vitest`, `@vitest/coverage-v8`, `@stryker-mutator/vitest-runner`) у цьому monorepo, бо `npm-module` забороняє `devDependencies` у published workspace `npm/`.
29
+
30
+ ## [1.27.3] - 2026-05-26
31
+
32
+ ### Fixed
33
+
34
+ - **`js-lint` coverage provider `detect()`**: у workspace-проєктах (типовий bun monorepo з hoisted node_modules і `npm-module` правилом, що забороняє devDeps у published workspace-у) перевіряємо `vitest` як у JS-root `package.json`, так і у кореневому. Раніше `detect()` повертав false у monorepo, де vitest коректно живе у кореневому `devDependencies` — `n-cursor coverage` мовчки виходив із «0 провайдерів».
35
+
36
+ ### Changed
37
+
38
+ - **Dog food-міграція cursor → vitest**: 100 `*.test.mjs` файлів у `npm/` перенесено з `bun:test` на `vitest`; `npm/stryker.config.mjs` оновлено до canonical baseline (vitest-runner + perTest + incremental); додано `npm/vitest.config.js`; кореневий `package.json#devDependencies` отримав `vitest`, `@vitest/coverage-v8`, `@stryker-mutator/vitest-runner`. Node-сумісність: `Bun.file().text()` → `readFile(..., 'utf8')`, `Bun.spawn` → `spawnSync`, `import.meta.dir` → `dirname(fileURLToPath(import.meta.url))`, EventEmitter duck-typing → `new EventEmitter()` (node-у `events.once` приймає лише EventEmitter екземпляри).
39
+ - **`npm/rules/vue/vue.mdc` v2.4**: секцію «Тестування» приведено у відповідність із новим canon-ом (`test.mdc` v2.4 → Vitest+happy-dom). Замість прямого заперечення Vitest у Vue-проєктах рекомендується frontend-варіант `vitest.config.js` з `environment: 'happy-dom'`.
40
+
7
41
  ## [1.27.2] - 2026-05-26
8
42
 
9
43
  ### Changed
10
44
 
11
- - **`package.json#dependencies`**: `@anthropic-ai/claude-code` (^1.0.0) → `@anthropic-ai/claude-agent-sdk` (^0.3.0). У claude-code v2.x пакет реструктуризовано в CLI-only (бінарі через optionalDependencies, без `sdk.mjs`); SDK з функцією `query` винесли в окремий пакет `@anthropic-ai/claude-agent-sdk`. Сигнатура `query({ prompt, options })` і поля `options.cwd/maxTurns/allowedTools` зберігаються.
45
+ - **`package.json#dependencies`**: `@anthropic-ai/claude-code` (^1.0.0) → `@anthropic-ai/claude-agent-sdk` (^0.3.0). У claude-code v2.x пакет реструктуризовано в CLI-only (binary через optionalDependencies, без `sdk.mjs`); SDK з функцією `query` винесли в окремий пакет `@anthropic-ai/claude-agent-sdk`. Сигнатура `query({ prompt, options })` і поля `options.cwd/maxTurns/allowedTools` зберігаються.
12
46
  - **`scripts/coverage-fix.mjs`**: дзеркальна заміна імпорту `@anthropic-ai/claude-code` → `@anthropic-ai/claude-agent-sdk`. Тіло споживача без змін.
13
47
 
14
48
  ### Fixed
15
49
 
16
- - **ADR Stop-hook у Node v26 / Zed**: `capture-decisions.sh` спавнить bare `claude -p` як subprocess. PATH у Zed Claude Agent-сесіях має `node_modules/.bin` попереду `/opt/homebrew/bin`, тож резолвив локальний `@anthropic-ai/claude-code@1.0.128`, який краш-падає на старті під Node 26 (`TypeError: Cannot read properties of undefined (reading 'prototype')` у bundled google-auth-library коді, який припускає, що `require('stream')` повертає клас зі `.prototype`). Хук фіксував `empty response from LLM CLI` і виходив без створення чернетки. Після зняття v1-залежності з канона `node_modules/.bin/claude` shadow зникає, subprocess резолвить системний `claude` (homebrew або інший global) — той працює під Node 26 без правок.
50
+ - **ADR Stop-hook у Node v26 / Zed**: `capture-decisions.sh` спавнить bare `claude -p` як subprocess. PATH у Zed Claude Agent-сесіях має `node_modules/.bin` попереду `/opt/homebrew/bin`, тож визначав локальний `@anthropic-ai/claude-code@1.0.128`, який краш-падає на старті під Node 26 (`TypeError: Cannot read properties of undefined (reading 'prototype')` у bundled google-auth-library коді, який припускає, що `require('stream')` повертає клас зі `.prototype`). Хук фіксував `empty response from LLM CLI` і виходив без створення чернетки. Після зняття v1-залежності з канона `node_modules/.bin/claude` shadow зникає, subprocess визначає системний `claude` (homebrew або інший global) — той працює під Node 26 без правок.
17
51
 
18
52
  ## [1.27.1] - 2026-05-26
19
53
 
@@ -25,7 +59,7 @@
25
59
 
26
60
  ### Changed
27
61
 
28
- - **`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).
62
+ - **`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-patching у пам'яті, без копіювання node_modules у sandbox (стара проблема command runner у Bun monorepo).
29
63
  - **`rules/test/js/stryker_config.mjs`**: концерн тепер копіює два canonical baseline-и у кожен JS-root: `stryker.config.mjs` + `vitest.config.js`. Ідемпотентність збережена — обидва файли копіюються лише якщо ще немає.
30
64
  - **`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.
31
65
  - **`rules/test/policy/package_json/template/package.json.contains.json`**: канон scripts тепер містить додатково `"test": ["vitest"]` (substring-вимога). `coverage` як було — `["n-cursor coverage"]`.
@@ -41,23 +75,23 @@
41
75
 
42
76
  ### Added
43
77
 
44
- - **`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`; частина ключів відсутня → додає лише відсутні в окремий блок у кінці, без зміни існуючих значень.
78
+ - **`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`; частина ключів відсутня → додає лише відсутні в окремий блок у кінці, без зміни існуючих значень.
45
79
  - **`rules/tauri/js/tests/cargo_mutants_config.test.mjs`**: 7 тестів — silent skip без Tauri, створення baseline, ідемпотентність (повторний прогон байт-в-байт), збереження ручних налаштувань, partial-merge (додаються лише відсутні ключі), кілька src-tauri у різних workspaces, augmentation поверх нейтрального test-rule baseline.
46
80
  - **`rules/tauri/tauri.mdc` v1.3**: нові розділи «Виявлення проєкту Tauri» (опис маркерів і workspace-обходу) та «Mutation-testing: семантика app shell та platform bridge» з фіксованою семантикою boundary-файлів і ідемпотентністю взаємодії з `test`-rule.
47
81
 
48
82
  ### Changed
49
83
 
50
84
  - **`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'а.
51
- - **`rules/test/test.mdc` v2.3**: додано розділ «Універсальний baseline і framework-specific tuning» — `test` володіє нейтральним baseline, framework-rules (tauri, capacitor) зобов'язані доповнювати ідемпотентно і не перетирати ручні налаштування.
85
+ - **`rules/test/test.mdc` v2.3**: додано розділ «Універсальний baseline і framework-specific tuning» — `test` володіє нейтральним baseline, framework-rules (tauri, capacitor) зобов'язані доповнювати без дублювання і не перетирати ручні налаштування.
52
86
  - **`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 живе в одному з пакетів, а не в корені.
53
- - **`rules/test/js/tests/cargo_mutants_config.test.mjs`**: тест базлайну тепер перевіряє відсутність framework-specific ключів (`additional_cargo_test_args`, `exclude_globs`) у нейтральному baseline-файлі.
87
+ - **`rules/test/js/tests/cargo_mutants_config.test.mjs`**: тест baseline тепер перевіряє відсутність framework-specific ключів (`additional_cargo_test_args`, `exclude_globs`) у нейтральному baseline-файлі.
54
88
 
55
89
  ## [1.26.2] - 2026-05-26
56
90
 
57
91
  ### Changed
58
92
 
59
- - **`rules/js-lint/policy/jscpd/template/.jscpd.json.snippet.json`**: канон тепер містить поле `ignore` із трьома обов'язковими патернами — `.claude/worktrees/**`, `**/dist/**`, `**/CHANGELOG.md`. `**/CHANGELOG.md` додано тому, що release-журнали різних пакетів структурно повторюються (заголовки `## [x.y.z] - YYYY-MM-DD`, секції `### Added` / `### Changed` / `### Fixed` за Keep a Changelog) і `jscpd` при `minLines: 25` фіксує їх як клон — false positive (кожен `CHANGELOG.md` per-package за каноном `n-changelog`). Існуючий rego (`policy/jscpd/jscpd.rego`) уже застосовує subset-of до будь-якого масиву в снипеті, тож зміна не потребує правок коду — лише новий test-case у `jscpd_test.rego`.
60
- - **`rules/js-lint/js-lint.mdc` v1.26**: оновлено приклад `.jscpd.json` і опис під ним (тепер посилається на снипет як source of truth і пояснює, чому `**/CHANGELOG.md` у каноні).
93
+ - **`rules/js-lint/policy/jscpd/template/.jscpd.json.snippet.json`**: канон тепер містить поле `ignore` із трьома обов'язковими патернами — `.claude/worktrees/**`, `**/dist/**`, `**/CHANGELOG.md`. `**/CHANGELOG.md` додано тому, що release-журнали різних пакетів структурно повторюються (заголовки `## [x.y.z] - YYYY-MM-DD`, секції `### Added` / `### Changed` / `### Fixed` за Keep a Changelog) і `jscpd` при `minLines: 25` фіксує їх як клон — false positive (кожен `CHANGELOG.md` per-package за каноном `n-changelog`). Існуючий rego (`policy/jscpd/jscpd.rego`) уже застосовує subset-of до будь-якого масиву в snippet, тож зміна не потребує правок коду — лише новий test-case у `jscpd_test.rego`.
94
+ - **`rules/js-lint/js-lint.mdc` v1.26**: оновлено приклад `.jscpd.json` і опис під ним (тепер посилається на snippet як source of truth і пояснює, чому `**/CHANGELOG.md` у каноні).
61
95
 
62
96
  ## [1.26.1] - 2026-05-26
63
97
 
@@ -73,7 +107,7 @@
73
107
 
74
108
  ### Added
75
109
 
76
- - **`k8s/js/manifests.mjs`**: нова `collectHttpRouteIngressForWorkload(dir, appLabel, fail)` — резолвить HTTPRoute → `-hl` Service → `selector.app` mapping і повертає унікальні TCP-порти з `backendRefs[].port` для workload з міткою `appLabel`. Викликається з `appendNetworkPolicyDocuments` і `regenerateLegacyNetworkPolicyDocsInFile` під час `check k8s`.
110
+ - **`k8s/js/manifests.mjs`**: нова `collectHttpRouteIngressForWorkload(dir, appLabel, fail)` — визначає HTTPRoute → `-hl` Service → `selector.app` mapping і повертає унікальні TCP-порти з `backendRefs[].port` для workload з міткою `appLabel`. Викликається з `appendNetworkPolicyDocuments` і `regenerateLegacyNetworkPolicyDocsInFile` під час `check k8s`.
77
111
  - **`k8s/js/manifests.mjs:buildNetworkPolicyYaml`**: опційний 4-й параметр `gclbPorts: number[]` — якщо непорожній, додає ingress-правило з `ipBlock` 35.191.0.0/16, 130.211.0.0/22, 10.0.0.0/8 і TCP-портами (відсортовано). Без параметра output байтово ідентичний baseline canon.
78
112
  - **`k8s.mdc` v1.42**: новий розділ «HTTPRoute → NetworkPolicy ingress (GCLB + Envoy)» з описом mapping і прикладом NetworkPolicy для HTTPRoute-paired workload.
79
113
 
@@ -86,7 +120,7 @@
86
120
  ### Fixed
87
121
 
88
122
  - **JSDoc**: дописано опис `@returns`/`@param`-описи й типи в `rules/js-lint/coverage/coverage.mjs`, `rules/k8s/js/manifests.mjs`, `rules/adr/js/tests/*.test.mjs`, `rules/test/js/tests/*.test.mjs`, `scripts/coverage-fix.mjs`, `scripts/post-tool-use-fix.mjs`, `scripts/utils/tests/resolve-*.test.mjs` (oxlint/eslint jsdoc-правила).
89
- - **`k8s/js/manifests.mjs`**: `JSON.parse(JSON.stringify(...))` → `structuredClone(...)` (unicorn `prefer-structured-clone`); інверсія негованої умови в `validateNetworkPolicyForWorkload` (eslint `no-negated-condition`).
123
+ - **`k8s/js/manifests.mjs`**: `JSON.parse(JSON.stringify(...))` → `structuredClone(...)` (unicorn `prefer-structured-clone`); інверсія запереченої умови в `validateNetworkPolicyForWorkload` (eslint `no-negated-condition`).
90
124
  - **`k8s/policy/network_policy/network_policy.rego`**: `list_contains` → `contains_item` (regal `avoid-get-and-list-prefix`); `items[i] == item` → `some candidate in items` (`prefer-some-in-iteration`); `workload_kind` без зайвого `if {}` (`unconditional-assignment`); helper-правила переміщено після всіх `deny`, щоб задовольнити `messy-rule`. `network_policy_test.rego` переформатовано через `opa fmt`.
91
125
  - **`scripts/tests/post-tool-use-fix.test.mjs`**: fake-child перероблено з `EventEmitter` на duck-typed `addListener`/`removeListener` (unicorn `prefer-event-target`).
92
126
  - **`scripts/tests/cli-entry.test.mjs`**: symlink-тест /tmp ↔ /private/tmp використовує `mkdtempSync` з префіксом, зібраним з частин (sonarjs `publicly-writable-directories`).
@@ -106,38 +140,38 @@
106
140
 
107
141
  ### Added
108
142
 
109
- - **`stryker.config.mjs` baseline**: `incremental: true` + `incrementalFile: 'reports/stryker/incremental.json'` — Stryker зберігає результати між запусками і відновлює після краш/kill (SIGURG). Важливо для машин з обмеженою RAM де Stryker вбивається системою після ~100 мутантів.
143
+ - **`stryker.config.mjs` baseline**: `incremental: true` + `incrementalFile: 'reports/stryker/incremental.json'` — Stryker зберігає результати між запусками і відновлює після краш/kill (сигнал ОС). Важливо для машин з обмеженою RAM де Stryker вбивається системою після ~100 мутантів.
110
144
 
111
145
  ## [1.25.1] - 2026-05-26
112
146
 
113
147
  ### Added
114
148
 
115
- - **`skills/coverage-fix/SKILL.md`** — автономна команда `/n-coverage-fix`: запускає `n-cursor coverage`, читає JSON-масив вижилих мутантів із секції `## Вижилі мутанти` у COVERAGE.md і ітеративно пише тести до конвергенції (max 3 ітерації). Включає заборону паралельного запуску (Stryker пише в одну директорію).
149
+ - **`skills/coverage-fix/SKILL.md`** — автономна команда `/n-coverage-fix`: запускає `n-cursor coverage`, читає JSON-масив вцілілих мутантів із секції `## Вцілілі мутанти` у COVERAGE.md і ітеративно пише тести до конвергенції (max 3 ітерації). Включає заборону паралельного запуску (Stryker пише в одну директорію).
116
150
 
117
151
  ### Changed
118
152
 
119
- - **`rules/test/coverage/coverage.mjs` → `renderMarkdown`**: секція вижилих мутантів перейменована `## Recommendations` → `## Вижилі мутанти`; доданий ` ```json ` блок з масивом survived перед таблицею — парситься скілами `/n-fix-tests` і `/n-coverage-fix`.
153
+ - **`rules/test/coverage/coverage.mjs` → `renderMarkdown`**: секція вцілілих мутантів перейменована `## Recommendations` → `## Вцілілі мутанти`; доданий ` ```json ` блок з масивом survived перед таблицею — парситься skills `/n-fix-tests` і `/n-coverage-fix`.
120
154
  - **`skills/fix-tests/SKILL.md`**: конвенція test-файлів оновлена — цільовий файл завжди `<dir>/tests/<basename>.test.mjs`; якщо знайдено co-located тест (`.test.js`/`.test.mjs`) — переноситься в `tests/` з оновленням imports.
121
155
 
122
156
  ## [1.19.2] - 2026-05-25
123
157
 
124
158
  ### Fixed
125
159
 
126
- - **`js-lint` coverage провайдер**: виправлено `bunx stryker run` → `bunx @stryker-mutator/core run`. Стара команда (`bunx stryker`) резолвить deprecated unscoped-пакет без CLI, через що `mutation.json` не створювався і coverage падав з помилкою.
160
+ - **`js-lint` coverage провайдер**: виправлено `bunx stryker run` → `bunx @stryker-mutator/core run`. Стара команда (`bunx stryker`) визначає deprecated unscoped-пакет без CLI, через що `mutation.json` не створювався і coverage падав з помилкою.
127
161
  - **`npm/stryker.config.mjs`**: додано `mutate: ['scripts/*.mjs', 'scripts/utils/*.mjs', 'rules/*/coverage/coverage.mjs']` — без обмеження Stryker намагався мутувати 422 файли, що робить coverage-прогін нереалістичним. `commandRunner.command` змінено на `bun test --parallel` (раніше `bun test` без флагу) — ізолює worker-процеси та запобігає git-race у withTmpCwd-тестах.
128
162
 
129
163
  ## [1.19.1] - 2026-05-25
130
164
 
131
165
  ### Fixed
132
166
 
133
- - **`bun test --parallel`** як default у `npm/package.json` (`test`, `test:coverage`). Без флагу bun-test крутить усі 95 файлів у одному процесі — а `withTmpCwd` (`scripts/utils/test-helpers.mjs`) міняє глобальний `process.cwd()`, через що тести гонять один за одного: `prev = process.cwd()` ловить tmp-dir сусіднього тесту, `chdir(prev)` на restore падає `ENOENT` (бо сусід уже видалив свій tmp), або `git commit` з `cwd: process.cwd()` злітає в реальний repo з `npm/CHANGELOG.md`/`npm/package.json` як stub-fixture. `--parallel` дає окремий worker-процес на файл (з `process.cwd()` per-process), що геть знімає race. Знизило 22 тести з fail до pass, час suite'у — 211с → 47с.
167
+ - **`bun test --parallel`** як default у `npm/package.json` (`test`, `test:coverage`). Без флагу bun-test крутить усі 95 файлів у одному процесі — а `withTmpCwd` (`scripts/utils/test-helpers.mjs`) міняє глобальний `process.cwd()`, через що тести гонять один за одного: `prev = process.cwd()` ловить tmp-dir сусіднього тесту, restore робочої директорії падає `ENOENT` (бо сусід уже видалив свій tmp), або `git commit` з `cwd: process.cwd()` злітає в реальний repo з `npm/CHANGELOG.md`/`npm/package.json` як stub-fixture. `--parallel` дає окремий worker-процес на файл (з `process.cwd()` per-process), що геть знімає race. Знизило 22 тести з fail до pass, час suite'у — 211с → 47с.
134
168
  - **`tests/integration-repo-checks.test.mjs`** — додано explicit `30000`ms timeout для `check-* на реальному репозиторії > узгоджені з поточним деревом cursor`. Тест послідовно ганяє 10 check-функцій із subprocess-викликами (shellcheck-стаб + conftest/opa/regal/kubeconform/kubescape) — на macOS виходить ~3-7с, дефолтний 5000ms-timeout bun-test'у не вистачає.
135
169
 
136
170
  ## [1.19.0] - 2026-05-25
137
171
 
138
172
  ### Added
139
173
 
140
- - **Pi.dev інтеграція** — CLI під час синку генерує `.pi/skills/<dir>/SKILL.md` для кожного скілу з `.cursor/skills/<dir>/` із frontmatter `name`+`description` (формат pi.dev: 1-64 chars, `[a-z0-9-]`). Тіло — делегат `Виконай інструкції зі скілу .cursor/skills/<dir>/SKILL.md.`, симетрично до `.claude/commands/<dir>.md`. Always-on, без флагу. Покриває керовані (з пакета) і локальні скіли; orphan-cleanup видаляє `.pi/skills/n-*` дири, яких немає у конфігу, і локальні дири, яких більше немає у `.cursor/skills/`.
174
+ - **Pi.dev інтеграція** — CLI під час синку генерує `.pi/skills/<dir>/SKILL.md` для кожного скілу з `.cursor/skills/<dir>/` із frontmatter `name`+`description` (формат pi.dev: 1-64 chars, `[a-z0-9-]`). Тіло — делегат `Виконай інструкції зі скілу .cursor/skills/<dir>/SKILL.md.`, симетрично до `.claude/commands/<dir>.md`. Always-on, без флагу. Покриває керовані (з пакета) і локальні скіли; orphan-cleanup видаляє `.pi/skills/n-*` директорії, яких немає у конфігу, і локальні директорії, яких більше немає у `.cursor/skills/`.
141
175
  - `npm/bin/n-cursor.js`: константа `PI_SKILLS_DIR='.pi/skills'`, функція `formatPiSkillFrontmatter(name, desc)`, синки `syncPiSkills`/`syncLocalOnlyPiSkills` + cleanups `removeOrphanManagedPiSkillDirs`/`removeOrphanLocalPiSkillDirs`. Новий `runSyncStep('❌ Pi skills: ', …)` після Commands-блоку у головному потоці.
142
176
 
143
177
  ## [1.18.3] - 2026-05-25
@@ -156,10 +190,10 @@
156
190
 
157
191
  ### Fixed
158
192
 
159
- - **`scripts/cli-entry.mjs::isRunAsCli`** + **`scripts/lib/run-rule-cli.mjs::isRunAsCli`** — функція приймала `()` без аргументів і всередині дивилася на власний `import.meta.url`, а не на caller'а. Через те, що `import.meta` лексично прив'язаний до файлу, де записаний, helper-функція ВСІГДА бачила свій файл — `cli-entry.mjs` / `run-rule-cli.mjs` — і ніколи не дорівнювала `process.argv[1]`. Результат: усі ~40 `if (isRunAsCli())` у `rules/<id>/fix.mjs` / `lint/*.mjs` / `bin/rename-yaml-extensions.mjs` ВСІГДА йшли в else-гілку, і `bun rules/<id>/fix.mjs` мовчки виходив `0` без жодного output'у. `npx @nitra/cursor fix <rule>` → `runFixCommand` → `spawnSync('bun', [fix.mjs])` → exit 0 без жодного reporter-звіту.
160
- - **Fix:** функція тепер приймає `metaUrl` параметром: `isRunAsCli(import.meta.url)`. Реалізація через `realpathSync(fileURLToPath(metaUrl)) === realpathSync(resolve(process.argv[1]))``realpath` знімає різницю «symlink vs canonical» (macOS `/tmp` ↔ `/private/tmp`, pnpm content-addressable links, `node_modules/.bin/*` shim).
193
+ - **`scripts/cli-entry.mjs::isRunAsCli`** + **`scripts/lib/run-rule-cli.mjs::isRunAsCli`** — функція приймала `()` без аргументів і всередині дивилася на власний `import.meta.url`, а не на caller'а. Через те, що `import.meta` лексично прив'язаний до файлу, де записаний, helper-функція ЗАВЖДИ бачила свій файл — `cli-entry.mjs` / `run-rule-cli.mjs` — і ніколи не дорівнювала `process.argv[1]`. Результат: усі ~40 `if (isRunAsCli())` у `rules/<id>/fix.mjs` / `lint/*.mjs` / `bin/rename-yaml-extensions.mjs` ЗАВЖДИ йшли в else-гілку, і `bun rules/<id>/fix.mjs` мовчки виходив `0` без жодного output'у. `npx @nitra/cursor fix <rule>` → `runFixCommand` → `spawnSync('bun', [fix.mjs])` → exit 0 без жодного reporter-звіту.
194
+ - **Fix:** функція тепер приймає `metaUrl` параметром: `isRunAsCli(import.meta.url)`. Реалізація через порівняння канонічних шляхівце знімає різницю «symlink vs canonical» (macOS `/tmp` ↔ `/private/tmp`, pnpm content-addressable links, `node_modules/.bin/*` shim).
161
195
  - **Консолідація:** `run-rule-cli.mjs::isRunAsCli` тепер `export { isRunAsCli } from '../cli-entry.mjs'` — одне джерело правди. Existing import paths у callers лишилися без змін.
162
- - **Callsites:** всі ~40 викликів `isRunAsCli()` оновлено на `isRunAsCli(import.meta.url)`.
196
+ - **Call sites:** всі ~40 викликів `isRunAsCli()` оновлено на `isRunAsCli(import.meta.url)`.
163
197
  - **Tests:** додано три нові кейси у `scripts/tests/cli-entry.test.mjs` (entry-detection через spawn-fixture, symlink-нормалізація через `/tmp` → `/private/tmp`, no-arg fallback). Fixture — `scripts/tests/fixtures/cli-entry-as-cli.mjs`.
164
198
 
165
199
  ## [1.18.0] - 2026-05-25
@@ -186,17 +220,17 @@
186
220
  ### Changed
187
221
 
188
222
  - Концерн `stryker_config`: gitignore-патерн `**/reports/stryker/.tmp/` + `**/reports/stryker/mutation.json` замінено на один broader `**/reports/stryker/` — увесь каталог Stryker-output-у. Покриває не лише `.tmp/` + `mutation.json`, а й HTML/dashboard-репорти якщо користувач додасть інші reporter-и. Існуючі дрібніші патерни в `.gitignore` користувача не видаляються (idempotent helper лише дописує), але стають надлишковими — користувач може почистити вручну за бажанням.
189
- - `test.mdc` 2.1 → 2.2: оновлено опис gitignore-керування під новий broader patern.
223
+ - `test.mdc` 2.1 → 2.2: оновлено опис gitignore-керування під новий broader pattern.
190
224
 
191
225
  ## [1.17.3] - 2026-05-24
192
226
 
193
227
  ### Added
194
228
 
195
- - Концерн `stryker_config` правила `test` тепер ідемпотентно додає у кореневий `.gitignore` патерни Stryker-output-у:
229
+ - Концерн `stryker_config` правила `test` тепер без дублювання додає у кореневий `.gitignore` патерни Stryker-output-у:
196
230
  - `**/reports/stryker/.tmp/` — in-place backup-каталог (з baseline-у `tempDirName`).
197
231
  - `**/reports/stryker/mutation.json` — JSON-репорт мутацій.
198
232
  - Header-секція `# Stryker mutation testing (test.mdc)`, sectioning через `ensureGitignoreEntries`.
199
- - Спільний helper `npm/scripts/utils/ensure-gitignore-entries.mjs` — append-only оновлювач `.gitignore` з header-секціями. Idempotent (точне співпадіння рядка після `trim`), створює файл якщо немає, зберігає trailing-newline. 5 unit-тестів.
233
+ - Спільний helper `npm/scripts/utils/ensure-gitignore-entries.mjs` — append-only модуль оновлення `.gitignore` з header-секціями. Idempotent (точне співпадіння рядка після `trim`), створює файл якщо немає, зберігає trailing-newline. 5 unit-тестів.
200
234
 
201
235
  ### Changed
202
236
 
@@ -209,8 +243,8 @@
209
243
 
210
244
  - Правило `test`: два нових концерни — `stryker_config` і `cargo_mutants_config`. Self-gating через `.n-cursor.json#rules`: концерн активний лише якщо відповідне залежне правило (`js-lint` / `rust`) enabled. **Iterate-all-workspaces**: при відсутності цільового файлу копіює canonical baseline у КОЖЕН workspace-каталог (не лише workspaces[0]).
211
245
  - `stryker.config.mjs` у кожному JS-root (всі workspaces з package.json, або cwd у single-package) — мінімум для роботи з `bun test`.
212
- - `.cargo/mutants.toml` у каталозі КОЖНОГО Cargo.toml-маніфесту: корінь + workspaces (з підтримкою Tauri-патерну `<ws>/src-tauri/Cargo.toml`) — комент-плейсхолдер; cargo-mutants має робочі defaults.
213
- - Спільні резолвери у `npm/scripts/utils/`: `resolveJsRoot` (single, для coverage-провайдера) + `resolveAllJsRoots` (plural, для test-концерну); `resolveCargoManifest` (single) + `resolveAllCargoManifests` (plural). Coverage-провайдери js-lint і rust реюзають single-варіанти.
246
+ - `.cargo/mutants.toml` у каталозі КОЖНОГО Cargo.toml-маніфесту: корінь + workspaces (з підтримкою Tauri-патерну `<ws>/src-tauri/Cargo.toml`) — коментар-плейсхолдер; cargo-mutants має робочі defaults.
247
+ - Спільні модулі визначення у `npm/scripts/utils/`: `resolveJsRoot` (single, для coverage-провайдера) + `resolveAllJsRoots` (plural, для test-концерну); `resolveCargoManifest` (single) + `resolveAllCargoManifests` (plural). Coverage-провайдери js-lint і rust повторно використовують single-варіанти.
214
248
 
215
249
  ### Changed
216
250
 
@@ -513,7 +547,7 @@
513
547
 
514
548
  ### Fixed
515
549
 
516
- - ADR-хук **`normalize-decisions.sh`**: `merge-into` більше не падає в `skip … target missing`, коли драфт треба влити в clean-ADR, який створює `rewrite` того самого батча, або в наявний clean-ADR, на який LLM послався голим `<slug>.md` без timestamp-префікса. Операції тепер застосовуються двома впорядкованими групами (спершу `delete`/`rewrite`, потім `merge-into`), а `target` резолвиться за трьома кроками: точна назва → slug-мапа rewrite-ів цього батча → єдиний наявний clean-файл із суфіксом `-<slug>.md`. Цикл застосування переведено з pipe на читання з файлу — лічильники `applied`/`skipped` виживають і потрапляють у фінальний рядок логу `done (applied N, skipped M)`. Зачеплено: [normalize-decisions.sh](.claude-template/hooks/normalize-decisions.sh).
550
+ - ADR-хук **`normalize-decisions.sh`**: `merge-into` більше не падає в `skip … target missing`, коли драфт треба влити в clean-ADR, який створює `rewrite` того самого батча, або в наявний clean-ADR, на який LLM послався голим `<slug>.md` без timestamp-префікса. Операції тепер застосовуються двома впорядкованими групами (спершу `delete`/`rewrite`, потім `merge-into`), а `target` визначається за трьома кроками: точна назва → slug-мапа rewrite-ів цього батча → єдиний наявний clean-файл із суфіксом `-<slug>.md`. Цикл застосування переведено з pipe на читання з файлу — лічильники `applied`/`skipped` виживають і потрапляють у фінальний рядок логу `done (applied N, skipped M)`. Зачеплено: [normalize-decisions.sh](.claude-template/hooks/normalize-decisions.sh).
517
551
 
518
552
  ## [1.13.67] - 2026-05-21
519
553
 
@@ -627,7 +661,7 @@
627
661
 
628
662
  ### Fixed
629
663
 
630
- - `lint-k8s`: `kubescape scan -` (stdin), доданий у 1.13.49 і збережений у 1.13.50, **не працює в kubescape v4.x** — `-` трактується як шлях до файлу й сканер виходить з `no resources found to scan` (fatal), тож `bun run lint` падав на `lint-k8s` навіть на чистих маніфестах. Прапорця `--input`/`--stdin` у CLI також немає. Тепер `runKubescapeManifest` пише зібраний kustomize-маніфест у тимчасовий файл під `os.tmpdir()` (через `fs.mkdtempSync`) і запускає **`kubescape scan <tmp-file>`**; тимчасова директорія прибирається у `finally`. Bump `k8s.mdc` `1.38` → `1.39`.
664
+ - `lint-k8s`: `kubescape scan -` (stdin), доданий у 1.13.49 і збережений у 1.13.50, **не працює в kubescape v4.x** — `-` трактується як шлях до файлу й сканер виходить з `no resources found to scan` (fatal), тож `bun run lint` падав на `lint-k8s` навіть на чистих manifest-файлах. Прапорця `--input`/`--stdin` у CLI також немає. Тепер `runKubescapeManifest` пише зібраний kustomize-маніфест у тимчасовий файл під `os.tmpdir()` (через `fs.mkdtempSync`) і запускає **`kubescape scan <tmp-file>`**; тимчасова директорія прибирається у `finally`. Bump `k8s.mdc` `1.38` → `1.39`.
631
665
 
632
666
  ## [1.13.50] - 2026-05-19
633
667
 
@@ -663,7 +697,7 @@
663
697
 
664
698
  ### Fixed
665
699
 
666
- - `inlineTemplateLinks` tests: оновлено очікувані рядки для фікстури `__fixtures__/inline-template/fix/foo/template/snippet.json` (перейшла на форматований варіант `{ "key": "val" }` ще в 1.13.38) та для інтеграційного тесту `security.mdc` (snippet `package.json` тепер multi-line після lint-проходу). Без зміни рантайм-логіки.
700
+ - `inlineTemplateLinks` tests: оновлено очікувані рядки для фікстури `__fixtures__/inline-template/fix/foo/template/snippet.json` (перейшла на форматований варіант `{ "key": "val" }` ще в 1.13.38) та для інтеграційними testing тесту `security.mdc` (snippet `package.json` тепер multi-line після lint-проходу). Без зміни рантайм-логіки.
667
701
  - `check-ga` тестова фікстура `setupCanonicalGaProject`: додано крок `Install conftest` у `.github/workflows/lint-ga.yml`, без якого `ga.lint_ga` rego-полісі забороняє workflow і `check()` повертав 1.
668
702
 
669
703
  ## [1.13.44] - 2026-05-18
@@ -1506,7 +1540,7 @@
1506
1540
 
1507
1541
  - **npm-module — компактний пакет: whitelist `files`, без `devDependencies`, тести/фікстури поза опублікованим деревом:** правило `npm-module.mdc` тепер вимагає максимально компактний tarball. (1) Поле `"files"` у `npm/package.json` обовʼязкове як whitelist (без нього npm пакує майже все). (2) `npm/package.json` не повинен містити `devDependencies` — інструментарій для розробки тримаємо у кореневому `package.json` монорепо, щоб `npm install @nitra/<pkg>` не тягнув його кінцевим користувачам. (3) Тести й фікстури не повинні потрапляти у tarball: канонічне місце — `npm/tests/` (не додається до `"files"`); це стосується і test-style каталогів (`tests/`, `__tests__/`, `fixtures/`, `__fixtures__/`, `spec/`, `test/`), і файлів за патернами `*.test.*` / `*.spec.*`, і JS/TS-файлів з імпортами test-фреймворків (`bun:test`, `node:test`, `vitest`, `@jest/globals`, `mocha`, `jest`, `ava`, …). **Виняток — Rego (`*_test.rego`):** за конвенцією conftest юніт-тест лежить поруч з полісі у тому самому `package`, тож rego-тести дозволені всередині опублікованого `policy/`-каталогу і входять у tarball.
1508
1542
  - **npm-module — пер-документні deny у rego (Rego-authoritative):** `npm/policy/npm_module/npm_package_json/npm_package_json.rego` розширено двома deny: (а) `"files"` як whitelist обовʼязковий (відсутній / не масив / порожній); (б) `"devDependencies"` мають бути відсутні або порожні. Додано `npm_package_json_test.rego` з happy-path + 7 негативних кейсів (`json.patch` фікстури). Покривається `bun run lint-rego` (`conftest verify`) і `bun run lint-conftest` (батч проти реального `npm/package.json`). Раніше я помилково реалізував ці перевірки у JS — це порушує `.cursor/rules/conftest.mdc` (Rego-default для пер-документних структурних перевірок). Тепер виправлено: JS-функцію `checkPackageCompactness` видалено з `check-npm-module.mjs` разом з виклик-сайтом.
1509
- - **npm-module — `check-npm-module.mjs` лишає лише FS/AST-частину:** функція `checkNoTestsInPublishedFiles` резолвить позитивні patterns поля `files`, віднімає негативні (підтримка `!…` glob з `*` / `**` / `?`), і для кожного файлу-кандидата ловить test-style ім'я каталога/файлу або імпорт тест-фреймворку через oxc-parser (`module.staticImports` + `require()` + динамічний `import()`). `*_test.rego` свідомо не входить у `TEST_FILE_PATTERNS` — дозволений виняток для conftest-конвенції (юніт-тест поруч з полісі у тому самому `package`).
1543
+ - **npm-module — `check-npm-module.mjs` лишає лише FS/AST-частину:** функція `checkNoTestsInPublishedFiles` визначає позитивні patterns поля `files`, віднімає негативні (підтримка `!…` glob з `*` / `**` / `?`), і для кожного файлу-кандидата ловить test-style ім'я каталога/файлу або імпорт тест-фреймворку через oxc-parser (`module.staticImports` + `require()` + динамічний `import()`). `*_test.rego` свідомо не входить у `TEST_FILE_PATTERNS` — дозволений виняток для conftest-конвенції (юніт-тест поруч з полісі у тому самому `package`).
1510
1544
  - **npm/package.json — приведено до правила:** видалено секцію `devDependencies` (`@nitra/cursor` вже є у корені як `workspace:*`). `policy/**/*_test.rego` свідомо лишаються у tarball — як виняток для conftest-конвенції.
1511
1545
  - **conftest.mdc + npm/.claude-template/npm-CLAUDE.md — гостріший Rego-first сигнал:** у `.cursor/rules/conftest.mdc` додано STOP-блок перед `Edit` будь-якого `check-<rule>.mjs` (стосується і нових перевірок, і розширення вже існуючих; типовий ляп — `if (pkg.<field>) fail(…)` у JS замість ще одного `deny contains` у відповідному rego-пакеті). Перший пункт алгоритму уточнено прикладом «заборона/наявність ключа верхнього рівня типу `devDependencies` / `scripts.<name>`». У `npm-CLAUDE.md` секцію «Перш ніж писати `check-*.mjs`» переписано у self-check з 3 пунктів і червоним прапором. Регенеровано `npm/CLAUDE.md`.
1512
1546
 
@@ -1608,7 +1642,7 @@
1608
1642
 
1609
1643
  ### Added
1610
1644
 
1611
- - **k8s / rego-полісі:** розширено `npm/policy/k8s/manifest/manifest.rego` (Deployment cpu+memory у `requests`, Hasura image pin із білим списком тегів, канонічний `topologySpreadConstraints` з мітки `app` самого Deployment). Додано `manifest_test.rego` із вхідними фікстурами; rego тестується через `conftest verify` (опційний крок у `bun run lint-rego`). JS у `check-k8s.mjs` лишається authoritative — нові правила Rego — швидкий gate для одиничного маніфеста.
1645
+ - **k8s / rego-полісі:** розширено `npm/policy/k8s/manifest/manifest.rego` (Deployment cpu+memory у `requests`, Hasura image pin із білим списком тегів, канонічний `topologySpreadConstraints` з мітки `app` самого Deployment). Додано `manifest_test.rego` із вхідними фікстурами; rego тестується через `conftest verify` (опційний крок у `bun run lint-rego`). JS у `check-k8s.mjs` лишається authoritative — нові правила Rego — швидкий gate для одиничного manifest.
1612
1646
  - **k8s / нові rego-пакети:** `npm/policy/k8s/gateway/` (Gateway API: backendRef з суфіксом `-hl`, redundant `namespace` у backendRef, HCP `targetRef.name` `-hl`); `npm/policy/k8s/kustomization/` (resources/patches алфавітне сортування, JSON6902 `remove`+`add` на той самий `path`); `npm/policy/k8s/svc_yaml/` (`Service.spec.type: ClusterIP`); `npm/policy/k8s/svc_hl_yaml/` (headless Service з суфіксом `-hl` і `clusterIP: None`); `npm/policy/k8s/base_kustomization/` (обов'язковий `namespace:`); `npm/policy/k8s/base_manifest/` (`metadata.namespace` у base, base-canon `cpu='0.02'`/`memory='128Mi'`); `npm/policy/k8s/kustomize_managed/` (заборона `metadata.namespace` у kustomize-managed файлах); `npm/policy/k8s/hasura_configmap/` (`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS: "true"`); `npm/policy/k8s/hasura_httproute/` (канон 4 правил Hasura: `/ql` Exact + `/ql/` Exact + PathPrefix + WebSocket); `npm/policy/k8s/hpa_pdb/` (структурний gate HPA/PDB: `apiVersion`, `behavior.scaleUp/Down`, `metrics`, `selector.matchLabels`). До кожного пакета додано `*_test.rego` фікстури.
1613
1647
  - **lint-rego:** додано опційний крок `conftest verify` у `npm/scripts/lint-rego.mjs` після `regal lint` для виконання `*_test.rego`. Якщо `conftest` не у PATH — крок мовчки пропускається з install-hint.
1614
1648
 
@@ -2176,7 +2210,7 @@
2176
2210
 
2177
2211
  ### Added
2178
2212
 
2179
- - `k8s.mdc` / `check-k8s.mjs`: у маршрутах Gateway API (**HTTPRoute**, **GRPCRoute**, **TCPRoute**, **TLSRoute**, **UDPRoute**, група `gateway.networking.k8s.io`) забороняється поле `namespace` у `spec.rules[*].backendRefs[*]` (і однини `backendRef`), якщо його значення збігається з `metadata.namespace` самого маршруту. За замовчуванням Gateway API резолвить backend у тому ж namespace, що й маршрут — дублювання у `backendRef` мертве й заважає Kustomize-overlay, що міняє namespace маршруту. Cross-namespace backendRef (з відмінним `namespace`) правило не торкається. Експортовано `collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, routeNs)`; перевіряється усередині існуючого `failIfGatewayRouteUsesNonHeadlessService` (той самий обхід дерева, що й для headless-перевірки). Додано приклад «погано/добре» у `k8s.mdc` і відповідні юніт-тести.
2213
+ - `k8s.mdc` / `check-k8s.mjs`: у маршрутах Gateway API (**HTTPRoute**, **GRPCRoute**, **TCPRoute**, **TLSRoute**, **UDPRoute**, група `gateway.networking.k8s.io`) забороняється поле `namespace` у `spec.rules[*].backendRefs[*]` (і однини `backendRef`), якщо його значення збігається з `metadata.namespace` самого маршруту. За замовчуванням Gateway API визначає backend у тому ж namespace, що й маршрут — дублювання у `backendRef` мертве й заважає Kustomize-overlay, що міняє namespace маршруту. Cross-namespace backendRef (з відмінним `namespace`) правило не торкається. Експортовано `collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, routeNs)`; перевіряється усередині існуючого `failIfGatewayRouteUsesNonHeadlessService` (той самий обхід дерева, що й для headless-перевірки). Додано приклад «погано/добре» у `k8s.mdc` і відповідні юніт-тести.
2180
2214
 
2181
2215
  ## [1.8.174] - 2026-05-05
2182
2216
 
@@ -2214,7 +2248,7 @@
2214
2248
  ### Added
2215
2249
 
2216
2250
  - `image.mdc` (v1.3) / `check-image.mjs`: нове правило `image` для оптимізації зображень через [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image). Перевіряє лише локальну конфігурацію (CI-workflow не вимагається — sharp/svgo тягнуть бінарні залежності, цінність на ubuntu-runner-ах нижча за час прогону): скрипт `lint-image` у `package.json` з обовʼязковим викликом `npx @nitra/minify-image --src=. --write --avif` (авто-оптимізація на місці + AVIF-двійники для PNG/JPEG/GIF), `bun run lint-image` в агрегованому `lint`, заборона `@nitra/minify-image` у `dependencies`/`devDependencies` (CLI лише через `npx`, симетрично до `markdownlint-cli2` у `text.mdc`) і рядок `.minify-image-cache.tsv` у `.gitignore` (або, рідше, у `files` пакета). AVIF-двійники (`<name>.<ext>.avif`) зберігаються в git як готові артефакти для віддачі браузеру.
2217
- - `image.mdc` (v1.3) / `check-image.mjs`: у `.vue` файлах кожного workspace-пакета raster-посилання мають вести на AVIF-двійник (`...png.avif`) у двох формах: (а) `import x from '...png|jpg|jpeg|gif'` (далі `:src="x"`); (б) прямі статичні атрибути `<img src="...png" />` у `<template>` (Vite перетворює їх на asset-імпорти при збірці). Реактивне `:src="..."` не сканується (JS-вираз — резолвиться через імпорт, який ловиться у формі (а)); `data-src=`, `obj.src=` у `<script>`, SVG-імпорти теж пропускаємо. Опт-аут на рівні воркспейс-пакета: `"@nitra/minify-image": { "disable-avif": true }` у `package.json` цього пакета. Дедуплікація обходу: при walk-у кореня `.` піддерева інших workspace-роди пропускаються (інакше `App.vue` у `demo/` доповідався б двічі).
2251
+ - `image.mdc` (v1.3) / `check-image.mjs`: у `.vue` файлах кожного workspace-пакета raster-посилання мають вести на AVIF-двійник (`...png.avif`) у двох формах: (а) `import x from '...png|jpg|jpeg|gif'` (далі `:src="x"`); (б) прямі статичні атрибути `<img src="...png" />` у `<template>` (Vite перетворює їх на asset-імпорти при збірці). Реактивне `:src="..."` не сканується (JS-вираз — визначається через імпорт, який ловиться у формі (а)); `data-src=`, `obj.src=` у `<script>`, SVG-імпорти теж пропускаємо. Опт-аут на рівні воркспейс-пакета: `"@nitra/minify-image": { "disable-avif": true }` у `package.json` цього пакета. Дедуплікація обходу: при walk-у кореня `.` піддерева інших workspace-роди пропускаються (інакше `App.vue` у `demo/` доповідався б двічі).
2218
2252
  - `auto-rules.mjs` / `auto-rules.md`: введено граф залежностей між правилами (`AUTO_RULE_DEPENDENCIES`, синтаксис у `auto-rules.md` — `rule - [other]`). Правило `image` описане як `image - [vue]` — варто автододати лише разом з `vue`, без дублювання вихідної умови «`.vue`-файли». Транзитивне розгортання дозволяє ланцюги (`a → b → c`) і поважає `disable-rules` (якщо vue вимкнено — image теж не додається).
2219
2253
  - `vue.mdc` (v1.4) / `check-vue.mjs`: посилено перевірку `vite.config` — окрім згадки `AutoImport` тепер вимагається, щоб у виклику `AutoImport({ imports: [...] })` був присутній рядковий елемент `'vue'`. Без цього `unplugin-auto-import` не надасть `ref` / `createApp` / тощо, і прибирати явні value-імпорти з `'vue'` стає небезпечно (зламає код). Якщо `'vue'` у `imports` відсутній — value-імпорти більше не оголошуються забороненими, а fail зʼявляється на конфізі vite. Балансована екстракція аргументів `AutoImport(...)` через `extractAutoImportCallArgs` працює для багаторядкових об'єктів.
2220
2254
 
package/bin/n-cursor.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * дістає `tool_input.file_path`, маршрутизує його у відповідні правила
14
14
  * (`*.mjs` → `js-lint`, `*.vue` → `js-lint style-lint vue` тощо) і викликає
15
15
  * `fix` лише з ними. Прописується автоматично в `.claude/settings.json`.
16
- * `npx \@nitra/cursor lint` — оркестратор lint-ланцюжка з кореневого `package.json` з тайменгом
16
+ * `npx \@nitra/cursor lint` — оркестратор lint-ланцюжка з кореневого `package.json` з вимірюванням часу
17
17
  * кожного `lint-*` / `oxfmt` скрипта (fail-fast); канонічна заміна
18
18
  * раніше ручного `lint-ga && lint-js && …` агрегатора.
19
19
  * `npx \@nitra/cursor lint-ga` — канонічний lint-ga (ga.mdc): preflight на `shellcheck` →
@@ -1456,7 +1456,7 @@ try {
1456
1456
  break
1457
1457
  }
1458
1458
  case 'lint': {
1459
- // Оркестратор lint-ланцюжка з тайменгом на кожен крок (fail-fast).
1459
+ // Оркестратор lint-ланцюжка з вимірюванням часу на кожен крок (fail-fast).
1460
1460
  // Замінює раніше використовуваний агрегатор `bun run lint-ga && bun run lint-js && …` у root package.json.
1461
1461
  process.exitCode = runLintCli()
1462
1462
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.27.2",
3
+ "version": "1.27.5",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -44,8 +44,9 @@
44
44
  "type": "module",
45
45
  "types": "./types/bin/n-cursor.d.ts",
46
46
  "scripts": {
47
- "test": "bun test --parallel",
48
- "test:coverage": "bun test --parallel --coverage --timeout 15000 scripts/tests/ rules/",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "test:coverage": "vitest run --coverage",
49
50
  "start": "bun ./bin/n-cursor.js",
50
51
  "rename-yaml-extensions": "bun ./bin/n-cursor.js rename-yaml-extensions"
51
52
  },
package/rules/bun/bun.mdc CHANGED
@@ -42,7 +42,7 @@ Lockfile у репозиторії: `bun.lock`.
42
42
  - Якщо залежність потрібна лише одному пакету, додавати її в директорії цього пакета.
43
43
  - У CI та локально запускати скрипти через `bun run`.
44
44
 
45
- В кореневому в package.json не повинно бути `dependencies`, а в `devDependencies` — тільки модулі `@nitra/*`. Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn.
45
+ В кореневому в package.json не повинно бути `dependencies`, а в `devDependencies` — тільки модулі `@nitra/*`. Виняток для цього dog food-репозиторію `@nitra/cursor`: root-only Vitest/Stryker peer/tools (`vitest`, `@vitest/coverage-v8`, `@stryker-mutator/vitest-runner`), бо published workspace `npm/` не має devDependencies за `npm-module.mdc`. Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn.
46
46
 
47
47
  - Заборонені top-level поля у root `package.json` (з причинами): [package.json.deny.json](./policy/package_json/template/package.json.deny.json)
48
48
 
@@ -5,7 +5,7 @@
5
5
  # (top-level fields заборонені у root).
6
6
  #
7
7
  # Логіка, що ЛИШАЄТЬСЯ у rego (inverse-patterns, не виносяться у template):
8
- # - `devDependencies` лише `@nitra/*` (inverse-pattern: every dep must match)
8
+ # - `devDependencies` лише `@nitra/*` + root-only тестові peer/tools для dog food (inverse-pattern)
9
9
  # - Агрегований `lint` скрипт (cross-script aggregation logic)
10
10
  #
11
11
  # Перевірки, які ЗАЛИШИЛИСЬ у JS (потребують FS / cross-file):
@@ -32,13 +32,13 @@ deny contains msg if {
32
32
  msg := sprintf("package.json: поле %s — %s", [field, reason])
33
33
  }
34
34
 
35
- # ── deny: devDependencies — лише `@nitra/*` (inverse pattern; не виноситься у template)
35
+ # ── deny: devDependencies — лише `@nitra/*` + root-only тестові peer/tools
36
36
 
37
37
  deny contains msg if {
38
38
  is_object(input.devDependencies)
39
39
  some name, _ in input.devDependencies
40
- not startswith(name, "@nitra/")
41
- msg := sprintf("Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: %s (bun.mdc)", [name])
40
+ not allowed_root_dev_dependency(name)
41
+ msg := sprintf("Кореневі devDependencies: дозволені лише @nitra/* або root-only test peers — прибери або перенеси: %s (bun.mdc)", [name])
42
42
  }
43
43
 
44
44
  # ── deny: агрегований lint-скрипт (cross-script aggregation logic) ───────
@@ -66,6 +66,16 @@ deny contains msg if {
66
66
 
67
67
  # ── helpers ────────────────────────────────────────────────────────────────
68
68
 
69
+ allowed_root_test_deps := {"vitest", "@vitest/coverage-v8", "@stryker-mutator/vitest-runner"}
70
+
71
+ allowed_root_dev_dependency(name) if {
72
+ startswith(name, "@nitra/")
73
+ } else if {
74
+ # Ці пакети потрібні на рівні root для dog food-прогонів Vitest/Stryker у монорепо,
75
+ # але npm-module забороняє класти devDependencies у published workspace `npm/`.
76
+ name in allowed_root_test_deps
77
+ }
78
+
69
79
  lint_prefixed_scripts := [name |
70
80
  some name, _ in object.get(input, "scripts", {})
71
81
  startswith(name, "lint-")
@@ -32,7 +32,7 @@ import {
32
32
 
33
33
  const execFileAsync = promisify(execFile)
34
34
 
35
- /** Кандидати інтеграційної гілки для feature-гілок (перша наявна; див. n-changelog.mdc). */
35
+ /** Кандидати інтеграційними тести гілки для feature-гілок (перша наявна; див. n-changelog.mdc). */
36
36
  const FEATURE_BASE_BRANCH_CANDIDATES = Object.freeze(['dev', 'main'])
37
37
 
38
38
  /** Гілка `dev`: local-only не активний (крім незакомічених registry-published). */
@@ -100,7 +100,7 @@ function packageHasAvifDisabled(pkg) {
100
100
  * `public/`, потім сам корінь пакета (на випадок mono-репо без `public/`), нарешті
101
101
  * `<cwd>/x.png` як legacy fallback (щоб не зламати проєкти з кореневими ассетами).
102
102
  * - голий шлях з принаймні одним `/` (`assets/img.png`, `start-page-ua/logo.png`) — у
103
- * HTML/Vue браузер резолвить його відносно документа, тому повертаємо relative-to-source
103
+ * HTML/Vue браузер визначає його відносно документа, тому повертаємо relative-to-source
104
104
  * та `<packageRoot>/public/<path>` як другий кандидат (Quasar-проєкти кладуть public-assets
105
105
  * саме туди).
106
106
  * - bare без `/` (`foo`) — ймовірно alias resolver (Vite/Webpack), резолвити не вміємо,
@@ -165,7 +165,7 @@ function resolveImageCandidates(importPath, sourceAbsPath, packageRootAbs) {
165
165
  * хоч одне посилання у `.vue`/`.html` (доповнюється у цій функції)
166
166
  * @param {RewriteStats} stats глобальні лічильники, що мутуються тут
167
167
  * @param {(msg: string) => void} fail callback при помилці
168
- * @returns {Promise<void>} резолвиться по завершенню перевірки одного пакета
168
+ * @returns {Promise<void>} визначається по завершенню перевірки одного пакета
169
169
  */
170
170
  async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePaths, usedAvifAbs, stats, fail) {
171
171
  const absRoot = join(process.cwd(), packageRoot)
@@ -22,15 +22,18 @@ const VITEST_HINT =
22
22
  /**
23
23
  * Чи у пакеті встановлено vitest (через dependencies або devDependencies).
24
24
  * @param {{dependencies?: Record<string,string>, devDependencies?: Record<string,string>}} pkg package.json
25
- * @returns {boolean}
25
+ * @returns {boolean} true, якщо `vitest` декларовано хоча б в одному dep-section
26
26
  */
27
27
  function hasVitestDep(pkg) {
28
28
  return Boolean(pkg.devDependencies?.vitest) || Boolean(pkg.dependencies?.vitest)
29
29
  }
30
30
 
31
31
  /**
32
- * Чи провайдер застосовний у поточному cwd. Активується, коли у JS-root знайдено
33
- * `vitest` як залежність інакше silent skip із hint у stderr (одноразово).
32
+ * Чи провайдер застосовний у поточному cwd. Активується, коли `vitest`
33
+ * декларовано у JS-root АБО у кореневому `package.json` (workspace-проєкт із
34
+ * hoisted node_modules — типовий патерн bun monorepo, де npm-module rule
35
+ * забороняє devDeps у published workspace-у, тож вони живуть у корені).
36
+ * Інакше silent skip із hint у stderr (одноразово).
34
37
  * @param {string} cwd корінь проєкту
35
38
  * @returns {Promise<boolean>} true, якщо проєкт сумісний з vitest-based coverage
36
39
  */
@@ -40,14 +43,19 @@ export async function detect(cwd) {
40
43
  const pkgPath = join(jsRoot, 'package.json')
41
44
  if (!existsSync(pkgPath)) return false
42
45
  const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
43
- if (!hasVitestDep(pkg)) {
44
- if (!detect._hinted) {
45
- console.error(VITEST_HINT)
46
- detect._hinted = true
46
+ if (hasVitestDep(pkg)) return true
47
+ if (jsRoot !== cwd) {
48
+ const rootPkgPath = join(cwd, 'package.json')
49
+ if (existsSync(rootPkgPath)) {
50
+ const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
51
+ if (hasVitestDep(rootPkg)) return true
47
52
  }
48
- return false
49
53
  }
50
- return true
54
+ if (!detect._hinted) {
55
+ console.error(VITEST_HINT)
56
+ detect._hinted = true
57
+ }
58
+ return false
51
59
  }
52
60
 
53
61
  /**
@@ -42,7 +42,7 @@ version: '1.26'
42
42
 
43
43
  Каталог `.claude/worktrees/` (робочі копії, які Claude Code створює через **superpowers:using-git-worktrees**) має ігноруватися: додай його у кореневий `.gitignore` (це штатне місце для не-комітних робочих копій), а в `.jscpd.json` додай `.claude/worktrees/**` у `ignore` як страховку на випадок запуску без `gitignore: true`. Без цього jscpd сканує паралельну копію репо в worktree і фіксує самозбіги між дзеркальними файлами.
44
44
 
45
- `**/CHANGELOG.md` теж у каноні `ignore`: release-журнали різних пакетів структурно повторюються (заголовки `## [x.y.z] - YYYY-MM-DD`, секції `### Added` / `### Changed` / `### Fixed` за Keep a Changelog), і `jscpd` за `minLines: 25` фіксує їх як клон, хоч це false positive — кожен `CHANGELOG.md` веде свій per-package журнал за каноном `n-changelog`, спільної історії не існує. Без цього в монорепо легко зловити блокуюче `bun run lint` на парі CHANGELOG-ів довжиною від ~25 рядків.
45
+ `**/CHANGELOG.md` теж у каноні `ignore`: release-журнали різних пакетів структурно повторюються (заголовки `## [x.y.z] - YYYY-MM-DD`, секції `### Added` / `### Changed` / `### Fixed` за Keep a Changelog), і `jscpd` за `minLines: 25` фіксує їх як клон, хоч це false positive — кожен `CHANGELOG.md` веде свій per-package журнал за каноном `n-changelog`, спільної історії не існує. Без цього в монорепо легко зловити критичне `bun run lint` на парі CHANGELOG-ів довжиною від ~25 рядків.
46
46
 
47
47
  ```json title=".jscpd.json"
48
48
  {
@@ -242,7 +242,7 @@ function reportZeroMssqlSourceViolations(counters, pass) {
242
242
  * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
243
243
  * @param {(msg: string) => void} pass pass callback
244
244
  * @param {(msg: string) => void} fail fail callback
245
- * @returns {Promise<void>} резолвиться по завершенню аудиту всіх знайдених джерел
245
+ * @returns {Promise<void>} визначається по завершенню аудиту всіх знайдених джерел
246
246
  */
247
247
  async function auditMssqlSources(repoRoot, ignorePaths, pass, fail) {
248
248
  const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot, ignorePaths)