@nitra/cursor 1.13.11 → 1.13.13

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.
@@ -0,0 +1,30 @@
1
+ name: Git AI
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+
7
+ concurrency:
8
+ group: ${{ github.ref }}-${{ github.workflow }}
9
+ cancel-in-progress: true
10
+
11
+ jobs:
12
+ git-ai:
13
+ if: github.event.pull_request.merged == true
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: write
17
+
18
+ steps:
19
+ - name: Install git-ai
20
+ run: |
21
+ curl -fsSL https://usegitai.com/install.sh | bash
22
+ echo "$HOME/.git-ai/bin" >> $GITHUB_PATH
23
+ - name: Run git-ai
24
+ id: run-git-ai
25
+ env:
26
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27
+ run: |
28
+ git config --global user.name "github-actions[bot]"
29
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
30
+ git-ai ci github run
@@ -1,44 +1,50 @@
1
- # Порт перевірок `validateLintGaWorkflowStructure` + `validateLintGaOnTriggers` з
2
- # `npm/scripts/check-ga.mjs` (ga.mdc).
1
+ # Перевірка `.github/workflows/lint-ga.yml` (ga.mdc).
3
2
  #
4
- # Запуск (локально):
5
- # conftest test .github/workflows/lint-ga.yml \
6
- # -p npm/policy/ga --namespace ga.lint_ga
7
- #
8
- # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
9
- # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
10
- # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
3
+ # Канон надходить через --data: { "template": { "snippet": ... } }
4
+ # Структура --data сформована з template/lint-ga.yml.snippet.yml.
11
5
  package ga.lint_ga
12
6
 
13
7
  import rego.v1
14
8
 
15
- # ── Очікувані значення ─────────────────────────────────────────────────────
16
-
17
- expected_name := "Lint GA"
18
-
19
- expected_branches := {"dev", "main"}
20
-
21
- expected_push_paths := {".github/actions/**", ".github/workflows/**"}
22
-
23
- # ── Аліаси на input ────────────────────────────────────────────────────────
24
- #
25
- # YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
9
+ # ── Аліаси ─────────────────────────────────────────────────────────────────
26
10
 
27
11
  gha_on := input["true"]
28
12
 
29
- # Job-id містить дефіс — звертаємося через `[…]`. Імʼя `job` (без префіксу пакету)
30
- # — щоб уникнути regal-правила `rule-name-repeats-package`.
31
13
  job := input.jobs["lint-ga"]
32
14
 
33
- # Усі `uses:` зі steps цього job-а — для перевірки членства.
34
15
  job_uses_set contains job.steps[_].uses
35
16
 
36
- # Усі `run:` зі steps цього job-а, склеєні в один blob — для substring-перевірки.
37
17
  job_run_blob := concat("\n", [run |
38
18
  run := job.steps[_].run
39
19
  ])
40
20
 
41
- # ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
21
+ expected_name := data.template.snippet.name
22
+
23
+ expected_push_branches := {b | some b in data.template.snippet.on.push.branches}
24
+
25
+ expected_pr_branches := {b | some b in data.template.snippet.on.pull_request.branches}
26
+
27
+ expected_push_paths := {p | some p in data.template.snippet.on.push.paths}
28
+
29
+ expected_runs_on := data.template.snippet.jobs["lint-ga"]["runs-on"]
30
+
31
+ expected_perms := data.template.snippet.jobs["lint-ga"].permissions
32
+
33
+ # Required `uses:` зі template — фільтруємо тільки кроки що мають `uses`.
34
+ expected_uses_set contains u if {
35
+ some step in data.template.snippet.jobs["lint-ga"].steps
36
+ u := object.get(step, "uses", "")
37
+ u != ""
38
+ }
39
+
40
+ # Required `run:` substrings — collected from steps with `run`.
41
+ expected_run_blob := concat("\n", [r |
42
+ some step in data.template.snippet.jobs["lint-ga"].steps
43
+ r := object.get(step, "run", "")
44
+ r != ""
45
+ ])
46
+
47
+ # ── deny rules ─────────────────────────────────────────────────────────────
42
48
 
