@nitra/cursor 12.3.2 → 12.4.0
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/.claude-template/hooks/capture-decisions.sh +36 -19
- package/.claude-template/hooks/lib/tooling-only.sh +16 -0
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/rules/bun/bun.mdc +5 -5
- package/rules/bun/policy/package_json/package_json.rego +0 -31
- package/rules/ga/ga.mdc +2 -4
- package/rules/ga/policy/lint_ga/lint_ga.rego +2 -2
- package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +1 -1
- package/rules/js-lint/js-lint.mdc +2 -5
- package/rules/js-lint/policy/lint_js_yml/template/lint-js.yml.snippet.yml +1 -5
- package/rules/js-lint/policy/package_json/template/package.json.snippet.json +0 -3
- package/rules/lint/js/docs/orchestrate.md +11 -12
- package/rules/lint/js/orchestrate.mjs +82 -5
- package/rules/php/js/docs/index.md +1 -0
- package/rules/php/js/docs/lint.md +20 -0
- package/rules/php/js/lint.mjs +13 -0
- package/rules/php/php.mdc +1 -3
- package/rules/php/policy/lint_php_yml/template/lint-php.yml.snippet.yml +1 -1
- package/rules/python/js/docs/index.md +1 -0
- package/rules/python/js/docs/lint.md +21 -0
- package/rules/python/js/lint.mjs +14 -0
- package/rules/python/lint/docs/lint.md +15 -312
- package/rules/python/lint/lint.mjs +11 -5
- package/rules/python/meta.json +1 -1
- package/rules/python/policy/lint_python_yml/template/lint-python.yml.snippet.yml +1 -1
- package/rules/python/python.mdc +1 -3
- package/rules/rego/rego.mdc +2 -6
- package/rules/rust/js/docs/index.md +1 -0
- package/rules/rust/js/docs/lint.md +21 -0
- package/rules/rust/js/lint.mjs +67 -0
- package/rules/rust/rust.mdc +2 -4
- package/rules/security/policy/package_json/package_json.rego +0 -19
- package/rules/security/security.mdc +5 -6
- package/rules/style-lint/policy/lint_style_yml/template/lint-style.yml.snippet.yml +1 -1
- package/rules/style-lint/policy/package_json/package_json.rego +0 -10
- package/rules/style-lint/style-lint.mdc +4 -6
- package/rules/text/js/formatting.mjs +7 -31
- package/rules/text/policy/lint_text/template/lint-text.yml.snippet.yml +1 -1
- package/rules/ga/policy/package_json/package_json.rego +0 -20
- package/rules/ga/policy/package_json/target.json +0 -8
- package/rules/ga/policy/package_json/template/package.json.contains.json +0 -1
- package/rules/php/policy/package_json/package_json.rego +0 -16
- package/rules/php/policy/package_json/target.json +0 -4
- package/rules/php/policy/package_json/template/package.json.contains.json +0 -5
- package/rules/python/policy/package_json/package_json.rego +0 -16
- package/rules/python/policy/package_json/target.json +0 -4
- package/rules/python/policy/package_json/template/package.json.contains.json +0 -5
- package/rules/rego/policy/package_json/package_json.rego +0 -16
- package/rules/rego/policy/package_json/target.json +0 -5
- package/rules/rego/policy/package_json/template/package.json.snippet.json +0 -1
- package/rules/rust/policy/package_json/package_json.rego +0 -18
- package/rules/rust/policy/package_json/target.json +0 -5
- package/rules/rust/policy/package_json/template/package.json.contains.json +0 -9
- package/rules/security/policy/package_json/template/package.json.contains.json +0 -1
- package/rules/security/policy/package_json/template/package.json.snippet.json +0 -5
- package/rules/style-lint/policy/package_json/template/package.json.contains.json +0 -5
|
@@ -3,324 +3,27 @@ type: JS Module
|
|
|
3
3
|
title: lint.mjs
|
|
4
4
|
resource: npm/rules/python/lint/lint.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 61a1e3c3
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
score: 90
|
|
9
|
+
issues: internal-name:runStandardLint,judge:inaccurate:0.99
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
7
11
|
---
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
загального лінт-пайплайну монорепозиторію. Крок виконує перевірку Python-частини
|
|
11
|
-
проєкту відповідно до правила `python.mdc` і базується на пакетному менеджері
|
|
12
|
-
[uv](https://docs.astral.sh/uv/).
|
|
13
|
+
## Огляд
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Забезпечує виконання обов'язкових кроків для валідації Python-коду відповідно до правил, визначених у `python.mdc`, використовуючи інструменти з [uv](https://docs.astral.sh/uv/). Якщо `pyproject.toml` відсутній у корені, процес завершується з кодом 0. Якщо файл присутній, але `uv` не знайдено в PATH, це розглядається як помилка. Обов'язкові кроки включають перевірку актуальності lock-файлу (`uv lock --check`) та збірку середовища (`uv sync --frozen`). Опціональні лінтери (`ruff`, `mypy`) запускаються лише за умови їх доступності через `uv run`. Цей процес реалізує канон патерну `lint-*` (серіалізація через `runStandardLint`).
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
файлу `pyproject.toml`, крок завершується успіхом (`exit code 0`) без запуску
|
|
18
|
-
будь-яких інструментів. Це дозволяє безпечно вмикати крок у репозиторіях
|
|
19
|
-
без Python-частини.
|
|
20
|
-
- Якщо `pyproject.toml` присутній, але бінарника `uv` немає в `PATH`, крок
|
|
21
|
-
завершується помилкою. Інших пакет-менеджерів (Poetry, pip, pdm тощо) модуль
|
|
22
|
-
не підтримує — `uv` є єдиним каноном.
|
|
23
|
-
- Обовʼязкові кроки `uv lock --check` і `uv sync --frozen` запускаються завжди,
|
|
24
|
-
якщо `uv` доступний.
|
|
25
|
-
- Опційні лінтери (`ruff check --fix`, `ruff format`, `mypy`) запускаються
|
|
26
|
-
лише якщо вони доступні через `uv run --frozen <tool> --version`. Якщо
|
|
27
|
-
відповідного інструмента у uv-середовищі немає — крок пропускається з
|
|
28
|
-
pass-повідомленням (аналогічно «optional vendor-tools» у `php.mdc`).
|
|
29
|
-
- `ruff` працює в auto-fix-режимі (`--fix`, потім `format`), тобто може
|
|
30
|
-
мутувати робоче дерево, подібно до `markdownlint-cli2 --fix` у `lint-text`
|
|
31
|
-
чи `clippy --fix` у `lint-rust`.
|
|
32
|
-
- Серіалізація запусків CLI організована через `runStandardLint` (а не через
|
|
33
|
-
безпосередній `withLock`) — це відповідає канону патерну `lint-*`, описаному
|
|
34
|
-
в `.cursor/rules/scripts.mdc` (секція «Серіалізація важких CLI-команд»).
|
|
17
|
+
## Поведінка
|
|
35
18
|
|
|
36
|
-
|
|
19
|
+
runLintPythonSteps виконує обов'язкові кроки для Python-лінтування за правилом python.mdc на базі [uv](https://docs.astral.sh/uv/). Якщо `pyproject.toml` відсутній, кроки пропускаються. Якщо `uv` не знайдено, виникає помилка. Виконує перевірку актуальності lock-файлу (`uv lock --check`) та збірку середовища (`uv sync --frozen`). Опціонально запускає лінтери (`ruff`, `mypy`) через `uv run`, якщо вони доступні.
|
|
20
|
+
runLintPython серіалізує запуск кроків лінтування Python через механізм `runStandardLint` та повертає код виходу.
|
|
37
21
|
|
|
38
|
-
|
|
39
|
-
2. CLI-точкою входу — при запуску напряму (`isRunAsCli`) виконує
|
|
40
|
-
`runLintPython()` і виставляє `process.exitCode`.
|
|
22
|
+
## Публічний API
|
|
41
23
|
|
|
42
|
-
|
|
24
|
+
runLintPythonSteps — Виконує внутрішні етапи перевірки коду Python без блокування.
|
|
25
|
+
runLintPython — Виконує публічну команду перевірки коду Python, забезпечуючи унікальність виконання на основі стану Git-дерева та використовуючи механізм блокування.
|
|
43
26
|
|
|
44
|
-
|
|
45
|
-
| -------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
-
| `runLintPythonSteps` | `function` | Виконує внутрішні кроки `lint-python` (без зовнішнього локу). Призначений для повторного використання з обгортки `runStandardLint` та для тестування. |
|
|
47
|
-
| `runLintPython` | `() => Promise<number>` | Публічна CLI-форма: запускає `runLintPythonSteps` через `runStandardLint`, який бере глобальний лок `lint-python` і дедупає прогони за станом git-дерева. |
|
|
27
|
+
## Гарантії поведінки
|
|
48
28
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Side effect модуля верхнього рівня: якщо файл запущений як CLI
|
|
52
|
-
(`isRunAsCli(import.meta.url)` повертає `true`), на верхньому рівні
|
|
53
|
-
виконується `await runLintPython()` і результат записується у
|
|
54
|
-
`process.exitCode`.
|
|
55
|
-
|
|
56
|
-
## Функції
|
|
57
|
-
|
|
58
|
-
### `runTool(label, cmd, args, pass, fail)`
|
|
59
|
-
|
|
60
|
-
Внутрішня (не експортується) функція-обгортка над `child_process.spawnSync`,
|
|
61
|
-
яка запускає вказаний CLI-крок і репортить результат через колбеки репортера.
|
|
62
|
-
|
|
63
|
-
- Сигнатура: `runTool(label: string, cmd: string, args: string[], pass: (msg: string) => void, fail: (msg: string) => void): boolean`
|
|
64
|
-
- Параметри:
|
|
65
|
-
- `label` — людиночитана назва кроку, використовується у повідомленнях
|
|
66
|
-
(`lint-python: <label> — OK` / `lint-python: <label> — помилка ...`).
|
|
67
|
-
- `cmd` — абсолютний шлях до виконуваного файлу (наприклад, отриманий через
|
|
68
|
-
`resolveCmd('uv')`).
|
|
69
|
-
- `args` — масив аргументів CLI.
|
|
70
|
-
- `pass` — callback репортера для успіху.
|
|
71
|
-
- `fail` — callback репортера для невдачі.
|
|
72
|
-
- Повертає: `true`, якщо процес завершився з `status === 0`, інакше `false`.
|
|
73
|
-
- Спосіб запуску: `spawnSync(cmd, args, { stdio: 'inherit', shell: false })`.
|
|
74
|
-
Це означає, що stdout/stderr CLI-кроку успадковуються від батьківського
|
|
75
|
-
процесу (видно користувачу), а інтерпретація аргументів shell-ом
|
|
76
|
-
вимкнена — аргументи передаються «as is».
|
|
77
|
-
- Обробка статусу: якщо `r.status` не число (наприклад, процес був убитий
|
|
78
|
-
сигналом), у повідомлення про помилку підставляється `1`.
|
|
79
|
-
- Side effects: запуск зовнішнього процесу; запис у stdout/stderr батька;
|
|
80
|
-
виклик `pass` або `fail` репортера.
|
|
81
|
-
|
|
82
|
-
### `uvToolAvailable(uv, tool)`
|
|
83
|
-
|
|
84
|
-
Внутрішня (не експортується) перевірка наявності лінтера всередині
|
|
85
|
-
uv-середовища.
|
|
86
|
-
|
|
87
|
-
- Сигнатура: `uvToolAvailable(uv: string, tool: string): boolean`
|
|
88
|
-
- Параметри:
|
|
89
|
-
- `uv` — абсолютний шлях до бінарника `uv`.
|
|
90
|
-
- `tool` — назва бінарника, що перевіряється (`ruff`, `mypy`, тощо).
|
|
91
|
-
- Повертає: `true`, якщо `uv run --frozen <tool> --version` завершився з
|
|
92
|
-
кодом `0`, інакше `false`.
|
|
93
|
-
- Спосіб запуску: `spawnSync(uv, ['run', '--frozen', tool, '--version'],
|
|
94
|
-
{ stdio: 'ignore', shell: false })`. `stdio: 'ignore'` гасить весь вивід
|
|
95
|
-
пробної команди, щоб не засмічувати лог.
|
|
96
|
-
- Side effects: запуск дочірнього процесу `uv run --frozen <tool> --version`.
|
|
97
|
-
Опція `--frozen` гарантує, що `uv` не намагатиметься оновлювати lock-файл
|
|
98
|
-
під час перевірки.
|
|
99
|
-
|
|
100
|
-
### `runLintPythonSteps(cwd?)`
|
|
101
|
-
|
|
102
|
-
Експортована функція. Виконує всю послідовність кроків `lint-python` без
|
|
103
|
-
зовнішнього серіалізаційного локу.
|
|
104
|
-
|
|
105
|
-
- Сигнатура: `runLintPythonSteps(cwd?: string): number`
|
|
106
|
-
- Параметри:
|
|
107
|
-
- `cwd` — корінь репозиторію. За замовчуванням `process.cwd()`.
|
|
108
|
-
- Повертає: код виходу — `0`, якщо всі обовʼязкові кроки пройшли успішно,
|
|
109
|
-
`1` — якщо хоча б один крок зафейлив. Кінцевий код повертається через
|
|
110
|
-
`reporter.getExitCode()` (інстансу `createCheckReporter`).
|
|
111
|
-
- Алгоритм:
|
|
112
|
-
1. Створює репортер: `const reporter = createCheckReporter()`,
|
|
113
|
-
дістає колбеки `{ pass, fail }`.
|
|
114
|
-
2. Перевіряє `existsSync(join(cwd, 'pyproject.toml'))`. Якщо файла
|
|
115
|
-
немає — викликає `pass(...)` з повідомленням «кроки Python пропущено»
|
|
116
|
-
і повертає `reporter.getExitCode()`.
|
|
117
|
-
3. `const uv = resolveCmd('uv')` — резолвить абсолютний шлях до `uv`.
|
|
118
|
-
Якщо `uv` не знайдено — `fail(...)` і повернення коду.
|
|
119
|
-
4. Виконує `runTool('uv lock --check', uv, ['lock', '--check'], pass, fail)`.
|
|
120
|
-
За невдачі — повертає поточний код (далі не йде).
|
|
121
|
-
5. Виконує `runTool('uv sync --frozen', uv, ['sync', '--frozen'], pass,
|
|
122
|
-
fail)`. За невдачі — повертає поточний код.
|
|
123
|
-
6. Створює локальний хелпер `runOptionalUvTool(tool, label, args)`
|
|
124
|
-
(див. нижче) і послідовно запускає:
|
|
125
|
-
- `runOptionalUvTool('ruff', 'ruff check --fix', ['check', '--fix', '.'])`
|
|
126
|
-
- `runOptionalUvTool('ruff', 'ruff format', ['format', '.'])`
|
|
127
|
-
- `runOptionalUvTool('mypy', 'mypy', ['.'])`
|
|
128
|
-
За першої ж справжньої невдачі (повертає `false`) — повернення поточного
|
|
129
|
-
коду виходу.
|
|
130
|
-
7. Повертає `reporter.getExitCode()`.
|
|
131
|
-
- Side effects:
|
|
132
|
-
- Запуск зовнішніх процесів (`uv lock`, `uv sync`, `uv run ruff`,
|
|
133
|
-
`uv run mypy`).
|
|
134
|
-
- `ruff check --fix` та `ruff format` можуть **модифікувати файли
|
|
135
|
-
проєкту** (auto-fix Python-коду).
|
|
136
|
-
- `uv sync --frozen` може створювати або оновлювати `.venv` (з повним
|
|
137
|
-
дотриманням `uv.lock`).
|
|
138
|
-
- Запис у stdout/stderr через `stdio: 'inherit'`.
|
|
139
|
-
|
|
140
|
-
### `runOptionalUvTool(tool, label, args)` (вкладена у `runLintPythonSteps`)
|
|
141
|
-
|
|
142
|
-
Внутрішній замикач, доступний лише всередині `runLintPythonSteps`. Захоплює
|
|
143
|
-
`uv`, `pass`, `fail` із зовнішньої області видимості.
|
|
144
|
-
|
|
145
|
-
- Сигнатура: `runOptionalUvTool(tool: string, label: string, args: string[]): boolean`
|
|
146
|
-
- Параметри:
|
|
147
|
-
- `tool` — імʼя інструмента (`ruff`, `mypy`).
|
|
148
|
-
- `label` — назва кроку для повідомлень.
|
|
149
|
-
- `args` — аргументи, які слід передати інструменту після `uv run --frozen <tool>`.
|
|
150
|
-
- Повертає: `true`, якщо крок успішно завершився **або** інструмент
|
|
151
|
-
відсутній у uv-середовищі (тоді крок пропускається з pass-повідомленням).
|
|
152
|
-
`false` повертається тільки коли інструмент доступний і завершився з
|
|
153
|
-
ненульовим статусом.
|
|
154
|
-
- Логіка:
|
|
155
|
-
1. `if (!uvToolAvailable(uv, tool))` → `pass(...)` з повідомленням «крок
|
|
156
|
-
пропущено» і повертає `true` (це коректне продовження пайплайну,
|
|
157
|
-
інструмент трактується як optional).
|
|
158
|
-
2. Інакше викликає `runTool(label, uv, ['run', '--frozen', tool, ...args],
|
|
159
|
-
pass, fail)`.
|
|
160
|
-
- Side effects: ті ж, що й у `runTool` / `uvToolAvailable` (запуск дочірніх
|
|
161
|
-
процесів, оновлення репортера).
|
|
162
|
-
|
|
163
|
-
### `runLintPython`
|
|
164
|
-
|
|
165
|
-
Публічна обгортка-стрілкова функція.
|
|
166
|
-
|
|
167
|
-
- Сигнатура: `runLintPython(): Promise<number>`
|
|
168
|
-
- Параметри: немає.
|
|
169
|
-
- Повертає: `Promise<number>` — код виходу, отриманий з `runStandardLint`.
|
|
170
|
-
- Реалізація: `runStandardLint(import.meta.dirname, runLintPythonSteps)`.
|
|
171
|
-
Сенс параметрів:
|
|
172
|
-
- `import.meta.dirname` — директорія самого модуля; використовується
|
|
173
|
-
`runStandardLint` як ідентифікатор для дедуплікації / стану git-дерева.
|
|
174
|
-
- `runLintPythonSteps` — функція кроків, яку `runStandardLint` викличе
|
|
175
|
-
всередині глобального локу `lint-python`.
|
|
176
|
-
- Серіалізація: `runStandardLint` бере глобальний лок `lint-python` (як
|
|
177
|
-
описано в `scripts.mdc`) та дедупає прогони за станом git-дерева, тому
|
|
178
|
-
паралельні виклики `runLintPython()` не перетинатимуться по запуску
|
|
179
|
-
`uv`.
|
|
180
|
-
- Side effects: ті самі, що й у `runLintPythonSteps`, плюс блокування на
|
|
181
|
-
файловому локу.
|
|
182
|
-
|
|
183
|
-
## CLI-вхід (верхній рівень модуля)
|
|
184
|
-
|
|
185
|
-
```js
|
|
186
|
-
if (isRunAsCli(import.meta.url)) {
|
|
187
|
-
process.exitCode = await runLintPython()
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
- Перевірка `isRunAsCli(import.meta.url)` встановлює, чи запущений файл
|
|
192
|
-
безпосередньо як CLI-точка входу (наприклад, `node lint.mjs` або через
|
|
193
|
-
`n-cursor`), а не імпортований як модуль.
|
|
194
|
-
- Якщо так — виконується top-level `await runLintPython()`, а результат
|
|
195
|
-
кладеться у `process.exitCode`. Це означає, що Node завершиться з цим
|
|
196
|
-
кодом після того, як event loop спорожніє.
|
|
197
|
-
- Якщо файл імпортовано як модуль, цей блок не виконується — викликач сам
|
|
198
|
-
вирішує, як використати експортовані функції.
|
|
199
|
-
|
|
200
|
-
## Залежності
|
|
201
|
-
|
|
202
|
-
### Стандартна бібліотека Node.js
|
|
203
|
-
|
|
204
|
-
- `node:child_process` → `spawnSync` — синхронний запуск зовнішніх процесів
|
|
205
|
-
(`uv`, `uv run …`).
|
|
206
|
-
- `node:fs` → `existsSync` — перевірка наявності `pyproject.toml`.
|
|
207
|
-
- `node:path` → `join` — побудова повного шляху до `pyproject.toml` від `cwd`.
|
|
208
|
-
|
|
209
|
-
### Внутрішні модулі репозиторію
|
|
210
|
-
|
|
211
|
-
- `../../../scripts/cli-entry.mjs` → `isRunAsCli` — детекція CLI-режиму
|
|
212
|
-
через `import.meta.url`.
|
|
213
|
-
- `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` —
|
|
214
|
-
фабрика репортера з методами `pass`, `fail`, `getExitCode`. Цей патерн
|
|
215
|
-
єдиний для всіх лінт-кроків.
|
|
216
|
-
- `../../../scripts/utils/resolve-cmd.mjs` → `resolveCmd` — пошук
|
|
217
|
-
виконуваного файлу в `PATH` (повертає абсолютний шлях або `null`).
|
|
218
|
-
- `../../../scripts/lib/run-standard-lint.mjs` → `runStandardLint` —
|
|
219
|
-
стандартизована обгортка над лінт-кроком (глобальний лок + дедуплікація
|
|
220
|
-
за станом git-дерева).
|
|
221
|
-
|
|
222
|
-
### Зовнішні бінарники (runtime-залежності)
|
|
223
|
-
|
|
224
|
-
- `uv` — обовʼязковий у `PATH`, якщо в репозиторії є `pyproject.toml`.
|
|
225
|
-
- `ruff` — опційний, перевіряється через `uv run --frozen ruff --version`.
|
|
226
|
-
- `mypy` — опційний, перевіряється через `uv run --frozen mypy --version`.
|
|
227
|
-
|
|
228
|
-
### Артефакти у проєкті
|
|
229
|
-
|
|
230
|
-
- `pyproject.toml` (у корені `cwd`) — тригер запуску Python-частини.
|
|
231
|
-
- `uv.lock` — використовується `uv lock --check` та `uv sync --frozen`,
|
|
232
|
-
має бути актуальним.
|
|
233
|
-
|
|
234
|
-
## Потік виконання / Використання
|
|
235
|
-
|
|
236
|
-
### Сценарій 1: Python-частини немає
|
|
237
|
-
|
|
238
|
-
1. `runLintPython()` → `runStandardLint(...)` → `runLintPythonSteps()`.
|
|
239
|
-
2. `existsSync('<cwd>/pyproject.toml')` повертає `false`.
|
|
240
|
-
3. Репортер фіксує pass-повідомлення «немає pyproject.toml у корені — кроки
|
|
241
|
-
Python пропущено».
|
|
242
|
-
4. Повертається `0`.
|
|
243
|
-
|
|
244
|
-
### Сценарій 2: Python є, але `uv` не встановлений
|
|
245
|
-
|
|
246
|
-
1. `existsSync('pyproject.toml')` → `true`.
|
|
247
|
-
2. `resolveCmd('uv')` → `null`.
|
|
248
|
-
3. `fail('lint-python: `uv` не знайдено в PATH ...')`.
|
|
249
|
-
4. Повертається `1`.
|
|
250
|
-
|
|
251
|
-
### Сценарій 3: Повний прогон з усіма лінтерами
|
|
252
|
-
|
|
253
|
-
1. `uv lock --check` — перевірка lock-файлу. За невдачі вихід `1`.
|
|
254
|
-
2. `uv sync --frozen` — інсталяція середовища строго за `uv.lock`. За
|
|
255
|
-
невдачі вихід `1`.
|
|
256
|
-
3. `uvToolAvailable(uv, 'ruff')` → `true` → `uv run --frozen ruff check
|
|
257
|
-
--fix .`. Може **змінити файли**.
|
|
258
|
-
4. `uv run --frozen ruff format .`. Також може **змінити файли**.
|
|
259
|
-
5. `uvToolAvailable(uv, 'mypy')` → `true` → `uv run --frozen mypy .`.
|
|
260
|
-
Лише читає, не змінює дерево.
|
|
261
|
-
6. Якщо всі кроки повернули `0` — підсумок `0`. Інакше — перший
|
|
262
|
-
ненульовий код розриває послідовність і повертається.
|
|
263
|
-
|
|
264
|
-
### Сценарій 4: `ruff` або `mypy` не встановлені у uv-середовищі
|
|
265
|
-
|
|
266
|
-
- Для відповідного інструмента `uvToolAvailable` поверне `false`.
|
|
267
|
-
- Виводиться pass-повідомлення «<tool> недоступний у uv-середовищі —
|
|
268
|
-
крок пропущено».
|
|
269
|
-
- Інші кроки виконуються штатно.
|
|
270
|
-
|
|
271
|
-
### Як викликати з коду
|
|
272
|
-
|
|
273
|
-
```js
|
|
274
|
-
import { runLintPython, runLintPythonSteps } from './lint.mjs'
|
|
275
|
-
|
|
276
|
-
// Стандартний шлях: з локом, дедуплікацією, асинхронно.
|
|
277
|
-
const code = await runLintPython()
|
|
278
|
-
process.exit(code)
|
|
279
|
-
|
|
280
|
-
// Прямий виклик без локу (наприклад, у тестах або з власною серіалізацією):
|
|
281
|
-
const codeRaw = runLintPythonSteps('/path/to/repo')
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Як викликати з CLI
|
|
285
|
-
|
|
286
|
-
Файл є виконуваною точкою входу для лінт-пайплайну. У звичайному монорепо
|
|
287
|
-
він викликається через спільний раннер (`n-cursor`, `bun run lint` тощо).
|
|
288
|
-
Прямий запуск:
|
|
289
|
-
|
|
290
|
-
```bash
|
|
291
|
-
node npm/rules/python/lint/lint.mjs
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Кодом виходу буде число з `runLintPython()` (`0` — OK, `1` — є помилки).
|
|
295
|
-
|
|
296
|
-
## Rebuild Test
|
|
297
|
-
|
|
298
|
-
За цією документацією можна відтворити модуль так:
|
|
299
|
-
|
|
300
|
-
1. Створити ES Module-файл, що імпортує `spawnSync` з `node:child_process`,
|
|
301
|
-
`existsSync` з `node:fs`, `join` з `node:path`, а також `isRunAsCli`,
|
|
302
|
-
`createCheckReporter`, `resolveCmd`, `runStandardLint` з відповідних
|
|
303
|
-
шляхів `../../../scripts/...`.
|
|
304
|
-
2. Реалізувати приватну `runTool(label, cmd, args, pass, fail)`:
|
|
305
|
-
`spawnSync` з `stdio: 'inherit'`, `shell: false`; при `status === 0`
|
|
306
|
-
викликати `pass`, інакше `fail` з кодом (типу number або `1` при
|
|
307
|
-
неприродному завершенні); повертати `boolean`.
|
|
308
|
-
3. Реалізувати `uvToolAvailable(uv, tool)`: `spawnSync(uv, ['run',
|
|
309
|
-
'--frozen', tool, '--version'], { stdio: 'ignore', shell: false })` →
|
|
310
|
-
`r.status === 0`.
|
|
311
|
-
4. Експортувати `runLintPythonSteps(cwd = process.cwd())`:
|
|
312
|
-
- створити репортер;
|
|
313
|
-
- якщо `pyproject.toml` відсутній → `pass(...)` і повернути код;
|
|
314
|
-
- резолвити `uv`; якщо немає → `fail(...)` і повернути;
|
|
315
|
-
- послідовно: `uv lock --check`, `uv sync --frozen` (обовʼязкові);
|
|
316
|
-
- опційні через локальну функцію-замикач `runOptionalUvTool`: `ruff
|
|
317
|
-
check --fix .`, `ruff format .`, `mypy .` — кожен через
|
|
318
|
-
`uvToolAvailable` + `runTool`;
|
|
319
|
-
- повернути `reporter.getExitCode()`.
|
|
320
|
-
5. Експортувати `runLintPython = () => runStandardLint(import.meta.dirname,
|
|
321
|
-
runLintPythonSteps)`.
|
|
322
|
-
6. На верхньому рівні: `if (isRunAsCli(import.meta.url)) process.exitCode =
|
|
323
|
-
await runLintPython()`.
|
|
324
|
-
|
|
325
|
-
Результат повинен поведінково збігтися з оригіналом: ті самі повідомлення,
|
|
326
|
-
ті самі коди виходу, така ж серіалізація та обробка опційних інструментів.
|
|
29
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -63,9 +63,11 @@ function uvToolAvailable(uv, tool) {
|
|
|
63
63
|
/**
|
|
64
64
|
* Внутрішні кроки `lint-python` без локу.
|
|
65
65
|
* @param {string} [cwd] корінь репозиторію
|
|
66
|
+
* @param {{ readOnly?: boolean }} [opts] readOnly → `ruff` без `--fix`, `ruff format --check` (нуль мутацій, CI/детект)
|
|
66
67
|
* @returns {number} 0 — OK, 1 — є помилки
|
|
67
68
|
*/
|
|
68
|
-
export function runLintPythonSteps(cwd = process.cwd()) {
|
|
69
|
+
export function runLintPythonSteps(cwd = process.cwd(), opts = {}) {
|
|
70
|
+
const readOnly = opts.readOnly === true
|
|
69
71
|
const reporter = createCheckReporter()
|
|
70
72
|
const { pass, fail } = reporter
|
|
71
73
|
|
|
@@ -98,8 +100,10 @@ export function runLintPythonSteps(cwd = process.cwd()) {
|
|
|
98
100
|
return runTool(label, uv, ['run', '--frozen', tool, ...args], pass, fail)
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
const ruffCheck = readOnly ? ['check', '.'] : ['check', '--fix', '.']
|
|
104
|
+
const ruffFormat = readOnly ? ['format', '--check', '.'] : ['format', '.']
|
|
105
|
+
if (!runOptionalUvTool('ruff', readOnly ? 'ruff check' : 'ruff check --fix', ruffCheck)) return reporter.getExitCode()
|
|
106
|
+
if (!runOptionalUvTool('ruff', readOnly ? 'ruff format --check' : 'ruff format', ruffFormat)) return reporter.getExitCode()
|
|
103
107
|
if (!runOptionalUvTool('mypy', 'mypy', ['.'])) return reporter.getExitCode()
|
|
104
108
|
|
|
105
109
|
return reporter.getExitCode()
|
|
@@ -107,10 +111,12 @@ export function runLintPythonSteps(cwd = process.cwd()) {
|
|
|
107
111
|
|
|
108
112
|
/**
|
|
109
113
|
* Публічна CLI-форма: серіалізує через `withLock('lint-python')` + дедуп за станом git-дерева.
|
|
114
|
+
* @param {{ readOnly?: boolean }} [opts] readOnly → детект без мутацій (проброс у кроки)
|
|
110
115
|
* @returns {Promise<number>} код виходу
|
|
111
116
|
*/
|
|
112
|
-
export const runLintPython = () =>
|
|
117
|
+
export const runLintPython = (opts = {}) =>
|
|
118
|
+
runStandardLint(import.meta.dirname, () => runLintPythonSteps(process.cwd(), opts))
|
|
113
119
|
|
|
114
120
|
if (isRunAsCli(import.meta.url)) {
|
|
115
|
-
process.exitCode = await runLintPython()
|
|
121
|
+
process.exitCode = await runLintPython({ readOnly: process.argv.includes('--read-only') })
|
|
116
122
|
}
|
package/rules/python/meta.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": { "glob": "pyproject.toml" } }
|
|
1
|
+
{ "auto": { "glob": "pyproject.toml" }, "lint": "full" }
|
package/rules/python/python.mdc
CHANGED
|
@@ -27,9 +27,7 @@ Python-проєкти ведуться **виключно** на [uv](https://do
|
|
|
27
27
|
|
|
28
28
|
## lint-python
|
|
29
29
|
|
|
30
|
-
Інструменти uv-екосистеми не мають єдиного CLI, що сам обходить репозиторій, тому
|
|
31
|
-
|
|
32
|
-
- Канон `package.json#scripts.lint-python` (substring requirement): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
|
|
30
|
+
Інструменти uv-екосистеми не мають єдиного CLI, що сам обходить репозиторій, тому python-лінт делегується у JS-скрипт-обгортку. Запуск — через **`n-cursor lint python`** (CI — `--read-only`); окремого `package.json`-скрипта немає.
|
|
33
31
|
|
|
34
32
|
Скрипт `rules/python/lint/lint.mjs`:
|
|
35
33
|
|
package/rules/rego/rego.mdc
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Opa, Rego — інструментарій (VS Code,
|
|
2
|
+
description: Opa, Rego — інструментарій (VS Code, opa/regal)
|
|
3
3
|
version: '1.1'
|
|
4
4
|
globs: "**/*.rego"
|
|
5
5
|
alwaysApply: false
|
|
@@ -12,17 +12,13 @@ alwaysApply: false
|
|
|
12
12
|
## Перевірка
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
15
|
+
n-cursor lint rego
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Цілі — `npm/rules/` (рекурсивно знаходить `.rego` у `<rule>/policy/<concern>/`). Інші *.rego поза деревом додай у `LINT_TARGETS` у `npm/rules/rego/lint/lint.mjs`.
|
|
19
19
|
|
|
20
20
|
`opa` і `regal` — лише у `PATH`, **не** додавай у `dependencies` / `devDependencies`.
|
|
21
21
|
|
|
22
|
-
### `package.json`
|
|
23
|
-
|
|
24
|
-
- Канон `scripts.lint-rego`: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
|
|
25
|
-
|
|
26
22
|
### `.vscode/extensions.json`
|
|
27
23
|
|
|
28
24
|
- Канон `recommendations` має містити `tsandall.opa` (LSP, format-on-save через `opa fmt`): [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: lint.mjs
|
|
4
|
+
resource: npm/rules/rust/js/lint.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 5d7c4123
|
|
7
|
+
score: 100
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Оркестраторний адаптер правила `rust` для `n-cursor lint`: rustfmt + clippy через `cargo`. Запускається на `n-cursor lint rust`. За відсутності `Cargo.toml` у корені — no-op (вихід 0). `cargo`/`rustfmt`/`clippy` резолвляться з PATH (Rust toolchain через rustup), не з npm-залежностей; якщо `cargo` відсутній за наявного `Cargo.toml` — помилка.
|
|
11
|
+
|
|
12
|
+
## Поведінка
|
|
13
|
+
|
|
14
|
+
1. `readOnly` (CI): `cargo fmt --all -- --check` + `cargo clippy --all-targets --all-features -- -D warnings` — детект без мутацій.
|
|
15
|
+
2. fix-режим: `cargo fmt --all` + `cargo clippy --fix` + фінальний `cargo clippy … -D warnings`.
|
|
16
|
+
3. Перший ненульовий cargo-крок спиняє ланцюг і повертає його код.
|
|
17
|
+
|
|
18
|
+
## Гарантії поведінки
|
|
19
|
+
|
|
20
|
+
- Read-only за наявності `readOnly`: cargo не мутує робоче дерево (`--check`, без `--fix`).
|
|
21
|
+
- Не звертається до мережі напряму (cargo-кроки можуть тягнути crates, але це поведінка тулчейну).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** @see ./docs/lint.md */
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
7
|
+
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Запускає cargo-крок і репортить результат.
|
|
11
|
+
* @param {string} label назва кроку
|
|
12
|
+
* @param {string} cargo абсолютний шлях до `cargo`
|
|
13
|
+
* @param {string[]} args аргументи
|
|
14
|
+
* @param {(m: string) => void} pass callback pass
|
|
15
|
+
* @param {(m: string) => void} fail callback fail
|
|
16
|
+
* @returns {boolean} true якщо крок успішний
|
|
17
|
+
*/
|
|
18
|
+
function runCargo(label, cargo, args, pass, fail) {
|
|
19
|
+
const r = spawnSync(cargo, args, { stdio: 'inherit', shell: false })
|
|
20
|
+
if (r.status === 0) {
|
|
21
|
+
pass(`lint-rust: ${label} — OK`)
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
const code = typeof r.status === 'number' ? r.status : 1
|
|
25
|
+
fail(`lint-rust: ${label} — помилка (код ${code}, rust.mdc)`)
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Оркестраторний адаптер `n-cursor lint rust`: rustfmt + clippy через cargo. Без `Cargo.toml` —
|
|
31
|
+
* no-op (0). `cargo`/`rustfmt`/`clippy` — Rust toolchain (rustup), не npm-залежності.
|
|
32
|
+
* readOnly (CI): `cargo fmt --all -- --check` + `cargo clippy … -D warnings` (нуль мутацій).
|
|
33
|
+
* fix: `cargo fmt --all` + `cargo clippy --fix` + фінальний `cargo clippy … -D warnings`.
|
|
34
|
+
* @param {string[] | undefined} _files ігнорується (cargo обходить crate сам)
|
|
35
|
+
* @param {string} [cwd] корінь
|
|
36
|
+
* @param {{ readOnly?: boolean }} [opts] readOnly → без мутацій
|
|
37
|
+
* @returns {number} exit code
|
|
38
|
+
*/
|
|
39
|
+
export function lint(_files, cwd = process.cwd(), opts = {}) {
|
|
40
|
+
const readOnly = opts.readOnly === true
|
|
41
|
+
const reporter = createCheckReporter()
|
|
42
|
+
const { pass, fail } = reporter
|
|
43
|
+
|
|
44
|
+
if (!existsSync(join(cwd, 'Cargo.toml'))) {
|
|
45
|
+
pass('lint-rust: немає Cargo.toml — кроки Rust пропущено')
|
|
46
|
+
return reporter.getExitCode()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cargo = resolveCmd('cargo')
|
|
50
|
+
if (!cargo) {
|
|
51
|
+
fail('lint-rust: `cargo` не знайдено в PATH (Rust toolchain через rustup, rust.mdc)')
|
|
52
|
+
return reporter.getExitCode()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fmtArgs = readOnly ? ['fmt', '--all', '--', '--check'] : ['fmt', '--all']
|
|
56
|
+
if (!runCargo(readOnly ? 'cargo fmt --check' : 'cargo fmt', cargo, fmtArgs, pass, fail)) {
|
|
57
|
+
return reporter.getExitCode()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!readOnly) {
|
|
61
|
+
const fixArgs = ['clippy', '--fix', '--allow-staged', '--allow-dirty', '--all-targets', '--all-features']
|
|
62
|
+
if (!runCargo('cargo clippy --fix', cargo, fixArgs, pass, fail)) return reporter.getExitCode()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
runCargo('cargo clippy -D warnings', cargo, ['clippy', '--all-targets', '--all-features', '--', '-D', 'warnings'], pass, fail)
|
|
66
|
+
return reporter.getExitCode()
|
|
67
|
+
}
|
package/rules/rust/rust.mdc
CHANGED
|
@@ -5,12 +5,10 @@ alwaysApply: false
|
|
|
5
5
|
version: '1.4'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
**rustfmt** ([rust-lang/rustfmt](https://github.com/rust-lang/rustfmt)) — форматер; **clippy** ([rust-lang/rust-clippy](https://github.com/rust-lang/rust-clippy)) — лінтер.
|
|
8
|
+
**rustfmt** ([rust-lang/rustfmt](https://github.com/rust-lang/rustfmt)) — форматер; **clippy** ([rust-lang/rust-clippy](https://github.com/rust-lang/rust-clippy)) — лінтер. Запуск — через **`n-cursor lint rust`** (адаптер `js/lint.mjs`): локально (fix) `cargo fmt --all` → `cargo clippy --fix --allow-staged --allow-dirty --all-targets --all-features` → фінальний `cargo clippy --all-targets --all-features -- -D warnings`; у `--read-only` (детект) — `cargo fmt --all -- --check` + `cargo clippy ... -- -D warnings`. Окремого `package.json`-скрипта немає. У CI cargo викликається напряму (див. `lint-rust.yml`).
|
|
9
9
|
|
|
10
10
|
`cargo`, `rustfmt`, `clippy` не додавай у `devDependencies` — це Rust toolchain, ставиться через `rustup` локально або через `dtolnay/rust-toolchain@stable` у CI.
|
|
11
11
|
|
|
12
|
-
Канон `scripts.lint-rust` (substring requirement): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
|
|
13
|
-
|
|
14
12
|
У `.vscode/extensions.json` `recommendations` мають містити `rust-lang.rust-analyzer` і `tamasfe.even-better-toml`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
|
|
15
13
|
|
|
16
14
|
Канон workflow `.github/workflows/lint-rust.yml`: [lint-rust.yml.snippet.yml](./policy/lint_rust_yml/template/lint-rust.yml.snippet.yml)
|
|
@@ -21,7 +19,7 @@ version: '1.4'
|
|
|
21
19
|
|
|
22
20
|
Tauri-проєкт завжди має `src-tauri/Cargo.toml`, тому правило `rust` активується автоматично разом з `tauri`. Поділ обов'язків:
|
|
23
21
|
|
|
24
|
-
- `rust` — `lint
|
|
22
|
+
- `rust` — лінт через `n-cursor lint rust`, `rust-analyzer`, `even-better-toml`, CI workflow.
|
|
25
23
|
- `tauri` — `tauri-apps.tauri-vscode` (див. **tauri.mdc**).
|
|
26
24
|
|
|
27
25
|
Обидва правила перевіряють `.vscode/extensions.json` за `contains`-семантикою; конкурентного запису немає.
|
|
@@ -5,14 +5,6 @@ package security.package_json
|
|
|
5
5
|
|
|
6
6
|
import rego.v1
|
|
7
7
|
|
|
8
|
-
# ── deny: кожен snippet leaf має співпадати з input ──────────────────────────
|
|
9
|
-
deny contains msg if {
|
|
10
|
-
some script_name, expected in data.template.snippet.scripts
|
|
11
|
-
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
12
|
-
actual != expected
|
|
13
|
-
msg := sprintf("package.json: scripts.%s має бути %q (security.mdc)", [script_name, expected])
|
|
14
|
-
}
|
|
15
|
-
|
|
16
8
|
# ── deny: жодного ключа з deny у dependencies/devDependencies ────────────────
|
|
17
9
|
deny contains msg if {
|
|
18
10
|
some pkg, reason in data.template.deny.dependencies
|
|
@@ -25,14 +17,3 @@ deny contains msg if {
|
|
|
25
17
|
pkg in object.keys(object.get(input, "devDependencies", {}))
|
|
26
18
|
msg := sprintf("package.json: devDependencies.%s — %s (security.mdc)", [pkg, reason])
|
|
27
19
|
}
|
|
28
|
-
|
|
29
|
-
# ── deny: рядкові поля з contains мають містити кожен substring ──────────────
|
|
30
|
-
# Перевіряємо лише наявні поля (якщо `scripts.<name>` відсутній — поле опціональне).
|
|
31
|
-
deny contains msg if {
|
|
32
|
-
some script_name, needles in data.template.contains.scripts
|
|
33
|
-
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
34
|
-
actual != ""
|
|
35
|
-
some needle in needles
|
|
36
|
-
not contains(actual, needle)
|
|
37
|
-
msg := sprintf("package.json: scripts.%s має містити %q (security.mdc)", [script_name, needle])
|
|
38
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Локальний та CI-секюріті-лінт через TruffleHog —
|
|
2
|
+
description: Локальний та CI-секюріті-лінт через TruffleHog — `.trufflehog-exclude`, канонічний placeholder `sample-secret` у прикладних файлах
|
|
3
3
|
globs: "**/.trufflehog-exclude,**/package.json,**/.github/workflows/**/*.yml"
|
|
4
4
|
alwaysApply: false
|
|
5
5
|
version: '2.1'
|
|
@@ -7,10 +7,10 @@ version: '2.1'
|
|
|
7
7
|
|
|
8
8
|
[TruffleHog](https://github.com/trufflesecurity/trufflehog) — глобальний CLI (як `shellcheck`, `conftest`); **не** додавай до `dependencies`/`devDependencies`.
|
|
9
9
|
|
|
10
|
-
## Канон `package.json
|
|
10
|
+
## Канон `package.json`
|
|
11
|
+
|
|
12
|
+
Скан запускається через `n-cursor lint security` (CI — `n-cursor lint security --read-only`); окремого `lint-*` скрипта в `package.json` немає.
|
|
11
13
|
|
|
12
|
-
- `lint-security` скрипт: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
|
|
13
|
-
- `lint` агрегатор повинен містити: [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
|
|
14
14
|
- Заборонено `trufflehog` у `dependencies`/`devDependencies`: [package.json.deny.json](./policy/package_json/template/package.json.deny.json)
|
|
15
15
|
|
|
16
16
|
**Зауваження:**
|
|
@@ -19,8 +19,7 @@ version: '2.1'
|
|
|
19
19
|
- `--no-update` — вимикає self-update check (CI-friendly).
|
|
20
20
|
- `--exclude-paths .trufflehog-exclude` — файл з regex-patterns, які треба пропускати (аналог `[allowlist].paths` із gitleaks).
|
|
21
21
|
- `--results=verified,unknown` — показує лише верифіковані секрети + ті, що TruffleHog не зміг перевірити (`unverified` дублікат відсіюється).
|
|
22
|
-
- `--fail` — exit-code `183` за наявності знахідок (
|
|
23
|
-
- Позиція в `lint`: за конвенцією після інших `lint-*` і перед `oxfmt`.
|
|
22
|
+
- `--fail` — exit-code `183` за наявності знахідок (щоб лінт падав).
|
|
24
23
|
|
|
25
24
|
## `.trufflehog-exclude` (рекомендована основа)
|
|
26
25
|
|