agent-quality-kit 0.5.0 → 0.6.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/README.md +36 -1
- package/README.ru.md +19 -1
- package/kit/docs/ready-made-rules.md +40 -0
- package/kit/gates/README.md +20 -0
- package/kit/gates/ci-actually-fails/README.md +42 -0
- package/kit/gates/ci-actually-fails/check.sh +93 -0
- package/kit/gates/ci-actually-fails/gate.yml +14 -0
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +14 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/ci.yml +12 -0
- package/kit/gates/gate-not-weakened/README.md +54 -0
- package/kit/gates/gate-not-weakened/check.sh +72 -0
- package/kit/gates/gate-not-weakened/gate.yml +15 -0
- package/kit/gates/gate-not-weakened/green/checkout.ts +8 -0
- package/kit/gates/gate-not-weakened/green/payments.py +6 -0
- package/kit/gates/gate-not-weakened/green/release.sh +2 -0
- package/kit/gates/gate-not-weakened/red/checkout.ts +9 -0
- package/kit/gates/gate-not-weakened/red/payments.py +6 -0
- package/kit/gates/gate-not-weakened/red/release.sh +2 -0
- package/kit/gates/promise-has-gate/README.md +50 -0
- package/kit/gates/promise-has-gate/check.sh +88 -0
- package/kit/gates/promise-has-gate/gate.yml +14 -0
- package/kit/gates/promise-has-gate/green/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/green/AGENTS.md +7 -0
- package/kit/gates/promise-has-gate/red/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/red/AGENTS.md +7 -0
- package/kit/gates/test-has-assertion/README.md +47 -0
- package/kit/gates/test-has-assertion/check.sh +194 -0
- package/kit/gates/test-has-assertion/gate.yml +15 -0
- package/kit/gates/test-has-assertion/green/checkout.test.ts +9 -0
- package/kit/gates/test-has-assertion/green/test_billing.py +17 -0
- package/kit/gates/test-has-assertion/red/checkout.test.ts +8 -0
- package/kit/gates/test-has-assertion/red/test_billing.py +14 -0
- package/kit/rules/general.md +14 -0
- package/llms.txt +1 -0
- package/package.json +3 -2
- package/tool/commands/doctor.mjs +44 -4
- package/tool/commands/gates.mjs +10 -2
- package/tool/commands/project.mjs +7 -1
- package/tool/i18n/en.mjs +17 -0
- package/tool/i18n/ru.mjs +18 -0
- package/tool/i18n/templates-en.mjs +9 -9
- package/tool/i18n/templates-ru.mjs +9 -9
- package/tool/lib/manifest.mjs +37 -1
- package/tool/lib/scope.mjs +96 -0
- package/tool/program.mjs +1 -0
- package/tool/selfcheck/gates.sh +20 -3
- package/tool/selfcheck/lifecycle.mjs +29 -0
- package/tool/selfcheck/smoke.sh +53 -0
- package/tool/selfcheck/units.mjs +85 -1
package/README.md
CHANGED
|
@@ -196,7 +196,7 @@ Already using [pre-commit](https://pre-commit.com)? Three lines in the file you
|
|
|
196
196
|
```yaml
|
|
197
197
|
repos:
|
|
198
198
|
- repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
|
|
199
|
-
rev: v0.
|
|
199
|
+
rev: v0.6.0
|
|
200
200
|
hooks:
|
|
201
201
|
- id: aqk # runs what the repository declares; blocks below AQK-1
|
|
202
202
|
# - id: aqk-doctor # read-only: the level and what is missing, blocks nothing
|
|
@@ -236,9 +236,26 @@ aqk find "print statements in production" # is there already such a gate — m
|
|
|
236
236
|
aqk doctor # what applies to this repository and what is missing
|
|
237
237
|
aqk add secrets-not-in-code # copies the check and its samples in, declares it
|
|
238
238
|
aqk doctor --run # runs the declared gates and shows the result
|
|
239
|
+
aqk doctor --run --since main # ... but only what the diff introduced
|
|
239
240
|
aqk ratchet no-print-in-prod # existing violations become debt, new ones are blocked
|
|
240
241
|
```
|
|
241
242
|
|
|
243
|
+
### The first run on a real project
|
|
244
|
+
|
|
245
|
+
An established repository carries years of debt. Run every gate over all of it and you get a wall
|
|
246
|
+
of red that nobody reads — so the tool gets switched off. `--since <ref>` narrows the output to
|
|
247
|
+
files the diff touched:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
aqk doctor --run --since main # only what this branch introduced
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Three outcomes, all of them said out loud. Findings inside the diff — red, as usual. Findings only
|
|
254
|
+
outside it — green, with the number that was hidden, never a silent "all clear". And a gate whose
|
|
255
|
+
output carries no paths at all (a commit-message check, a CI-config check) **cannot** be narrowed:
|
|
256
|
+
it stays red, and says why. Calling it green because there was nothing to narrow would be exactly
|
|
257
|
+
the silence this tool exists to remove.
|
|
258
|
+
|
|
242
259
|
Every `doctor --run` rewrites `.aqk/last-run.md` — a short report of what actually ran and how
|
|
243
260
|
long it took. The list of gates in the manifest says nothing about how many of them are alive
|
|
244
261
|
right now; the report does. The file is ephemeral — keep it in your own `.gitignore`.
|
|
@@ -289,6 +306,24 @@ catalogue may grow to hundreds of entries; a given project still sees about a do
|
|
|
289
306
|
An entry is accepted only if its arbiter goes red on the red sample, stays quiet on the green
|
|
290
307
|
one, and names a real failure it caught. A machine checks this: `bash tool/selfcheck/gates.sh`.
|
|
291
308
|
|
|
309
|
+
### Four entries that watch the agent, not the code
|
|
310
|
+
|
|
311
|
+
Ruff, ESLint and gitleaks already find bad code, and AQK calls them where it can rather than
|
|
312
|
+
reinventing them. These four look elsewhere — at the moment the **signal** about bad code is
|
|
313
|
+
switched off, which is what a coding agent does when the task is phrased as "make it pass":
|
|
314
|
+
|
|
315
|
+
| Entry | What it catches |
|
|
316
|
+
|---|---|
|
|
317
|
+
| `gate-not-weakened` | the fix was a suppression, not a fix: bare `# noqa`, `eslint-disable` with no rule named, `@ts-ignore`, `--no-verify` |
|
|
318
|
+
| `ci-actually-fails` | a pipeline step that renders a verdict but cannot fail — `run: pytest \|\| true`, `continue-on-error: true` |
|
|
319
|
+
| `test-has-assertion` | a test that cannot fail: empty body, `assert True`, a skip with no reason given |
|
|
320
|
+
| `promise-has-gate` | a rule in `AGENTS.md` with no enforcer named — neither a gate nor, honestly, a human |
|
|
321
|
+
|
|
322
|
+
Each was measured on nineteen third-party repositories (~25 000 files) before it entered the
|
|
323
|
+
catalogue, and two further entries were **cancelled by that measurement**: one because
|
|
324
|
+
[`agents-lint`](https://github.com/giacomo/agents-lint) already does it better, one because
|
|
325
|
+
91 of its 120 findings turned out to be a legitimate pattern.
|
|
326
|
+
|
|
292
327
|
## The guides as a single file
|
|
293
328
|
|
|
294
329
|
```bash
|
package/README.ru.md
CHANGED
|
@@ -198,7 +198,7 @@ aqk badge --check # в конвейере: код 1 в тот день, ког
|
|
|
198
198
|
```yaml
|
|
199
199
|
repos:
|
|
200
200
|
- repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
|
|
201
|
-
rev: v0.
|
|
201
|
+
rev: v0.6.0
|
|
202
202
|
hooks:
|
|
203
203
|
- id: aqk # запускает объявленное; роняет коммит ниже AQK-1
|
|
204
204
|
# - id: aqk-doctor # только осмотр: уровень и чего не хватает, ничего не роняет
|
|
@@ -286,6 +286,24 @@ vendor/
|
|
|
286
286
|
Запись принимается, только если её арбитр краснеет на красном образце, молчит на зелёном и
|
|
287
287
|
назван реальный отказ, который она поймала. Проверяет это машина: `bash tool/selfcheck/gates.sh`.
|
|
288
288
|
|
|
289
|
+
### Четыре записи, которые смотрят на агента, а не на код
|
|
290
|
+
|
|
291
|
+
Ruff, ESLint и gitleaks и так находят плохой код — AQK зовёт их, где может, вместо того чтобы
|
|
292
|
+
писать своё. Эти четыре смотрят в другое место: на момент, когда **сигнал** о плохом коде
|
|
293
|
+
выключают. Именно это делает агент, когда задача сформулирована как «сделай, чтобы прошло»:
|
|
294
|
+
|
|
295
|
+
| Запись | Что ловит |
|
|
296
|
+
|---|---|
|
|
297
|
+
| `gate-not-weakened` | починкой было подавление: голый `# noqa`, `eslint-disable` без имени правила, `@ts-ignore`, `--no-verify` |
|
|
298
|
+
| `ci-actually-fails` | шаг конвейера, который выносит вердикт, но не может провалиться — `run: pytest \|\| true`, `continue-on-error: true` |
|
|
299
|
+
| `test-has-assertion` | тест, который не может провалиться: пустое тело, `assert True`, пропуск без причины |
|
|
300
|
+
| `promise-has-gate` | правило в `AGENTS.md`, у которого не назван сторож — ни гейт, ни, честно, человек |
|
|
301
|
+
|
|
302
|
+
Каждая измерена на девятнадцати чужих репозиториях (~25 000 файлов) до внесения в каталог, и ещё
|
|
303
|
+
две записи этот же замер **отменил**: одну — потому что
|
|
304
|
+
[`agents-lint`](https://github.com/giacomo/agents-lint) делает это лучше, другую — потому что
|
|
305
|
+
91 находка из 120 оказалась законным приёмом.
|
|
306
|
+
|
|
289
307
|
## Методички одним файлом
|
|
290
308
|
|
|
291
309
|
```bash
|
|
@@ -149,6 +149,46 @@ ruff check --select TRY400 --statistics . # сколько находок У
|
|
|
149
149
|
|
|
150
150
|
---
|
|
151
151
|
|
|
152
|
+
## Заглушка вместо реализации: почти всё уже покрыто
|
|
153
|
+
|
|
154
|
+
Самый ожидаемый способ, которым агент «заканчивает» задачу, — подпись без работы. Мы собирались
|
|
155
|
+
писать на это запись каталога и не стали: замер по девятнадцати репозиториям (~25 000 файлов)
|
|
156
|
+
показал, что своей доли почти не остаётся.
|
|
157
|
+
|
|
158
|
+
| Что | Чем ловится | Не забыть |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| пустое тело функции в JS и TS | `eslint` [`no-empty-function`](https://eslint.org/docs/latest/rules/no-empty-function) | функция с комментарием внутри не считается пустой |
|
|
161
|
+
| абстрактный метод, не переопределённый в конкретном классе | `pylint` [`W0223`](https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/abstract-method.html) | `--disable=all --enable=W0223` — остальное берёт ruff |
|
|
162
|
+
| `raise NotImplemented` вместо `NotImplementedError` | `ruff` [`F901`](https://docs.astral.sh/ruff/rules/raise-not-implemented/) | входит в группу `F` |
|
|
163
|
+
| лишний `pass` рядом с настоящим кодом | `ruff` `PIE790` | |
|
|
164
|
+
|
|
165
|
+
**Чего мы НЕ стали делать и почему.** Пустое тело (`pass`) — не признак недоделки: из 120 находок
|
|
166
|
+
первой версии 91 оказалась законной. Это null-объекты (`NoOpSpan` в sentry-python), безопасные
|
|
167
|
+
заглушки провайдера в pr-agent — там прямо стоит комментарий «safe no-op stubs», —
|
|
168
|
+
необязательные обработчики. А `raise NotImplementedError` в методе класса и есть питоновский
|
|
169
|
+
способ объявить абстракцию, даже без `abc`: так написаны `ContentDecoder` в httpx и интерфейс
|
|
170
|
+
плагина в pre-commit. За вычетом этих двух классов на 25 000 файлах не осталось ни одной находки.
|
|
171
|
+
|
|
172
|
+
## Обвес самого агента: тоже есть готовое
|
|
173
|
+
|
|
174
|
+
К осени 2026 появился отдельный класс инструментов — линтеры не кода, а того, что читает агент:
|
|
175
|
+
`AGENTS.md`, `CLAUDE.md`, файлы навыков, конфиги хуков и MCP. Писать своё здесь незачем.
|
|
176
|
+
|
|
177
|
+
| Инструмент | Что проверяет | Состояние на 2026-09-06 |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| [`agnix`](https://github.com/agent-sh/agnix) | 455 правил: структура `CLAUDE.md`/`AGENTS.md`/`SKILL.md`, синтаксис конфигов MCP и хуков, соглашения об именах, **мёртвые ссылки на файлы**. Есть автопочинка и LSP | 404 ⭐, Rust, активен. `npm i -g agnix`, `brew`, `pip`, `cargo` |
|
|
180
|
+
| [`agents-lint`](https://github.com/giacomo/agents-lint) | мёртвые npm-скрипты, упомянутые в `AGENTS.md`, устаревшие рамки, деревья каталогов в контексте | 13 ⭐, TypeScript, последний коммит март 2026 |
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npm install -g agnix && agnix --strict .
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Чего они НЕ делают — и почему у AQK остаётся своя половина.** Все они проверяют документ:
|
|
187
|
+
формат, существование путей, наличие скриптов. Ни один не спрашивает, **подкреплено ли обещание
|
|
188
|
+
командой с кодом возврата**. «Мы никогда не коммитим секреты» — грамматически безупречная
|
|
189
|
+
строка, на которую ни один из них ничего не скажет. Разделение простое: обвес агента проверяет
|
|
190
|
+
`agnix`, исполнимость обещаний — `promise-has-gate`.
|
|
191
|
+
|
|
152
192
|
## А если проект не на Python?
|
|
153
193
|
|
|
154
194
|
Ничего не меняется. Запись каталога держит **одно намерение и несколько исполнителей**, и
|
package/kit/gates/README.md
CHANGED
|
@@ -70,6 +70,26 @@
|
|
|
70
70
|
Плюс шестое, без которого запись не принимается: **доказательство** — реальный отказ, который
|
|
71
71
|
она поймала. «Это хорошая практика» не принимается.
|
|
72
72
|
|
|
73
|
+
## Зрелость записи не пишут руками
|
|
74
|
+
|
|
75
|
+
Седьмого поля нет: зрелость **считается** из доказательства. Ссылается `proof` на журнал шишек —
|
|
76
|
+
запись зрелая; не ссылается — условная, и это видно в приёмке каталога. Написать себе
|
|
77
|
+
`lifecycle: stable` нельзя, приёмка такую запись отклонит.
|
|
78
|
+
|
|
79
|
+
Это не придирка к форме. У всех трёх соседей, чей каталог мы разбирали, поле зрелости есть, и
|
|
80
|
+
у всех троих его заполняет автор: `lifecycle` у зондов Scorecard, `future`/`obsolete` у
|
|
81
|
+
критериев значка OpenSSF. Значение, написанное автором, означает доверие к автору. Каталог, где
|
|
82
|
+
зрелость объявляют, к сотне записей превращается в список, в котором нельзя выбрать.
|
|
83
|
+
|
|
84
|
+
Объявляется ровно одно состояние — **`deprecated`**, потому что «эту запись больше не ставят»
|
|
85
|
+
из её файлов не выводится никак. Вместе с ним обязателен `superseded_by` с именем существующей
|
|
86
|
+
записи, и `aqk add` тогда отказывает в установке, назвав преемника:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
lifecycle: deprecated
|
|
90
|
+
superseded_by: no-print-in-prod
|
|
91
|
+
```
|
|
92
|
+
|
|
73
93
|
## Запись без переносимого рецепта
|
|
74
94
|
|
|
75
95
|
Иногда переносимой проверки быть не может: чтобы понять, вызывают ли функцию, нужен граф
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Проверка в конвейере может провалиться
|
|
2
|
+
|
|
3
|
+
**Намерение.** Шаг выполнен, круг зелёный, проверка не сработала. `run: pytest || true` —
|
|
4
|
+
это строка в логе, а не проверка.
|
|
5
|
+
|
|
6
|
+
**Какой отказ это поймало.** Дыру нашли у себя. Запись `gates-run-in-ci` отвечает на вопрос
|
|
7
|
+
«упомянут ли гейт в конфиге конвейера» и на этом останавливается — то есть конфиг с
|
|
8
|
+
`run: pytest || true` проходил её зелёным. «Упомянут» и «работает» — разные утверждения, и весь
|
|
9
|
+
этот стандарт стоит на том, чтобы их не путать; у себя мы их спутали.
|
|
10
|
+
|
|
11
|
+
Замер по пятнадцати чужим репозиториям подтвердил, что класс живой: у `reviewdog` два шага с
|
|
12
|
+
его собственными линтерами идут под `continue-on-error: true`. Запись в журнале:
|
|
13
|
+
`incidents/README.md`, 2026-09-06.
|
|
14
|
+
|
|
15
|
+
**Что именно проверяется.** Конфиг разбирается по шагам. Шаг красный, если он **и** выносит
|
|
16
|
+
вердикт, **и** не может провалиться.
|
|
17
|
+
|
|
18
|
+
| Выносит вердикт, если | Не может провалиться, если |
|
|
19
|
+
|---|---|
|
|
20
|
+
| команда есть в списке запускалок (`pytest`, `eslint`, `go test`, `golangci-lint`, `mypy`, `cargo clippy`, …) | шаг помечен `continue-on-error: true` |
|
|
21
|
+
| команда объявлена гейтом в `.aqk.yml` **этого** проекта | задача помечена `allow_failure: true` (gitlab) |
|
|
22
|
+
| в **имени шага** стоит слово `lint`, `test`, `check`, `verify`, `audit`, `scan`, `coverage` | провал погашен в самой команде: `\|\| true`, `\|\| :`, `\|\| exit 0` |
|
|
23
|
+
|
|
24
|
+
Гашение прямо в команде красится только для закрытого списка запускалок: `docker network create … || true` — это идемпотентность, а не выключенная проверка.
|
|
25
|
+
|
|
26
|
+
**Готовый аналог.** Не нашли. [`actionlint`](https://github.com/rhysd/actionlint) разбирает
|
|
27
|
+
синтаксис workflow, [`zizmor`](https://github.com/woodruffw/zizmor) ищет в них дыры
|
|
28
|
+
безопасности — ни тот, ни другой не спрашивает, может ли шаг провалиться. `continue-on-error`
|
|
29
|
+
для них — законная настройка, каковой она и является: незаконной её делает то, ЧТО под ней
|
|
30
|
+
стоит, а это знает только проект.
|
|
31
|
+
|
|
32
|
+
**Чего НЕ ловит.**
|
|
33
|
+
|
|
34
|
+
- **Проверку, которую не по чему опознать.** Задача `mutation-diff` с именем «Mutation score on
|
|
35
|
+
changed files» под `continue-on-error` (нашлась в `kodus-ai`, автор сам пометил её «advisory —
|
|
36
|
+
does not block») не опознаётся: в имени нет слова-приметы, а команда не из списка. Объяви такую
|
|
37
|
+
проверку гейтом в `.aqk.yml` — тогда она станет видна точно, а не по догадке.
|
|
38
|
+
- **Провал, погашенный внутри скрипта.** `set +e`, `trap`, `exit 0` в конце `run: |` — это уже
|
|
39
|
+
логика скрипта, а не конфиг конвейера.
|
|
40
|
+
- **Шаг, который проходит по другой причине.** Тест, всегда возвращающий 0, — не эта запись,
|
|
41
|
+
а `test-has-assertion`.
|
|
42
|
+
- **`if: always()`** маскировкой не считается: он про порядок выполнения, а не про вердикт.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Конвейер, который гасит провал проверки: шаг выполнен, круг зелёный, проверка не сработала.
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ ОТДЕЛЬНО ОТ «гейт запускается конвейером». Та проверка отвечает на вопрос «упомянут ли»,
|
|
5
|
+
# и на этом останавливается. Мы нашли дыру в собственной оснастке: конфиг с `run: pytest || true`
|
|
6
|
+
# проходил её зелёным — команда упомянута, а провалиться не может никогда. «Упомянут» и
|
|
7
|
+
# «работает» — разные утверждения, и весь этот стандарт стоит на том, чтобы их не путать.
|
|
8
|
+
DIR="${1:-.}"
|
|
9
|
+
|
|
10
|
+
CI=$(find "$DIR/.github/workflows" "$DIR/.gitlab-ci.yml" "$DIR/.circleci" "$DIR/Jenkinsfile" \
|
|
11
|
+
-type f 2>/dev/null)
|
|
12
|
+
[ -z "$CI" ] && { echo "конвейера нет — эта проверка не про тебя"; exit 0; }
|
|
13
|
+
|
|
14
|
+
# Что считается ПРОВЕРКОЙ. Список намеренно закрытый: маскировка бывает законной — необязательная
|
|
15
|
+
# выгрузка отчёта, публикация артефакта, уведомление. Красить всякий `continue-on-error` значит
|
|
16
|
+
# получить гейт, который выключат первым. Красим только гашение того, что выносит вердикт.
|
|
17
|
+
RUNNERS='aqk|doctor --run|pytest|tox|nox|unittest|jest|vitest|mocha|jasmine|karma|playwright|cypress|eslint|tsc|ruff|flake8|pylint|mypy|pyright|bandit|semgrep|gitleaks|trivy|rubocop|golangci-lint|golint|govet|go vet|go test|staticcheck|shellcheck|hadolint|actionlint|codespell|reviewdog|cargo test|cargo clippy|mvn|gradle|phpstan|psalm|npm test|npm run (test|lint|check|typecheck)|yarn (test|lint)|pnpm (test|lint)|make (test|lint|check)'
|
|
18
|
+
|
|
19
|
+
# Закрытый список не поспевает: замер по чужим репозиториям нашёл шаг «Run reviewdog
|
|
20
|
+
# (github-pr-check)» под `continue-on-error: true`, и ни одно имя из списка в нём не звучало.
|
|
21
|
+
# Поэтому вторая примета — СЛОВО в имени шага или в команде. Целым словом: «checkout» не
|
|
22
|
+
# «check», иначе первый же `actions/checkout` красил бы каждый конвейер на свете.
|
|
23
|
+
WORDS='([Ll]int|[Tt]est|[Cc]heck|[Vv]erify|[Aa]udit|[Ss]can|[Tt]ypecheck|[Cc]overage)([^A-Za-z]|$)'
|
|
24
|
+
|
|
25
|
+
# Проверка из манифеста — тоже проверка, как бы она ни называлась в этом проекте.
|
|
26
|
+
MAN="$DIR/.aqk.yml"
|
|
27
|
+
if [ -f "$MAN" ]; then
|
|
28
|
+
KEYS=$(tr -d '\r' < "$MAN" | awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && /^[[:space:]]+[A-Za-z0-9_-]+:/{
|
|
29
|
+
sub(/^[[:space:]]*[A-Za-z0-9_-]*:[[:space:]]*/,""); gsub(/^"|"$/,"");
|
|
30
|
+
n=split($0,w," "); for(i=1;i<=n;i++) if (index(w[i],"/")) { print w[i]; break }
|
|
31
|
+
}')
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
BAD=""
|
|
35
|
+
for F in $CI; do
|
|
36
|
+
# Разбор ПО ШАГАМ, а не по строкам. Построчно проверка врала в обе стороны: законный
|
|
37
|
+
# `continue-on-error` на шаге выгрузки отчёта красил соседний шаг с тестами, а слово «test»
|
|
38
|
+
# внутри перечисления типов коммита («feat|fix|test|chore») делало проверкой строку, которая
|
|
39
|
+
# ничего не проверяет. Замер по чужим конвейерам дал 4 ложных из 10 — переписано на блоки.
|
|
40
|
+
#
|
|
41
|
+
# Шаг начинается элементом списка («- ») или ключом верхнего уровня: так устроен и github,
|
|
42
|
+
# и gitlab, где `allow_failure` живёт на уровне задачи.
|
|
43
|
+
RES=$(tr -d '\r' < "$F" | awk -v runners="$RUNNERS" -v words="$WORDS" -v keys="$KEYS" -v file="$F" '
|
|
44
|
+
function isComment(l) { return l ~ /^[[:space:]]*#/ }
|
|
45
|
+
function looksLikeCheck(l, j, nk) {
|
|
46
|
+
if (isComment(l)) return 0
|
|
47
|
+
# `uses:` — чужое действие. Его провал бывает законно необязательным: выгрузка отчёта,
|
|
48
|
+
# комментарий в пул-реквест, уведомление. Вердикт выносит то, что ЗАПУСКАЮТ.
|
|
49
|
+
if (l ~ /^[[:space:]]*(-[[:space:]]+)?uses[[:space:]]*:/) return 0
|
|
50
|
+
if (l ~ runners) return 1
|
|
51
|
+
# Проверка, объявленная в манифесте ЭТОГО проекта, — тоже проверка, как бы она ни
|
|
52
|
+
# называлась. Это самая точная примета из трёх: не догадка по имени, а список, который
|
|
53
|
+
# проект написал сам.
|
|
54
|
+
nk = split(keys, K, "\n")
|
|
55
|
+
for (j = 1; j <= nk; j++) if (K[j] != "" && index(l, K[j])) return 1
|
|
56
|
+
# Слово-примета — ТОЛЬКО в имени шага. В теле команды оно ловит своё же упоминание:
|
|
57
|
+
# «grep -oE (feat|fix|test|chore)» — это разбор заголовка коммита, а не проверка.
|
|
58
|
+
if (l ~ /^[[:space:]]*(-[[:space:]]+)?name[[:space:]]*:/ && l ~ words) return 1
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
function isMask(l) {
|
|
62
|
+
return !isComment(l) && l ~ /^[[:space:]]*(continue-on-error|allow_failure|ignore_failure)[[:space:]]*:[[:space:]]*(true|yes)/
|
|
63
|
+
}
|
|
64
|
+
function isBoundary(l) { return l ~ /^[[:space:]]*-[[:space:]]/ || l ~ /^[A-Za-z_.-]+[[:space:]]*:/ }
|
|
65
|
+
function flush( ) {
|
|
66
|
+
if (blockStart && blockCheck && blockMask)
|
|
67
|
+
printf "%s:%d: проверка не может провалиться — шаг под %s\n", file, blockCheckLine, blockMaskText
|
|
68
|
+
blockStart = 0; blockCheck = 0; blockMask = 0
|
|
69
|
+
}
|
|
70
|
+
{
|
|
71
|
+
# Гашение прямо в команде красится только для ЗАКРЫТОГО списка запускалок: «|| true» на
|
|
72
|
+
# вспомогательной команде внутри скрипта (`docker network create … || true`) — это
|
|
73
|
+
# идемпотентность, а не выключенная проверка.
|
|
74
|
+
if (!isComment($0) && $0 ~ runners && $0 ~ /\|\|[[:space:]]*(true|:|exit[[:space:]]+0)/) {
|
|
75
|
+
line = $0; sub(/^[[:space:]]+/, "", line)
|
|
76
|
+
printf "%s:%d: провал погашен прямо в команде: %s\n", file, NR, substr(line, 1, 90)
|
|
77
|
+
}
|
|
78
|
+
if (isBoundary($0)) flush()
|
|
79
|
+
if (!blockStart) blockStart = NR
|
|
80
|
+
if (!blockCheck && looksLikeCheck($0)) { blockCheck = 1; blockCheckLine = NR }
|
|
81
|
+
if (isMask($0)) { blockMask = 1; blockMaskText = $0; sub(/^[[:space:]]+/, "", blockMaskText) }
|
|
82
|
+
}
|
|
83
|
+
END { flush() }' 2>/dev/null)
|
|
84
|
+
[ -z "$RES" ] || BAD="$BAD$RES
|
|
85
|
+
"
|
|
86
|
+
done
|
|
87
|
+
|
|
88
|
+
LEFT="$(printf '%s' "$BAD" | grep -v '^$')"
|
|
89
|
+
[ -z "$LEFT" ] && exit 0
|
|
90
|
+
printf '%s\n' "$LEFT"
|
|
91
|
+
echo " почини: убери «|| true» и «continue-on-error» с шага, который выносит вердикт."
|
|
92
|
+
echo " шаг, который не может провалиться, — это не проверка, а строка в логе."
|
|
93
|
+
exit 1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
intent: шаг конвейера, выносящий вердикт, может провалиться — а не только выполниться
|
|
2
|
+
intent_en: a pipeline step that renders a verdict can actually fail, not merely run
|
|
3
|
+
|
|
4
|
+
# Только там, где конвейер есть. Проверять его отсутствие — дело другой записи.
|
|
5
|
+
trigger:
|
|
6
|
+
has_ci: true
|
|
7
|
+
|
|
8
|
+
recipes:
|
|
9
|
+
any: bash {gate}/check.sh {dir}
|
|
10
|
+
|
|
11
|
+
proof: incidents/README.md, 2026-09-06 «упомянут и работает — разные утверждения» — дыра
|
|
12
|
+
найдена в собственной оснастке (`gates-run-in-ci` пропускал `pytest || true` зелёным),
|
|
13
|
+
замер по пятнадцати чужим репозиториям подтвердил класс: у `reviewdog` два шага
|
|
14
|
+
собственных линтеров идут под `continue-on-error: true`
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Подавление проверки — точечное и с причиной
|
|
2
|
+
|
|
3
|
+
**Намерение.** Когда проверка краснеет, у пишущего два выхода: починить код или заглушить
|
|
4
|
+
проверку. Второй дешевле и с виду неотличим от первого — конвейер зелёный, диф маленький.
|
|
5
|
+
|
|
6
|
+
Для агента это выход по умолчанию: задача сформулирована как «сделай, чтобы прошло», и
|
|
7
|
+
`@ts-ignore` решает её буквально. Дефект при этом остаётся, а сигнал о нём исчезает навсегда —
|
|
8
|
+
это тот же класс, что молчащий гейт, только оплаченный одной строкой.
|
|
9
|
+
|
|
10
|
+
**Какой отказ это поймало.** Замер по пятнадцати чужим репозиториям (20 774 файла): 22 находки,
|
|
11
|
+
19 настоящих. Среди них — `// @ts-nocheck` первой строкой боевой страницы в `kodus-ai`
|
|
12
|
+
(проверка типов выключена для целого файла), `# noqa E501` без двоеточия в `pr-agent`
|
|
13
|
+
(flake8 такую форму читает как безадресный `# noqa`: погашено не одно правило, а все),
|
|
14
|
+
`/** @ts-ignore */` и `.strict().argv // eslint-disable-line` без имени правила в `repolinter`.
|
|
15
|
+
Запись в журнале: `incidents/README.md`, 2026-09-06.
|
|
16
|
+
|
|
17
|
+
**Что именно проверяется.** Красным делается только **безадресное** подавление:
|
|
18
|
+
|
|
19
|
+
| Красное | Зелёное |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `# noqa` | `# noqa: F401 — причина` |
|
|
22
|
+
| `# type: ignore` | `# type: ignore[no-any-return]` |
|
|
23
|
+
| `# flake8: noqa`, `# ruff: noqa`, `# mypy: ignore-errors`, `# pylint: disable=all` | точечная форма с кодом |
|
|
24
|
+
| `/* eslint-disable */`, `// eslint-disable-next-line` без правила | `// eslint-disable-next-line no-console -- причина` |
|
|
25
|
+
| `// @ts-ignore`, `// @ts-nocheck` | `// @ts-expect-error -- причина` |
|
|
26
|
+
| `#[allow(warnings)]`, `@SuppressWarnings("all")`, `//nolint`, `rubocop:disable all`, `shellcheck disable` без `=SC…` | те же с именем правила |
|
|
27
|
+
| `--no-verify` в скрипте или конфиге конвейера | обычный коммит |
|
|
28
|
+
|
|
29
|
+
`@ts-ignore` красный всегда — единственное исключение из правила «безадресное». У него есть
|
|
30
|
+
строго лучший брат: `@ts-expect-error` краснеет сам, как только становится не нужен, то есть
|
|
31
|
+
не переживает починку кода. `@ts-ignore` переживает и молчит дальше.
|
|
32
|
+
|
|
33
|
+
**Готовый аналог.** Есть, и под каждый язык свой:
|
|
34
|
+
[`eslint-plugin-eslint-comments`](https://github.com/mysticatea/eslint-plugin-eslint-comments)
|
|
35
|
+
с правилом `require-description` — для JavaScript и TypeScript;
|
|
36
|
+
[`flake8-noqa`](https://github.com/plinss/flake8-noqa) — для Python, он же ловит сломанную форму
|
|
37
|
+
`# noqa E501`. Если проект на одном языке — ставь их, они точнее: разбирают код, а не текст.
|
|
38
|
+
Эта запись нужна там, где языков несколько или где ставить нечего: она переносима, ничего не
|
|
39
|
+
требует установить и покрывает то, чего нет ни у одного из двоих — `@ts-ignore` как класс,
|
|
40
|
+
`--no-verify` в конвейере, `@SuppressWarnings("all")`.
|
|
41
|
+
|
|
42
|
+
**Чего НЕ ловит.**
|
|
43
|
+
|
|
44
|
+
- **Подавление внутри строкового литерала считается настоящим.** Фикстуры инструментов, которые
|
|
45
|
+
сами обрабатывают подавления, дают ложные срабатывания: 3 из 22 на замере — тесты
|
|
46
|
+
`react-doctor` и `ratchets`. Отличить строку от комментария надёжно можно только разбором
|
|
47
|
+
языка, а его у переносимой проверки нет. Такие места — в `.aqkignore` или под храповик.
|
|
48
|
+
- **Точечное подавление без причины проходит.** `# noqa: E501` принимается, даже если рядом нет
|
|
49
|
+
ни слова о том, зачем. Требовать объяснение от каждой строки значит получить гейт, который
|
|
50
|
+
выключат целиком; за объяснением — к `require-description` из `eslint-plugin-eslint-comments`.
|
|
51
|
+
- **Не видит настройку в конфиге.** Правило, выключенное в `.eslintrc` или `pyproject.toml`, —
|
|
52
|
+
такое же ослабление, но это уже не подавление в коде, а решение проекта, записанное явно.
|
|
53
|
+
- **Не знает, было ли подавление оправдано.** Проверка отвечает на вопрос «названо ли, что
|
|
54
|
+
именно погашено», а не «стоило ли гасить».
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Подавление проверки без адреса: «выключено всё» вместо «выключено вот это».
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ ИМЕННО ЭТО. Когда проверка краснеет, у пишущего два выхода: починить код или заглушить
|
|
5
|
+
# проверку. Второй дешевле и с виду неотличим от первого — конвейер зелёный, диф маленький.
|
|
6
|
+
# Для агента это выход по умолчанию: ему поставлена задача «сделай, чтобы прошло».
|
|
7
|
+
#
|
|
8
|
+
# ГРАНИЦА НАМЕРЕННО УЗКАЯ. Красным делается не всякое подавление, а безадресное: `# noqa` без
|
|
9
|
+
# кода, `eslint-disable` без имени правила, `@SuppressWarnings("all")`. Точечное подавление с
|
|
10
|
+
# названным правилом — законный инструмент, и требовать объяснения от каждого значит получить
|
|
11
|
+
# гейт, который выключат. Одно исключение — `@ts-ignore`: у него есть строго лучший брат
|
|
12
|
+
# `@ts-expect-error`, который сам краснеет, когда становится не нужен.
|
|
13
|
+
DIR="${1:-.}"
|
|
14
|
+
. "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
|
|
15
|
+
|
|
16
|
+
OUT=""
|
|
17
|
+
add() { [ -z "$1" ] || OUT="$OUT$1
|
|
18
|
+
"; }
|
|
19
|
+
|
|
20
|
+
# --- безадресные подавления в коде -------------------------------------------
|
|
21
|
+
# `# noqa` без двоеточия с кодом; `type: ignore` без [кода]; файловые выключатели целиком.
|
|
22
|
+
add "$(grep -rnE '(^|[^A-Za-z0-9_])#[[:space:]]*(noqa|type:[[:space:]]*ignore)([[:space:]]*$|[[:space:]]+[^:[])' \
|
|
23
|
+
$(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
|
|
24
|
+
add "$(grep -rnE '#[[:space:]]*(flake8:[[:space:]]*noqa[[:space:]]*$|ruff:[[:space:]]*noqa[[:space:]]*$|mypy:[[:space:]]*ignore-errors|pylint:[[:space:]]*(skip-file|disable=all))' \
|
|
25
|
+
$(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
|
|
26
|
+
|
|
27
|
+
# eslint-disable без имени правила: голая директива гасит ВСЁ в файле или на строке.
|
|
28
|
+
#
|
|
29
|
+
# ВЕДУЩИЙ КОММЕНТАРИЙ ОБЯЗАТЕЛЕН — «(//|/\*|\*)[^\"']*». Замер по пятнадцати чужим
|
|
30
|
+
# репозиториям показал целый класс ложных: строка ПРО подавление внутри кавычек
|
|
31
|
+
# («title: 'No @ts-ignore'», текст правила в наборе тестов) читалась как подавление.
|
|
32
|
+
# Директива живёт в комментарии; упоминание в строковом литерале — не она.
|
|
33
|
+
add "$(grep -rnE '(//|/\*|\*)[^\"'"'"']*eslint-disable(-next-line|-line)?[[:space:]]*(\*/|$|--)' \
|
|
34
|
+
$(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
|
|
35
|
+
|
|
36
|
+
# @ts-ignore и @ts-nocheck. Первый гасит ошибку молча и остаётся, когда ошибки уже нет;
|
|
37
|
+
# @ts-expect-error на его месте краснеет, как только становится лишним.
|
|
38
|
+
add "$(grep -rnE '(//|/\*|\*)[^\"'"'"']*@ts-(ignore|nocheck)' \
|
|
39
|
+
$(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
|
|
40
|
+
|
|
41
|
+
# Остальные экосистемы — только безадресная форма.
|
|
42
|
+
add "$(grep -rnE '#!?\[allow\((warnings|unused)\)\]|@SuppressWarnings\((\{[^}]*)?"all"|//[[:space:]]*nolint[[:space:]]*($|//)|rubocop:disable[[:space:]]+all|shellcheck[[:space:]]+disable[[:space:]]*($|[^=])' \
|
|
43
|
+
$(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
|
|
44
|
+
|
|
45
|
+
# --- обход самого конвейера ---------------------------------------------------
|
|
46
|
+
# `--no-verify` пропускает хуки коммита. В committed-скрипте или конфиге конвейера это не
|
|
47
|
+
# настройка, а выключенная защита: смотрим шире кода — в оболочечные скрипты и конфиги.
|
|
48
|
+
# `-e`, а НЕ `--`. Двойное тире завершает разбор опций, и все флаги `--include` после него
|
|
49
|
+
# grep считает именами файлов: проверка читала весь репозиторий вместо оболочечных скриптов и
|
|
50
|
+
# конфигов. Нашлось замером — в выдаче оказались CHANGELOG чужих проектов.
|
|
51
|
+
add "$(grep -rnE -e '--no-verify' $(skip_grep) \
|
|
52
|
+
--include=*.sh --include=*.yml --include=*.yaml --include=*.json --include=*.toml \
|
|
53
|
+
--include=*.mk --include=Makefile "$DIR" 2>/dev/null)"
|
|
54
|
+
|
|
55
|
+
# Собственное определение записи — не подавление, а перечень того, что ищется. Без этого
|
|
56
|
+
# проверка находит сама себя в любом проекте, куда её поставили: `check.sh` содержит все
|
|
57
|
+
# образцы разом. Тот же класс, что образцы red/green, только файл другой.
|
|
58
|
+
SELFDIR="$(basename "$(dirname "$0")")"
|
|
59
|
+
LEFT="$(printf '%s' "$OUT" | grep -v '^$' | own_samples_filter "$DIR" \
|
|
60
|
+
| grep -vE "(^|/)$SELFDIR/(check\.sh|README\.md)")"
|
|
61
|
+
# Сгенерированные файлы правят не руками: подавление в них поставил инструмент.
|
|
62
|
+
LEFT="$(printf '%s\n' "$LEFT" | while IFS= read -r L; do
|
|
63
|
+
F="${L%%:*}"
|
|
64
|
+
[ -n "$F" ] || continue
|
|
65
|
+
is_generated "$F" || printf '%s\n' "$L"
|
|
66
|
+
done | grep -v '^$')"
|
|
67
|
+
|
|
68
|
+
[ -z "$LEFT" ] && exit 0
|
|
69
|
+
printf '%s\n' "$LEFT"
|
|
70
|
+
echo " почини: назови, ЧТО подавляешь и зачем — «# noqa: E501», «eslint-disable-next-line no-console -- причина»,"
|
|
71
|
+
echo " «@ts-expect-error» вместо «@ts-ignore». Безадресное подавление гасит и то, что сломается завтра."
|
|
72
|
+
exit 1
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
intent: проверка выключается точечно и с причиной, а не целиком
|
|
2
|
+
intent_en: a check is suppressed narrowly and with a reason, never wholesale
|
|
3
|
+
|
|
4
|
+
# Применимо везде, где есть код: подавление проверки — приём, а не язык. Список маркеров
|
|
5
|
+
# покрывает python, typescript, javascript, go, rust, java, ruby и оболочку сразу.
|
|
6
|
+
trigger:
|
|
7
|
+
always: true
|
|
8
|
+
|
|
9
|
+
recipes:
|
|
10
|
+
any: bash {gate}/check.sh {dir}
|
|
11
|
+
|
|
12
|
+
proof: incidents/README.md, 2026-09-06 «замер по пятнадцати чужим репозиториям» — 22 находки
|
|
13
|
+
на 20 774 файлах, из них 19 настоящих: `@ts-nocheck` на боевой странице в kodus-ai,
|
|
14
|
+
сломанный `# noqa E501` без двоеточия в pr-agent (flake8 читает его как безадресный),
|
|
15
|
+
`/** @ts-ignore */` и `eslint-disable-line` без имени правила в repolinter
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// @ts-expect-error -- у gateway нет типов; строка покраснеет сама, когда они появятся
|
|
2
|
+
import { pay } from "./gateway";
|
|
3
|
+
|
|
4
|
+
export async function checkout(cart: unknown) {
|
|
5
|
+
// eslint-disable-next-line no-console -- это программа командной строки, вывод и есть интерфейс
|
|
6
|
+
console.log(cart);
|
|
7
|
+
return pay(cart as never);
|
|
8
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# У правила виден сторож
|
|
2
|
+
|
|
3
|
+
**Намерение.** Это центральный вопрос всего комплекта. `AGENTS.md` — обычный текст: «никогда не
|
|
4
|
+
коммитим секреты» и «мы очень стараемся» выглядят одинаково и стоят одинаково, пока никто не
|
|
5
|
+
спросил, чем первое отличается от второго.
|
|
6
|
+
|
|
7
|
+
**Какой отказ это поймало.** Свой собственный. Прогон по нашему же `AGENTS.md`: тринадцать
|
|
8
|
+
железных правил, и **ни одно** не говорило, кто за ним следит. После разметки выяснилось, что
|
|
9
|
+
одиннадцать из тринадцати исполняет человек, а не машина. Это не стало хуже — стало видно.
|
|
10
|
+
Правило, за которым следит человек, законно; правило, о котором никто не знает, кто за ним
|
|
11
|
+
следит, через месяц отличается от лозунга только длиной. Запись в журнале:
|
|
12
|
+
`incidents/README.md`, 2026-09-06.
|
|
13
|
+
|
|
14
|
+
**Что именно проверяется.** Каждый пункт списка в точке входа, который выглядит правилом
|
|
15
|
+
(выделен жирным либо содержит слово долженствования или запрета), обязан нести пометку:
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
- **Секреты не в коде.** <!-- aqk: secrets-not-in-code -->
|
|
19
|
+
- **План до кода.** <!-- aqk: человек -->
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Имя гейта сверяется с блоком `gates:` манифеста: пометка, ведущая в никуда, — тоже красное.
|
|
23
|
+
Пометка живёт в комментарии разметки, поэтому в готовом документе её не видно.
|
|
24
|
+
|
|
25
|
+
**Почему пометка, а не угадывание.** Сопоставлять обещание с гейтом по совпадению слов — значит
|
|
26
|
+
выдавать вердикт по догадке; догадка, выданная за факт, и есть то, против чего построен весь
|
|
27
|
+
стандарт. Пометка ставится один раз и делает документ честнее: у каждого правила видно, кто его
|
|
28
|
+
сторожит.
|
|
29
|
+
|
|
30
|
+
**Первый прогон в зрелом проекте покрасит всё.** Так и задумано, и лечится одной командой:
|
|
31
|
+
`aqk ratchet promise-has-gate` — существующие правила становятся долгом, который может только
|
|
32
|
+
сокращаться, а новое правило без сторожа краснеет сразу.
|
|
33
|
+
|
|
34
|
+
**Готовый аналог.** Не нашли, и искали внимательно. К осени 2026 появился целый класс линтеров
|
|
35
|
+
агентского обвеса — [`agnix`](https://github.com/agent-sh/agnix) (455 правил, активен),
|
|
36
|
+
[`agents-lint`](https://github.com/giacomo/agents-lint), ctxlint, AgentLint. Все они проверяют
|
|
37
|
+
**документ**: формат, живые ли ссылки, существуют ли упомянутые скрипты. Ни один не спрашивает,
|
|
38
|
+
**исполнимо** ли обещание. Ставь `agnix` рядом — он закрывает то, чего не делаем мы, а мы
|
|
39
|
+
закрываем то, чего не делает он.
|
|
40
|
+
|
|
41
|
+
**Чего НЕ ловит.**
|
|
42
|
+
|
|
43
|
+
- **Не проверяет, что гейт делает то, что обещано.** Пометка `<!-- aqk: secrets-not-in-code -->`
|
|
44
|
+
на правиле про длину функций пройдёт. Связь объявляет человек; машина сторожит только то, что
|
|
45
|
+
связь есть и ведёт в существующий гейт.
|
|
46
|
+
- **`<!-- aqk: человек -->` не проверяется ничем** — это признание, а не проверка. Его ценность
|
|
47
|
+
в том, что признание сделано вслух и его видно в дифе, когда правил становится больше.
|
|
48
|
+
- **Правило, не оформленное пунктом списка**, не опознаётся: абзац прозы обещанием не считается.
|
|
49
|
+
- **Только точка входа**, объявленная в `entry:`. Правила, разложенные по десяти файлам
|
|
50
|
+
документации, эта запись не обойдёт.
|