@nitra/cursor 1.9.6 → 1.9.8

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,26 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.9.8] - 2026-05-12
8
+
9
+ ### Changed
10
+
11
+ - **Корінь монорепо:** у `eslint.config.js` після `...getConfig(...)` додано перевизначення `sonarjs/cognitive-complexity` на `['warn', 20]` (поріг 20, severity `warn`).
12
+
13
+ ## [1.9.7] - 2026-05-12
14
+
15
+ ### Changed
16
+
17
+ - **js-lint (mdc v1.18 → v1.19) — `depcheck` мігровано на `knip`:** канонічний `lint-js` тепер `bunx oxlint --fix && bunx eslint --fix . && bunx jscpd . && bunx knip` (раніше — без `bunx knip`); крок `bunx knip` додано і в приклад workflow `lint-js.yml`. У корені має бути `knip.json` з мінімальним `ignoreDependencies: ["graphql"]` (peer-залежність, яку `knip` не розпізнає як використану). Пакет `knip` окремо в `devDependencies` не оголошуй — `bunx` тягне його ad-hoc. `CANONICAL_LINT_JS` у `npm/scripts/check-js-lint.mjs` і `canonical_lint_js` у `npm/policy/js_lint/package_json/package_json.rego` оновлено; додано `checkKnipConfig` (наявність файла + `ignoreDependencies` містить `graphql`) і `deny`-правило у `npm/policy/js_lint/lint_js_yml/` на відсутність `bunx knip` у `run:` кроці lint-js workflow.
18
+
19
+ - **ga (mdc v1.8 → v1.9) — заборона `depcheck` у workflow-файлах:** додано полісі `ga.workflow_common.deny` на будь-який виклик `depcheck` (через `npx`/`bunx`/`npm exec`/`pnpm exec` чи як standalone-команду) у `run:` кроку `.github/workflows/*.yml`. Перевірка невикористаних залежностей виконується разом з рештою лінтерів у `bun run lint-js` (`bunx knip`), окремий depcheck-крок у workflow зайвий. У `npm/mdc/ga.mdc` додано буліт «`depcheck`: не використовувати» з посиланням на `js-lint.mdc` і `ga.workflow_common`.
20
+
21
+ - **ci (тільки в цьому репо) — lint-ga встановлює conftest; knip.json налаштовано під монорепо:** `.github/workflows/lint-ga.yml` отримав крок `Install conftest` (curl-розпаковка релізу), бо `check-ga.mjs::runAllGaRego` ходить у `runConftestBatch` і hard-fail без бінарника. Кореневий `knip.json` розширено `workspaces.npm.entry` (всі CLI/scripts/tests як entry points — інакше knip false-positive репортить їх як unused), `ignoreBinaries` для `cspell`/`oxfmt`/`stylelint`/`vite` (всі через `bunx`/`npx`, не з deps), і `ignoreDependencies` для workspace self-refs. Це налаштування є кастомним для цього репо; інші проєкти налаштовують `knip.json` під свою структуру.
22
+
23
+ ### Removed
24
+
25
+ - **js-run (mdc v1.6 → v1.7) — секцію «depcheck у GitHub Actions з path-фільтром» прибрано:** правило про обовʼязковий `npx depcheck --ignores="graphql,bun"` з `working-directory` у path-scoped workflow більше не діє — `depcheck` повністю мігровано на `knip` (див. js-lint.mdc), окремий крок у per-package workflow не потрібен. Файл `npm/scripts/utils/depcheck-workflow.mjs` видалено. У `npm/scripts/check-js-run.mjs` прибрано `checkDepcheckInWorkflows`, імпорти `findDepcheckViolationsForPackage` / `readAllWorkflowFiles` і параметр `workflows` у `checkWorkspacePackage`. У `npm/tests/check-js-run-fixture.test.mjs` видалено `describe('check-js-run: depcheck у path-scoped workflow', …)` (9 тест-кейсів) і допоміжну `writeRepoWithCronJobAndWorkflow`. У `.github/workflows/npm-publish.yml` прибрано крок `npx depcheck --ignores="graphql,bun,bun:test,@nitra/cursor"` з `working-directory: npm` — `lint-js` workflow покриває цю перевірку через `bunx knip`.
26
+
7
27
  ## [1.9.6] - 2026-05-12