43
49
  deny contains msg if {
44
50
  input.name != expected_name
@@ -46,17 +52,17 @@ deny contains msg if {
46
52
  }
47
53
 
48
54
  deny contains msg if {
49
- not push_branches_have_dev_and_main
55
+ not branches_superset_of(gha_on.push.branches, expected_push_branches)
50
56
  msg := "lint-ga.yml: on.push.branches має містити dev і main (ga.mdc)"
51
57
  }
52
58
 
53
59
  deny contains msg if {
54
- not pr_branches_have_dev_and_main
60
+ not branches_superset_of(gha_on.pull_request.branches, expected_pr_branches)
55
61
  msg := "lint-ga.yml: on.pull_request.branches має містити dev і main (ga.mdc)"
56
62
  }
57
63
 
58
64
  deny contains msg if {
59
- not push_paths_have_required
65
+ not paths_superset_of(gha_on.push.paths, expected_push_paths)
60
66
  msg := "lint-ga.yml: on.push.paths має містити .github/actions/** і .github/workflows/** (ga.mdc)"
61
67
  }
62
68
 
@@ -66,13 +72,13 @@ deny contains msg if {
66
72
  }
67
73
 
68
74
  deny contains msg if {
69
- job["runs-on"] != "ubuntu-latest"
70
- msg := "lint-ga.yml: runs-on має бути ubuntu-latest (ga.mdc)"
75
+ job["runs-on"] != expected_runs_on
76
+ msg := sprintf("lint-ga.yml: runs-on має бути %s (ga.mdc)", [expected_runs_on])
71
77
  }
72
78
 
73
79
  deny contains msg if {
74
- job.permissions.contents != "read"
75
- msg := "lint-ga.yml: permissions мають бути contents: read (ga.mdc)"
80
+ job.permissions.contents != expected_perms.contents
81
+ msg := sprintf("lint-ga.yml: permissions.contents має бути %s (ga.mdc)", [expected_perms.contents])
76
82
  }
77
83
 
78
84
  deny contains msg if {
@@ -81,38 +87,23 @@ deny contains msg if {
81
87
  }
82
88
 
83
89
  deny contains msg if {
84
- not "actions/checkout@v6" in job_uses_set
85
- msg := "lint-ga.yml: має бути uses: actions/checkout@v6 (ga.mdc)"
86
- }
87
-
88
- deny contains msg if {
89
- not "./.github/actions/setup-bun-deps" in job_uses_set
90
- msg := "lint-ga.yml: має бути uses: ./.github/actions/setup-bun-deps (ga.mdc)"
91
- }
92
-
93
- deny contains msg if {
94
- not "astral-sh/setup-uv@v8.0.0" in job_uses_set
95
- msg := "lint-ga.yml: має бути uses: astral-sh/setup-uv@v8.0.0 (ga.mdc)"
90
+ some required_use in expected_uses_set
91
+ not required_use in job_uses_set
92
+ msg := sprintf("lint-ga.yml: має бути uses: %s (ga.mdc)", [required_use])
96
93
  }
97
94
 
98
95
  deny contains msg if {
96
+ expected_run_blob != ""
99
97
  not contains(job_run_blob, "bun run lint-ga")
100
98
  msg := "lint-ga.yml: має бути крок run: bun run lint-ga (ga.mdc)"
101
99
  }
102
100
 
103
101
  # ── helpers ────────────────────────────────────────────────────────────────
104
102
 
105
- push_branches_have_dev_and_main if {
106
- branches := gha_on.push.branches
107
- expected_branches & {b | some b in branches} == expected_branches
108
- }
109
-
110
- pr_branches_have_dev_and_main if {
111
- branches := gha_on.pull_request.branches
112
- expected_branches & {b | some b in branches} == expected_branches
103
+ branches_superset_of(actual, expected) if {
104
+ expected & {b | some b in actual} == expected
113
105
  }
114
106
 
115
- push_paths_have_required if {
116
- paths := gha_on.push.paths
117
- expected_push_paths & {p | some p in paths} == expected_push_paths
107
+ paths_superset_of(actual, expected) if {
108
+ expected & {p | some p in actual} == expected
118
109
  }
@@ -0,0 +1,35 @@
1
+ name: Lint GA
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - dev
7
+ - main
8
+ paths:
9
+ - '.github/actions/**'
10
+ - '.github/workflows/**'
11
+ pull_request:
12
+ branches:
13
+ - dev
14
+ - main
15
+
16
+ concurrency:
17
+ group: ${{ github.ref }}-${{ github.workflow }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ lint-ga:
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ contents: read
25
+ steps:
26
+ - uses: actions/checkout@v6
27
+ with:
28
+ persist-credentials: false
29
+
30
+ - uses: ./.github/actions/setup-bun-deps
31
+
32
+ - uses: astral-sh/setup-uv@v8.0.0
33
+
34
+ - name: Lint GA
35
+ run: bun run lint-ga
@@ -2,18 +2,20 @@
2
2
  * CLI-обгортка над канонічним `lint-text` (text.mdc): послідовно
3
3
  * 1) `cspell .` — перевірка правопису з `@nitra/cspell-dict`;
4
4
  * 2) `runShellcheckText()` — авто-фікс і фінальна перевірка `*.sh` через `shellcheck`;
5
- * 3) `bunx markdownlint-cli2 --fix "**\/*.md" "**\/*.mdc"` авто-фікс Markdown;
6
- * 4) `runV8rWithGlobs()` schema-валідація json/json5/yaml/yml/toml через v8r з каталогом `@nitra/cursor`.
5
+ * 3) `runDotenvLinter()` авто-фікс і фінальна перевірка `.env*` через `dotenv-linter`;
6
+ * 4) `bunx markdownlint-cli2 --fix "**\/*.md" "**\/*.mdc"` авто-фікс Markdown;
7
+ * 5) `runV8rWithGlobs()` — schema-валідація json/json5/yaml/yml/toml через v8r з каталогом `@nitra/cursor`.
7
8
  *
