agent-quality-kit 0.14.0 → 0.16.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.
Files changed (74) hide show
  1. package/README.md +88 -18
  2. package/README.ru.md +91 -19
  3. package/kit/docs/ai/index.md +1 -0
  4. package/kit/docs/ai/operational-gates.md +275 -0
  5. package/kit/gates/_target.sh +53 -0
  6. package/kit/gates/ci-actually-fails/check.sh +18 -3
  7. package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +10 -0
  8. package/kit/gates/entry-commands-exist/check.sh +88 -12
  9. package/kit/gates/hook-actually-fires/README.md +12 -0
  10. package/kit/gates/hook-actually-fires/check.sh +66 -6
  11. package/kit/gates/hook-actually-fires/gate.yml +2 -2
  12. package/kit/gates/hook-actually-fires/green/.claude/hooks/auto-format.sh +3 -0
  13. package/kit/gates/hook-actually-fires/green/.claude/hooks/block-dangerous.sh +3 -0
  14. package/kit/gates/hook-actually-fires/green/.claude/hooks/done.sh +3 -0
  15. package/kit/gates/hook-actually-fires/green/.claude/hooks/idle.sh +3 -0
  16. package/kit/gates/hook-actually-fires/green/.claude/hooks/prompt.sh +3 -0
  17. package/kit/gates/hook-actually-fires/green/.claude/hooks/session.mjs +1 -0
  18. package/kit/gates/hook-actually-fires/green/.claude/hooks/stop-gate.sh +3 -0
  19. package/kit/gates/hook-actually-fires/green/.claude/settings.json +12 -0
  20. package/kit/gates/test-not-adjusted/README.md +31 -0
  21. package/llms.txt +26 -7
  22. package/package.json +1 -1
  23. package/tool/commands/context.mjs +39 -41
  24. package/tool/commands/doctor-catalog.mjs +35 -10
  25. package/tool/commands/doctor.mjs +18 -26
  26. package/tool/commands/feedback.mjs +231 -0
  27. package/tool/commands/gates.mjs +12 -6
  28. package/tool/commands/project.mjs +11 -13
  29. package/tool/commands/prompt.mjs +2 -1
  30. package/tool/commands/report.mjs +19 -3
  31. package/tool/commands/vitals.mjs +9 -3
  32. package/tool/i18n/en-docs.mjs +19 -2
  33. package/tool/i18n/en-gates.mjs +35 -0
  34. package/tool/i18n/en.mjs +29 -2
  35. package/tool/i18n/ru-docs.mjs +18 -2
  36. package/tool/i18n/ru-gates.mjs +36 -0
  37. package/tool/i18n/ru.mjs +27 -2
  38. package/tool/lib/adopt.mjs +58 -4
  39. package/tool/lib/ask.mjs +118 -0
  40. package/tool/lib/brief.mjs +17 -38
  41. package/tool/lib/core.mjs +49 -12
  42. package/tool/lib/execution.mjs +32 -1
  43. package/tool/lib/gate-worker.mjs +4 -1
  44. package/tool/lib/manifest.mjs +39 -13
  45. package/tool/lib/prove.mjs +3 -3
  46. package/tool/lib/run.mjs +142 -10
  47. package/tool/program.mjs +6 -0
  48. package/tool/selfcheck/smoke/_fixture.mjs +13 -1
  49. package/tool/selfcheck/smoke/fail-closed.test.mjs +96 -1
  50. package/tool/selfcheck/smoke/feedback-send.test.mjs +87 -0
  51. package/tool/selfcheck/smoke/first-run.test.mjs +67 -3
  52. package/tool/selfcheck/smoke/preflight.test.mjs +83 -0
  53. package/tool/selfcheck/smoke/verdict.test.mjs +50 -4
  54. package/tool/selfcheck/smoke/version-sync.test.mjs +140 -0
  55. package/tool/selfcheck/smoke.sh +106 -4
  56. package/tool/selfcheck/units-ask.mjs +85 -0
  57. package/tool/selfcheck/units-brief.mjs +3 -13
  58. package/tool/selfcheck/units-context.mjs +2 -1
  59. package/tool/selfcheck/units-execution.mjs +37 -1
  60. package/tool/selfcheck/units-feedback.mjs +137 -0
  61. package/tool/selfcheck/units-level.mjs +41 -1
  62. package/tool/selfcheck/units-repo.mjs +75 -0
  63. package/tool/selfcheck/units-vitals.mjs +27 -0
  64. package/kit/gates/entry-links-exist/README.md +0 -27
  65. package/kit/gates/entry-links-exist/check.sh +0 -33
  66. package/kit/gates/entry-links-exist/gate.yml +0 -17
  67. package/kit/gates/entry-links-exist/green/AGENTS.md +0 -10
  68. package/kit/gates/entry-links-exist/green/rules/general.md +0 -3
  69. package/kit/gates/entry-links-exist/red/AGENTS.md +0 -3
  70. package/kit/gates/no-phantom-package/README.md +0 -84
  71. package/kit/gates/no-phantom-package/check.sh +0 -168
  72. package/kit/gates/no-phantom-package/gate.yml +0 -20
  73. package/kit/gates/no-phantom-package/green/AGENTS.md +0 -15
  74. package/kit/gates/no-phantom-package/red/AGENTS.md +0 -15
