@nitra/cursor 1.27.3 → 1.27.6

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 (43) 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 +61 -32
  5. package/bin/n-cursor.js +2 -2
  6. package/package.json +1 -1
  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/js-lint.mdc +1 -1
  12. package/rules/js-mssql/js/deps.mjs +1 -1
  13. package/rules/k8s/js/manifests.mjs +19 -19
  14. package/rules/k8s/k8s.mdc +9 -9
  15. package/rules/k8s/policy/network_policy/network_policy.rego +5 -5
  16. package/rules/tauri/js/cargo_mutants_config.mjs +3 -3
  17. package/rules/tauri/tauri.mdc +2 -2
  18. package/rules/test/coverage/coverage.mjs +4 -4
  19. package/rules/test/js/cargo_mutants_config.mjs +2 -2
  20. package/rules/test/js/data/cargo_mutants_config/mutants.toml.baseline +1 -1
  21. package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +1 -1
  22. package/rules/test/js/stryker_config.mjs +3 -3
  23. package/rules/test/test.mdc +5 -5
  24. package/rules/text/js/forbidden-prettier.mjs +59 -0
  25. package/rules/text/js/formatting.mjs +1 -4
  26. package/rules/text/policy/package_json/package_json.rego +16 -0
  27. package/rules/text/text.mdc +1 -1
  28. package/rules/vue/vue.mdc +1 -1
  29. package/schemas/v8r-catalog.json +6 -0
  30. package/scripts/coverage-fix.mjs +12 -12
  31. package/scripts/lib/run-lint-cli.mjs +5 -5
  32. package/scripts/lib/run-lint-step.mjs +1 -1
  33. package/scripts/lib/run-rule.mjs +1 -1
  34. package/scripts/lib/timing-summary.mjs +3 -3
  35. package/scripts/post-tool-use-fix.mjs +4 -4
  36. package/scripts/sync-claude-config.mjs +2 -2
  37. package/scripts/utils/ensure-gitignore-entries.mjs +2 -2
  38. package/scripts/utils/resolve-cargo-manifest.mjs +1 -1
  39. package/scripts/utils/resolve-js-root.mjs +1 -1
  40. package/scripts/utils/walkDir.mjs +2 -2
  41. package/skills/coverage-fix/SKILL.md +15 -12
  42. package/skills/fix-tests/SKILL.md +13 -13
  43. /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,6 +4,35 @@
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.6] - 2026-05-27
8
+
9
+ ### Added
10
+
11
+ - **`test/coverage` — розширені мутаційні тести**: 18 нових тест-кейсів у `rules/test/coverage/tests/coverage.test.mjs` для покриття 19 вцілілих мутантів (StringLiteral, ConditionalExpression, ArrowFunction, BlockStatement-гілки). Загальна кількість тестів: 37.
12
+
13
+ ## [1.27.5] - 2026-05-26
14
+
15
+ ### Added
16
+
17
+ - **`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 старий короткий список.
18
+ - **`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`).
19
+
20
+ ### Changed
21
+
22
+ - **`rules/text/js/formatting.mjs`**: inline стародавній цикл по `['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']` видалено — Prettier-FS-сторону тепер цілком покриває окремий concern `text.forbidden-prettier`.
23
+ - **`rules/text/text.mdc` (`## Перевірка`)**: явно зафіксовано, що `npx @nitra/cursor fix text` падає на `.prettierignore`, `.prettierrc*`, `prettier.config.*` і будь-який `package.json#scripts` із токеном `prettier`.
24
+
25
+ ## [1.27.4] - 2026-05-26
26
+
27
+ ### Fixed
28
+
29
+ - **`lint-text` / v8r**: локальний catalog тепер явно матчить `tsconfig.json` у `.pi/extensions/*/` та `npm/.pi-template/extensions/*/`, щоб schema validation не падала на hidden/template шляхах.
30
+ - **`text` rule**: додано Rego-перевірку, що забороняє `prettier` у `package.json#scripts`; canonical formatter лишається `oxfmt`.
31
+
32
+ ### Changed
33
+
34
+ - **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/`.
35
+
7
36
  ## [1.27.3] - 2026-05-26
8
37
 
9
38
  ### Fixed
@@ -12,19 +41,19 @@
12
41
 
13
42
  ### Changed
14
43
 
15
- - **Dogfooding-міграція 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 інстанси).
44
+ - **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 екземпляри).
16
45
  - **`npm/rules/vue/vue.mdc` v2.4**: секцію «Тестування» приведено у відповідність із новим canon-ом (`test.mdc` v2.4 → Vitest+happy-dom). Замість прямого заперечення Vitest у Vue-проєктах рекомендується frontend-варіант `vitest.config.js` з `environment: 'happy-dom'`.
17
46
 
18
47
  ## [1.27.2] - 2026-05-26
19
48
 
20
49
  ### Changed
21
50
 
22
- - **`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` зберігаються.
51
+ - **`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` зберігаються.
23
52
  - **`scripts/coverage-fix.mjs`**: дзеркальна заміна імпорту `@anthropic-ai/claude-code` → `@anthropic-ai/claude-agent-sdk`. Тіло споживача без змін.
24
53
 
25
54
  ### Fixed
26
55
 
27
- - **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 без правок.
56
+ - **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 без правок.
28
57
 
29
58
  ## [1.27.1] - 2026-05-26
30
59
 
@@ -36,7 +65,7 @@
36
65
 
37
66
  ### Changed
38
67
 
39
- - **`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).
68
+ - **`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).
40
69
  - **`rules/test/js/stryker_config.mjs`**: концерн тепер копіює два canonical baseline-и у кожен JS-root: `stryker.config.mjs` + `vitest.config.js`. Ідемпотентність збережена — обидва файли копіюються лише якщо ще немає.
41
70
  - **`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.