8
9
  * Перший ненульовий код з ланцюжка повертається як код виходу; наступні кроки не запускаються.
9
10
  * Експортовано як `runLintTextCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-text`.
10
11
  */
11
12
  import { runLintStep } from '../../../scripts/utils/run-lint-step.mjs'
13
+ import { runDotenvLinter } from './run-dotenv-linter.mjs'
12
14
  import { runShellcheckText } from './run-shellcheck.mjs'
13
15
  import { runV8rWithGlobs } from './run-v8r.mjs'
14
16
 
15
17
  /**
16
- * Виконує канонічний `lint-text`: cspell → run-shellcheck → markdownlint-cli2 → run-v8r.
18
+ * Виконує канонічний `lint-text`: cspell → run-shellcheck → run-dotenv-linter → markdownlint-cli2 → run-v8r.
17
19
  * Першу помилку повертаємо як код виходу; наступні кроки не запускаються.
18
20
  * Усі кроки синхронні (`spawnSync` + sync-ентрі з пакета), тому функція не async.
19
21
  * @returns {number} 0 — все OK, інакше — код першого кроку, що впав
@@ -26,6 +28,10 @@ export function runLintTextCli() {
26
28
  const shellcheckCode = runShellcheckText()
27
29
  if (shellcheckCode !== 0) return shellcheckCode
28
30
 
31
+ console.log('\n▶ dotenv-linter (авто-фікс + фінальна перевірка .env*)')
32
+ const dotenvCode = runDotenvLinter()
33
+ if (dotenvCode !== 0) return dotenvCode
34
+
29
35
  const markdownlintCode = runLintStep('markdownlint', 'bunx', ['markdownlint-cli2', '--fix', '**/*.md', '**/*.mdc'])
30
36
  if (markdownlintCode !== 0) return markdownlintCode
31
37
 
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Запуск dotenv-linter у ланцюжку lint-text: спочатку авто-фікс, потім фінальна перевірка.
3
+ *
4
+ * dotenv-linter — швидкий лінтер для `.env`-файлів (LowercaseKey, DuplicatedKey, IncorrectDelimiter,
5
+ * UnorderedKey тощо). Інструмент очікується у PATH і **не** додається в `dependencies`/`devDependencies`
6
+ * (аналогічно shellcheck). Якщо `dotenv-linter` відсутній — друкуємо підказки встановлення
7
+ * (`brew install dotenv-linter` на macOS) і повертаємо 1.
8
+ *
9
+ * Файли шукає сам `dotenv-linter` у режимі `-r` (рекурсивно по дереву проєкту). Виключаємо
10
+ * `node_modules` і `.envrc` (direnv shell-синтаксис, не key=value формат). `.bak`-файли інструмент
11
+ * ігнорує самостійно. Якщо `.env*`-файлів немає, dotenv-linter повертає 0 ("Nothing to check").
12
+ *
13
+ * Авто-фікс — один прогон `dotenv-linter fix -r --no-backup --quiet . --exclude …` (інструмент сам
14
+ * застосовує усі виправлення без потреби в diff/patch, на відміну від shellcheck). Після цього —
15
+ * фінальний `dotenv-linter check -r --quiet . --exclude …`; будь-яке залишкове порушення — ненульовий
16
+ * код виходу.
17
+ */
18
+ import { spawnSync } from 'node:child_process'
19
+ import { resolve } from 'node:path'
20
+
21
+ import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
22
+ import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
23
+
24
+ /** Каталоги/файли, які виключаємо з рекурсивного сканування dotenv-linter. */
25
+ const EXCLUDED_PATHS = ['node_modules', '.envrc']
26
+
27
+ /**
28
+ * Друкує підказки встановлення dotenv-linter у stderr.
29
+ * @returns {void}
30
+ */
31
+ function printDotenvLinterInstallHints() {
32
+ process.stderr.write(
33
+ [
34
+ '❌ dotenv-linter не знайдено в PATH.',
35
+ 'Встанови інструмент і повтори lint-text:',
36
+ ' macOS: brew install dotenv-linter',
37
+ ' Linux: curl -sSfL https://git.io/JLbXn | sh -s -- -b /usr/local/bin',
38
+ ' cargo: cargo install dotenv-linter',
39
+ ''
40
+ ].join('\n')
41
+ )
42
+ }
43
+
44
+ /**
45
+ * Будує перелік аргументів `--exclude <path>` для dotenv-linter.
46
+ * @returns {string[]} плоский масив `['--exclude', 'node_modules', '--exclude', '.envrc']`
47
+ */
48
+ function buildExcludeArgs() {
49
+ return EXCLUDED_PATHS.flatMap(p => ['--exclude', p])
50
+ }
51
+
52
+ /**
53
+ * Запускає dotenv-linter з авто-фіксом і фінальною перевіркою.
54
+ * @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
55
+ * @returns {number} 0 — OK; 1 — інструмент відсутній або є залишкові порушення
56
+ */
57
+ export function runDotenvLinter(cwd = process.cwd()) {
58
+ const root = resolve(cwd)
59
+ const bin = resolveCmd('dotenv-linter')
60
+ if (!bin) {
61
+ printDotenvLinterInstallHints()
62
+ return 1
63
+ }
64
+
65
+ const exclude = buildExcludeArgs()
66
+ const fixRun = spawnSync(bin, ['fix', '-r', '--no-backup', '--quiet', ...exclude, '.'], {
67
+ cwd: root,
68
+ encoding: 'utf8',
69
+ env: process.env,
70
+ stdio: ['ignore', 'pipe', 'pipe']
71
+ })
72
+ if (fixRun.error) {
73
+ process.stderr.write(`${fixRun.error.message}\n`)
74
+ return 1
75
+ }
76
+
77
+ const checkRun = spawnSync(bin, ['check', '-r', '--quiet', ...exclude, '.'], {
78
+ cwd: root,
79
+ encoding: 'utf8',
80
+ env: process.env,
81
+ stdio: ['ignore', 'pipe', 'pipe']
82
+ })
83
+ if (checkRun.error) {
84
+ process.stderr.write(`${checkRun.error.message}\n`)
85
+ return 1
86
+ }
87
+ if (checkRun.status === 0) return 0
88
+ if (checkRun.stdout?.length) process.stdout.write(checkRun.stdout)
89
+ if (checkRun.stderr?.length) process.stderr.write(checkRun.stderr)
90
+ return 1
91
+ }
92
+
93
+ if (isRunAsCli()) {
94
+ process.exitCode = runDotenvLinter()
95
+ }
@@ -1,10 +1,10 @@
1
1
  ---