@@ -0,0 +1,275 @@
1
+ # Эксплуатационные гейты из живого AI-проекта: что переносить, а что не притворять универсальным
2
+
3
+ > Проверено по коду `audit_project` 15.09.2026, ссылки на первоисточники сверены 16.09.2026. Это актуальная карта практики, а не ручной
4
+ > снимок production-метрик. Живые значения остаются в Prometheus, логах и отчётах прогонов.
5
+ >
6
+ > Главная граница: здесь описаны **намерения и устройство арбитров**. Реализация конкретного
7
+ > Django/Celery/Compose-проекта остаётся в его репозитории и объявляется в `.aqk.yml` командой,
8
+ > а не копируется в AQK.
9
+
10
+ ## Один вывод
11
+
12
+ Зелёная фича ещё не означает зелёную систему. Query budget одной ручки, memory budget одной
13
+ задачи и тест одного worker не отвечают на вопрос, выдержит ли одновременно работающий флот
14
+ общую RAM, PostgreSQL, Redis, диск и очередь.
15
+
16
+ Поэтому бюджетов всегда два:
17
+
18
+ 1. **локальный** — SQL, время, память и корректность одного endpoint/task;
19
+ 2. **системный** — p95, error rate, суммарный working set, очереди и OOM под смешанной нагрузкой.
20
+
21
+ ## Карта по факту
22
+
23
+ | Область | Что действительно держит машина | Честная граница |
24
+ |---|---|---|
25
+ | N+1 | 21 тестовый файл и 63 вызова `assertNumQueries` / `django_assert_max_num_queries`; полный pytest блокирует рост известных бюджетов | нет требования, что **каждая новая** list/paginated-ручка получила budget-тест |
26
+ | DirectPG N+1 | отдельный тест проверяет не только число запросов, но и отсутствие линейного роста на увеличенном наборе | это проектный арбитр, обычный Django-счётчик его не заменяет |
27
+ | OOM после факта | `ContainerOOM` на росте `container_oom_events_total`; выражение имеет firing и quiet сценарии `promtool` | cAdvisor в основном описывает работающие контейнеры; история exited/restart требует отдельного источника |
28
+ | Лимиты observability | тест разбирает Compose и держит сумму лимитов tier под явным капом | это кап одного tier, не бюджет всего production-флота и не резерв RAM хоста |
29
+ | Celery topology | объявленные/используемые очереди сравниваются с `worker -Q` в production Compose | имена и структура конфигурации проектные |
30
+ | Celery redelivery | `visibility_timeout` сравнивается с максимальным hard time limit и запасом | длинный timeout ухудшает скорость возврата работы после принудительной смерти worker |
31
+ | Публичные метрики | publisher обязан попасть в реестр; быстрый pre-commit ловит забытое имя, pytest проверяет глубокую проводку | наличие имени не доказывает, что production реально его собирает |
32
+ | Prometheus rules | синтаксис и выбранная семантика правил проверяются `promtool`; после deploy сверяется набор реально загруженных правил | не каждое правило уже имеет отдельные firing/quiet samples |
33
+ | Скрипты-гейты | `check-gate-wiring.py` требует автоматический запуск либо честную запись в реестре ручных инструментов | «запускается» не означает «полезен»; это другой вопрос |
34
+ | Ошибки и trace | GlitchTip, Sentry SDK, Loki/Alloy и `request_id` связывают исключение с окружением запроса | доступность самой цепочки также нуждается в heartbeat/alert |
35
+ | Backup | расписание, ротация, метрика успеха, `BackupStale` и тесты скрипта | копия на том же сервере не переживёт потерю сервера; restore надо репетировать отдельно |
36
+
37
+ ## Что ещё только видно, но не закрыто
38
+
39
+ Наличие графика или Telegram-alert не равно предотвращению.
40
+
41
+ ### Память
42
+
43
+ - нет ранней полосы «контейнер долго держится выше доли своего лимита»;
44
+ - нет отношения `sum(container working_set) / host RAM` для всего флота;
45
+ - нет host-level RAM/swap/PSI прибора уровня node exporter;
46
+ - нет долговечной истории OOM/restart для уже остановившихся контейнеров;
47
+ - нет memory budgets для всех тяжёлых pytest-сценариев;
48
+ - сумма отдельных потолков не является резервированием общей памяти.
49
+
50
+ Последний пункт особенно важен. Docker ограничивает отдельный сервис, но несколько сервисов
51
+ могут одновременно приблизиться к своим пределам и исчерпать хост. Поэтому static Compose gate
52
+ и runtime fleet gate решают разные задачи; один нельзя выдавать за другой.
53
+
54
+ ### PostgreSQL и Redis
55
+
56
+ Экспортёры есть, но нужны отдельные решения по сигналам:
57
+
58
+ - насыщение соединений и ожидание PgBouncer;
59
+ - длинные транзакции, deadlocks, temp bytes и блокировки;
60
+ - память Redis broker при `noeviction`;
61
+ - rejected writes, а не только evictions;
62
+ - возраст старейшей работы и ожидаемое время разбора очереди, а не одна глубина.
63
+
64
+ ### Нагрузка
65
+
66
+ - Python load harness измеряет p50/p95/p99 и RPS, но плохие числа сами не дают красный код;
67
+ - k6 thresholds существуют лишь у отдельных ручных сценариев;
68
+ - нет обязательного смешанного прогона HTTP + Celery + отчёты + документы + интеграции;
69
+ - нет nightly/pre-release запуска;
70
+ - нет регулярного soak для утечек, high-watermark процессов и накопления соединений.
71
+
72
+ ## Как строится честный OOM-контур
73
+
74
+ Одного правила `any OOM` мало. Нужны пять независимых слоёв.
75
+
76
+ ### 1. Статические лимиты сервисов
77
+
78
+ Production Compose задаёт `deploy.resources.limits.memory` или эквивалент. Проверка разбирает
79
+ эффективную конфигурацию, а не ищет слово `memory` grep-ом. Для каждого обязательного сервиса
80
+ должно быть ясно, почему лимит такой и каким замером он получен.
81
+
82
+ ### 2. Конфигурационный бюджет
83
+
84
+ Сумма лимитов выбранного tier не растёт молча. Это храповик конфигурации, а не обещание, что RAM
85
+ зарезервирована. Поднять кап можно только вместе с измерением хоста и записанной причиной.
86
+
87
+ ### 3. Runtime budget флота
88
+
89
+ Под смешанной нагрузкой считается:
90
+
91
+ ```text
92
+ max_over_time((sum(container_memory_working_set_bytes))[15m:1m]) / host_memory_bytes
93
+ ```
94
+
95
+ Точный PromQL зависит от labels и источника host memory. Порог нельзя копировать вслепую; для
96
+ малого сервера разумная стартовая красная полоса должна оставлять запас ядру, Docker, page cache
97
+ и пикам PostgreSQL. В исследованном проекте рабочей гипотезой остаётся 70%, но она ещё не стала
98
+ доказанным гейтом.
99
+
100
+ ### 4. Раннее предупреждение контейнера
101
+
102
+ Устойчивое превышение доли лимита предупреждает до OOM. Рабочая гипотеза проекта — около 85%,
103
+ но её надо откалибровать по пикам и шуму. Мгновенный spike и десять минут давления — разные
104
+ события.
105
+
106
+ ### 5. OOM и restart после факта
107
+
108
+ Рост `container_oom_events_total` — critical. Но исчезновение ряда остановленного контейнера не
109
+ должно превратить аварию в тишину: Docker events/inspect или другой collector хранит exit reason,
110
+ restart count и timestamp независимо от текущей жизни контейнера.
111
+
112
+ ### Доказательство правила
113
+
114
+ Для alert нужны минимум два `promtool`-сценария:
115
+
116
+ - counter вырос внутри окна → alert firing с ожидаемыми labels/annotations;
117
+ - counter не растёт → alert отсутствует.
118
+
119
+ Синтаксически корректное выражение `vector(0)` обязано сделать первый сценарий красным. Если вся
120
+ батарея остаётся зелёной, семантика OOM не проверяется.
121
+
122
+ ## Как строится N+1-гейт
123
+
124
+ Один статический поиск `select_related` ничего не доказывает. Арбитр должен наблюдать полный
125
+ операционный путь.
126
+
127
+ 1. Создать больше одного связанного объекта; для list endpoint обычно нужны десятки строк.
128
+ 2. Выполнить настоящий HTTP-запрос или функцию задачи.
129
+ 3. Зафиксировать постоянный максимум SQL, а не `текущее число + 1`.
130
+ 4. Увеличить объём fixture и проверить, что число запросов не растёт линейно.
131
+ 5. Запускать тест в общей блокирующей батарее.
132
+
133
+ Django официально предоставляет `assertNumQueries`. Для pytest допустима обёртка с верхним
134
+ пределом, если она печатает выполненные запросы при превышении. Direct SQL, Trino и внешние базы
135
+ нуждаются в своём счётчике — ORM-инструмент не видит их по построению.
136
+
137
+ Честная граница: наличие двадцати одного budget-теста не доказывает покрытие двадцать второй ручки.
138
+ Отдельный coverage-gate возможен только тогда, когда проект умеет машинно определить множество
139
+ ручек, для которых budget обязателен. Проверять слово в имени теста недостаточно.
140
+
141
+ ## Два инварианта Celery
142
+
143
+ ### Очередь имеет потребителя
144
+
145
+ Сопоставляются три факта:
146
+
147
+ - очереди, объявленные в настройках;
148
+ - очереди, используемые routes, task decorators и `apply_async`;
149
+ - очереди, которые реально слушают production workers.
150
+
151
+ Новая очередь без consumer красит CI. Документ или диаграмма не являются источником истины.
152
+
153
+ ### Доставка не опережает hard timeout
154
+
155
+ Redis visibility timeout определяет, когда неacknowledged message возвращается в очередь. Если
156
+ задача законно выполняется дольше, второй worker может получить её повторно. Проектный гейт
157
+ сравнивает отношения настроек и fail-closed обрабатывает неразрешимые значения.
158
+
159
+ Это не универсальная формула для всех Celery-систем: ETA/retry и принудительное завершение
160
+ создают обратную цену слишком большого timeout. Инвариант должен учитывать модель задач проекта.
161
+
162
+ ## Метрика и алерт — это цепочка, а не файл
163
+
164
+ Полный путь выглядит так:
165
+
166
+ ```text
167
+ код публикует имя
168
+ → exporter его принимает
169
+ → Prometheus scrape видит ряд
170
+ → правило вычисляется правильно
171
+ → Alertmanager маршрутизирует
172
+ → человек получает сообщение
173
+ → runbook говорит, что делать
174
+ ```
175
+
176
+ Для каждого шва нужен свой арбитр. Unit-test publisher не доказывает scrape. `promtool check`
177
+ не доказывает смысл выражения. Тест выражения не доказывает, что production загрузил новый файл.
178
+ После deploy полезна read-only сверка repo rule names с Prometheus API.
179
+
180
+ ## Нагрузочный гейт и soak
181
+
182
+ Короткий mixed scenario должен иметь код возврата и одновременно держать:
183
+
184
+ - error rate;
185
+ - p95 ключевых пользовательских путей;
186
+ - throughput или время разбора очередей;
187
+ - суммарный working set флота;
188
+ - отсутствие OOM/restart;
189
+ - при необходимости соединения PostgreSQL и rejected writes Redis.
190
+
191
+ Короткий прогон не ловит медленную утечку. Soak запускается отдельно и реже, с теми же
192
+ инвариантами, но с проверкой тренда памяти, соединений и очередей во времени. Смешивать soak с
193
+ каждым pre-push нельзя: дорогой гейт начнут обходить.
194
+
195
+ ## Где здесь агент
196
+
197
+ Агент не должен решать, случилась ли аномалия. Сначала детерминированный detector:
198
+
199
+ ```text
200
+ Prometheus rule или baseline script
201
+ → фильтрация и группировка события
202
+ → краткоживущий read-only агент
203
+ → гипотеза + evidence + confidence + связанный deploy
204
+ → issue / intent / merge request
205
+ → решение человека
206
+ ```
207
+
208
+ По расписанию работает baseline script. Модель вызывается только по нарушению полосы. На первом
209
+ этапе ей достаточно read-only доступа к Prometheus, Loki, error tracker и истории deploy; права
210
+ на production DB, Docker socket, merge и произвольный runbook не нужны.
211
+
212
+ ## Как подключать это через AQK без дублей
213
+
214
+ AQK-манифест объявляет **существующую команду проекта**:
215
+
216
+ ```yaml
217
+ gates:
218
+ project-verify: "bash scripts/verify.sh"
219
+ prometheus-rules: "docker run --rm ... promtool test rules alerts_test.yml"
220
+ groups:
221
+ code: [project-verify]
222
+ observability: [prometheus-rules]
223
+ ```
224
+
225
+ Внутренние шаги `project-verify` не перечисляются второй раз в манифесте. При падении агрегатор
226
+ обязан назвать конкретный шаг. Отдельным gate становится только команда, которую агрегатор
227
+ честно не запускает.
228
+
229
+ Один gate AQK получает не больше пяти минут. Если полный verifier проекта дольше, в манифесте
230
+ нужен его быстрый блокирующий режим, а `--full` — отдельный прямой или CI-прогон. Иначе AQK будет
231
+ честно возвращать «не смог проверить: timeout», но интеграция не станет рабочим локальным gate.
232
+
233
+ Не надо:
234
+
235
+ - копировать project-specific Python checks в `kit/gates`;
236
+ - объявлять grep по словам `memory` или `assertNumQueries` доказательством;
237
+ - указывать похожий каталог как `samples`, если структура AQK red/green там отсутствует;
238
+ - повышать уровень AQK заглушками;
239
+ - запускать `aqk init --force` поверх зрелого корпуса правил без review.
240
+
241
+ Новая запись каталога AQK появляется после трёх доказательств: устойчиво опознаваемая конструкция,
242
+ red/green samples и переносимый рецепт с понятной границей ложных срабатываний. До этого это
243
+ методика или собственный gate проекта — и это честное состояние.
244
+
245
+ ## Порядок для нового проекта
246
+
247
+ 1. До кода записать измеримые AC: SQL, p95, error rate, memory, очередь, идемпотентность.
248
+ 2. Быструю детерминированную статику поставить в pre-commit.
249
+ 3. Query и task budgets поставить в обычный CI.
250
+ 4. Compose limits и alert rules проверять нативными валидаторами.
251
+ 5. Mixed load запускать nightly и перед релизом; soak — отдельно, например еженедельно.
252
+ 6. Любой OOM сделать красным исходом и сохранить причину после смерти контейнера.
253
+ 7. Только после стабилизации сигналов подключать read-only агента к нескольким critical alerts.
254
+
255
+ ## Официальные источники
256
+
257
+ - [Django 5.2: `assertNumQueries`](https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.TransactionTestCase.assertNumQueries)
258
+ - [Prometheus: unit testing rules через `promtool`](https://prometheus.io/docs/prometheus/latest/configuration/unit_testing_rules/)
259
+ - [cAdvisor: Prometheus metrics, включая `container_oom_events_total`](https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md)
260
+ - [Docker Compose: resource limits](https://docs.docker.com/reference/compose-file/deploy/#resources)
261
+ - Docker про то, что сумма отдельных limits не гарантирует запас хоста — дословно:
262
+ «Using `--reserve-memory` and `--limit-memory` does not guarantee that Docker will not use more
263
+ memory on your host than you want», страница
264
+ [`docker service create`](https://docs.docker.com/reference/cli/docker/service/create/).
265
+ Якорь раздела не указан намеренно: страница собирается на стороне браузера, и проверить
266
+ существование якоря запросом нельзя — а ссылка на несуществующий якорь молча ведёт наверх.
267
+ - [Celery Redis: visibility timeout и redelivery](https://docs.celeryq.dev/en/main/getting-started/backends-and-brokers/redis.html#visibility-timeout)
268
+
269
+ ## Финальный принцип
270
+
271
+ > Сначала детерминированный сигнал и ограничение. Потом агент. Никогда наоборот.
272
+
273
+ Повторившаяся ошибка должна оставлять после себя не ещё один абзац, а арбитр с кодом возврата.
274
+ Если переносимого арбитра пока нет, граница называется вслух — текст не получает фальшивое имя
275
+ «гейт».
@@ -0,0 +1,53 @@
1
+ # Общий шов для проверок, судящих ПУТЬ, взятый из текста.
2
+ #
3
+ # ЗАЧЕМ ЭТОТ ФАЙЛ. Замеры 14-15 сентября 2026 по 290 чужим репозиториям нашли у нас двенадцать
4
+ # ложных срабатываний. Разбор показал, что это не двенадцать ошибок, а ОДНА, повторённая
5
+ # двенадцатью способами: мы судили строку как путь на диске, не зная, чем она разрешается.
6
+ # Разрешало её то, чего у нас нет, — адрес на github.com, переменная окружения, генератор сайта,
7
+ # соседний репозиторий вики, проза вокруг.
8
+ #
9
+ # Чинить такое по одному значит растить в каждом гейте свой список исключений; через три правки
10
+ # списки разойдутся, и дважды пойманная беда вернётся через тот гейт, куда её не дописали.
11
+ # Поэтому решение одно и здесь.
12
+ #
13
+ # ТРИ ИСХОДА, А НЕ ДВА — тот же договор, что у всего комплекта:
14
+ # disk путь ведёт на файл в этом каталоге, судить можно
15
+ # unresolved разрешается чем-то, чего у нас нет: МОЛЧАТЬ НЕЛЬЗЯ, но и обвинять нельзя
16
+ # skip не путь вовсе (сеть, почта, якорь) — говорить не о чем
17
+
18
+ # Якорь и строка запроса — не часть имени файла. `assets/x.gif?raw=1` лежит на диске как
19
+ # `assets/x.gif`, `tutorial/#install` — это адрес страницы. Чистится ЗДЕСЬ, а не у зовущего:
20
+ # иначе каждый гейт будет чистить по-своему, и один забудет.
21
+ clean_target() {
22
+ T="${1%%#*}"
23
+ printf '%s' "${T%%\?*}"
24
+ }
25
+
26
+ # Возвращает слово исхода, а для unresolved — ещё и причину через двоеточие.
27
+ classify_target() {
28
+ T="$1"
29
+ case "$T" in
30
+ ""|\#*) echo "skip"; return ;;
31
+ # ЛЮБАЯ СХЕМА, В ЛЮБОМ РЕГИСТРЕ. Замер 2026-09-15: `file://Users/...` и
32
+ # `Https://conventionalcommits.org` с заглавной H объявлены битыми файлами. Раньше узнавались
33
+ # только `http://` и `https://` строчными — и это была догадка о том, как люди пишут.
34
+ *://*|mailto:*|MAILTO:*) echo "skip"; return ;;
35
+ # Адрес почты целью ссылки без «mailto:». Замер: три таких объявлены битыми файлами.
36
+ *@*.*) echo "skip"; return ;;
37
+ esac
38
+ case "$T" in
39
+ # Путь, уходящий выше корня: так сам GitHub предлагает ссылаться на выпуски и задачи —
40
+ # `../../releases` считается от адреса файла в вебе. Проверено: страница отдаёт 200.
41
+ ../*) echo "unresolved:адрес на github.com" ;;
42
+ # Путь от корня САЙТА, а не репозитория.
43
+ /*) echo "unresolved:адрес на github.com" ;;
44
+ # Страница вики живёт в отдельном репозитории `<репо>.wiki`, которого мы не скачиваем.
45
+ wiki/*) echo "unresolved:вики в отдельном репозитории" ;;
46
+ # Путь с косой чертой на конце и страница `.html` — это адрес опубликованного сайта:
47
+ # mkdocs, docusaurus, pkgdown собирают их при публикации. Проверено: 200 на их сайтах.
48
+ */|*.html|*.htm) echo "unresolved:страницу собирает генератор сайта" ;;
49
+ # Нераскрытая переменная: значение задаётся снаружи.
50
+ *'$'*) echo "unresolved:путь зависит от переменной" ;;
51
+ *) echo "disk" ;;
52
+ esac
53
+ }
@@ -90,6 +90,12 @@ for F in $CI; do
90
90
  for (j = 1; j <= nr; j++) if (R[j] != "" && R[j] == id) return 1