42
71
  - **`rules/test/policy/package_json/template/package.json.contains.json`**: канон scripts тепер містить додатково `"test": ["vitest"]` (substring-вимога). `coverage` як було — `["n-cursor coverage"]`.
@@ -52,23 +81,23 @@
52
81
 
53
82
  ### Added
54
83
 
55
- - **`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`; частина ключів відсутня → додає лише відсутні в окремий блок у кінці, без зміни існуючих значень.
84
+ - **`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`; частина ключів відсутня → додає лише відсутні в окремий блок у кінці, без зміни існуючих значень.
56
85
  - **`rules/tauri/js/tests/cargo_mutants_config.test.mjs`**: 7 тестів — silent skip без Tauri, створення baseline, ідемпотентність (повторний прогон байт-в-байт), збереження ручних налаштувань, partial-merge (додаються лише відсутні ключі), кілька src-tauri у різних workspaces, augmentation поверх нейтрального test-rule baseline.
57
86
  - **`rules/tauri/tauri.mdc` v1.3**: нові розділи «Виявлення проєкту Tauri» (опис маркерів і workspace-обходу) та «Mutation-testing: семантика app shell та platform bridge» з фіксованою семантикою boundary-файлів і ідемпотентністю взаємодії з `test`-rule.
58
87
 
59
88
  ### Changed
60
89
 
61
90
  - **`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'а.
62
- - **`rules/test/test.mdc` v2.3**: додано розділ «Універсальний baseline і framework-specific tuning» — `test` володіє нейтральним baseline, framework-rules (tauri, capacitor) зобов'язані доповнювати ідемпотентно і не перетирати ручні налаштування.
91
+ - **`rules/test/test.mdc` v2.3**: додано розділ «Універсальний baseline і framework-specific tuning» — `test` володіє нейтральним baseline, framework-rules (tauri, capacitor) зобов'язані доповнювати без дублювання і не перетирати ручні налаштування.
63
92
  - **`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 живе в одному з пакетів, а не в корені.
64
- - **`rules/test/js/tests/cargo_mutants_config.test.mjs`**: тест базлайну тепер перевіряє відсутність framework-specific ключів (`additional_cargo_test_args`, `exclude_globs`) у нейтральному baseline-файлі.
93
+ - **`rules/test/js/tests/cargo_mutants_config.test.mjs`**: тест baseline тепер перевіряє відсутність framework-specific ключів (`additional_cargo_test_args`, `exclude_globs`) у нейтральному baseline-файлі.
65
94
 