8
28
 
9
29
  ### Changed
package/mdc/ga.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Правила форматів для .github/workflows
3
- version: '1.8'
3
+ version: '1.9'
4
4
  globs: ".github/workflows/*.yml"
5
5
  alwaysApply: false
6
6
  ---
@@ -287,6 +287,8 @@ rules:
287
287
 
288
288
  **MegaLinter:** не використовувати; прибрати workflow, конфіги (`.mega-linter.yml`, `.megalinter.yaml`, `.mega-linter.yaml`), залежності та згадки в CI / pre-commit / документації.
289
289
 
290
+ **`depcheck`:** не використовувати у `.github/workflows/*.yml` — мігровано на `knip` (див. `js-lint.mdc`). Перевірка невикористаних залежностей виконується разом з рештою лінтерів у `lint-js`, окремий крок `npx depcheck` у workflow не потрібен і блокується полісі `ga.workflow_common`.
291
+
290
292
  ## Перевірка
291
293
 
292
294
  - `bun run lint-ga`
package/mdc/js-lint.mdc CHANGED
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  description: Перевірка JavaScript коду
3
3
  alwaysApply: true
4
- version: '1.18'
4
+ version: '1.19'
5
5
  ---
6
6
 
7
- **oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.9.2`** (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint); пакет **`@e18e/eslint-plugin`** окремо не додавай. Пакети oxlint/eslint/jscpd не додавай без потреби монорепо.
7
+ **oxlint**, **ESLint**, **jscpd**, **knip**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`**, **`bunx knip`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.9.2`** (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint); пакет **`@e18e/eslint-plugin`** окремо не додавай. Пакети oxlint/eslint/jscpd/knip не додавай без потреби монорепо.
8
8
 
9
9
  ```json title=".vscode/extensions.json"
