@nitra/cursor 1.8.180 → 1.8.185

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,49 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.8.185] - 2026-05-06
8
+
9
+ ### Changed
10
+
11
+ - `image` (mdc v1.4 → v1.5): прапорець `--avif` у `lint-image` тепер **заборонений** (інакше `bun run lint` плодив би `.avif` для зображень, що ніде не вживаються); канонічний `lint-image` — `npx @nitra/minify-image --src=. --write`. AVIF-генерацію виконує **виключно** `npx @nitra/cursor check image`. Секцію «AVIF-імпорти у `.vue`» переписано: тепер вона документує триетапну логіку `check image` — (1) запуск `npx @nitra/minify-image --src=. --write --avif`, (2) авто-заміна raster-посилань у `.vue`/`.html` на `.avif` у кожному workspace-пакеті, (3) прибирання AVIF-сиріт (файли `.avif` без жодного посилання у `.vue`/`.html` видаляються — AVIF лишається лише там, де заміна реально вдалася).
12
+ - `check-image.mjs`: `checkLintImageScript` більше не вимагає `--avif`, натомість фейлить за його наявністю; додано `runAvifGeneration` (best-effort `npx ... --avif`, опт-аут через `NITRA_CURSOR_NO_AVIF_RUN=1` для тестів), `cleanupOrphanAvifs` (видаляє `<...>.avif` без живого посилання), `hasAnyRasterImage`, `resolveImagePath`. `checkVueAvifImportsInPackage` тепер не лише валідує, а й переписує raster-посилання на `.avif` (коли AVIF-двійник реально існує на диску); якщо `.avif` нема — фейл, як раніше. Сканування поширено на `.html` файли (раніше було тільки `.vue`).
13
+ - `tests/check-image.test.mjs`: `CANONICAL_LINT_IMAGE` без `--avif`; кейс «без `--avif`» перейменовано/перекинуто на «з забороненим `--avif`»; додано тести на orphan-cleanup (`.avif` без посилань видаляється) та авто-заміну raster-імпорту, коли `.avif`-сусід реально існує.
14
+
15
+ ## [1.8.184] - 2026-05-06
16
+
17
+ ### Added
18
+
19
+ - `check-js-run.mjs`: програмна перевірка нового правила «depcheck у GitHub Actions з path-фільтром». Для кожного backend workspace-пакета сканується `.github/workflows/*.yml`; якщо `on.push.paths` або `on.pull_request.paths` містить glob, що починається з `<rootDir>/`, у job очікується крок `npx depcheck` з `working-directory: <rootDir>` і `--ignores`, що містить мінімум `graphql,bun` (інші значення допустимі). Логіка — у новому `scripts/utils/depcheck-workflow.mjs` (парсинг `--ignores="…"` з підтримкою single/double-quote і unquoted формату; класифікація `missing` / `wrong-cwd` / `missing-ignores`).
20
+ - `check-js-run-fixture.test.mjs`: 9 нових кейсів — нема `.github/workflows/`, глобальні paths без скоупу пакета, scoped-paths без depcheck (fail), depcheck з неправильним `working-directory` (fail), без `--ignores` (fail), `--ignores` без `bun` (fail), валідний з extra-ignores (pass), вкладений `cron-jobs/foo/src/**` як scope (pass).
21
+ - `.github/workflows/npm-publish.yml`: додано власний крок `npx depcheck --ignores="graphql,bun,bun:test,@nitra/cursor"` з `working-directory: npm`, щоб репо `@nitra/cursor` саме відповідало новому правилу js-run (`paths: ['npm/**']` обмежено пакетом `npm`); extra-ignores потрібні для self-reference `@nitra/cursor` у devDependencies та для `bun:test` як bun-built-in.
22
+
23
+ ## [1.8.183] - 2026-05-06
24
+
25
+ ### Changed
26
+
27
+ - `ga` (mdc v1.6 → v1.7): додано **універсальну** вимогу — кожен workflow у `.github/workflows/*.yml` обов'язково містить блок `concurrency` з `group: ${{ github.ref }}-${{ github.workflow }}` і `cancel-in-progress: true`. Без винятків — scheduled cleanup-воркфлоу, `pull_request: types: [closed]`, publish-воркфлоу теж. Канонічні приклади у правилі (`clean-ga-workflows.yml`, `clean-merged-branch.yml`, `git-ai.yml`) оновлено й тепер містять цей блок.
28
+ - `check-ga.mjs`: нова перевірка `verifyConcurrencyBlock` — запускається на кожному `*.yml` у `.github/workflows/` і структурно перевіряє рівно два поля (`concurrency.group` дорівнює канонічному рядку, `concurrency.cancel-in-progress === true`); відсутність блоку, інший `group` або `cancel-in-progress: false` — fail. Спільний `validateConcurrencyOnRoot` додано в усі канонічні структурні валідатори (clean-ga-workflows, clean-merged-branch, lint-ga, git-ai), щоб ці workflow перевірялися й через шаблонну, і через універсальну логіку.
29
+
30
+ ## [1.8.182] - 2026-05-06
31
+
32
+ ### Changed
33
+
34
+ - `js-run` (mdc v1.2 → v1.3): додано секцію **«depcheck у GitHub Actions з path-фільтром»** — якщо в `.github/workflows/*.yml` тригер `paths:` обмежено каталогом одного backend-пакета (наприклад `cron-jobs/refund-loyalty-points/**`), у job має бути крок `npx depcheck --ignores="graphql,bun"` з `working-directory`, що вказує на той самий каталог. Список `--ignores` обов'язково містить мінімум `graphql,bun` (peer-залежність GraphQL та рантайм Bun, які depcheck не розпізнає коректно), але може бути розширений значеннями через кому без пробілів. Не застосовується до глобальних workflow без `paths:` або з кореневими `**/*.js` патернами.
35
+
36
+ ## [1.8.181] - 2026-05-06
37
+
38
+ ### Changed
39
+
40
+ - `scripts/utils/find-package-json-paths.mjs`: винесено спільну `findAllPackageJsonPaths(repoRoot, ignorePaths)` з `check-js-bun-db.mjs` і `check-js-mssql.mjs`, щоб усунути jscpd-дублювання. Самі check-скрипти тепер імпортують її, як інші утиліти з `utils/`.
41
+ - `scripts/utils/walkDir.mjs` / `scripts/utils/load-cursor-config.mjs`: trimming trailing-slash переписано з регулярки `/\\/+$/` на `while (s.endsWith('/'))`, щоб уникнути попередження `sonarjs/slow-regex` (потенційний backtracking) — поведінка не змінилась.
42
+ - `scripts/check-k8s.mjs`: `failIfExplicitPatchTargetsHaveRedundantGroupVersion` рефакторено — логіка одного запису винесена в новий хелпер `describePatchTargetRedundancy`, основна функція тепер просто будує повідомлення з результату (зменшено sonarjs/cognitive-complexity 24→<15, поведінка не змінилась).
43
+ - `scripts/claude-stop-hook.mjs`: `readStdin` і `runStopHookCli` переписані з `new Promise(resolve => …)` на `events.once(stream, 'end' | 'exit')` — підказка `eslint-plugin-promise/avoid-new`, поведінка не змінилась.
44
+
45
+ ### Fixed
46
+
47
+ - `scripts/utils/bun-sql-scan.mjs`: у JSDoc `findBunSqlUnsafeUseWithoutAllowMarkerInText` прибрано вкладений приклад із backslash-backtick (`sql\\`...\\${value}...\\``), який ламав парсер коментарів oxlint і призводив до false-positive `eslint-plugin-jsdoc(require-param)`/`(require-returns)` на функції з валідним JSDoc — текст переписано без екранованих backtick-ів.
48
+ - Десятки `eslint-plugin-jsdoc` правил у `npm/scripts/**` та `npm/tests/**`: додано відсутні описи `@param` / `@returns` (включно зі спільним `ignorePaths`-аргументом у нових сигнатурах walkDir-обгорток), прибрано неприпустимі дефолтні значення в JSDoc (`[name=...]`) — без зміни поведінки.
49
+
7
50
  ## [1.8.180] - 2026-05-05