66
95
  ## [1.26.2] - 2026-05-26
67
96
 
68
97
  ### Changed
69
98
 
70
- - **`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`.
71
- - **`rules/js-lint/js-lint.mdc` v1.26**: оновлено приклад `.jscpd.json` і опис під ним (тепер посилається на снипет як source of truth і пояснює, чому `**/CHANGELOG.md` у каноні).
99
+ - **`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`.
100
+ - **`rules/js-lint/js-lint.mdc` v1.26**: оновлено приклад `.jscpd.json` і опис під ним (тепер посилається на snippet як source of truth і пояснює, чому `**/CHANGELOG.md` у каноні).
72
101
 
73
102
  ## [1.26.1] - 2026-05-26
74
103
 
@@ -84,7 +113,7 @@
84
113
 
85
114
  ### Added
86
115
 
87
- - **`k8s/js/manifests.mjs`**: нова `collectHttpRouteIngressForWorkload(dir, appLabel, fail)` — резолвить HTTPRoute → `-hl` Service → `selector.app` mapping і повертає унікальні TCP-порти з `backendRefs[].port` для workload з міткою `appLabel`. Викликається з `appendNetworkPolicyDocuments` і `regenerateLegacyNetworkPolicyDocsInFile` під час `check k8s`.
116
+ - **`k8s/js/manifests.mjs`**: нова `collectHttpRouteIngressForWorkload(dir, appLabel, fail)` — визначає HTTPRoute → `-hl` Service → `selector.app` mapping і повертає унікальні TCP-порти з `backendRefs[].port` для workload з міткою `appLabel`. Викликається з `appendNetworkPolicyDocuments` і `regenerateLegacyNetworkPolicyDocsInFile` під час `check k8s`.
88
117
  - **`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.
89
118
  - **`k8s.mdc` v1.42**: новий розділ «HTTPRoute → NetworkPolicy ingress (GCLB + Envoy)» з описом mapping і прикладом NetworkPolicy для HTTPRoute-paired workload.
90
119
 
@@ -97,7 +126,7 @@
97
126
  ### Fixed
98
127
 
99
128
  - **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-правила).
100
- - **`k8s/js/manifests.mjs`**: `JSON.parse(JSON.stringify(...))` → `structuredClone(...)` (unicorn `prefer-structured-clone`); інверсія негованої умови в `validateNetworkPolicyForWorkload` (eslint `no-negated-condition`).
129
+ - **`k8s/js/manifests.mjs`**: `JSON.parse(JSON.stringify(...))` → `structuredClone(...)` (unicorn `prefer-structured-clone`); інверсія запереченої умови в `validateNetworkPolicyForWorkload` (eslint `no-negated-condition`).
101
130
  - **`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`.
102
131
  - **`scripts/tests/post-tool-use-fix.test.mjs`**: fake-child перероблено з `EventEmitter` на duck-typed `addListener`/`removeListener` (unicorn `prefer-event-target`).
103
132
  - **`scripts/tests/cli-entry.test.mjs`**: symlink-тест /tmp ↔ /private/tmp використовує `mkdtempSync` з префіксом, зібраним з частин (sonarjs `publicly-writable-directories`).
@@ -117,38 +146,38 @@
117
146
 
118
147
  ### Added
119
148
 
120
- - **`stryker.config.mjs` baseline**: `incremental: true` + `incrementalFile: 'reports/stryker/incremental.json'` — Stryker зберігає результати між запусками і відновлює після краш/kill (SIGURG). Важливо для машин з обмеженою RAM де Stryker вбивається системою після ~100 мутантів.
149
+ - **`stryker.config.mjs` baseline**: `incremental: true` + `incrementalFile: 'reports/stryker/incremental.json'` — Stryker зберігає результати між запусками і відновлює після краш/kill (сигнал ОС). Важливо для машин з обмеженою RAM де Stryker вбивається системою після ~100 мутантів.
121
150
 
122
151
  ## [1.25.1] - 2026-05-26
123
152
 
124
153
  ### Added
125
154
 
126
- - **`skills/coverage-fix/SKILL.md`** — автономна команда `/n-coverage-fix`: запускає `n-cursor coverage`, читає JSON-масив вижилих мутантів із секції `## Вижилі мутанти` у COVERAGE.md і ітеративно пише тести до конвергенції (max 3 ітерації). Включає заборону паралельного запуску (Stryker пише в одну директорію).
155
+ - **`skills/coverage-fix/SKILL.md`** — автономна команда `/n-coverage-fix`: запускає `n-cursor coverage`, читає JSON-масив вцілілих мутантів із секції `## Вцілілі мутанти` у COVERAGE.md і ітеративно пише тести до конвергенції (max 3 ітерації). Включає заборону паралельного запуску (Stryker пише в одну директорію).
127
156
 