10
10
  {
@@ -22,7 +22,7 @@ version: '1.18'
22
22
  {
23
23
  "type": "module",
24
24
  "scripts": {
25
- "lint-js": "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd ."
25
+ "lint-js": "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd . && bunx knip"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@nitra/eslint-config": "^3.9.2"
@@ -61,6 +61,22 @@ version: '1.18'
61
61
  .claude/worktrees/
62
62
  ```
63
63
 
64
+ ## knip
65
+
66
+ Перевірку невикористаних залежностей і експортів виконує **knip** (заміна `depcheck`). Викликається у скрипті `lint-js` і в CI разом з oxlint/eslint/jscpd — окремий крок у CI не потрібен.
67
+
68
+ У корені проєкту має бути `knip.json` із `ignoreDependencies` для `graphql` (peer-залежність, часто використовується без прямого імпорту):
69
+
70
+ ```json title="knip.json"
71
+ {
72
+ "ignoreDependencies": [
73
+ "graphql"
74
+ ]
75
+ }
76
+ ```
77
+
78
+ Пакет `knip` окремо в `devDependencies` не додавай — `bunx knip` тягне його ad-hoc, як oxlint/eslint/jscpd.
79
+
64
80
  ## jscpd: рефакторинг і структура
65
81
 
66
82
  Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
@@ -118,6 +134,7 @@ jobs:
118
134
  bunx oxlint
119
135
  bunx eslint .
120
136
  bunx jscpd .
137
+ bunx knip
121
138
  ```
122
139
 
123
140
  Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
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.6'
4
+ version: '1.7'
5
5
  ---
6
6
 
7
7
  ## Область застосування
@@ -209,26 +209,6 @@ await setTimeout(500)
209
209
 
210
210
  Імпорт `setTimeout` з `node:timers/promises` затіняє глобальний таймер у файлі — якщо в тому ж файлі потрібен callback-варіант, імпортуй його під іншим іменем (наприклад, `import { setTimeout as setTimeoutCb } from 'node:timers'`).
211
211
 
212
- ## depcheck у GitHub Actions з path-фільтром
213
-
214
- Якщо в `.github/workflows/*.yml` є тригер з `paths:`, який обмежує запуск workflow змінами в каталозі конкретного backend-пакета, в job цього workflow має бути крок `npx depcheck` з `working-directory`, який вказує на той самий каталог пакета. Це гарантує, що декларація залежностей у `package.json` пакета відповідає реальним імпортам — інакше можна випадково зламати білд після видалення «зайвої» залежності, яка насправді використовується через побічний імпорт.
215
-
216
- Список `--ignores` **обов'язково** містить як мінімум `graphql,bun` (це ті, які `depcheck` не вміє коректно розпізнавати: `graphql` — peer-залежність, що часто використовується без прямого імпорту в коді; `bun` — рантайм, не npm-пакет). За потреби список можна розширити іншими модулями, специфічними для пакета — список значень розділяється комою без пробілів.
217
-
218
- ```yaml title="Приклад: workflow для cron-jobs/refund-loyalty-points"
219
- on:
220
- push:
221
- paths:
222
- - 'cron-jobs/refund-loyalty-points/**'
223
-
224
- # …
225
-
226
- - run: npx depcheck --ignores="graphql,bun"
227
- working-directory: cron-jobs/refund-loyalty-points
228
- ```
229
-
230
- Правило не застосовується до workflow без `paths:` або з `paths:`, який не звужує тригер до одного backend-пакета (наприклад, кореневі `lint-*.yml` з глобальними `**/*.js`).
231
-
232
212
  ## Перевірка
233
213
 
234
214
  `npx @nitra/cursor check js-run` — зокрема для кожного backend workspace-пакета з каталогом **`src/`** перевіряє наявність **`jsconfig.json`** і збіг вмісту з каноном вище. Додатково для файлів у каталозі `#conn/` (за замовчуванням `src/conn/`) перевіряється:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.9.6",
3
+ "version": "1.9.8",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -33,6 +33,11 @@ forbidden_step_substrings := {
33
33
  "bun install": "використовуй .github/actions/setup-bun-deps замість bun install",
34
34
  }
35
35
 
36
+ # Заборонені бінарки у `run:` кроках (ga.mdc). `depcheck` мігровано на `knip`
37
+ # у `lint-js.mdc` — окремий крок у workflow не потрібен. Регексп ловить виклики
38
+ # через `npx`, `bunx`, `npm exec`, або як standalone-команду на початку рядка.
39
+ forbidden_run_command_patterns := {"depcheck": `(?:^|[\s;&|])(?:npx|bunx|npm exec|pnpm exec)?[ \t]*depcheck\b`}
40
+
36
41
  # Шаблони довгих повідомлень — через `concat`, щоб дотримуватися regal style/line-length.
37
42
 
38
43
  concurrency_missing_template := concat(" ", [
@@ -73,6 +78,19 @@ deny contains msg if {
73
78
  msg := sprintf("jobs.%s.steps[%d]: %s (ga.mdc)", [entry.job_id, entry.step_index, hint])
74
79
  }
75
80
 
81
+ # ── deny: depcheck у будь-якому `run:` ────────────────────────────────────
82
+ #
83
+ # `depcheck` мігровано на `knip` (js-lint.mdc); `knip` вже запускається у lint-js
84
+ # CI як частина `bunx knip` у скрипті, тож окремий depcheck-крок зайвий і має
85
+ # бути видалений з workflow-файлів.
86
+
87
+ deny contains msg if {
88
+ some entry in all_flat_steps
89
+ some name, pattern in forbidden_run_command_patterns
90
+ regex.match(pattern, step_run_text(entry.step))
91
+ msg := sprintf("jobs.%s.steps[%d]: `%s` заборонено у workflow — мігровано на knip (js-lint.mdc, ga.mdc)", [entry.job_id, entry.step_index, name])
92
+ }
93
+
76
94
  # ── deny: shell-продовження `\` перед переносом рядка у `run:` ─────────────
77
95
  #
78
96
  # `\` + `\n` — bash line-continuation; у workflow замінюй на folded block `>-`
@@ -71,6 +71,11 @@ deny contains msg if {
71
71
  msg := "lint-js.yml: у run немає `bunx jscpd .` (js-lint.mdc)"
72
72
  }
73
73
 
74
+ deny contains msg if {
75
+ not contains(all_run_blob, "bunx knip")
76
+ msg := "lint-js.yml: у run немає `bunx knip` (js-lint.mdc)"
77
+ }
78
+
74
79
  # ── deny: --fix у CI заборонено ───────────────────────────────────────────
75
80
 
76
81
  deny contains msg if {
@@ -17,7 +17,7 @@ package js_lint.package_json
17
17
 
18
18
  import rego.v1
19
19
 
20
- canonical_lint_js := "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd ."
20
+ canonical_lint_js := "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd . && bunx knip"
21
21
 
22
22
  # ── deny: `lint-js` скрипт ─────────────────────────────────────────────────
23
23
 
@@ -26,7 +26,7 @@ export const OXLINT_CANONICAL_JSON_PATH = join(
26
26
  )
27
27
 
28
28
  /** Очікуваний локальний скрипт. */
29
- export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
29
+ export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd . && bunx knip'
30
30
 
31
31
  /** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
32
32
  export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vscode-github-actions', 'oxc.oxc-vscode']
@@ -472,6 +472,34 @@ async function checkJscpdConfig(passFn, failFn) {
472
472
  }
473
473
  }
474
474
 
475
+ /**
476
+ * Перевіряє `knip.json`: файл існує і `ignoreDependencies` містить `graphql`
477
+ * (peer-залежність, що часто використовується без прямого імпорту — без цього
478
+ * `knip` фолсово репортить її як unused).
479
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
480
+ * @param {(msg: string) => void} failFn callback при помилці
481
+ */
482
+ async function checkKnipConfig(passFn, failFn) {
483
+ if (!existsSync('knip.json')) {
484
+ failFn('knip.json не існує — додай конфіг з ignoreDependencies: ["graphql"] (js-lint.mdc)')
485
+ return
486
+ }
487
+ let cfg
488
+ try {
489
+ cfg = JSON.parse(await readFile('knip.json', 'utf8'))
490
+ } catch {
491
+ failFn('knip.json не є валідним JSON')
492
+ return
493
+ }
494
+ passFn('knip.json існує')
495
+ const ignoreDeps = cfg?.ignoreDependencies
496
+ if (Array.isArray(ignoreDeps) && ignoreDeps.includes('graphql')) {
497
+ passFn('knip.json: ignoreDependencies містить "graphql"')
498
+ } else {
499
+ failFn('knip.json: ignoreDependencies має містити "graphql" (peer-залежність) (js-lint.mdc)')
500
+ }
501
+ }
502
+
475
503
  /**
476
504
  * Перевіряє відповідність проєкту правилам js-lint.mdc
477
505
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
@@ -486,6 +514,7 @@ export async function check() {
486
514
  await checkVscodeExtensions(pass, fail)
487
515
  await checkLintJsWorkflows(pass, fail)
488
516
  await checkJscpdConfig(pass, fail)
517
+ await checkKnipConfig(pass, fail)
489
518
 
490
519
  for (const dup of ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']) {
491
520
  if (existsSync(dup)) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй flat config`)
@@ -23,10 +23,6 @@
23
23
  * кожен `env.X` має бути закритий літеральним викликом `checkEnv(['X', ...])`
24
24
  * у тому ж файлі або коментарем `// \@nitra/cursor ignore-next-line checkEnv`
25
25
  * на попередньому рядку (див. `utils/check-env-scan.mjs`);
26
- * - «depcheck у GitHub Actions з path-фільтром»: для кожного workflow з `paths:`,
27
- * обмеженим каталогом цього пакета (`<rootDir>/...`), має бути крок
28
- * `npx depcheck --ignores="graphql,bun"` (плюс інші, за потреби) з
29
- * `working-directory: <rootDir>` (див. `utils/depcheck-workflow.mjs`);
30
26
  * - «Паузи через setTimeout»: `new Promise(resolve => setTimeout(resolve, ms))` (з/без `await`)
31
27
  * треба замінити на `await setTimeout(ms)` з `node:timers/promises`
32
28
  * (див. `utils/promise-settimeout-scan.mjs`);
@@ -44,7 +40,6 @@ import {
44
40
  } from './utils/bunyan-imports.mjs'
45
41
  import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './utils/check-env-scan.mjs'
46
42
  import { createCheckReporter } from './utils/check-reporter.mjs'
47
- import { findDepcheckViolationsForPackage, readAllWorkflowFiles } from './utils/depcheck-workflow.mjs'
48
43
  import { findConnFileRuleViolations, isConnFileRulesSourceFile } from './utils/conn-file-rules.mjs'
49
44
  import {
50
45
  findConnFactoryImportsInText,
@@ -310,12 +305,11 @@ async function checkPromiseSetTimeoutPause(absPackageRoot, sourcePaths, label, f
310
305
  * Перевіряє відповідність правилам js-run.mdc для одного workspace-пакета.
311
306
  * @param {string} rootDir відносний шлях workspace (не `'.'`)
312
307
  * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
313
- * @param {{ relPath: string, content: string }[]} workflows кешований список workflow-файлів репо
314
308
  * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
315
309
  * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
316
310
  * @returns {Promise<void>} завершується після перевірок цього пакета
317
311
  */
318
- async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, passFn) {
312
+ async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
319
313
  const label = `[${rootDir}] `
320
314
  const absPackageRoot = join(process.cwd(), rootDir)
321
315
  const pkgJson = await loadPackageJson(rootDir)
@@ -365,33 +359,6 @@ async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, pass
365
359
  }
366
360
 
367
361
  checkOtelConfigmap(rootDir, passFn)
368
-
369
- checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn)
370
- }
371
-
372
- /**
373
- * Перевіряє правило «depcheck у workflow» для одного пакета.
374
- *
375
- * Для кожного `.github/workflows/*.yml`, чий `paths:` обмежено до `<rootDir>/...`,
376
- * має бути крок `npx depcheck --ignores="graphql,bun"` з `working-directory: <rootDir>`.
377
- * Якщо в репо немає каталогу `.github/workflows`, перевірка no-op.
378
- * @param {string} rootDir відносний шлях workspace-пакета
379
- * @param {{ relPath: string, content: string }[]} workflows кешований список workflow-файлів
380
- * @param {string} label префікс повідомлення `[<pkg>] `
381
- * @param {(msg: string) => void} fail callback при помилці
382
- * @param {(msg: string) => void} passFn успішне повідомлення
383
- * @returns {void}
384
- */
385
- function checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn) {
386
- if (workflows.length === 0) return
387
- const violations = findDepcheckViolationsForPackage(workflows, rootDir.replaceAll('\\', '/'))
388
- if (violations.length === 0) {
389
- passFn(`${label}depcheck у path-scoped workflow налаштовано (або відсутній path-scoped workflow для пакета)`)
390
- return
391
- }
392
- for (const v of violations) {
393
- fail(`${label}${v}`)
394
- }
395
362
  }
396
363
 
397
364
  /**
@@ -454,9 +421,8 @@ export async function check() {
454
421
  }
455
422
 
456
423
  const ignorePaths = await loadCursorIgnorePaths(process.cwd())
457
- const workflows = await readAllWorkflowFiles(process.cwd())
458
424
  for (const r of workspaceRoots) {
459
- await checkWorkspacePackage(r, ignorePaths, workflows, fail, pass)
425
+ await checkWorkspacePackage(r, ignorePaths, fail, pass)
460
426
  }
461
427
 
462
428
  return reporter.getExitCode()
@@ -32,12 +32,7 @@ import { promisify } from 'node:util'
32
32
 
33
33
  import { parseSync } from 'oxc-parser'
34
34
 
35
- import {
36
- dynamicImportModule,
37
- langFromPath,
38
- requireCallModule,
39
- walkAstWithAncestors
40
- } from './utils/ast-scan-utils.mjs'
35
+ import { dynamicImportModule, langFromPath, requireCallModule, walkAstWithAncestors } from './utils/ast-scan-utils.mjs'
41
36
  import { createCheckReporter } from './utils/check-reporter.mjs'
42
37
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
43
38
  import { walkDir } from './utils/walkDir.mjs'
@@ -385,9 +380,7 @@ export function globToRegex(glob) {
385
380
  */
386
381
  async function collectPublishedFiles(filesField) {
387
382
  const positives = filesField.filter(p => typeof p === 'string' && !p.startsWith('!'))
388
- const negatives = filesField
389
- .filter(p => typeof p === 'string' && p.startsWith('!'))
390
- .map(p => globToRegex(p.slice(1)))
383
+ const negatives = filesField.filter(p => typeof p === 'string' && p.startsWith('!')).map(p => globToRegex(p.slice(1)))
391
384
  /** @type {Set<string>} */
392
385
  const collected = new Set()
393
386
  for (const entry of positives) {
@@ -1,205 +0,0 @@
1
- /**
2
- * Аналіз GitHub Actions workflow на правило «depcheck для path-scoped backend-пакета»
3
- * (див. секцію в `npm/mdc/js-run.mdc`).
4
- *
5
- * Алгоритм для одного workspace-пакета (`<rootDir>`):
6
- * 1. Шукаємо всі workflow, у яких `on.push.paths` або `on.pull_request.paths` містить
7
- * glob, що починається з `<rootDir>/` — це означає, що workflow обмежено саме цим пакетом
8
- * (повністю або частково).
9
- * 2. У кожному такому workflow має бути крок, чий `run` починається з `npx depcheck …`,
10
- * `working-directory` дорівнює `<rootDir>`, а список `--ignores="…"` містить
11
- * щонайменше `graphql` і `bun` (інші значення допустимі).
12
- *
13
- * Якщо паттерн `paths:` стосується цього пакета, але крок depcheck відсутній / без потрібних
14
- * ignores / у неправильному working-directory — фіксується порушення.
15
- *
16
- * Workflow без `paths:` або з глобальними патернами (`**\/*.js`, `npm/**`) ігноруються —
17
- * вони не «належать» жодному окремому пакету і виходять за межі правила.
18
- */
19
- import { readdir, readFile } from 'node:fs/promises'
20
- import { join, relative } from 'node:path'
21
-
22
- import { flattenWorkflowSteps, getStepRun, parseWorkflowYaml } from './gha-workflow.mjs'
23
-
24
- const WORKFLOWS_DIR_REL = '.github/workflows'
25
- const REQUIRED_IGNORES = ['graphql', 'bun']
26
- // `npx depcheck` як ціла команда у одному рядку shell-скрипту.
27
- // `[^\n]*` обмежено явним `\n`-stop'ом — `*` не може backtrack-нутися за межі рядка.
28
- const DEPCHECK_RUN_RE = /(?:^|[\s;&|])npx[ \t]+depcheck\b([^\n]*)/u
29
- // `--ignores=…` або `--ignores …` з трьома формами значення (двійкові, одинарні, без лапок).
30
- // Розділювач — або `=` з опційними пробілами, або один+ пробіл. Альтернативи значення
31
- // не перетинаються (стартують з різних символів), тож backtrack-у між ними нема.
32
- const IGNORES_FLAG_RE = /--ignores(?:=[ \t]*|[ \t]+)(?:"([^"]*)"|'([^']*)'|([^\s"']+))/u
33
-
34
- /**
35
- * Нормалізує шлях: бекслеші → forward, обрізає trailing-слеші. Без regex-у на trailing,
36
- * щоб не тригерити `sonarjs/slow-regex` на `\/+$`.
37
- * @param {string} p вхідний шлях
38
- * @returns {string} нормалізований шлях
39
- */
40
- function normalizePath(p) {
41
- let end = p.length
42
- while (end > 0) {
43
- const cp = p.codePointAt(end - 1)
44
- if (cp !== 47 && cp !== 92) break
45
- end--
46
- }
47
- let out = end === p.length ? p : p.slice(0, end)
48
- if (out.includes('\\')) out = out.replaceAll('\\', '/')
49
- return out
50
- }
51
-
52
- /**
53
- * Чи містить workflow.on[event].paths хоча б один patten, що починається з `<pkgRoot>/`.
54
- * @param {Record<string, unknown>} root корінь workflow
55
- * @param {string} pkgRoot відносний (POSIX) шлях каталогу пакета (наприклад `cron-jobs/refund-loyalty-points`)
56
- * @returns {boolean} `true`, якщо знайдено хоча б один підходящий glob
57
- */
58
- export function workflowHasPathsScopedToPackage(root, pkgRoot) {
59
- const prefix = `${normalizePath(pkgRoot)}/`
60
- const on = root?.on
61
- if (!on || typeof on !== 'object') return false
62
- for (const event of /** @type {const} */ (['push', 'pull_request'])) {
63
- const ev = /** @type {Record<string, unknown>} */ (on)[event]
64
- if (!ev || typeof ev !== 'object') continue
65
- const paths = /** @type {Record<string, unknown>} */ (ev).paths
66
- if (!Array.isArray(paths)) continue
67
- if (paths.some(p => typeof p === 'string' && p.startsWith(prefix))) return true
68
- }
69
- return false
70
- }
71
-
72
- /**
73
- * Розбирає `--ignores="a,b,c"` (також `--ignores=a,b`, single-quotes тощо) з аргументів `npx depcheck`.
74
- * @param {string} depcheckArgs частина рядка `run` після `npx depcheck`
75
- * @returns {string[] | null} масив значень ignores або `null`, якщо прапор відсутній
76
- */
77
- export function parseDepcheckIgnoresArg(depcheckArgs) {
78
- const m = IGNORES_FLAG_RE.exec(depcheckArgs)
79
- if (!m) return null
80
- const raw = m[1] ?? m[2] ?? m[3] ?? ''
81
- return raw
82
- .split(',')
83
- .map(s => s.trim())
84
- .filter(s => s.length > 0)
85
- }
86
-
87
- /**
88
- * Шукає `npx depcheck` у `run` кроку. Повертає рядок аргументів після `npx depcheck` або `null`.
89
- * @param {string} runText значення `run:` (можливо багаторядкове)
90
- * @returns {string | null} текст аргументів depcheck або `null`
91
- */
92
- export function extractDepcheckArgs(runText) {
93
- if (typeof runText !== 'string' || runText.length === 0) return null
94
- const m = DEPCHECK_RUN_RE.exec(runText)
95
- return m ? m[1] : null
96
- }
97
-
98
- /**
99
- * Чи `working-directory` кроку дорівнює очікуваному pkgRoot (з нормалізацією слешів і хвостових `/`).
100
- * @param {Record<string, unknown>} step об'єкт кроку
101
- * @param {string} pkgRoot очікуваний шлях
102
- * @returns {boolean} `true`, якщо збігаються
103
- */
104
- export function stepWorkingDirectoryEquals(step, pkgRoot) {
105
- const wd = step['working-directory']
106
- if (typeof wd !== 'string') return false
107
- return normalizePath(wd) === normalizePath(pkgRoot)
108
- }
109
-
110
- /**
111
- * Перевіряє один workflow на наявність валідного depcheck-кроку для пакета.
112
- * @param {Record<string, unknown>} root корінь workflow
113
- * @param {string} pkgRoot відносний шлях пакета
114
- * @returns {{ kind: 'ok' } | { kind: 'missing' } | { kind: 'wrong-cwd', actual: string } | { kind: 'missing-ignores', missing: string[] }} результат
115
- */
116
- export function evaluateDepcheckStepForPackage(root, pkgRoot) {
117
- /** @type {{ args: string, step: Record<string, unknown> }[]} */
118
- const depcheckSteps = []
119
- for (const { step } of flattenWorkflowSteps(root)) {
120
- const args = extractDepcheckArgs(getStepRun(step))
121
- if (args !== null) depcheckSteps.push({ args, step })
122
- }
123
- if (depcheckSteps.length === 0) return { kind: 'missing' }
124
-
125
- // Серед усіх знайдених depcheck-кроків шукаємо хоча б один, що відповідає пакету.
126
- const stepsForThisPackage = depcheckSteps.filter(s => stepWorkingDirectoryEquals(s.step, pkgRoot))
127
- if (stepsForThisPackage.length === 0) {
128
- const actual = depcheckSteps
129
- .map(s => /** @type {string} */ (s.step['working-directory'] ?? '<repo root>'))
130
- .join(', ')
131
- return { kind: 'wrong-cwd', actual }
132
- }
133
-
134
- for (const { args } of stepsForThisPackage) {
135
- const ignores = parseDepcheckIgnoresArg(args) ?? []
136
- const missing = REQUIRED_IGNORES.filter(req => !ignores.includes(req))
137
- if (missing.length === 0) return { kind: 'ok' }
138
- }
139
- // Усі знайдені кроки існують, але жоден не має повного списку обов'язкових ignores —
140
- // повертаємо missing з першого, щоб дати конкретний фідбек.
141
- const firstMissing = REQUIRED_IGNORES.filter(
142
- req => !(parseDepcheckIgnoresArg(stepsForThisPackage[0].args) ?? []).includes(req)
143
- )
144
- return { kind: 'missing-ignores', missing: firstMissing }
145
- }
146
-
147
- /**
148
- * Зчитує всі `.github/workflows/*.yml` (без `*.yaml` — за правилом n-ga) з коренем у `repoRoot`.
149
- * @param {string} repoRoot абсолютний корінь репозиторію
150
- * @returns {Promise<{ relPath: string, content: string }[]>} список workflow-файлів
151
- */
152
- export async function readAllWorkflowFiles(repoRoot) {
153
- const dir = join(repoRoot, WORKFLOWS_DIR_REL)
154
- /** @type {{ relPath: string, content: string }[]} */
155
- const out = []
156
- let entries
157
- try {
158
- entries = await readdir(dir, { withFileTypes: true })
159
- } catch {
160
- return out
161
- }
162
- for (const ent of entries) {
163
- if (!ent.isFile() || !ent.name.endsWith('.yml')) continue
164
- const abs = join(dir, ent.name)
165
- const content = await readFile(abs, 'utf8')
166
- out.push({ relPath: relative(repoRoot, abs).split('\\').join('/'), content })
167
- }
168
- return out
169
- }
170
-
171
- /**
172
- * Знаходить порушення правила depcheck для конкретного workspace-пакета.
173
- *
174
- * Повертає список повідомлень про порушення (порожній — все ok). Для кожного workflow,
175
- * чий `paths:` обмежено до цього пакета, перевіряє, що серед кроків є валідний `npx depcheck`
176
- * з потрібним `working-directory` та `--ignores`.
177
- * @param {{ relPath: string, content: string }[]} workflows список workflow-файлів (з `readAllWorkflowFiles`)
178
- * @param {string} pkgRoot відносний шлях workspace-пакета
179
- * @returns {string[]} повідомлення про порушення, по одному на workflow
180
- */
181
- export function findDepcheckViolationsForPackage(workflows, pkgRoot) {
182
- /** @type {string[]} */
183
- const violations = []
184
- for (const { relPath, content } of workflows) {
185
- const root = parseWorkflowYaml(content)
186
- if (!root) continue
187
- if (!workflowHasPathsScopedToPackage(root, pkgRoot)) continue
188
- const result = evaluateDepcheckStepForPackage(root, pkgRoot)
189
- if (result.kind === 'ok') continue
190
- if (result.kind === 'missing') {
191
- violations.push(
192
- `${relPath}: paths обмежено до '${pkgRoot}/**', але немає кроку 'npx depcheck --ignores="graphql,bun"' з working-directory: ${pkgRoot}`
193
- )
194
- } else if (result.kind === 'wrong-cwd') {
195
- violations.push(
196
- `${relPath}: 'npx depcheck' знайдено, але working-directory не дорівнює '${pkgRoot}' (фактично: ${result.actual})`
197
- )
198
- } else {
199
- violations.push(
200
- `${relPath}: 'npx depcheck' у '${pkgRoot}' має містити --ignores з '${result.missing.join(',')}' (мінімум: graphql,bun)`
201
- )
202
- }
203
- }
204
- return violations
205
- }