8
51
 
9
52
  ### Changed
@@ -55,9 +98,6 @@
55
98
  ### Added
56
99
 
57
100
  - Нове правило `changelog` (`mdc/changelog.mdc` + `scripts/check-changelog.mjs`): для «звичайних» Bun-монорепо проєктів вимагає, щоб у кожному workspace, який змінився відносно базової гілки `dev`, у поточному PR було підвищено `version` у `<ws>/package.json` і додано запис `## [version] - YYYY-MM-DD` у `<ws>/CHANGELOG.md` (Keep a Changelog 1.1.0). Перевірка PR-scoped: на самій гілці `dev` пропускається; на feature-гілці bump і запис достатньо зробити **один раз — як суму по всьому PR**, без бамп-шуму в проміжних комітах. Воркспейс `npm/` пропускається — його CHANGELOG покриває окреме правило `npm-module`. У `auto-rules.md` / `auto-rules.mjs` `changelog` додано до автодетекту з умовою «у корені є `package.json`» і до `AUTO_RULE_ORDER` між `capacitor` і `docker`.
58
-
59
- ### Added
60
-
61
101
  - `.n-cursor.json` поле `ignore` (`schemas/n-cursor.json`): тепер не лише сигнал для AI, а й керує обходом усіх `check-*.mjs` / `run-*.mjs` — перелічені каталоги повністю виключаються з `walkDir`, як `node_modules` чи `.git`. Дозволяє безпечно тримати vendored Helm-чарти, генеровані маніфести, legacy-дерева у репо без false-positive’ів від check-скриптів. Розширено опис у схемі (стандартні виключення додавати не треба) і README отримав секцію «Виключення цілих дерев».
62
102
  - `scripts/utils/load-cursor-config.mjs`: нова утиліта `loadCursorIgnorePaths(root)` — читає поле `ignore` з `.n-cursor.json` і нормалізує до абсолютних posix-шляхів без trailing-slash; пропускає не-рядки та порожні елементи; повертає `[]`, якщо файлу/поля нема або JSON невалідний.
63
103
  - `scripts/utils/walkDir.mjs`: третій аргумент `ignorePaths` (за замовчуванням `[]`) — каталоги, які пропускаються разом з усім вмістом. Збіг — за повним шляхом (точний або з префіксом `/`), а не за basename, тож `postgres-master-test/` не пропускається коли в ignore лише `postgres-master/`. Стандартні пропуски (`node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`) працюють як раніше.