128
157
  ### Changed
129
158
 
130
- - **`rules/test/coverage/coverage.mjs` → `renderMarkdown`**: секція вижилих мутантів перейменована `## Recommendations` → `## Вижилі мутанти`; доданий ` ```json ` блок з масивом survived перед таблицею — парситься скілами `/n-fix-tests` і `/n-coverage-fix`.
159
+ - **`rules/test/coverage/coverage.mjs` → `renderMarkdown`**: секція вцілілих мутантів перейменована `## Recommendations` → `## Вцілілі мутанти`; доданий ` ```json ` блок з масивом survived перед таблицею — парситься skills `/n-fix-tests` і `/n-coverage-fix`.
131
160
  - **`skills/fix-tests/SKILL.md`**: конвенція test-файлів оновлена — цільовий файл завжди `<dir>/tests/<basename>.test.mjs`; якщо знайдено co-located тест (`.test.js`/`.test.mjs`) — переноситься в `tests/` з оновленням imports.
132
161
 
133
162
  ## [1.19.2] - 2026-05-25
134
163
 
135
164
  ### Fixed
136
165
 
137
- - **`js-lint` coverage провайдер**: виправлено `bunx stryker run` → `bunx @stryker-mutator/core run`. Стара команда (`bunx stryker`) резолвить deprecated unscoped-пакет без CLI, через що `mutation.json` не створювався і coverage падав з помилкою.
166
+ - **`js-lint` coverage провайдер**: виправлено `bunx stryker run` → `bunx @stryker-mutator/core run`. Стара команда (`bunx stryker`) визначає deprecated unscoped-пакет без CLI, через що `mutation.json` не створювався і coverage падав з помилкою.
138
167
  - **`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-тестах.
139
168
 
140
169
  ## [1.19.1] - 2026-05-25
141
170
 
142
171
  ### Fixed
143
172
 
144
- - **`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с.
173
+ - **`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с.
145
174
  - **`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'у не вистачає.
146
175
 
147
176
  ## [1.19.0] - 2026-05-25
148
177
 
149
178
  ### Added
150
179
 
151
- - **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/`.
180
+ - **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/`.
152
181
  - `npm/bin/n-cursor.js`: константа `PI_SKILLS_DIR='.pi/skills'`, функція `formatPiSkillFrontmatter(name, desc)`, синки `syncPiSkills`/`syncLocalOnlyPiSkills` + cleanups `removeOrphanManagedPiSkillDirs`/`removeOrphanLocalPiSkillDirs`. Новий `runSyncStep('❌ Pi skills: ', …)` після Commands-блоку у головному потоці.
153
182
 
154
183
  ## [1.18.3] - 2026-05-25
@@ -167,10 +196,10 @@
167
196
 
168
197
  ### Fixed
169
198
 
170
- - **`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-звіту.
171
- - **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).
199
+ - **`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-звіту.
200
+ - **Fix:** функція тепер приймає `metaUrl` параметром: `isRunAsCli(import.meta.url)`. Реалізація через порівняння канонічних шляхівце знімає різницю «symlink vs canonical» (macOS `/tmp` ↔ `/private/tmp`, pnpm content-addressable links, `node_modules/.bin/*` shim).
172
201
  - **Консолідація:** `run-rule-cli.mjs::isRunAsCli` тепер `export { isRunAsCli } from '../cli-entry.mjs'` — одне джерело правди. Existing import paths у callers лишилися без змін.
