agent-quality-kit 0.2.2
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/LICENSE +21 -0
- package/README.md +155 -0
- package/kit/docs/ai/agent-harness-playbook.md +596 -0
- package/kit/docs/ai/ai-native-development.md +371 -0
- package/kit/docs/ai/ai-sdlc.md +221 -0
- package/kit/docs/ai/anthropic-ai-native-sdlc-2026-08.md +294 -0
- package/kit/docs/ai/app-owner-strategy.md +921 -0
- package/kit/docs/ai/deep-research-2026-07.md +161 -0
- package/kit/docs/ai/harness-best-practices.md +385 -0
- package/kit/docs/ai/index.md +64 -0
- package/kit/docs/ai/project-baseline.md +261 -0
- package/kit/docs/ai/quality-gates-checklist.md +322 -0
- package/kit/docs/ai/sources-building-with-agents.md +111 -0
- package/kit/docs/ai/stream-2026-08-ai-coding-panel.md +304 -0
- package/kit/docs/ready-made-rules.md +170 -0
- package/kit/gates/README.md +231 -0
- package/kit/gates/_skip.sh +75 -0
- package/kit/gates/commit-explains-itself/README.md +45 -0
- package/kit/gates/commit-explains-itself/check.sh +63 -0
- package/kit/gates/commit-explains-itself/gate.yml +10 -0
- package/kit/gates/commit-explains-itself/green/COMMIT_MSG +6 -0
- package/kit/gates/commit-explains-itself/red/COMMIT_MSG +3 -0
- package/kit/gates/complexity-limit/README.md +37 -0
- package/kit/gates/complexity-limit/check.sh +44 -0
- package/kit/gates/complexity-limit/gate.yml +13 -0
- package/kit/gates/complexity-limit/green/flat.py +10 -0
- package/kit/gates/complexity-limit/red/deep.py +9 -0
- package/kit/gates/dead-code/README.md +30 -0
- package/kit/gates/dead-code/gate.yml +23 -0
- package/kit/gates/dead-code/green/mod.py +9 -0
- package/kit/gates/dead-code/red/mod.py +9 -0
- package/kit/gates/deps-are-pinned/README.md +29 -0
- package/kit/gates/deps-are-pinned/check.sh +49 -0
- package/kit/gates/deps-are-pinned/gate.yml +9 -0
- package/kit/gates/deps-are-pinned/green/nodep-go/go.mod +3 -0
- package/kit/gates/deps-are-pinned/green/package-lock.json +3 -0
- package/kit/gates/deps-are-pinned/green/package.json +4 -0
- package/kit/gates/deps-are-pinned/green/requirements.txt +2 -0
- package/kit/gates/deps-are-pinned/red/package.json +4 -0
- package/kit/gates/deps-are-pinned/red/requirements.txt +2 -0
- package/kit/gates/deps-are-pinned/red/withdep-go/go.mod +5 -0
- package/kit/gates/duplicate-code/README.md +40 -0
- package/kit/gates/duplicate-code/check.sh +58 -0
- package/kit/gates/duplicate-code/gate.yml +12 -0
- package/kit/gates/duplicate-code/green/common.py +9 -0
- package/kit/gates/duplicate-code/green/use.py +9 -0
- package/kit/gates/duplicate-code/red/a.py +12 -0
- package/kit/gates/duplicate-code/red/b.py +12 -0
- package/kit/gates/entry-links-exist/README.md +22 -0
- package/kit/gates/entry-links-exist/check.sh +24 -0
- package/kit/gates/entry-links-exist/gate.yml +16 -0
- package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
- package/kit/gates/entry-links-exist/green/rules/general.md +3 -0
- package/kit/gates/entry-links-exist/red/AGENTS.md +3 -0
- package/kit/gates/file-size-limit/README.md +22 -0
- package/kit/gates/file-size-limit/check.sh +34 -0
- package/kit/gates/file-size-limit/gate.yml +9 -0
- package/kit/gates/file-size-limit/green/a.py +251 -0
- package/kit/gates/file-size-limit/green/b.py +251 -0
- package/kit/gates/file-size-limit/red/big.py +601 -0
- package/kit/gates/gate-has-samples/README.md +29 -0
- package/kit/gates/gate-has-samples/check.sh +48 -0
- package/kit/gates/gate-has-samples/gate.yml +9 -0
- package/kit/gates/gate-has-samples/green/.aqk.yml +10 -0
- package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/check.sh +2 -0
- package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/green/good.py +2 -0
- package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/red/bad.py +1 -0
- package/kit/gates/gate-has-samples/red/.aqk.yml +10 -0
- package/kit/gates/gate-has-samples/red/gates/no-print-in-prod/check.sh +2 -0
- package/kit/gates/gates-are-runnable/README.md +23 -0
- package/kit/gates/gates-are-runnable/check.sh +35 -0
- package/kit/gates/gates-are-runnable/gate.yml +9 -0
- package/kit/gates/gates-are-runnable/green/.aqk.yml +9 -0
- package/kit/gates/gates-are-runnable/green/checks/lint.sh +2 -0
- package/kit/gates/gates-are-runnable/red/.aqk.yml +6 -0
- package/kit/gates/gates-run-in-ci/README.md +29 -0
- package/kit/gates/gates-run-in-ci/check.sh +42 -0
- package/kit/gates/gates-run-in-ci/gate.yml +12 -0
- package/kit/gates/gates-run-in-ci/green/.aqk.yml +6 -0
- package/kit/gates/gates-run-in-ci/green/.github/workflows/ci.yml +7 -0
- package/kit/gates/gates-run-in-ci/green/checks/lint.sh +2 -0
- package/kit/gates/gates-run-in-ci/red/.aqk.yml +6 -0
- package/kit/gates/gates-run-in-ci/red/.github/workflows/ci.yml +7 -0
- package/kit/gates/gates-run-in-ci/red/checks/lint.sh +2 -0
- package/kit/gates/lesson-has-outcome/README.md +37 -0
- package/kit/gates/lesson-has-outcome/check.sh +50 -0
- package/kit/gates/lesson-has-outcome/gate.yml +11 -0
- package/kit/gates/lesson-has-outcome/green/.aqk.yml +2 -0
- package/kit/gates/lesson-has-outcome/green/incidents/README.md +32 -0
- package/kit/gates/lesson-has-outcome/red/.aqk.yml +2 -0
- package/kit/gates/lesson-has-outcome/red/incidents/README.md +13 -0
- package/kit/gates/no-print-in-prod/README.md +44 -0
- package/kit/gates/no-print-in-prod/check.sh +36 -0
- package/kit/gates/no-print-in-prod/gate.yml +15 -0
- package/kit/gates/no-print-in-prod/green/docs.ts +15 -0
- package/kit/gates/no-print-in-prod/green/legacy.py +9 -0
- package/kit/gates/no-print-in-prod/green/main.go +8 -0
- package/kit/gates/no-print-in-prod/green/main.rs +4 -0
- package/kit/gates/no-print-in-prod/green/service.py +8 -0
- package/kit/gates/no-print-in-prod/red/main.go +8 -0
- package/kit/gates/no-print-in-prod/red/main.rs +4 -0
- package/kit/gates/no-print-in-prod/red/service.py +3 -0
- package/kit/gates/secrets-not-in-code/README.md +29 -0
- package/kit/gates/secrets-not-in-code/check.sh +18 -0
- package/kit/gates/secrets-not-in-code/gate.yml +9 -0
- package/kit/gates/secrets-not-in-code/green/settings.py +4 -0
- package/kit/gates/secrets-not-in-code/red/settings.py +2 -0
- package/kit/gates/swallowed-error/README.md +26 -0
- package/kit/gates/swallowed-error/check.sh +54 -0
- package/kit/gates/swallowed-error/gate.yml +12 -0
- package/kit/gates/swallowed-error/green/loader.py +11 -0
- package/kit/gates/swallowed-error/green/run.js +8 -0
- package/kit/gates/swallowed-error/red/loader.py +5 -0
- package/kit/gates/swallowed-error/red/run.js +3 -0
- package/kit/gates/todo-without-task/README.md +26 -0
- package/kit/gates/todo-without-task/check.sh +19 -0
- package/kit/gates/todo-without-task/gate.yml +12 -0
- package/kit/gates/todo-without-task/green/order.py +9 -0
- package/kit/gates/todo-without-task/red/order.py +8 -0
- package/kit/ratchet/ratchet.sh +62 -0
- package/kit/rules/general.md +55 -0
- package/kit/rules/security.md +33 -0
- package/kit/rules/testing.md +46 -0
- package/package.json +41 -0
- package/tool/commands/doctor.mjs +230 -0
- package/tool/commands/gates.mjs +445 -0
- package/tool/commands/project.mjs +316 -0
- package/tool/lib/core.mjs +98 -0
- package/tool/lib/manifest.mjs +140 -0
- package/tool/lib/repo.mjs +270 -0
- package/tool/lib/templates.mjs +187 -0
- package/tool/program.mjs +81 -0
- package/tool/selfcheck/conditional.sh +24 -0
- package/tool/selfcheck/gates.sh +127 -0
- package/tool/selfcheck/smoke.sh +539 -0
- package/tool/selfcheck/syntax.sh +23 -0
- package/tool/selfcheck/units.mjs +105 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Каталог обещаний — норма записи
|
|
2
|
+
|
|
3
|
+
Каталог хранит обещания проекта, превращённые в команды. Здесь — правило, по которому запись
|
|
4
|
+
принимается или отклоняется. Проверяет это машина: `bash tool/selfcheck/gates.sh`.
|
|
5
|
+
|
|
6
|
+
## Сперва ищи готовое правило, потом пиши своё
|
|
7
|
+
|
|
8
|
+
**Прежде чем заводить запись — проверь, нет ли такой проверки в готовом инструменте.** У `ruff`
|
|
9
|
+
964 правила в 61 группе, у `eslint` сотни, у `semgrep` — реестр чужих. Мы знаем от силы два
|
|
10
|
+
процента.
|
|
11
|
+
|
|
12
|
+
**Где искать и какими командами — `kit/docs/ready-made-rules.md`.** Там карта всех 61 группы,
|
|
13
|
+
список инструментов по языкам и команда, которая считает находки по группе в вашем репозитории.
|
|
14
|
+
|
|
15
|
+
Цена этого правила измерена. Разбор синтаксического дерева по всему проекту искал места, где
|
|
16
|
+
ошибку записывают в лог без трейса: час работы, 80 находок. Существует правило `TRY400`, которое
|
|
17
|
+
находит их одной строкой в конфиге и точнее.
|
|
18
|
+
|
|
19
|
+
**Готовое лучше своего не потому, что чужое, а потому что** его поддерживают без нас, оно
|
|
20
|
+
подробнее (три правила там, где у нас одно) и его знают в лицо в чужих проектах.
|
|
21
|
+
|
|
22
|
+
Поэтому в записи **рецепт под язык берёт готовое правило**, а своя переносимая проверка остаётся
|
|
23
|
+
запасной — для стеков, где готового нет.
|
|
24
|
+
|
|
25
|
+
### Три ступени выбора — как это работает на любом языке
|
|
26
|
+
|
|
27
|
+
Запись каталога описывает **одно намерение и несколько исполнителей**. Программа выбирает так:
|
|
28
|
+
|
|
29
|
+
1. **готовый инструмент под язык проекта** — если он установлен. Точнее и подробнее;
|
|
30
|
+
2. **переносимая проверка** `any` — наша, без сторонних программ, работает где угодно;
|
|
31
|
+
3. **ничего** — тогда запись честно скрывается, а не притворяется работающей.
|
|
32
|
+
|
|
33
|
+
Поэтому проект на Rust, Go или C++ не остаётся без сторожа: пока рецепта `rust: cargo clippy …`
|
|
34
|
+
в записи нет, работает переносимая проверка. Появится рецепт — она уступит место готовому.
|
|
35
|
+
|
|
36
|
+
**Принести рецепт под новый язык — самый дешёвый и самый ценный вклад:** намерение уже доказано,
|
|
37
|
+
образцы уже лежат, нужна одна строка.
|
|
38
|
+
|
|
39
|
+
**Ограничение формата:** рецепты выбираются **по языку проекта**, поэтому инструмент, к языку не
|
|
40
|
+
привязанный, рецептом не выражается. Пример — проверка ссылок в документации: `lychee` сильнее
|
|
41
|
+
нашей переносимой проверки, но ключа под него в записи нет. Такие инструменты называются в README
|
|
42
|
+
записи словами, и это честнее, чем прятать их отсутствие.
|
|
43
|
+
|
|
44
|
+
### И обратная половина, без которой правило вредно
|
|
45
|
+
|
|
46
|
+
**Свой гейт законен, когда он про НАШЕ решение.** Паритет очередей, свежесть карты проекта,
|
|
47
|
+
неизменяемость применённой миграции, реестр долга — этого в библиотеках нет и быть не может:
|
|
48
|
+
они не знают, как устроен ваш проект.
|
|
49
|
+
|
|
50
|
+
Проверять надо оба направления. Пример из практики: неразрывный пробел не ловит **ни одно** из
|
|
51
|
+
964 правил `ruff` — там свой гейт единственный способ. А запрет отладочной печати дублировал
|
|
52
|
+
готовое `T20` годами.
|
|
53
|
+
|
|
54
|
+
**В README каждой записи обязан быть раздел «готовый аналог»**: есть он или нет, и если нет — что
|
|
55
|
+
именно проверено. «Не искал» и «нет» — разные утверждения.
|
|
56
|
+
|
|
57
|
+
## Пять полей записи
|
|
58
|
+
|
|
59
|
+
Галочка «внедрено» отвечает не на тот вопрос: она означает «пакет стоит», а нужно «сторож
|
|
60
|
+
работает». Поэтому запись — не галочка, а пять полей:
|
|
61
|
+
|
|
62
|
+
| Поле | Что в нём | Файл |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| **намерение** | какой класс дефекта ловит, одной фразой | `intent` в `gate.yml` |
|
|
65
|
+
| **арбитр** | команда, возвращающая 0 или не 0, под каждый стек | `recipes` в `gate.yml` |
|
|
66
|
+
| **триггер** | условие, при котором запись касается репозитория | `trigger` в `gate.yml` |
|
|
67
|
+
| **красный образец** | код, на котором гейт обязан сработать | каталог `red/` |
|
|
68
|
+
| **зелёный образец** | правильный код, на котором обязан молчать | каталог `green/` |
|
|
69
|
+
|
|
70
|
+
Плюс шестое, без которого запись не принимается: **доказательство** — реальный отказ, который
|
|
71
|
+
она поймала. «Это хорошая практика» не принимается.
|
|
72
|
+
|
|
73
|
+
## Запись без переносимого рецепта
|
|
74
|
+
|
|
75
|
+
Иногда переносимой проверки быть не может: чтобы понять, вызывают ли функцию, нужен граф
|
|
76
|
+
вызовов, а не поиск по тексту. Такая запись законна — но обязана сказать, **каким рецептом
|
|
77
|
+
написаны её образцы**:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
samples_for: python
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Без этого поля проверка отклонит запись: угадывать нельзя. В системе может стоять `npx`, и
|
|
84
|
+
питоновские образцы поедут проверяться фронтовым инструментом — так и вышло при первой попытке.
|
|
85
|
+
|
|
86
|
+
Где нужного инструмента нет, проверка каталога говорит «НЕ ПРОВЕРЕНА здесь — нужен такой-то» и
|
|
87
|
+
считает это **отдельно** от принятых и от отклонённых. «Условная запись» и «нечем проверить
|
|
88
|
+
здесь» — разные состояния.
|
|
89
|
+
|
|
90
|
+
Конвейер такие инструменты ставит: «не проверено» не должно становиться нормой именно на той
|
|
91
|
+
машине, где проверка обязана быть строгой.
|
|
92
|
+
|
|
93
|
+
Триггер у такой записи — по языкам, а не `always`: показывать её тому, кому её нечем исполнить,
|
|
94
|
+
значит показывать работу, которую он сделать не сможет.
|
|
95
|
+
|
|
96
|
+
## Четыре способа, которыми гейт врёт
|
|
97
|
+
|
|
98
|
+
Все примеры настоящие, из `audit_project`:
|
|
99
|
+
|
|
100
|
+
| Как врёт | Живой пример |
|
|
101
|
+
|---|---|
|
|
102
|
+
| **Зомби** — стоит, не вызывается | четыре пакета в зависимостях с нулём упоминаний в коде |
|
|
103
|
+
| **Декоративный** — работает, ничего не доказывает | фаззер API: только `GET`, 5 примеров, единственная проверка «не 500», да ещё необязательный |
|
|
104
|
+
| **Врущий** — краснеет на правильном коде | правило ругалось на то написание, которое само предписывает |
|
|
105
|
+
| **Устаревшая запись** — файл утверждает то, чего нет | раздел числил защиту работающей через три недели после её отключения |
|
|
106
|
+
|
|
107
|
+
Пятый способ: **гейт, который не запускается.** Проверка жила только в конвейере, а правка её
|
|
108
|
+
конфига конвейер вообще не создавала — пути не были в белом списке.
|
|
109
|
+
|
|
110
|
+
Шестой, из разбора другого проекта: **гейт, скопировавший чужое решение вместо ссылки на него.**
|
|
111
|
+
Список того, что считается тестом, был записан в `pytest.ini` — и ещё раз, вручную, в пяти разных
|
|
112
|
+
проверках. Копии разъезжались годами и тихо. Проверка одним вопросом: «что будет с этим гейтом,
|
|
113
|
+
если завтра поправят источник?» Правильный ответ — «результат изменится сам», а не «нужно
|
|
114
|
+
поправить ещё и здесь».
|
|
115
|
+
|
|
116
|
+
Зелёный образец закрывает третий способ, и он важнее красного: что гейт ловит брак, проверяют
|
|
117
|
+
при установке; что он молчит на исправном коде — не проверяют почти никогда.
|
|
118
|
+
|
|
119
|
+
## Красный образец проверяется по выводу, а не по коду возврата
|
|
120
|
+
|
|
121
|
+
Отсутствующий скрипт тоже даёт ненулевой код: «гейт покраснел» неотличимо от «гейта нет».
|
|
122
|
+
Поэтому фильтр различает три случая — арбитр не запускается, арбитр промолчал на красном,
|
|
123
|
+
арбитр покраснел на красном молча.
|
|
124
|
+
|
|
125
|
+
Дефект не ждут — его **изготавливают нарочно**. И если образец не краснеет, первым делом
|
|
126
|
+
доказывают, что он вообще нарушает правило: трижды подряд «дефект гейта» оказывался неверным
|
|
127
|
+
образцом.
|
|
128
|
+
|
|
129
|
+
## Сканирующий гейт пропускает красные образцы
|
|
130
|
+
|
|
131
|
+
Красный образец — намеренно сломанный код, лежащий в репозитории. Проверка, которая ходит по
|
|
132
|
+
всему проекту, обязана его исключать, иначе будет вечно краснеть на том, что сама же и положила,
|
|
133
|
+
и её начнут обходить вместе со всеми остальными.
|
|
134
|
+
|
|
135
|
+
Исключение снимается, когда проверяют сам образец: тогда каталог `red` и есть цель проверки.
|
|
136
|
+
|
|
137
|
+
## Сканирующий гейт не проверяет чужое и служебное
|
|
138
|
+
|
|
139
|
+
Кроме красных образцов исключаются: каталог, разложенный самим комплектом; история версий;
|
|
140
|
+
всё, что принесли пакетный менеджер и сборка. Гейт, краснеющий на том, что положил не человек,
|
|
141
|
+
выключат вместе со всеми остальными.
|
|
142
|
+
|
|
143
|
+
Проверка на отладочную печать вдобавок не читает прозу: упоминание `print(` в методичке — это
|
|
144
|
+
текст про печать, а не печать.
|
|
145
|
+
|
|
146
|
+
## Какие условия программа умеет считать
|
|
147
|
+
|
|
148
|
+
Все условия внутри `trigger` складываются: запись показывается, когда выполнены **все**.
|
|
149
|
+
|
|
150
|
+
| Условие | Что значит |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `always: true` | касается любого репозитория |
|
|
153
|
+
| `langs: python, go` | есть файлы хотя бы одного из этих языков |
|
|
154
|
+
| `files_gt: 200` / `files_lt: 50` | размер репозитория |
|
|
155
|
+
| `has_gates: true` | в манифесте объявлен хотя бы один гейт |
|
|
156
|
+
| `has_ci: true` | есть конфиг конвейера |
|
|
157
|
+
| `has_db: true` | есть миграции или файлы `.sql` |
|
|
158
|
+
| `has_docker: true` | есть Dockerfile или compose |
|
|
159
|
+
| `has_deps: true` | есть файл зависимостей |
|
|
160
|
+
| `has_tests: true` | есть каталог тестов или файлы вида `*_test.*` |
|
|
161
|
+
| `has_env: true` | есть файл окружения |
|
|
162
|
+
|
|
163
|
+
Любое из `has_*` принимает и `false` — «показывать тем, у кого этого нет». Условие, которого
|
|
164
|
+
программа не знает, отклоняется явно, а не пропускается молча.
|
|
165
|
+
|
|
166
|
+
Признаки берутся **осмотром файлов, а не анкетой**. Ответы человека — это мнение; файлы — факт.
|
|
167
|
+
|
|
168
|
+
## Два разных списка исключений
|
|
169
|
+
|
|
170
|
+
**Общий** (`_skip.sh`) — чужое и служебное: окружение, зависимости, сборка, история версий.
|
|
171
|
+
Один на все проверки: разъехавшись, он однажды окажется полным в одной и дырявым в другой. Там же
|
|
172
|
+
живёт единый список расширений кода (`only_code`) и фильтр комментариев (`drop_comments`).
|
|
173
|
+
Не «для порядка»: пока список расширений был переписан в каждой проверке заново, четыре из них
|
|
174
|
+
разошлись, и `.mjs` не оказалось ни в одной — предел размера файла молчал на программе самого
|
|
175
|
+
комплекта.
|
|
176
|
+
|
|
177
|
+
**Свой у каждой проверки** — места, где эта конкретная конструкция законна. Печать в
|
|
178
|
+
вспомогательном скрипте — способ говорить с человеком, а не забытая отладка; секрет в том же
|
|
179
|
+
скрипте — такой же секрет. Общим этот список быть не может.
|
|
180
|
+
|
|
181
|
+
Мера обоих одна: **гейт, который на 86% состоит из ложных сработок, выключают целиком.**
|
|
182
|
+
Цифра не выдумана — столько дал первый прогон по настоящему проекту.
|
|
183
|
+
|
|
184
|
+
## Триггер, записанный словами, — это не триггер
|
|
185
|
+
|
|
186
|
+
«Внедрить при условии X» не сработает: никто не сравнивает список с реальностью. Условие обязано
|
|
187
|
+
быть **запросом к репозиторию**, который умеет вычислить программа: «есть файлы `*.py` и нет
|
|
188
|
+
проверки печати» — а не «когда появится питон».
|
|
189
|
+
|
|
190
|
+
Живой пример провала: модели одной технологии в проекте были, линтера под неё не было, запись
|
|
191
|
+
лежала с формулировкой «когда появятся модели». Условие наступило давно, никто не заметил.
|
|
192
|
+
|
|
193
|
+
## Область гейта решается замером, а не планом
|
|
194
|
+
|
|
195
|
+
План исключал один каталог из проверки; замер показал, что там лежит самая сложная функция.
|
|
196
|
+
Гейт написан в тот же день по факту, а не по замыслу.
|
|
197
|
+
|
|
198
|
+
## Инструкция по починке в тексте ошибки
|
|
199
|
+
|
|
200
|
+
Гейт пишет не «нарушение правила X», а **что именно сделать** и куда посмотреть.
|
|
201
|
+
|
|
202
|
+
> **ПОЧЕМУ.** Текст ошибки попадает прямо в контекст агента. С инструкцией он чинит сам; без неё
|
|
203
|
+
> — гадает, тратит контекст и часто чинит не то.
|
|
204
|
+
|
|
205
|
+
## Храповик для долга
|
|
206
|
+
|
|
207
|
+
Если правило вводится в проект, где старый код ему не соответствует, — заведи файл со списком
|
|
208
|
+
текущих нарушений. Гейт разрешает список **укорачивать** и запрещает **удлинять**.
|
|
209
|
+
|
|
210
|
+
> **ПОЧЕМУ не «большая чистка» и не advisory.** Чистка откладывается навсегда, потому что она
|
|
211
|
+
> большая. Advisory не блокирует ничего, его листают, и правило не действует. Храповик даёт
|
|
212
|
+
> действующее правило со дня установки, не требуя трогать старый код.
|
|
213
|
+
>
|
|
214
|
+
> **Проверка, что это храповик, а не советчик:** «может ли новый код добавить нарушение и пройти?»
|
|
215
|
+
> Может — значит, гейта нет.
|
|
216
|
+
|
|
217
|
+
**Чего храповик не ловит.** Он держит **множество** нарушений, а не их величину: нарушение, уже
|
|
218
|
+
лежащее в реестре, может усугубляться незаметно. Файл на 1357 строк при пределе 500 записан
|
|
219
|
+
долгом — и вырастет до трёх тысяч, не покраснев. Поэтому число в сообщении гейта ставят в позицию
|
|
220
|
+
номера строки (`файл:1357: длиннее предела`): ключ реестра эту часть отбрасывает, и запись не
|
|
221
|
+
дёргается от каждой правки. Цена решения — рост в реестре не виден. Величину долга держат глазами
|
|
222
|
+
при разборе реестра, а не храповиком.
|
|
223
|
+
|
|
224
|
+
## Срок годности у режима предупреждения
|
|
225
|
+
|
|
226
|
+
Если гейт временно не блокирует — рядом стоит **дата**, после которой он либо блокирует, либо
|
|
227
|
+
удаляется. Не «когда-нибудь ужесточим».
|
|
228
|
+
|
|
229
|
+
> **ПОЧЕМУ.** Условие, записанное словами («внедрим, когда появятся модели данных»), не
|
|
230
|
+
> срабатывает: никто не сравнивает список с реальностью. Срабатывает только то, что умеет
|
|
231
|
+
> вычислить машина, — дата или запрос к репозиторию.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Что сканирующие проверки не читают. Общий список: разъехавшись по шести проверкам, он
|
|
3
|
+
# однажды окажется полным в одной и дырявым в другой.
|
|
4
|
+
#
|
|
5
|
+
# ПОЧЕМУ ЭТО ВАЖНЕЕ, ЧЕМ КАЖЕТСЯ. На настоящем проекте из 36 тысяч файлов 1722 находки из 1831
|
|
6
|
+
# пришли из чужого кода в окружении. Гейт, который на 94% состоит из чужих нарушений, читать
|
|
7
|
+
# никто не будет — его выключат.
|
|
8
|
+
|
|
9
|
+
SKIP_NAMES=".git .aqk node_modules .venv venv env __pycache__ .mypy_cache .pytest_cache
|
|
10
|
+
.tox .ruff_cache site-packages dist build target out .next .nuxt .svelte-kit coverage
|
|
11
|
+
htmlcov staticfiles vendor bower_components .gradle .idea .vscode
|
|
12
|
+
playwright-report test-results storybook-static .turbo .cache .parcel-cache
|
|
13
|
+
migrations"
|
|
14
|
+
|
|
15
|
+
# Образцы гейтов — искусственный код по построению: красный сломан нарочно, зелёный бывает
|
|
16
|
+
# вырожденным (двести одинаковых строк, чтобы показать предел размера). Сканировать их — значит
|
|
17
|
+
# ловить то, что положил не человек. Исключение снимается, когда цель проверки — сам образец.
|
|
18
|
+
#
|
|
19
|
+
# ПОЧЕМУ ПО ПУТИ, А НЕ ПО ИМЕНИ. `--exclude-dir=red`/`-name red -prune` совпадают с ЛЮБОЙ папкой
|
|
20
|
+
# red в проекте — а это имя не выдуманное: реальный секрет в пользовательской папке `red/`
|
|
21
|
+
# (red-team тесты, что угодно) становился невидим для secrets-not-in-code во всех проектах,
|
|
22
|
+
# куда ставили гейт. Фильтр ниже смотрит на путь целиком: только `gates/<имя>/red|green/`,
|
|
23
|
+
# а не голое имя каталога.
|
|
24
|
+
own_samples_filter() {
|
|
25
|
+
case "${1:-}" in
|
|
26
|
+
*/red|*/red/|*/green|*/green/) cat ;;
|
|
27
|
+
*) grep -vE '/gates/[^/]+/(red|green)(/|$)' ;;
|
|
28
|
+
esac
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Расширения, где `print` и маркеры долга — конструкции языка, а не текст. Проверять по ним
|
|
32
|
+
# файлы настроек, разметку и отчёты бессмысленно: там те же слова означают другое.
|
|
33
|
+
# Модульные варианты перечислены наравне с обычными: без .mjs проверки молча не смотрели
|
|
34
|
+
# в программу самого aqk, целиком написанную в этом расширении.
|
|
35
|
+
CODE_EXT="py js jsx mjs cjs ts tsx mts cts vue go rb java cs php rs kt swift scala"
|
|
36
|
+
|
|
37
|
+
include_code() {
|
|
38
|
+
for E in $CODE_EXT; do printf -- '--include=*.%s ' "$E"; done
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Сгенерированный файл не правят руками — предъявлять его размер или сложность человеку
|
|
42
|
+
# бессмысленно и вредно: он выключит проверку целиком.
|
|
43
|
+
# Опознаём по общепринятой шапке в первых пяти строках.
|
|
44
|
+
is_generated() {
|
|
45
|
+
head -5 "$1" 2>/dev/null | grep -qiE '@generated|do not edit|autogenerated|auto-generated|generated by|сгенерирован'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Для grep: --exclude-dir на каждое имя. Образцы гейтов сюда не входят — см. own_samples_filter:
|
|
49
|
+
# у --exclude-dir нет способа смотреть на путь целиком, только на голое имя папки.
|
|
50
|
+
skip_grep() {
|
|
51
|
+
for N in $SKIP_NAMES; do printf -- '--exclude-dir=%s ' "$N"; done
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Для find: -name X -prune -o …
|
|
55
|
+
skip_find() {
|
|
56
|
+
for N in $SKIP_NAMES; do printf -- '-name %s -prune -o ' "$N"; done
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Отбрасывает находки, сделанные в строках-комментариях. Вход — вывод `grep -rn` в виде
|
|
60
|
+
# «файл:строка:код». Комментарий не выполняется: находка в нём означает, что проверка читает
|
|
61
|
+
# текст, а не код. Найдено прогоном по audit_project: семь находок из JSDoc и закомментированных
|
|
62
|
+
# строк. Проверкам, которые ищут ИМЕННО в комментариях (маркеры долга), этот фильтр не нужен.
|
|
63
|
+
drop_comments() { grep -vE '^[^:]*:[0-9]+:[[:space:]]*(//|\*|#|/\*|--)' ; }
|
|
64
|
+
|
|
65
|
+
# Отбор файлов кода из потока путей. Один список на все проверки: раньше каждая несла свой,
|
|
66
|
+
# переписанный заново, и они разошлись — четыре проверки, четыре разных набора, и `.mjs` не было
|
|
67
|
+
# ни в одном. Программа самого комплекта целиком в `.mjs`: предел размера файла молчал на ней
|
|
68
|
+
# при 1366 строках и пределе 500.
|
|
69
|
+
#
|
|
70
|
+
# Почему фильтром потока, а не выражением для find: набор `-name *.py` пришлось бы подставлять
|
|
71
|
+
# без кавычек, и оболочка развернула бы звёздочку по файлам текущего каталога.
|
|
72
|
+
#
|
|
73
|
+
# `sh` в списке нет намеренно: печать в скрипте — его способ говорить с человеком, и запрет
|
|
74
|
+
# отладочной печати покрасил бы каждый из них.
|
|
75
|
+
only_code() { grep -E "\.($(printf '%s' "$CODE_EXT" | tr ' ' '|'))$"; }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Коммит несёт мини-отчёт
|
|
2
|
+
|
|
3
|
+
**Намерение.** Дифф показывает, ЧТО изменилось. Он не показывает, понял ли автор — человек или
|
|
4
|
+
агент — замысел, и в чём остаются сомнения. Тест проверяет реализацию против спеки; сам замысел
|
|
5
|
+
он проверить не может.
|
|
6
|
+
|
|
7
|
+
**Какой отказ это поймало.** Разбор чужого корпуса практик (журнал, 2026-09-04): 416 из 1069
|
|
8
|
+
коммитов одного проекта за 90 дней оказались `fix` — не фичей, а стабилизацией уже выкаченного;
|
|
9
|
+
диагноз владельца — «долг копится там, где человека убрали из центра принятия решений». Дешёвый
|
|
10
|
+
артефакт-привычка снижает шанс, что решение принято и забыто без следа.
|
|
11
|
+
|
|
12
|
+
**Почему машина, а не внимательность.** Написать отчёт «на будущее» — первое, что пропускают под
|
|
13
|
+
давлением дедлайна. Гейт делает это ценой, а не пожеланием.
|
|
14
|
+
|
|
15
|
+
**Готовый аналог.** Не искал: это не класс проверок, который держат готовые линтеры — они читают
|
|
16
|
+
код, а не историю коммитов.
|
|
17
|
+
|
|
18
|
+
**Чего НЕ ловит.** Не проверяет качество отчёта, только его наличие — «Сделано: починил» и «Не
|
|
19
|
+
уверен: не знаю» формально пройдут. Не проверяет, что отчёт правдив. Это ограничение того же
|
|
20
|
+
рода, что у любого чек-листа: полнота формы, а не смысл. Проверяет только последний коммит, а не
|
|
21
|
+
всю историю — если гейт поставили посреди работы, старые коммиты не переписывает и не осуждает.
|
|
22
|
+
|
|
23
|
+
**Когда не применяется.** Коммит, который трогает только журнал уроков (`lessons:` из манифеста,
|
|
24
|
+
по умолчанию `incidents/`), отчёта в теле не требует: сама запись и есть отчёт, причём подробнее,
|
|
25
|
+
и её сторожит `lesson-has-outcome`. Без этого исключения гейт краснел на каждой записи, сделанной
|
|
26
|
+
командой `aqk note`, — то есть инструмент воевал сам с собой. Исключение узкое: достаточно одного
|
|
27
|
+
файла за пределами журнала, чтобы требование вернулось.
|
|
28
|
+
|
|
29
|
+
Второй случай — **мелкий клон**. `actions/checkout` по умолчанию берёт один коммит; у него не
|
|
30
|
+
видно родителя, и git выдаёт всё дерево как изменённое. Состав коммита в таком клоне не
|
|
31
|
+
определить, поэтому гейт пропускает себя, напечатав причину и способ починки. Если хочешь, чтобы
|
|
32
|
+
он работал и в конвейере, дай тому два коммита истории:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
with:
|
|
37
|
+
fetch-depth: 2
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Гейт всё-таки местный: подсказка «допиши в тело коммита» выполнима до пуша, а не после.
|
|
41
|
+
|
|
42
|
+
**Образцы.** `red/COMMIT_MSG` — обычное тело коммита без отчёта. `green/COMMIT_MSG` — то же самое
|
|
43
|
+
плюс `Сделано:` и `Не уверен:`. Образцы — текстовые файлы, а не настоящий git: арбитр в реальном
|
|
44
|
+
проекте читает `git log -1`, а вложенный `.git` внутри каталога комплекта создал бы embedded-
|
|
45
|
+
репозиторий, который сам по себе стал бы проблемой версионирования.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Коммит без мини-отчёта — риск того же класса, что несоответствие спеки коду: диффом можно
|
|
3
|
+
# проверить реализацию, но не то, понял ли автор замысел. Отчёт — дешёвый артефакт, который
|
|
4
|
+
# оставляет след для человека, читающего историю позже.
|
|
5
|
+
DIR="${1:-.}"
|
|
6
|
+
|
|
7
|
+
# Образцы (gates.sh) читают тело коммита из файла — реальный git log там взять неоткуда, а
|
|
8
|
+
# вложенный .git внутри каталога комплекта сам по себе создал бы embedded-репозиторий.
|
|
9
|
+
# В настоящем проекте COMMIT_MSG не бывает — читается последний реальный коммит.
|
|
10
|
+
if [ -f "$DIR/COMMIT_MSG" ]; then
|
|
11
|
+
MSG=$(cat "$DIR/COMMIT_MSG")
|
|
12
|
+
else
|
|
13
|
+
if ! (cd "$DIR" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1); then
|
|
14
|
+
echo "не git-репозиторий — проверять нечего"
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
# Мелкий клон: у коммита не видно родителя, и git выдаёт всё дерево как изменённое. Состав
|
|
18
|
+
# коммита в этом случае не определить, а требовать отчёт вслепую нельзя — конвейер краснел бы
|
|
19
|
+
# на каждой записи журнала. Пропускаем, назвав причину: гейт этот всё равно местный, чинят его
|
|
20
|
+
# до пуша, а не после. Причина печатается, а не прячется.
|
|
21
|
+
if [ "$(cd "$DIR" && git rev-parse --is-shallow-repository 2>/dev/null)" = "true" ] &&
|
|
22
|
+
! (cd "$DIR" && git rev-parse -q --verify HEAD^ >/dev/null 2>&1); then
|
|
23
|
+
echo "мелкий клон: состав коммита не определить — проверка пропущена"
|
|
24
|
+
echo " чтобы она работала в конвейере, дай ему два коммита истории:"
|
|
25
|
+
echo " actions/checkout@v4 с fetch-depth: 2"
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
MSG=$(cd "$DIR" && git log -1 --format=%B 2>/dev/null)
|
|
30
|
+
|
|
31
|
+
# Коммит, который трогает только журнал, отчёта в теле не требует: сама запись и есть отчёт,
|
|
32
|
+
# причём подробнее — и её сторожит `lesson-has-outcome`. Иначе гейт воюет с командой `note`,
|
|
33
|
+
# которая коммитит запись сама. Путь журнала берётся из манифеста, а не угадывается.
|
|
34
|
+
# «#…» режется так же, как это делает сама программа при разборе манифеста
|
|
35
|
+
# (tool/lib/manifest.mjs): один файл обязан читаться по одним правилам.
|
|
36
|
+
LESSONS=$(sed -n 's/^lessons:[[:space:]]*//p' "$DIR/.aqk.yml" 2>/dev/null | head -1 |
|
|
37
|
+
sed 's/#.*$//' | tr -d '"'"'"' \r' | sed 's/[[:space:]]*$//')
|
|
38
|
+
[ -z "$LESSONS" ] && LESSONS="incidents"
|
|
39
|
+
case "$LESSONS" in http*) LESSONS="" ;; esac # journal по адресу, а не путём — не применимо
|
|
40
|
+
if [ -n "$LESSONS" ]; then
|
|
41
|
+
FILES=$(cd "$DIR" && git show --pretty=format: --name-only HEAD 2>/dev/null | grep -v '^$')
|
|
42
|
+
if [ -n "$FILES" ]; then
|
|
43
|
+
OUTSIDE=$(printf '%s\n' "$FILES" | grep -v "^$LESSONS/" | grep -v "^$LESSONS\$")
|
|
44
|
+
if [ -z "$OUTSIDE" ]; then
|
|
45
|
+
echo "коммит трогает только журнал ($LESSONS) — запись и есть отчёт"
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
[ -z "$MSG" ] && { echo "нет ни одного коммита — проверять нечего"; exit 0; }
|
|
52
|
+
|
|
53
|
+
MISSING=""
|
|
54
|
+
printf '%s\n' "$MSG" | grep -q "^Сделано:" || MISSING="$MISSING «Сделано:»"
|
|
55
|
+
printf '%s\n' "$MSG" | grep -q "^Не уверен:" || MISSING="$MISSING «Не уверен:»"
|
|
56
|
+
|
|
57
|
+
if [ -n "$MISSING" ]; then
|
|
58
|
+
echo "последний коммит без мини-отчёта — не хватает:$MISSING"
|
|
59
|
+
echo " почини: допиши в тело коммита короткие разделы Сделано: / Не уверен: —"
|
|
60
|
+
echo " не пересказ диффа, а что понял и в чём сомневаешься."
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
exit 0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
intent: коммит несёт мини-отчёт — что сделано и в чём агент не уверен
|
|
2
|
+
|
|
3
|
+
trigger:
|
|
4
|
+
always: true
|
|
5
|
+
|
|
6
|
+
recipes:
|
|
7
|
+
any: bash {gate}/check.sh {dir}
|
|
8
|
+
|
|
9
|
+
proof: incidents/README.md — «2026-09-04 — дифф проверяет реализацию, но не замысел»: 416 из 1069
|
|
10
|
+
коммитов чужого проекта за 90 дней оказались fix — стабилизацией уже выкаченного, не фичей
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Функция не вырастает до сложности, в которой её нельзя удержать в голове
|
|
2
|
+
|
|
3
|
+
**Намерение.** Ограничить сложность одной функции: сколько в ней ветвлений, инструкций и
|
|
4
|
+
уровней вложенности.
|
|
5
|
+
|
|
6
|
+
**Какой отказ это поймало.** Ревизия гейтов в `audit_project` показала: **сложность функций не
|
|
7
|
+
мерил никто**. Нашлось 75 функций сложнее нормы, худшая — с **29 ветвлениями**, и лежала она в
|
|
8
|
+
контуре проверки целостности файлов. Запись в журнале: `incidents/README.md`, 2026-08-27.
|
|
9
|
+
|
|
10
|
+
**Почему машина, а не внимательность.** Сложность растёт по одному условию за раз. Ни одна
|
|
11
|
+
правка не выглядит как «пора выделять функцию», а момент, когда стало поздно, проходит незаметно.
|
|
12
|
+
Для агента это дороже, чем для человека: в таком месте он начинает переписывать вместо правки.
|
|
13
|
+
|
|
14
|
+
**Готовый аналог есть, и он точнее.** В Python это `ruff --select C901,PLR0912,PLR0915`:
|
|
15
|
+
цикломатическая сложность, число ветвлений, число инструкций. В TypeScript — правила `complexity`
|
|
16
|
+
и `max-depth` в eslint. Рецепты под эти стеки берут готовое.
|
|
17
|
+
|
|
18
|
+
**Переносимая проверка грубее, и это признаётся.** Она меряет только **глубину вложенности** —
|
|
19
|
+
косвенный признак: функция может иметь двадцать ветвлений подряд без единого вложения и проверку
|
|
20
|
+
пройти. Она для стеков, где готового инструмента нет, а не замена ему.
|
|
21
|
+
|
|
22
|
+
**Разметке предел мягче.** В `.jsx`, `.tsx`, `.vue` и `.svelte` пять уровней вложенности — это
|
|
23
|
+
обычная вёрстка, а не сложная логика. Там предел выше на три. Без этой поправки проверка краснела
|
|
24
|
+
на нормальных компонентах живого проекта — шесть находок из девяти были именно такими.
|
|
25
|
+
|
|
26
|
+
**Предел настраивается** переменной `AQK_MAX_DEPTH`, по умолчанию пять уровней.
|
|
27
|
+
|
|
28
|
+
**Тяжёлая проверка.** На проекте в четыре тысячи файлов — около семи секунд. Место такой
|
|
29
|
+
проверки — перед пушем и в конвейере, а не на каждый коммит: гейт, который заставляет ждать,
|
|
30
|
+
начинают обходить.
|
|
31
|
+
|
|
32
|
+
**Чего НЕ ловит.** Функцию с двадцатью ветвлениями подряд без вложенности — переносимая мера
|
|
33
|
+
считает только глубину. Не различает функции внутри файла: меряется худшее место файла, а не
|
|
34
|
+
каждая функция отдельно. Готовые правила умеют и то, и другое.
|
|
35
|
+
|
|
36
|
+
**Образцы.** `red/` — восемь уровней вложенности. `green/` — тот же смысл, разбитый на две
|
|
37
|
+
функции с ранним выходом.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Грубая мера сложности: глубина вложенности. Это НЕ замена цикломатической сложности —
|
|
3
|
+
# готовые правила считают ветвления и инструкции и делают это точнее. Здесь запасной вариант
|
|
4
|
+
# для стеков, где готового инструмента нет.
|
|
5
|
+
#
|
|
6
|
+
# ЗАЧЕМ ВООБЩЕ. 29 ветвлений в одной функции — это код, который никто не держит в голове
|
|
7
|
+
# целиком: ни человек, ни агент. Агент в таком месте начинает переписывать вместо правки.
|
|
8
|
+
DIR="${1:-.}"
|
|
9
|
+
. "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
|
|
10
|
+
MAX="${AQK_MAX_DEPTH:-5}"
|
|
11
|
+
|
|
12
|
+
# shellcheck disable=SC2046
|
|
13
|
+
find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
14
|
+
| while IFS= read -r F; do is_generated "$F" || printf '%s\n' "$F"; done \
|
|
15
|
+
| xargs -r awk -v MAX="$MAX" '
|
|
16
|
+
# Один обход на все файлы: процесс на каждый файл дал 19 секунд на 4000 файлов.
|
|
17
|
+
# Разметка вложена по природе: пять уровней тегов — это не сложная логика, а обычная
|
|
18
|
+
# вёрстка. Меряя её тем же пределом, гейт краснеет на нормальном коде и его выключают.
|
|
19
|
+
function limit() { return (wf ~ /\.(jsx|tsx|vue|svelte)$/) ? MAX + 3 : MAX }
|
|
20
|
+
function flush() { if (worst > limit()) print wf ":" wl ": вложенность " worst ", предел " limit() }
|
|
21
|
+
FNR == 1 { flush(); worst = 0; wl = 0; wf = FILENAME }
|
|
22
|
+
/^[[:space:]]*$/ { next }
|
|
23
|
+
{
|
|
24
|
+
# ширина отступа: табуляция считается за четыре пробела
|
|
25
|
+
line = $0; n = 0
|
|
26
|
+
while (match(line, /^[ \t]/)) {
|
|
27
|
+
n += (substr(line, 1, 1) == "\t") ? 4 : 1
|
|
28
|
+
line = substr(line, 2)
|
|
29
|
+
}
|
|
30
|
+
depth = int(n / 4)
|
|
31
|
+
if (depth > worst) { worst = depth; wl = FNR }
|
|
32
|
+
}
|
|
33
|
+
END { flush() }
|
|
34
|
+
' > /tmp/.cplx.$$ 2>/dev/null
|
|
35
|
+
|
|
36
|
+
if [ -s /tmp/.cplx.$$ ]; then
|
|
37
|
+
cat /tmp/.cplx.$$; rm -f /tmp/.cplx.$$
|
|
38
|
+
echo " почини: выдели вложенные ветки в отдельные функции или выйди раньше."
|
|
39
|
+
echo " такой код не держат в голове целиком — ни человек, ни агент; агент начинает"
|
|
40
|
+
echo " переписывать его вместо правки."
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
rm -f /tmp/.cplx.$$
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
intent: функция не вырастает до сложности, в которой её нельзя удержать в голове
|
|
2
|
+
|
|
3
|
+
trigger:
|
|
4
|
+
always: true
|
|
5
|
+
|
|
6
|
+
recipes:
|
|
7
|
+
# Переносимая мера грубая — глубина вложенности. Готовые правила считают ветвления и
|
|
8
|
+
# инструкции, это точнее; см. kit/docs/ready-made-rules.md
|
|
9
|
+
any: bash {gate}/check.sh {dir}
|
|
10
|
+
python: ruff check --select C901,PLR0912,PLR0915 {dir}
|
|
11
|
+
typescript: eslint --rule '{"complexity":["error",10],"max-depth":["error",4]}' {dir}
|
|
12
|
+
|
|
13
|
+
proof: incidents/README.md — «2026-08-27 ревизия гейтов», п. 6: 75 функций сложнее нормы, худшая с 29 ветвлениями
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Код, который никто не вызывает, не остаётся в проекте
|
|
2
|
+
|
|
3
|
+
**Намерение.** Функции, классы и переменные без единого обращения удаляются, а не копятся.
|
|
4
|
+
|
|
5
|
+
**Какой отказ это поймало.** Ревизия гейтов в `audit_project`: **мёртвые функции бэкенда не искал
|
|
6
|
+
никто.** Импорты закрывал линтер, фронт закрывал `knip`, бэкенд был пуст. Первый же прогон дал
|
|
7
|
+
реестр на **148 записей**. Запись в журнале: `incidents/README.md`, 2026-08-27.
|
|
8
|
+
|
|
9
|
+
**Почему это про агентов особенно.** Агенты **не удаляют код** — обучение награждает за
|
|
10
|
+
написанное, не за удалённое. Отсюда мёртвые области, которые не ловит даже беглый взгляд: код
|
|
11
|
+
выглядит рабочим, он просто никому не нужен.
|
|
12
|
+
|
|
13
|
+
**Переносимой проверки здесь нет намеренно.** Чтобы понять, вызывают ли функцию, нужен граф
|
|
14
|
+
вызовов, а не поиск по тексту. Проверка на `grep` давала бы ложные срабатывания на каждом
|
|
15
|
+
динамическом вызове — и её выключили бы в первый день.
|
|
16
|
+
|
|
17
|
+
Это тот случай, когда без готового инструмента не обойтись. Под каждый распространённый стек он
|
|
18
|
+
есть: `vulture` для Python, `knip` для TypeScript и JavaScript, `staticcheck -checks U1000` для
|
|
19
|
+
Go, `dead_code` в clippy для Rust.
|
|
20
|
+
|
|
21
|
+
**Что это значит на практике.** На машине без нужного инструмента проверка каталога скажет
|
|
22
|
+
«не проверена — нужен один из: vulture, npx, staticcheck, cargo» и не выдаст её за рабочую.
|
|
23
|
+
|
|
24
|
+
**Чего НЕ ловит.** Динамические вызовы: то, что зовут по имени из строки, из настроек или через
|
|
25
|
+
отражение. Поэтому у `vulture` есть уровень уверенности, а находки читают глазами прежде, чем
|
|
26
|
+
удалять. Первый прогон такого инструмента — **лавина**, и разбирают её по частям, а не разом.
|
|
27
|
+
|
|
28
|
+
**Готовый аналог — это и есть он сам.** Своей проверки здесь не будет.
|
|
29
|
+
|
|
30
|
+
**Образцы.** `red/` — функция, которую никто не зовёт. `green/` — та же функция, вызванная.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
intent: код, который никто не вызывает, не остаётся в проекте
|
|
2
|
+
|
|
3
|
+
# Только там, где есть готовый инструмент: переносимой проверки здесь быть не может, а
|
|
4
|
+
# показывать запись тем, кому её нечем исполнить, — значит показывать работу, которую
|
|
5
|
+
# человек сделать не сможет.
|
|
6
|
+
trigger:
|
|
7
|
+
langs: python, typescript, javascript, go, rust
|
|
8
|
+
|
|
9
|
+
# Переносимого рецепта здесь нет намеренно: чтобы понять, вызывают ли функцию, нужен граф
|
|
10
|
+
# вызовов, а не поиск по тексту. Это тот случай, когда без готового инструмента не обойтись —
|
|
11
|
+
# и он есть под каждый распространённый стек.
|
|
12
|
+
recipes:
|
|
13
|
+
python: vulture --min-confidence 60 {dir}
|
|
14
|
+
typescript: npx --yes knip@6 --directory {dir}
|
|
15
|
+
javascript: npx --yes knip@6 --directory {dir}
|
|
16
|
+
go: staticcheck -checks U1000 {dir}/...
|
|
17
|
+
rust: cargo clippy --manifest-path {dir}/Cargo.toml -- -D dead_code
|
|
18
|
+
|
|
19
|
+
# Каким рецептом написаны образцы: без переносимого рецепта проверка обязана знать это точно,
|
|
20
|
+
# иначе питоновские образцы поедут проверяться фронтовым инструментом.
|
|
21
|
+
samples_for: python
|
|
22
|
+
|
|
23
|
+
proof: incidents/README.md — «2026-08-27 ревизия гейтов», п. 5: мёртвые функции бэкенда не искал никто, реестр на 148 записей
|