@@ -154,8 +194,8 @@
154
194
 
155
195
  ### Changed
156
196
 
157
- - `js-bun-db.mdc` (v1.4): `sql.unsafe(...)` тепер заборонено за замовчуванням — допустимо лише для підстановки назви таблиці/колонки чи dynamic SQL/DDL з code-controlled значенням; інакше переробляємо на tagged template `sql\`...${value}...\``. Кожен легітимний виклик має супроводжуватись маркером `// allow-unsafe: <причина>` на тому ж рядку або рядком вище.
158
- - `check-js-bun-db.mjs`: замість вузької перевірки `sql.unsafe(\`...${expr}...\`)` тепер сканер `findBunSqlUnsafeUseWithoutAllowMarkerInText` падає на будь-якому `<obj>.unsafe(...)` без маркера-коментаря з непорожньою причиною (line- або block-коментар на тому ж рядку чи безпосередньо перед викликом).
197
+ - `js-bun-db.mdc` (v1.4): `sql.unsafe(...)` тепер заборонено за замовчуванням — допустимо лише для підстановки назви таблиці/колонки чи dynamic SQL/DDL з code-controlled значенням; інакше переробляємо на tagged template `sql\`...${value}...\``. Кожен легітимний виклик має супроводжуватись маркером`// allow-unsafe: <причина>` на тому ж рядку або рядком вище.
198
+ - `check-js-bun-db.mjs`: замість вузької перевірки `sql.unsafe` із tagged-template і інтерполяцією тепер сканер `findBunSqlUnsafeUseWithoutAllowMarkerInText` падає на будь-якому `obj.unsafe(...)` без маркера-коментаря з непорожньою причиною (line- або block-коментар на тому ж рядку чи безпосередньо перед викликом).
159
199
  - `ast-scan-utils.mjs`: додано `parseProgramAndCommentsOrNull` — окремий вхід для перевірок, яким потрібні коментарі поряд з AST.
160
200
 
161
201
  ## [1.8.159] - 2026-05-01
package/mdc/ga.mdc CHANGED
@@ -1,11 +1,21 @@
1
1
  ---
2
2
  description: Правила форматів для .github/workflows
3
3
  alwaysApply: true
4
- version: '1.6'
4
+ version: '1.7'
5
5
  ---
6
6
 
7
7
  У `.github/workflows/` лише **`.yml`**. Мають бути **`clean-ga-workflows.yml`**, **`clean-merged-branch.yml`**, **`lint-ga.yml`**, **`git-ai.yml`**. Якщо є **`apply-k8s.yml`** / **`apply-nats-consumer.yml`** — paths у тригері як у фрагментах.
8
8
 
9
+ **Кожен** workflow у `.github/workflows/*.yml` **обов'язково** містить блок `concurrency` з фіксованим `group` та `cancel-in-progress: true`:
10
+
11
+ ```yaml
12
+ concurrency:
13
+ group: ${{ github.ref }}-${{ github.workflow }}
14
+ cancel-in-progress: true
15
+ ```
16
+
17
+ Без винятків — у scheduled cleanup-воркфлоу, у `pull_request: types: [closed]`, у publish-воркфлоу теж. Це уникає паралельних запусків того самого workflow на тій самій ref і скасовує попередні в чергу нових.
18
+
9
19
  Повинен бути файл .github/workflows/clean-ga-workflows.yml, зі змістом:
10
20
 
11
21
  ```yaml
@@ -18,6 +28,10 @@ on:
18
28
  # Allow workflow to be manually run from the GitHub UI
19
29
  workflow_dispatch: {}
20
30
 
31
+ concurrency:
32
+ group: ${{ github.ref }}-${{ github.workflow }}
33
+ cancel-in-progress: true
34
+
21
35
  jobs:
22
36
  cleanup_old_workflows:
23
37
  runs-on: ubuntu-latest
@@ -47,6 +61,10 @@ on:
47
61
  # Allow workflow to be manually run from the GitHub UI
48
62
  workflow_dispatch: {}
49
63
 
64
+ concurrency:
65
+ group: ${{ github.ref }}-${{ github.workflow }}
66
+ cancel-in-progress: true
67
+
50
68
  jobs:
51
69
  cleanup_old_branches:
52
70
  runs-on: ubuntu-latest
@@ -119,6 +137,10 @@ on:
119
137
  pull_request:
120
138
  types: [closed]
121
139
 
140
+ concurrency:
141
+ group: ${{ github.ref }}-${{ github.workflow }}
142
+ cancel-in-progress: true
143
+
122
144
  jobs:
123
145
  git-ai:
124
146
  if: github.event.pull_request.merged == true
package/mdc/image.mdc CHANGED
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  description: Оптимізація зображень через @nitra/minify-image у локальному lint
3
3
  alwaysApply: true
4
- version: '1.4'
4
+ version: '1.5'
5
5
  ---
6
6
 
7
- CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (≥ **3.2.0**) запускається через `npx` (як `markdownlint-cli2` у text.mdc) і **не** додається в `dependencies` / `devDependencies`. Канонічний `lint-image` — авто-оптимізація з прапорцями `--write --avif`: стискає raster/SVG на місці й створює AVIF-двійники (`<name>.<ext>.avif`) поряд з кожним PNG/JPEG/GIF. Split-cache робить повторні прогони дешевими — і локально, і після `git clone`.
7
+ CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (≥ **3.2.0**) запускається через `npx` (як `markdownlint-cli2` у text.mdc) і **не** додається в `dependencies` / `devDependencies`. Канонічний `lint-image` — авто-оптимізація з прапорцем `--write`: стискає raster/SVG на місці. **AVIF-генерація (`--avif`) у `lint-image` заборонена** — її виконує лише `npx @nitra/cursor check image`, який заодно прибирає AVIF-сироти (див. секцію «AVIF-імпорти у `.vue`»). Split-cache робить повторні прогони дешевими — і локально, і після `git clone`.
8
8
 
9
9
  Перевірка лише локальна — у CI `lint-image` не запускаємо (sharp/svgo тягнуть бінарні залежності, цінність на ubuntu-runner-ах нижча за час прогону). Окремий workflow `lint-image.yml` створювати не треба.
10
10
 
@@ -14,12 +14,12 @@ CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (
14
14
  {
15
15
  "scripts": {
16
16
  "lint": "bun run lint-js && bun run lint-text && bun run lint-ga && bun run lint-image && oxfmt .",
17
- "lint-image": "npx @nitra/minify-image --src=. --write --avif"
17
+ "lint-image": "npx @nitra/minify-image --src=. --write"
18
18
  }
19
19
  }
20
20
  ```