173
- - **Callsites:** всі ~40 викликів `isRunAsCli()` оновлено на `isRunAsCli(import.meta.url)`.
202
+ - **Call sites:** всі ~40 викликів `isRunAsCli()` оновлено на `isRunAsCli(import.meta.url)`.
174
203
  - **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`.
175
204
 
176
205
  ## [1.18.0] - 2026-05-25
@@ -197,17 +226,17 @@
197
226
  ### Changed
198
227
 
199
228
  - Концерн `stryker_config`: gitignore-патерн `**/reports/stryker/.tmp/` + `**/reports/stryker/mutation.json` замінено на один broader `**/reports/stryker/` — увесь каталог Stryker-output-у. Покриває не лише `.tmp/` + `mutation.json`, а й HTML/dashboard-репорти якщо користувач додасть інші reporter-и. Існуючі дрібніші патерни в `.gitignore` користувача не видаляються (idempotent helper лише дописує), але стають надлишковими — користувач може почистити вручну за бажанням.
200
- - `test.mdc` 2.1 → 2.2: оновлено опис gitignore-керування під новий broader patern.
229
+ - `test.mdc` 2.1 → 2.2: оновлено опис gitignore-керування під новий broader pattern.
201
230
 
202
231
  ## [1.17.3] - 2026-05-24
203
232
 
204
233
  ### Added
205
234
 
206
- - Концерн `stryker_config` правила `test` тепер ідемпотентно додає у кореневий `.gitignore` патерни Stryker-output-у:
235
+ - Концерн `stryker_config` правила `test` тепер без дублювання додає у кореневий `.gitignore` патерни Stryker-output-у:
207
236
  - `**/reports/stryker/.tmp/` — in-place backup-каталог (з baseline-у `tempDirName`).
208
237
  - `**/reports/stryker/mutation.json` — JSON-репорт мутацій.
209
238
  - Header-секція `# Stryker mutation testing (test.mdc)`, sectioning через `ensureGitignoreEntries`.
210
- - Спільний helper `npm/scripts/utils/ensure-gitignore-entries.mjs` — append-only оновлювач `.gitignore` з header-секціями. Idempotent (точне співпадіння рядка після `trim`), створює файл якщо немає, зберігає trailing-newline. 5 unit-тестів.
239
+ - Спільний helper `npm/scripts/utils/ensure-gitignore-entries.mjs` — append-only модуль оновлення `.gitignore` з header-секціями. Idempotent (точне співпадіння рядка після `trim`), створює файл якщо немає, зберігає trailing-newline. 5 unit-тестів.
211
240
 
212
241
  ### Changed
213
242
 
@@ -220,8 +249,8 @@
220
249
 
221
250
  - Правило `test`: два нових концерни — `stryker_config` і `cargo_mutants_config`. Self-gating через `.n-cursor.json#rules`: концерн активний лише якщо відповідне залежне правило (`js-lint` / `rust`) enabled. **Iterate-all-workspaces**: при відсутності цільового файлу копіює canonical baseline у КОЖЕН workspace-каталог (не лише workspaces[0]).
222
251
  - `stryker.config.mjs` у кожному JS-root (всі workspaces з package.json, або cwd у single-package) — мінімум для роботи з `bun test`.
223
- - `.cargo/mutants.toml` у каталозі КОЖНОГО Cargo.toml-маніфесту: корінь + workspaces (з підтримкою Tauri-патерну `<ws>/src-tauri/Cargo.toml`) — комент-плейсхолдер; cargo-mutants має робочі defaults.
224
- - Спільні резолвери у `npm/scripts/utils/`: `resolveJsRoot` (single, для coverage-провайдера) + `resolveAllJsRoots` (plural, для test-концерну); `resolveCargoManifest` (single) + `resolveAllCargoManifests` (plural). Coverage-провайдери js-lint і rust реюзають single-варіанти.
252
+ - `.cargo/mutants.toml` у каталозі КОЖНОГО Cargo.toml-маніфесту: корінь + workspaces (з підтримкою Tauri-патерну `<ws>/src-tauri/Cargo.toml`) — коментар-плейсхолдер; cargo-mutants має робочі defaults.
253
+ - Спільні модулі визначення у `npm/scripts/utils/`: `resolveJsRoot` (single, для coverage-провайдера) + `resolveAllJsRoots` (plural, для test-концерну); `resolveCargoManifest` (single) + `resolveAllCargoManifests` (plural). Coverage-провайдери js-lint і rust повторно використовують single-варіанти.
225
254
 