91
91
  return 0
92
92
  }
93
+ # Погашенная команда, о которой ещё не решено. Печатается, когда стало видно, что после неё
94
+ # в блоке ничего нет: тогда исход шага действительно погашен.
95
+ function reportMask( ) {
96
+ if (maskLine) printf "%s:%d: провал погашен прямо в команде: %s\n", file, maskLine, substr(maskText, 1, 90)
97
+ maskLine = 0
98
+ }
93
99
  function flush( ) {
94
100
  if (blockStart && blockCheck && blockMask && !isRedeemed(blockId))
95
101
  printf "%s:%d: проверка не может провалиться — шаг под %s\n", file, blockCheckLine, blockMaskText
@@ -100,8 +106,17 @@ for F in $CI; do
100
106
  # вспомогательной команде внутри скрипта (`docker network create … || true`) — это
101
107
  # идемпотентность, а не выключенная проверка.
102
108
  if (!isComment($0) && $0 ~ runners && $0 ~ /\|\|[[:space:]]*(true|:|exit[[:space:]]+0)/) {
103
- line = $0; sub(/^[[:space:]]+/, "", line)
104
- printf "%s:%d: провал погашен прямо в команде: %s\n", file, NR, substr(line, 1, 90)
109
+ # ОБВИНЯЕМ, ТОЛЬКО ЕСЛИ ПОГАШЕННАЯ КОМАНДА — ПОСЛЕДНЯЯ В БЛОКЕ. Замер 2026-09-14 по
110
+ # семидесяти чужим конвейерам дал одно-единственное срабатывание, и оно было ЛОЖНЫМ:
111
+ # `pnpm eslint src > out.txt || true` строкой ниже сверяется `diff` с эталоном —
112
+ # инструмент ОБЯЗАН выйти ненулевым, а вердикт выносит следующая команда. Шаг
113
+ # проваливается прекрасно. Инструмент, который обвиняет напрасно, выключают целиком,
114
+ # поэтому здесь молчание честнее догадки.
115
+ maskLine = NR; maskText = $0; sub(/^[[:space:]]+/, "", maskText)
116
+ } else if (maskLine && !isComment($0) && $0 !~ /^[[:space:]]*$/) {
117
+ # Печать вердикта не выносит: `echo` после гашения ничего не меняет.
118
+ if ($0 ~ /^[[:space:]]*(-[[:space:]]+)?[A-Za-z0-9_.-]+[[:space:]]*:/) reportMask()
119
+ else if ($0 !~ /^[[:space:]]*(echo|printf|cat|ls)[[:space:]]/) maskLine = 0
105
120
  }
106
121
  if (isBoundary($0)) flush()
107
122
  if (!blockStart) blockStart = NR
@@ -111,7 +126,7 @@ for F in $CI; do
111
126
  blockId = $0; sub(/^[^:]*:[[:space:]]*/, "", blockId); gsub(/[[:space:]"'"'"']/, "", blockId)
112
127
  }