21
21
 
22
- Якщо в `package.json` уже є агрегований `lint`, додай у його ланцюжок `bun run lint-image` (як `bun run lint-text`, `bun run lint-js`, `bun run lint-ga`). Так розробник, що локально гонить `bun run lint`, перед фіксацією одразу бачить, чи зросли зображення, і отримує свіжі AVIF-двійники.
22
+ Якщо в `package.json` уже є агрегований `lint`, додай у його ланцюжок `bun run lint-image` (як `bun run lint-text`, `bun run lint-js`, `bun run lint-ga`). Так розробник, що локально гонить `bun run lint`, перед фіксацією одразу бачить, чи зросли зображення.
23
23
 
24
24
  ## Split-cache
25
25
 
@@ -47,13 +47,19 @@ rm -f .minify-image-cache.tsv
47
47
  # прибери відповідний рядок з .gitignore, якщо був
48
48
  ```
49
49
 
50
- AVIF-двійники (`<name>.<ext>.avif`) **зберігаємо в git** — це готові артефакти для віддачі браузеру (без них ефект від `--avif` втрачається на чистому checkout-і).
50
+ AVIF-двійники (`<name>.<ext>.avif`) **зберігаємо в git** — це готові артефакти для віддачі браузеру (без них ефект від AVIF втрачається на чистому checkout-і).
51
51
 
52
52
  ## AVIF-імпорти у `.vue`
53
53
 
54
- Раз `--avif` гарантує `<name>.<ext>.avif` поряд з кожним PNG/JPEG/GIF, у `.vue` файлах потрібно посилатись саме на AVIF-двійник, а не на оригінал:
54
+ AVIF-двійники (`<name>.<ext>.avif`) генерує **виключно** `npx @nitra/cursor check image` — у `lint-image` прапорець `--avif` заборонений (інакше `bun run lint` плодить непотрібні `.avif` для зображень, що ніде не використовуються). Перевірка робить три кроки в порядку:
55
55
 
56
- ```vue title="App.vue (правильно)"
56
+ 1. Запускає `npx @nitra/minify-image --src=. --write --avif` — генерує `<name>.<ext>.avif` поряд з кожним PNG/JPEG/GIF.
57
+ 2. Сканує `.vue` (а також `.html`) файли в кожному workspace-пакеті (root + workspaces) і автоматично переписує raster-посилання на AVIF-двійник у двох формах:
58
+ - **Імпорт-пов'язані** — `import x from '...png|jpg|jpeg|gif'` (далі `:src="x"` у шаблоні);
59
+ - **Прямі статичні** — `<img src="...png" />` у `<template>` (Vite перетворює такий шлях на asset-імпорт на етапі збірки, тож вимога та сама).
60
+ 3. Видаляє AVIF-сироти: ходить по всіх `<...>.avif` у репозиторії; якщо на двійник не лишилось жодного посилання у `.vue`/`.html` — файл видаляється. **AVIF на диску лишається лише там, де заміна реально відбулась** — тому невикористані оригінали не накопичують `.avif`-«хвости».
61
+
62
+ ```vue title="App.vue (після check image)"
57
63
  <script setup>
58
64
  import welcomeImage from './assets/welcome.png.avif'
59
65
  </script>
@@ -63,19 +69,10 @@ import welcomeImage from './assets/welcome.png.avif'
63
69
  </template>
64
70
  ```
65
71
 
66
- ```vue title="App.vue (неправильно — втрачає AVIF)"
67
- <script setup>
68
- import welcomeImage from './assets/welcome.png'
69
- </script>
70
- ```
71
-
72
- Перевірка `check image` сканує `.vue` файли в кожному workspace-пакеті (root + workspaces) і вимагає AVIF-двійник для двох форм:
73
-
74
- 1. **Імпорт-пов'язані** — `import x from '...png|jpg|jpeg|gif'` (далі `:src="x"` у шаблоні).
75
- 2. **Прямі статичні** — `<img src="...png" />` у `<template>` (Vite перетворює такий шлях на asset-імпорт на етапі збірки, тож вимога та сама).
76
-
77
72
  Реактивне `:src="..."` (з JS-виразом — змінною, тернарником, викликом тощо) **не сканується** — значення обчислюється у рантаймі й шлях туди потрапляє через імпорт або інший резолвинг, який ловить імпорт-перевірка вище. SVG не торкаємо (vector → AVIF безглуздо). Атрибути `data-src=`, `obj.src=` у `<script>` тощо також пропускаються.