226
255
  ### Changed
227
256
 
@@ -524,7 +553,7 @@
524
553
 
525
554
  ### Fixed
526
555
 
527
- - 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).
556
+ - 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).
528
557
 
529
558
  ## [1.13.67] - 2026-05-21
530
559
 
@@ -638,7 +667,7 @@
638
667
 
639
668
  ### Fixed
640
669
 
641
- - `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`.
670
+ - `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`.
642
671
 
643
672
  ## [1.13.50] - 2026-05-19
644
673
 
@@ -674,7 +703,7 @@
674
703
 
675
704
  ### Fixed
676
705
 
677
- - `inlineTemplateLinks` tests: оновлено очікувані рядки для фікстури `__fixtures__/inline-template/fix/foo/template/snippet.json` (перейшла на форматований варіант `{ "key": "val" }` ще в 1.13.38) та для інтеграційного тесту `security.mdc` (snippet `package.json` тепер multi-line після lint-проходу). Без зміни рантайм-логіки.
706
+ - `inlineTemplateLinks` tests: оновлено очікувані рядки для фікстури `__fixtures__/inline-template/fix/foo/template/snippet.json` (перейшла на форматований варіант `{ "key": "val" }` ще в 1.13.38) та для інтеграційними testing тесту `security.mdc` (snippet `package.json` тепер multi-line після lint-проходу). Без зміни рантайм-логіки.
678
707
  - `check-ga` тестова фікстура `setupCanonicalGaProject`: додано крок `Install conftest` у `.github/workflows/lint-ga.yml`, без якого `ga.lint_ga` rego-полісі забороняє workflow і `check()` повертав 1.
679
708
 
680
709
  ## [1.13.44] - 2026-05-18
@@ -1517,7 +1546,7 @@
1517
1546
 
1518
1547
  - **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.
1519
1548
  - **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` разом з виклик-сайтом.
1520
- - **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`).
1549
+ - **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`).
1521
1550
  - **npm/package.json — приведено до правила:** видалено секцію `devDependencies` (`@nitra/cursor` вже є у корені як `workspace:*`). `policy/**/*_test.rego` свідомо лишаються у tarball — як виняток для conftest-конвенції.
1522
1551
  - **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`.
1523
1552
 
@@ -1619,7 +1648,7 @@
1619
1648
 
1620
1649
  ### Added
1621
1650
 
1622
- - **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 для одиничного маніфеста.
1651
+ - **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.
1623
1652
  - **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` фікстури.
1624
1653
  - **lint-rego:** додано опційний крок `conftest verify` у `npm/scripts/lint-rego.mjs` після `regal lint` для виконання `*_test.rego`. Якщо `conftest` не у PATH — крок мовчки пропускається з install-hint.
1625
1654
 
@@ -2187,7 +2216,7 @@
2187
2216
 
2188
2217
  ### Added
2189
2218
 
2190
- - `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` і відповідні юніт-тести.
2219
+ - `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` і відповідні юніт-тести.
2191
2220
 
2192
2221
  ## [1.8.174] - 2026-05-05
2193
2222
 
@@ -2225,7 +2254,7 @@
2225
2254
  ### Added
2226
2255
 
2227
2256
  - `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 як готові артефакти для віддачі браузеру.
2228
- - `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/` доповідався б двічі).
2257
+ - `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/` доповідався б двічі).
2229
2258
  - `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 теж не додається).
2230
2259
  - `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` працює для багаторядкових об'єктів.
2231
2260
 
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.3",
3
+ "version": "1.27.6",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
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)
@@ -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)