113
128
  }
114
- END { flush() }' 2>/dev/null)
129
+ END { reportMask(); flush() }' 2>/dev/null)
115
130
  [ -z "$RES" ] || BAD="$BAD$RES
116
131
  "
117
132
  done
@@ -28,3 +28,13 @@ jobs:
28
28
  - name: провалить сборку, если линтер был красным
29
29
  if: steps.lint.outcome == 'failure'
30
30
  run: exit 1
31
+
32
+ # ЗАКОННОЕ ГАШЕНИЕ: инструмент ОБЯЗАН выйти ненулевым, а вердикт выносит следующая
33
+ # команда того же блока. Найдено замером 2026-09-14 по семидесяти чужим конвейерам:
34
+ # это было ЕДИНСТВЕННОЕ срабатывание гейта на всём замере — и оно оказалось ложным
35
+ # (HorusGoul/eslint-plugin-react-render-types, .github/workflows/ci.yml:51). Снимок
36
+ # вывода линтера сверяется с эталоном, шаг проваливается на `diff`.
37
+ - name: линтер как снимок
38
+ run: |
39
+ pnpm eslint src > /tmp/lint-output.txt || true
40
+ diff lint-snapshot.txt /tmp/lint-output.txt
@@ -41,12 +41,28 @@ ENTRIES=$( { find "$DIR" -maxdepth 1 -type f \( -iname 'agents.md' -o -iname 'cl
41
41
  # --- что в проекте есть ---------------------------------------------------------
42
42
  # Скрипты — только из блока "scripts" каждого package.json (рабочие пространства тоже: команда
43
43
  # из свода часто живёт в пакете, а не в корне). Файл склеивается в строку: блок бывает и в одну.
44
- SCRIPTS=$(find "$DIR" $(skip_find) -type f -name package.json -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$' \
45
- | while IFS= read -r P; do
46
- tr '\r\n' ' ' < "$P" | sed -n 's/.*"scripts"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' \
47
- | grep -oE '"[^"]+"[[:space:]]*:' | sed 's/^"//; s/"[[:space:]]*:$//'
48
- done)
44
+ # ИМЕНА СКРИПТОВ ЧИТАЕТ РАЗБОРЩИК JSON, А НЕ РЕГУЛЯРКА. Найдено 2026-09-15 на
45
+ # `jantimon/web-performance-debugger`: их скрипт `prebuild` содержит
46
+ # `rmSync('dist',{recursive:true,force:true})` фигурная скобка ВНУТРИ значения. Прежний разбор
47
+ # `[^}]*` обрывался на ней, и шесть существующих скриптов объявлялись несуществующими. Письмо
48
+ # ушло бы живому человеку с неправдой.
49
+ #
50
+ # Структуру разбирает тот, кто умеет её разбирать. `package.json` означает проект на Node, и node
51
+ # там почти наверняка есть; но «почти» нам не годится — без него мы НЕ ПРОВЕРЯЕМ скрипты и
52
+ # говорим об этом, а не додумываем регуляркой.
53
+ NPM_BLIND=""
54
+ if command -v node >/dev/null 2>&1; then
55
+ SCRIPTS=$(find "$DIR" $(skip_find) -type f -name package.json -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$' \
56
+ | while IFS= read -r P; do
57
+ node -e 'try{const s=require("fs").readFileSync(process.argv[1],"utf8");const j=JSON.parse(s);for(const k of Object.keys(j.scripts||{}))console.log(k)}catch(e){}' "$P"
58
+ done)
59
+ else
60
+ SCRIPTS=""
61
+ NPM_BLIND=1
62
+ fi
49
63
  HAS_PKG=$(find "$DIR" $(skip_find) -type f -name package.json -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$' | head -1)
64
+ HAS_JUST=$(find "$DIR" $(skip_find) -type f \( -name justfile -o -name Justfile -o -name .justfile \) -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$' | head -1)
65
+ HAS_MK=$(find "$DIR" $(skip_find) -type f \( -name Makefile -o -name makefile -o -name GNUmakefile -o -name '*.mk' \) -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$' | head -1)
50
66
 
51
67
  # Цели make: строка вне рецепта, слова до двоеточия; «VAR := x» — присваивание, не цель.
52
68
  TARGETS=$(find "$DIR" $(skip_find) -type f \( -name Makefile -o -name makefile -o -name GNUmakefile -o -name '*.mk' \) -print 2>/dev/null \
@@ -54,11 +70,35 @@ TARGETS=$(find "$DIR" $(skip_find) -type f \( -name Makefile -o -name makefile -
54
70
  awk '/^[^\t#][^=]*:([^=]|$)/ { sub(/:.*/, ""); n = split($0, a, /[ \t]+/); for (i = 1; i <= n; i++) if (a[i] != "") print a[i] }' "$M"
55
71
  done)
56
72
 
73
+ # ПОДКЛЮЧЁННЫЙ ФАЙЛ, КОТОРОГО У НАС НЕТ, ДЕЛАЕТ ПЕРЕЧЕНЬ ЦЕЛЕЙ НЕПОЛНЫМ. Найдено 2026-09-15 на
74
+ # `exoscale/cli`: их Makefile начинается с `include go.mk/init.mk`, а `go.mk` — подмодуль git,
75
+ # которого в клоне нет. Цели `build` и `test-verbose` объявлены там, и наше «такой цели нет» было
76
+ # бы письмом с неправдой в живую компанию.
77
+ #
78
+ # Молчим ОБО ВСЕХ целях make, а не о конкретной: какая именно объявлена в неподключённом файле,
79
+ # мы знать не можем. Это третий исход — «не смогли проверить», и он назван вслух, а не спрятан.
80
+ MK_BLIND=""
81
+ for M in $(find "$DIR" $(skip_find) -type f \( -name Makefile -o -name makefile -o -name GNUmakefile -o -name '*.mk' \) -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$'); do
82
+ for INC in $(sed -n 's/^[[:space:]]*-\{0,1\}include[[:space:]]\{1,\}//p' "$M" 2>/dev/null | tr ' ' '\n' | grep -v '^$'); do
83
+ case "$INC" in
84
+ *'$'*) continue ;;
85
+ esac
86
+ [ -e "$(dirname "$M")/$INC" ] || MK_BLIND="$MK_BLIND$INC
87
+ "
88
+ done
89
+ done
90
+
57
91
  # Рецепты just: «имя:», «@имя арг:», «alias имя := …».
58
92
  RECIPES=$(find "$DIR" $(skip_find) -type f \( -name justfile -o -name Justfile -o -name .justfile \) -print 2>/dev/null \
59
93
  | own_samples_filter "$DIR" | grep -v '^$' | while IFS= read -r J; do
94
+ # ПАРАМЕТР РЕЦЕПТА СОДЕРЖИТ ЗНАК РАВЕНСТВА. Найдено 2026-09-15 на `2mawi2/para`: их
95
+ # `release BUMP="patch":` — обычный рецепт со значением по умолчанию. Прежнее правило
96
+ # требовало `[^:=]*` до двоеточия и такой рецепт не видело: существующая команда
97
+ # объявлялась несуществующей. Присваивание (`version := "1.0"`) отсекается отдельно —
98
+ # у него двоеточие СРАЗУ перед равенством, а у рецепта равенство стоит до двоеточия.
60
99
  awk '/^alias[ \t]+/ { print $2; next }
61
- /^@?[A-Za-z_][A-Za-z0-9_-]*([ \t][^:=]*)?:([^=]|$)/ { sub(/^@/, ""); sub(/[ \t:].*/, ""); print }' "$J"
100
+ /^@?[A-Za-z_][A-Za-z0-9_-]*[ \t]*:=/ { next }
101
+ /^@?[A-Za-z_][A-Za-z0-9_-]*([ \t][^:]*)?:/ { sub(/^@/, ""); sub(/[ \t:].*/, ""); print }' "$J"
62
102
  done)
63
103
 
64
104
  has() { printf '%s\n' "$2" | grep -qxF -- "$1"; }
@@ -82,20 +122,41 @@ while IFS= read -r E; do
82
122
  CODE=$(awk '/^[ \t]*(```|~~~)/ { f = !f; next }
83
123
  f { print; next }
84
124
  { while (match($0, /`[^`]+`/)) { print substr($0, RSTART + 1, RLENGTH - 2); $0 = substr($0, RSTART + RLENGTH) } }' "$E")
85
- grep -oE '(npm|pnpm|yarn|bun)[[:space:]]+run[[:space:]]+[A-Za-z][A-Za-z0-9_:.-]*[*<{]?' "$E" \
86
- | grep -v '[*<{:]$' | sed 's/\.$//; s/[[:space:]][[:space:]]*/ /g' | sort -u > "$TMP"
125
+ # «BUN RUN <ФАЙЛ>» — ЗАПУСК ФАЙЛА, А НЕ СКРИПТА. Найдено 2026-09-15 на `Gerstep/HumanCompiler`:
126
+ # `bun run scripts/generate-plugin.ts <profile>` путь прочитался как имя скрипта «scripts».
127
+ # Отсекаем по признаку пути: косая черта или расширение исполняемого файла.
128
+ grep -oE '(npm|pnpm|yarn|bun)[[:space:]]+run[[:space:]]+[A-Za-z][A-Za-z0-9_:./-]*[*<{]?' "$E" \
129
+ | grep -v '[*<{:]$' | grep -vE '[[:space:]][A-Za-z0-9_.-]*/' \
130
+ | grep -vE '\.(ts|js|mjs|cjs|tsx|jsx|py|sh)$' \
131
+ | sed 's/\.$//; s/[[:space:]][[:space:]]*/ /g' | sort -u > "$TMP"
87
132
  while IFS= read -r CMD; do
133
+ # Без разборщика JSON перечень скриптов неполон, и «такого скрипта нет» — догадка.
134
+ [ -n "$NPM_BLIND" ] && continue
88
135
  N=${CMD##* }
89
136
  has "$N" "$SCRIPTS" && continue
90
- if [ -n "$HAS_PKG" ]; then report "$E" "$CMD" "такого скрипта нет ни в одном package.json"
91
- else report "$E" "$CMD" "package.json в репозитории нет вовсе"; fi
137
+ # СОБСТВЕННОЙ СБОРКИ У РЕПОЗИТОРИЯ НЕТ значит свод, скорее всего, описывает ДРУГОЙ проект.
138
+ # Найдено 2026-09-15 на `Aurealibe/claude-config` и `Weaverse/.agents`: это сборники правил,
139
+ # которые ставят в проект-получатель, и своего package.json у них нет и не должно быть.
140
+ # Про какой проект написано «npm run build», снаружи не видно — обвинять нельзя.
141
+ [ -n "$HAS_PKG" ] || { NO_BUILD=1; continue; }
142
+ report "$E" "$CMD" "такого скрипта нет ни в одном package.json"
92
143
  done < "$TMP"
93
- for N in $(printf '%s\n' "$CODE" | grep -E '(^|[^A-Za-z0-9_-])make[[:space:]]' | grep -vE -- '-C|--directory|-f[[:space:]]|--file' \
144
+ # «MAKE» ОБЫЧНЫЙ АНГЛИЙСКИЙ ГЛАГОЛ, как и «just». Найдено 2026-09-15 на
145
+ # `andyhartzler/my-bluebubbles-web`: «make it go away», «make one of these go away» дали цели
146
+ # «it» и «one». У них выше по файлу незакрытый блок кода, и весь текст после него читается как
147
+ # команды — но чинить надо не разбор блоков, а предмет обвинения: Makefile в репозитории нет
148
+ # вовсе, сравнивать не с чем.
149
+ [ -n "$HAS_MK" ] && for N in $(printf '%s\n' "$CODE" | grep -E '(^|[^A-Za-z0-9_-])make[[:space:]]' | grep -vE -- '-C|--directory|-f[[:space:]]|--file' \
94
150
  | grep -oE '(^|[^A-Za-z0-9_-])make([[:space:]]+-[A-Za-z0-9]+)*[[:space:]]+[A-Za-z][A-Za-z0-9_.-]*=?' \
95
151
  | grep -v '=$' | sed 's/.*[[:space:]]//' | sort -u); do
152
+ [ -n "$MK_BLIND" ] && continue
96
153
  has "$N" "$TARGETS" || report "$E" "make $N" "такой цели нет ни в одном Makefile"
97
154
  done
98
- for N in $(printf '%s\n' "$CODE" | grep -oE '(^|[^A-Za-z0-9_-])just[[:space:]]+[A-Za-z][A-Za-z0-9_-]*' \
155
+ # «JUST» ОБЫЧНОЕ АНГЛИЙСКОЕ СЛОВО, и в прозе оно стоит чаще, чем в роли запускалки. Найдено
156
+ # 2026-09-15: «I just uploaded a new video» и «fuzzy output (`just over`)» дали находки
157
+ # «рецепт uploaded» и «рецепт over» в репозиториях, где justfile нет вовсе. Без justfile
158
+ # сравнивать не с чем — обвинение без предмета.
159
+ [ -n "$HAS_JUST" ] && for N in $(printf '%s\n' "$CODE" | grep -oE '(^|[^A-Za-z0-9_-])just[[:space:]]+[A-Za-z][A-Za-z0-9_-]*' \
99
160
  | sed 's/.*[[:space:]]//' | sort -u); do
100
161
  has "$N" "$RECIPES" || report "$E" "just $N" "такого рецепта нет в justfile"
101
162
  done
@@ -107,4 +168,19 @@ if [ "$MISS" = 1 ]; then
107
168
  echo " почини: верни команду в проект или исправь свод. Агент берёт команды из свода дословно:"
108
169
  echo " несуществующая команда — это «Missing script» у агента и «проверил» про проверку, которой нет."
109
170
  fi
171
+ # ЧТО МЫ НЕ СМОГЛИ ПОСМОТРЕТЬ — говорится вслух и при находках, и без них. Молчание здесь
172
+ # означало бы «цели make проверены», а они не проверены вовсе.
173
+ if [ -n "$NO_BUILD" ]; then
174
+ echo " не проверено: команды npm. Своего package.json у репозитория нет — похоже, свод описывает"
175
+ echo " другой проект, в который его ставят. Про какой именно, снаружи не видно."
176
+ fi
177
+ if [ -n "$NPM_BLIND" ]; then
178
+ echo " не проверено: скрипты npm. Разобрать package.json нечем — на этой машине нет node."
179
+ echo " почини: поставь node либо прогони проверку там, где он есть."
180
+ fi
181
+ if [ -n "$MK_BLIND" ]; then
182
+ echo " не проверено: цели make. Makefile подключает файлы, которых нет в этом каталоге:"
183
+ printf '%s' "$MK_BLIND" | sort -u | sed 's/^/ /'
184
+ echo " обычно это подмодуль git — тогда проверять надо там, где он выгружен."
185
+ fi
110
186
  exit "$MISS"
@@ -5,6 +5,18 @@
5
5
  линтером. Ошибку в имени события Claude Code **не показывает** — хук просто никогда не
6
6
  вызывается. Настройка выглядит как защита и защитой не является.
7
7
 
8
+ **Третий класс, добавленный 2026-09-14 — и нашёл его чужой инструмент.** `agnix`, прогнанный по
9
+ нашему же репозиторию, сказал «Script file not found» о **шести** хуках в нашем **зелёном**
10
+ образце: гейт с именем «хук правда срабатывает» говорил «чисто» о настройке, где ни один хук
11
+ сработать не мог, потому что ни одного из шести файлов не существовало. Теперь команда хука
12
+ сверяется с диском.
13
+
14
+ Граница узкая: красится только **относительный путь со слэшем** (`.claude/hooks/x.sh`,
15
+ `scripts/guard.sh`). Программа из PATH (`npx`, `prettier`, `bash -c …`) не судится — у неё нет
16
+ пути, и «не нашли» было бы обвинением по догадке. Абсолютный путь пропускается: он про чужую
17
+ машину, а не про этот репозиторий. `$CLAUDE_PROJECT_DIR` снимается как приставка — это и есть
18
+ корень проекта.
19
+
8
20
  **Какой отказ это поймало.** Замер по 48 чужим настройкам `.claude/settings.json`, снятым с
9
21
  GitHub 2026-09-07. Шесть настоящих находок в четырёх репозиториях:
10
22