78
73
 
74
+ Якщо raster-посилання у `.vue`/`.html` не вдалось переписати (наприклад, оригіналу немає на диску, тож і `.avif` не згенерувався) — `check image` падає з помилкою на конкретний файл, як раніше.
75
+
79
76
  ### Опт-аут для конкретного пакета
80
77
 
81
78
  У workspace-пакеті, де AVIF-імпорти небажані (наприклад, мобільний бандл, де AVIF-підтримка не гарантована), додай у `package.json` цього пакета:
@@ -92,8 +89,8 @@ import welcomeImage from './assets/welcome.png'
92
89
 
93
90
  ## Заборонені залежності
94
91
 
95
- `@nitra/minify-image` не повинен зʼявлятися ні в `dependencies`, ні в `devDependencies` кореневого `package.json` — CLI запускається лише через `npx` (так само, як `markdownlint-cli2` у text.mdc). Якщо потрібен явний пін — закладай діапазон версій npm у самому виклику (`npx @nitra/minify-image@^3 --src=. --write --avif`).
92
+ `@nitra/minify-image` не повинен зʼявлятися ні в `dependencies`, ні в `devDependencies` кореневого `package.json` — CLI запускається лише через `npx` (так само, як `markdownlint-cli2` у text.mdc). Якщо потрібен явний пін — закладай діапазон версій npm у самому виклику (`npx @nitra/minify-image@^3 --src=. --write`).
96
93
 
97
94
  ## Перевірка
98
95
 
99
- `npx @nitra/cursor check image` (охоплює `lint-image` з обовʼязковими `--src=.`, `--write`, `--avif`, агрегований `lint`, `.n-minify-image.tsv` НЕ в `.gitignore` (має бути в git), відсутність застарілого `.minify-image-cache.tsv` у корені, AVIF-імпорти у `.vue` файлах кожного workspace-пакета).
96
+ `npx @nitra/cursor check image` (охоплює `lint-image` з обовʼязковими `--src=.`, `--write` і **забороненим** `--avif`; агрегований `lint`; `.n-minify-image.tsv` НЕ в `.gitignore` має бути в git; відсутність застарілого `.minify-image-cache.tsv` у корені; запуск AVIF-генерації + авто-заміна raster-посилань на `.avif` у `.vue`/`.html` кожного workspace-пакета + прибирання AVIF-сиріт).
package/mdc/js-run.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
3
3
  alwaysApply: true
4
- version: '1.2'
4
+ version: '1.3'
5
5
  ---
6
6
 
7
7
  ## Область застосування
@@ -136,6 +136,26 @@ console.log(env.OPTIONAL_ENV_VAR)
136
136
  `// @nitra/cursor ignore-next-line checkEnv` безпосередньо перед використанням
137
137
  (escape-hatch для legacy-коду, не для нових файлів).
138
138
 
139
+ ## depcheck у GitHub Actions з path-фільтром
140
+
141
+ Якщо в `.github/workflows/*.yml` є тригер з `paths:`, який обмежує запуск workflow змінами в каталозі конкретного backend-пакета, в job цього workflow має бути крок `npx depcheck` з `working-directory`, який вказує на той самий каталог пакета. Це гарантує, що декларація залежностей у `package.json` пакета відповідає реальним імпортам — інакше можна випадково зламати білд після видалення «зайвої» залежності, яка насправді використовується через побічний імпорт.
142
+
143
+ Список `--ignores` **обов'язково** містить як мінімум `graphql,bun` (це ті, які `depcheck` не вміє коректно розпізнавати: `graphql` — peer-залежність, що часто використовується без прямого імпорту в коді; `bun` — рантайм, не npm-пакет). За потреби список можна розширити іншими модулями, специфічними для пакета — список значень розділяється комою без пробілів.
144
+
145
+ ```yaml title="Приклад: workflow для cron-jobs/refund-loyalty-points"
146
+ on:
147
+ push:
148
+ paths:
149
+ - 'cron-jobs/refund-loyalty-points/**'
150
+
151
+ # …
152
+
153
+ - run: npx depcheck --ignores="graphql,bun"
154
+ working-directory: cron-jobs/refund-loyalty-points
155
+ ```
156
+
157
+ Правило не застосовується до workflow без `paths:` або з `paths:`, який не звужує тригер до одного backend-пакета (наприклад, кореневі `lint-*.yml` з глобальними `**/*.js`).
158
+
139
159
  ## Перевірка
140
160
 
141
161
  `npx @nitra/cursor check js-run`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.180",
3
+ "version": "1.8.185",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -44,11 +44,11 @@
44
44
  "oxc-parser": "^0.128.0",
45
45
  "yaml": "^2.8.3"
46
46
  },
47
+ "devDependencies": {
48
+ "@nitra/cursor": "^1.8.170"
49
+ },
47
50
  "engines": {
48
51
  "bun": ">=1.3",
49
52
  "node": ">=25"
50
- },
51
- "devDependencies": {
52
- "@nitra/cursor": "^1.8.170"
53
53
  }