2
- description: Обробка та перевірка текстових файлів, oxfmt, cspell, shellcheck (sh), markdownlint-cli2, v8r, CI
2
+ description: Обробка та перевірка текстових файлів, oxfmt, cspell, shellcheck (sh), dotenv-linter (.env*), markdownlint-cli2, v8r, CI
3
3
  alwaysApply: true
4
- version: '1.26'
4
+ version: '1.27'
5
5
  ---
6
6
 
7
- **oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **shellcheck** (tracked `*.sh` у `lint-text`), **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** / **timonwong.shellcheck**, workflow **`lint-text`**.
7
+ **oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **shellcheck** (tracked `*.sh` у `lint-text`), **[dotenv-linter](https://dotenv-linter.github.io/)** (`.env*` у `lint-text`), **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** / **timonwong.shellcheck**, workflow **`lint-text`**.
8
8
 
9
9
  ```json title=".vscode/extensions.json"
10
10
  {
@@ -115,6 +115,8 @@ version: '1.26'
115
115
 
116
116
  **shellcheck:** інструмент лише в **`PATH`** (як у **ga.mdc** для `lint-ga`), **не** додавай до `dependencies` / `devDependencies`. Якщо `shellcheck` відсутній — встанови локально (**macOS:** `brew install shellcheck`; **Debian/Ubuntu:** `sudo apt-get install -y shellcheck`; **Arch:** `sudo pacman -S shellcheck`). Канонічний **`lint-text`** делегує до CLI **`n-cursor lint-text`** (бінарка з `node_modules/.bin/` пакету `@nitra/cursor`): після **`cspell .`** виконується **`runShellcheckText()`** з пакета — циклічно **`shellcheck -f diff`** і **`patch -p1`** для авто-виправлень, потім повний прогін **`shellcheck`** по всіх tracked `*.sh` (у git) або по `**/*.sh` без `node_modules`. Потрібен також **`patch`** у `PATH` (на macOS зазвичай уже є).
117
117
 
118
+ **dotenv-linter:** після shellcheck CLI запускає **`runDotenvLinter()`** з пакета — рекурсивно по `.env*`-файлах проєкту через **`dotenv-linter fix -r --no-backup --quiet . --exclude node_modules --exclude .envrc`**, далі такий самий **`check`** для фінальної перевірки. Інструмент має бути в **`PATH`** і **не** додається в `dependencies` / `devDependencies`. Якщо `dotenv-linter` відсутній — встанови локально (**macOS:** `brew install dotenv-linter`; **Linux:** `curl -sSfL https://git.io/JLbXn | sh -s -- -b /usr/local/bin`; через **cargo:** `cargo install dotenv-linter`).
119
+
118
120
  У v8r **немає** прапорця тихого режиму; CLI-обгортка **`n-cursor lint-text`** на четвертому кроці викликає **`runV8rWithGlobs()`** з пакета (реалізація — `npm/rules/text/js/run-v8r.mjs`): під капотом послідовні **`bunx v8r`** для кожного типу (**json**, **json5**, **yml**, **yaml**, **toml**), бо один процес v8r з кількома глобами падає з **98**, якщо хоч один glob порожній, і тоді інші розширення не перевіряються. Вивід при кодах **0** і **98** не показується. Каталог схем **`schemas/v8r-catalog.json`** пакета `@nitra/cursor` обгортка підставляє в v8r сама.
119
121
 
120
122
  ```json title="package.json"
@@ -9,12 +9,16 @@ version: '1.1'
9
9
 
10
10
  Скіл прибирає з проєкту все, що належить **ru-середовищу**. Залишаються тільки `dev` (як база) та `ua` як активне продакшн-середовище. Працюй послідовно по секціях нижче — після кожної секції перевіряй, що проєкт лишається консистентним (`kustomization.yaml` посилається лише на наявні файли, GitHub Actions-вирази синтаксично коректні).
11
11
 
12
+ ## 0. Що НЕ чіпати
13
+
14
+ **`node_modules/`** (і будь-які `**/node_modules/`) — повністю виключаються з аналізу й модифікацій. Це згенеровані залежності: збіги на `ru`, `values-ru.*`, `cr.yandex` тощо там нерелевантні і відновляться при наступному `install`. Усі `find` / `git grep` / автозаміни мають пропускати ці шляхи. Аналогічно не чіпай `.git/`, `dist/`, `build/`, `.next/`, `.nuxt/`, `.output/`, `coverage/`.
15
+
12
16
  ## 1. Директорії з назвою `ru`
13
17
 
14
- Видали всі директорії з назвою `ru` у проєкті:
18
+ Видали всі директорії з назвою `ru` у проєкті (крім тих, що всередині `node_modules`):
15
19
 
16
20
  ```bash
17
- find . -type d -name "ru" -exec rm -rf {} +
21
+ find . -type d \( -name node_modules -o -name .git \) -prune -o -type d -name "ru" -print -exec rm -rf {} +
18
22
  ```
19
23
 
20
24
  Це чистить як `country/ru/`, так і `k8s/<...>/ru/` (overlay у kustomize). Після видалення overlay `ru/` обов’язково прибери відповідний запис у `resources:` у батьківському `kustomization.yaml`, якщо він там лишився.
@@ -27,7 +31,8 @@ find . -type d -name "ru" -exec rm -rf {} +
27
31
  - будь-які файли, що закінчуються на `-ru` або `-ru.<ext>`, наприклад `site/.env.prod-ru`, `*.env.prod-ru`, `*.prod-ru.conf`
28
32
 
29
33
  ```bash
30
- find . -type f \( -name "values-ru.*" -o -name "*-ru" -o -name "*.prod-ru" -o -name "*.prod-ru.*" \) -delete
34
+ find . -type d \( -name node_modules -o -name .git \) -prune -o \
35
+ -type f \( -name "values-ru.*" -o -name "*-ru" -o -name "*.prod-ru" -o -name "*.prod-ru.*" \) -print -delete
31
36
  ```
32
37
 
33
38
  ## 3. `.github/workflows/*.yml`
@@ -194,5 +199,5 @@ const tr = {
194
199
  ## 6. Після очистки
195
200
 
196
201
  - Переконайся, що `kustomization.yaml` у кожній директорії `k8s/` не посилається на видалені overlay або файли.
197
- - Пройдись `git grep` по репозиторію на залишки: `git grep -n -i -e '\bru\b' -e cr\.yandex -e country/ru -e prod-ru -e values-ru -e "'ya'"` — переглянь усі знахідки вручну, бо `ru` як слово може траплятися в легітимних контекстах (наприклад, `truncate`, `Aurum`, `cruft`). Видаляй лише ті входження, що належать ru-середовищу.
202
+ - Пройдись `git grep` по репозиторію на залишки: `git grep -n -i -e '\bru\b' -e cr\.yandex -e country/ru -e prod-ru -e values-ru -e "'ya'"` — переглянь усі знахідки вручну, бо `ru` як слово може траплятися в легітимних контекстах (наприклад, `truncate`, `Aurum`, `cruft`). Видаляй лише ті входження, що належать ru-середовищу. `git grep` за замовчуванням пропускає невідстежувані шляхи, тож `node_modules/` у вихід не потрапить, поки воно у `.gitignore`.
198
203
  - Перевір CI локально: `npx @nitra/cursor check abie` (якщо правило **abie** ввімкнене у проєкті).