54
54
  }
@@ -384,6 +384,7 @@ export function ignoreBranchesIncludesRequired(ignoreBranches, required) {
384
384
  /**
385
385
  * Збирає абсолютні шляхи до **.yaml** / **.yml** під деревом, де є сегмент **k8s**.
386
386
  * @param {string} root корінь репозиторію
387
+ * @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
387
388
  * @returns {Promise<string[]>} відсортовані шляхи
388
389
  */
389
390
  async function findK8sYamlFiles(root, ignorePaths = []) {
@@ -41,7 +41,7 @@ const NPM_VIEW_TIMEOUT_MS = 10_000
41
41
  /**
42
42
  * Тихо запускає `git` і повертає stdout або `null` при будь-якій помилці.
43
43
  * @param {string[]} args аргументи `git`
44
- * @returns {Promise<string | null>}
44
+ * @returns {Promise<string | null>} stdout процесу або `null` при будь-якій помилці виконання
45
45
  */
46
46
  async function gitOrNull(args) {
47
47
  try {
@@ -54,7 +54,7 @@ async function gitOrNull(args) {
54
54
 
55
55
  /**
56
56
  * Чи робочий каталог — git-репозиторій.
57
- * @returns {Promise<boolean>}
57
+ * @returns {Promise<boolean>} `true`, якщо `git rev-parse --is-inside-work-tree` повернув `true`
58
58
  */
59
59
  async function isInsideGitRepo() {
60
60
  const out = await gitOrNull(['rev-parse', '--is-inside-work-tree'])
@@ -63,7 +63,7 @@ async function isInsideGitRepo() {
63
63
 
64
64
  /**
65
65
  * Назва поточної гілки (або `HEAD` для detached state).
66
- * @returns {Promise<string | null>}
66
+ * @returns {Promise<string | null>} назва гілки чи `'HEAD'`, або `null` (поза git / помилка)
67
67
  */
68
68
  async function currentBranchName() {
69
69
  const out = await gitOrNull(['rev-parse', '--abbrev-ref', 'HEAD'])
@@ -73,7 +73,7 @@ async function currentBranchName() {
73
73
  /**
74
74
  * Знаходить ref для базової гілки. Перевага локальному `dev`, далі `origin/dev`. Повертає `null`,
75
75
  * якщо жоден не існує.
76
- * @returns {Promise<string | null>}
76
+ * @returns {Promise<string | null>} назва ref-а (`dev` чи `origin/dev`) або `null`, якщо жоден не знайдено
77
77
  */
78
78
  async function resolveBaseRef() {
79
79
  for (const ref of [BASE_BRANCH, `origin/${BASE_BRANCH}`]) {
@@ -88,8 +88,8 @@ async function resolveBaseRef() {
88
88
  /**
89
89
  * Точка розгалуження поточної гілки від `baseRef`. На feature-гілці = коли вона відгалузилась;
90
90
  * на `main` після merge `dev → main` = поточний `dev`. Повертає `null`, якщо merge-base нема.
91
- * @param {string} baseRef
92
- * @returns {Promise<string | null>}
91
+ * @param {string} baseRef SHA або ref-name бази (зазвичай `dev` / `origin/dev`)
92
+ * @returns {Promise<string | null>} SHA точки розгалуження або `null`, якщо merge-base нема
93
93
  */
94
94
  async function resolveMergeBase(baseRef) {
95
95
  const out = await gitOrNull(['merge-base', baseRef, 'HEAD'])
@@ -104,9 +104,9 @@ async function resolveMergeBase(baseRef) {
104
104
  * Для кореня `.` — це точка плюс magic-виключення кожного підворкспейсу через `:(exclude)<sub>/`,
105
105
  * щоб зміни всередині sub-workspace не вважалися змінами кореня.
106
106
  * Для звичайного воркспейсу — просто `<ws>/`.
107
- * @param {string} ws
108
- * @param {string[]} subWorkspaces
109
- * @returns {string[]}
107
+ * @param {string} ws шлях воркспейсу (`'.'` для кореня, інакше — відносний шлях, як у `workspaces`)
108
+ * @param {string[]} subWorkspaces усі під-воркспейси (зокрема для `'.'` потрібно виключити їх)
109
+ * @returns {string[]} pathspec для git: масив, що передається після `--`
110
110
  */
111
111
  function pathspecForWorkspace(ws, subWorkspaces) {
112
112
  if (ws !== '.') return [`${ws}/`]
@@ -119,9 +119,9 @@ function pathspecForWorkspace(ws, subWorkspaces) {
119
119
  * `git diff --quiet <baseRef> -- <pathspec>` ловить committed-зміни на цій гілці й незбережені
120
120
  * правки tracked-файлів. Untracked-файли — `git ls-files --others --exclude-standard`.
121
121
  * @param {string} baseRef SHA або ref-name (зокрема merge-base)
122
- * @param {string} ws
123
- * @param {string[]} subWorkspaces
124
- * @returns {Promise<boolean>}
122
+ * @param {string} ws шлях воркспейсу (`'.'` для кореня)
123
+ * @param {string[]} subWorkspaces усі під-воркспейси для коректного формування pathspec кореня
124
+ * @returns {Promise<boolean>} `true`, якщо в межах воркспейсу є будь-які зміни (committed або untracked)
125
125
  */
126
126
  async function workspaceHasChangesAgainstBase(baseRef, ws, subWorkspaces) {
127
127
  const pathspec = pathspecForWorkspace(ws, subWorkspaces)
@@ -129,8 +129,7 @@ async function workspaceHasChangesAgainstBase(baseRef, ws, subWorkspaces) {
129
129
  await execFileAsync('git', ['diff', '--quiet', baseRef, '--', ...pathspec])
130
130
  } catch (error) {
131
131
  const code = /** @type {{ code?: number }} */ (error).code
132
- if (code === 1) return true
133
- return false
132
+ return code === 1
134
133
  }
135
134
  const untracked = await gitOrNull(['ls-files', '--others', '--exclude-standard', '--', ...pathspec])
136
135
  return typeof untracked === 'string' && untracked.trim().length > 0
@@ -138,9 +137,9 @@ async function workspaceHasChangesAgainstBase(baseRef, ws, subWorkspaces) {
138
137
 
139
138
  /**
140
139
  * Версія з `<ws>/package.json` на `baseRef` або `null`.
141
- * @param {string} baseRef
142
- * @param {string} ws
143
- * @returns {Promise<string | null>}
140
+ * @param {string} baseRef SHA або ref-name (зазвичай merge-base) для `git show`
141
+ * @param {string} ws шлях воркспейсу (`'.'` для кореня)
142
+ * @returns {Promise<string | null>} значення поля `version` або `null`, якщо файла нема / JSON некоректний
144
143
  */
145
144
  async function readBaseVersion(baseRef, ws) {
146
145
  const wsPath = ws === '.' ? 'package.json' : `${ws}/package.json`
@@ -156,9 +155,9 @@ async function readBaseVersion(baseRef, ws) {
156
155
 
157
156
  /**
158
157
  * Чи містить текст `CHANGELOG.md` запис `## [version]` (з опційним `- YYYY-MM-DD`).
159
- * @param {string} text
160
- * @param {string} version
161
- * @returns {boolean}
158
+ * @param {string} text вміст CHANGELOG.md
159
+ * @param {string} version версія, яку шукаємо у форматі Keep a Changelog
160
+ * @returns {boolean} `true`, якщо запис для `version` знайдено
162
161
  */
163
162
  function changelogHasVersionEntry(text, version) {
164
163
  const escaped = version.replaceAll(/[.+*?^$()[\]{}|\\]/g, String.raw`\$&`)
@@ -168,8 +167,8 @@ function changelogHasVersionEntry(text, version) {
168
167
 
169
168
  /**
170
169
  * Зчитує `<ws>/package.json`. `null`, якщо файл відсутній або JSON некоректний.
171
- * @param {string} ws
172
- * @returns {Promise<Record<string, unknown> | null>}
170
+ * @param {string} ws шлях воркспейсу (`'.'` для кореня)
171
+ * @returns {Promise<Record<string, unknown> | null>} розпарсений `package.json` або `null`
173
172
  */
174
173
  async function readPackageJsonOrNull(ws) {
175
174
  const path = join(ws, 'package.json')
@@ -186,8 +185,8 @@ async function readPackageJsonOrNull(ws) {
186
185
 
187
186
  /**
188
187
  * Воркспейс публікується в npm: має непорожній `name`, не `private: true`, і має масив `files`.
189
- * @param {Record<string, unknown> | null} pkg
190
- * @returns {boolean}
188
+ * @param {Record<string, unknown> | null} pkg розпарсений `package.json` (або `null`)
189
+ * @returns {boolean} `true`, якщо пакет придатний для публікації в npm
191
190
  */
192
191
  function isNpmPublishable(pkg) {
193
192
  if (!pkg) return false
@@ -199,8 +198,8 @@ function isNpmPublishable(pkg) {
199
198
  /**
200
199
  * Опублікована версія пакета в npm-реєстрі. `null` — пакет не знайдено / нема мережі / помилка.
201
200
  * Дефолтна імплементація — `npm view <name> version` із таймаутом, щоб не блокуватись офлайн.
202
- * @param {string} name
203
- * @returns {Promise<string | null>}
201
+ * @param {string} name повна назва пакета (включно зі скоупом)
202
+ * @returns {Promise<string | null>} опублікована версія або `null` (нема пакета / офлайн)
204
203
  */
205
204
  async function defaultGetPublishedVersion(name) {
206
205
  try {
@@ -214,10 +213,10 @@ async function defaultGetPublishedVersion(name) {
214
213
 
215
214
  /**
216
215
  * Перевіряє масив `files` у `<ws>/package.json`: якщо оголошено — має містити `"CHANGELOG.md"`.
217
- * @param {Record<string, unknown> | null} pkg
218
- * @param {string} ws
219
- * @param {(msg: string) => void} pass
220
- * @param {(msg: string) => void} fail
216
+ * @param {Record<string, unknown> | null} pkg розпарсений `package.json` воркспейсу
217
+ * @param {string} ws шлях воркспейсу (`'.'` для кореня)
218
+ * @param {(msg: string) => void} pass callback при успішній перевірці
219
+ * @param {(msg: string) => void} fail callback при помилці
221
220
  */
222
221
  function checkFilesArrayContainsChangelog(pkg, ws, pass, fail) {
223
222
  if (!pkg || !Array.isArray(pkg.files)) return
@@ -231,10 +230,10 @@ function checkFilesArrayContainsChangelog(pkg, ws, pass, fail) {
231
230
 
232
231
  /**
233
232
  * Перевіряє наявність запису у `<ws>/CHANGELOG.md` для версії `version`.
234
- * @param {string} ws
235
- * @param {string} version
236
- * @param {(msg: string) => void} pass
237
- * @param {(msg: string) => void} fail
233
+ * @param {string} ws шлях воркспейсу (`'.'` для кореня)
234
+ * @param {string} version версія, для якої очікується запис
235
+ * @param {(msg: string) => void} pass callback при успішній перевірці
236
+ * @param {(msg: string) => void} fail callback при помилці
238
237
  * @returns {Promise<boolean>} `false`, якщо файл відсутній або немає запису
239
238
  */
240
239
  async function verifyChangelogEntry(ws, version, pass, fail) {
@@ -257,11 +256,11 @@ async function verifyChangelogEntry(ws, version, pass, fail) {
257
256
  * npm-published режим: порівнює локальну `version` з опублікованою в реєстрі. Якщо вони
258
257
  * відрізняються — вимагає запис у CHANGELOG і `"CHANGELOG.md"` у `files`. Якщо реєстр недосяжний,
259
258
  * правило fail-safe пасує (щоб офлайн-розробка не блокувалась).
260
- * @param {string} ws
261
- * @param {Record<string, unknown>} pkg
262
- * @param {(name: string) => Promise<string | null>} getPublishedVersion
263
- * @param {(msg: string) => void} pass
264
- * @param {(msg: string) => void} fail
259
+ * @param {string} ws шлях воркспейсу (`'.'` для кореня)
260
+ * @param {Record<string, unknown>} pkg розпарсений `package.json` воркспейсу
261
+ * @param {(name: string) => Promise<string | null>} getPublishedVersion стаб/реальна функція отримання опублікованої версії
262
+ * @param {(msg: string) => void} pass callback при успішній перевірці
263
+ * @param {(msg: string) => void} fail callback при помилці
265
264
  */
266
265
  async function checkPublishedWorkspace(ws, pkg, getPublishedVersion, pass, fail) {
267
266
  const label = ws === '.' ? '<root>' : ws
@@ -289,10 +288,10 @@ async function checkPublishedWorkspace(ws, pkg, getPublishedVersion, pass, fail)
289
288
  * local-only режим: PR-scoped перевірка проти `dev` через `git merge-base`. Викликається лише
290
289
  * для воркспейсів, де є реальні зміни щодо merge-base.
291
290
  * @param {string} mergeBase SHA точки розгалуження
292
- * @param {string} ws
293
- * @param {Record<string, unknown> | null} pkg
294
- * @param {(msg: string) => void} pass
295
- * @param {(msg: string) => void} fail
291
+ * @param {string} ws шлях воркспейсу (`'.'` для кореня)
292
+ * @param {Record<string, unknown> | null} pkg розпарсений `package.json` воркспейсу (або `null`)
293
+ * @param {(msg: string) => void} pass callback при успішній перевірці
294
+ * @param {(msg: string) => void} fail callback при помилці
296
295
  */
297
296
  async function checkLocalOnlyChangedWorkspace(mergeBase, ws, pkg, pass, fail) {
298
297
  const label = ws === '.' ? '<root>' : ws
@@ -315,12 +314,12 @@ async function checkLocalOnlyChangedWorkspace(mergeBase, ws, pkg, pass, fail) {
315
314
 
316
315
  /**
317
316
  * Виконує local-only перевірку для всіх workspace-ів, у яких немає npm-published режиму.
318
- * @param {string[]} localOnlyWorkspaces
319
- * @param {Map<string, Record<string, unknown> | null>} pkgByWs
320
- * @param {string[]} subWorkspaces
321
- * @param {(msg: string) => void} pass
322
- * @param {(msg: string) => void} fail
323
- * @returns {Promise<void>}
317
+ * @param {string[]} localOnlyWorkspaces список шляхів local-only воркспейсів
318
+ * @param {Map<string, Record<string, unknown> | null>} pkgByWs мапа: шлях воркспейсу → розпарсений `package.json` (або `null`)
319
+ * @param {string[]} subWorkspaces усі під-воркспейси (для коректного pathspec кореня)
320
+ * @param {(msg: string) => void} pass callback при успішній перевірці
321
+ * @param {(msg: string) => void} fail callback при помилці
322
+ * @returns {Promise<void>} резолвиться по завершенню перевірок усіх local-only воркспейсів
324
323
  */
325
324
  async function runLocalOnlyChecks(localOnlyWorkspaces, pkgByWs, subWorkspaces, pass, fail) {
326
325
  if (localOnlyWorkspaces.length === 0) return
@@ -358,7 +357,7 @@ async function runLocalOnlyChecks(localOnlyWorkspaces, pkgByWs, subWorkspaces, p
358
357
 
359
358
  /**
360
359
  * Перевіряє відповідність проєкту правилу changelog.mdc.
361
- * @param {object} [opts]
360
+ * @param {object} [opts] опції перевірки
362
361
  * @param {(name: string) => Promise<string | null>} [opts.getPublishedVersion] перевизначення для тестів
363
362
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
364
363
  */