agent-quality-kit 0.8.0 → 0.10.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 +154 -12
- package/README.ru.md +185 -27
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/ai/project-baseline.md +14 -0
- package/kit/docs/api-e2e.md +214 -0
- package/kit/docs/ready-made-rules.md +188 -0
- package/kit/gates/README.md +22 -0
- package/kit/gates/api-contract-has-arbiter/README.md +63 -0
- package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
- package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
- package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
- package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
- package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
- package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
- package/kit/gates/ci-actually-fails/check.sh +9 -1
- package/kit/gates/color-from-token/check.sh +5 -1
- package/kit/gates/commit-explains-itself/check.sh +15 -0
- package/kit/gates/complexity-limit/red/deep.go +17 -0
- package/kit/gates/complexity-limit/red/deep.rs +17 -0
- package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
- package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
- package/kit/gates/lesson-has-outcome/check.sh +5 -1
- package/kit/gates/mcp-server-resolves/README.md +62 -0
- package/kit/gates/mcp-server-resolves/check.sh +110 -0
- package/kit/gates/mcp-server-resolves/gate.yml +18 -0
- package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
- package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
- package/kit/gates/protection-not-removed/README.md +67 -0
- package/kit/gates/protection-not-removed/check.sh +92 -0
- package/kit/gates/protection-not-removed/gate.yml +10 -0
- package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
- package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
- package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
- package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
- package/kit/gates/todo-without-task/red/later.go +6 -0
- package/kit/gates/todo-without-task/red/later.rs +4 -0
- package/llms.txt +25 -4
- package/package.json +3 -6
- package/tool/commands/context.mjs +37 -6
- package/tool/commands/doctor.mjs +139 -16
- package/tool/commands/probe.mjs +228 -0
- package/tool/commands/project.mjs +18 -2
- package/tool/commands/prove.mjs +1 -0
- package/tool/commands/vitals.mjs +167 -0
- package/tool/i18n/en-docs.mjs +48 -0
- package/tool/i18n/en-gates.mjs +309 -0
- package/tool/i18n/en.mjs +26 -279
- package/tool/i18n/index.mjs +36 -3
- package/tool/i18n/ru-docs.mjs +48 -0
- package/tool/i18n/ru-gates.mjs +311 -0
- package/tool/i18n/ru.mjs +26 -278
- package/tool/lib/banner.mjs +59 -0
- package/tool/lib/brief.mjs +192 -0
- package/tool/lib/cadence.mjs +57 -0
- package/tool/lib/core.mjs +3 -0
- package/tool/lib/history.mjs +82 -0
- package/tool/lib/manifest.mjs +146 -15
- package/tool/lib/prove.mjs +11 -1
- package/tool/lib/repo.mjs +43 -3
- package/tool/program.mjs +33 -0
- package/tool/selfcheck/smoke/_fixture.mjs +89 -0
- package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
- package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
- package/tool/selfcheck/smoke.sh +528 -6
- package/tool/selfcheck/units-banner.mjs +65 -0
- package/tool/selfcheck/units-brief.mjs +97 -0
- package/tool/selfcheck/units-cadence.mjs +69 -0
- package/tool/selfcheck/units-context.mjs +3 -1
- package/tool/selfcheck/units-level.mjs +147 -1
- package/tool/selfcheck/units-probe.mjs +100 -0
- package/tool/selfcheck/units-repo.mjs +164 -0
- package/tool/selfcheck/units-vitals.mjs +81 -0
- package/tool/selfcheck/units.mjs +3 -75
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Спецификация API лежит — а держит ли её кто-нибудь.
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ ИМЕННО ЭТО. `openapi.yaml` — обещание чужому коду: вот поля, вот типы, вот что
|
|
5
|
+
# обязательно. Обещание, которое никто не сверяет с сервером, расходится с ним молча и
|
|
6
|
+
# обнаруживается у потребителя. Это наш центральный класс, только на уровне API.
|
|
7
|
+
#
|
|
8
|
+
# ЗАМЕР, ИЗ КОТОРОГО ВЗЯЛАСЬ ЗАПИСЬ (2026-09-09). Стенд: сервер врёт в каждом поле ответа —
|
|
9
|
+
# `id` строкой вместо числа, обязательного `email` нет вовсе, `created_at` не дата. Пятисоток
|
|
10
|
+
# при этом нет, коды ответа честные. Кто что сказал:
|
|
11
|
+
# spectral (spectral:oas) -> код 0, три замечания: нет контакта, описания, тегов
|
|
12
|
+
# schemathesis, умолчания -> код 1, три нарушения схемы в ответах
|
|
13
|
+
# schemathesis -c not_a_server_error -> код 0, «18 из 18 прошли»
|
|
14
|
+
# Отсюда обе красные ветки этой проверки: держать спецификацию некому — и держать поручено
|
|
15
|
+
# тому, кто по построению не может покраснеть.
|
|
16
|
+
DIR="${1:-.}"
|
|
17
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
18
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
19
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
20
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
21
|
+
exit 2
|
|
22
|
+
fi
|
|
23
|
+
. "$SKIP_LIB"
|
|
24
|
+
|
|
25
|
+
# --- есть ли договор ----------------------------------------------------------
|
|
26
|
+
# По имени файла, а не по расположению: спецификацию кладут в корень, в `docs/`, рядом с
|
|
27
|
+
# приложением. Расширение обязательно разбираемое — `openapi.md` это рассказ о договоре.
|
|
28
|
+
# `-print` в конце ОБЯЗАТЕЛЕН. Без него действие по умолчанию применяется ко всему выражению,
|
|
29
|
+
# и ветка `-prune -o` печатает сами обойдённые каталоги: в списке спецификаций оказывались
|
|
30
|
+
# `./.git` и `./.aqk`. Найдено аудитом фич 2026-09-09, образцами не ловилось.
|
|
31
|
+
SPECS=$(find "$DIR" $(skip_find) -type f \
|
|
32
|
+
\( -iname 'openapi*.yaml' -o -iname 'openapi*.yml' -o -iname 'openapi*.json' \
|
|
33
|
+
-o -iname 'swagger*.yaml' -o -iname 'swagger*.yml' -o -iname 'swagger*.json' \
|
|
34
|
+
-o -iname 'asyncapi*.yaml' -o -iname 'asyncapi*.yml' -o -iname 'asyncapi*.json' \) \
|
|
35
|
+
-print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$')
|
|
36
|
+
[ -z "$SPECS" ] && { echo "спецификации API здесь нет — эта проверка не про тебя"; exit 0; }
|
|
37
|
+
|
|
38
|
+
# --- кто её держит ------------------------------------------------------------
|
|
39
|
+
# Четыре семьи держателей, и они отвечают на РАЗНЫЕ вопросы — подробности в README:
|
|
40
|
+
# сверка с сервером schemathesis, dredd, portman, newman, pact — «документ не врёт»
|
|
41
|
+
# линт документа spectral, redocly, vacuum, openapi-spec-validator, swagger-cli
|
|
42
|
+
# ломающие правки oasdiff — «вчерашний клиент переживёт сегодняшний выпуск»
|
|
43
|
+
# потребитель openapi-typescript, orval, oapi-codegen, openapi-generator, kubb —
|
|
44
|
+
# типы порождены договором, и расхождение ломает сборку
|
|
45
|
+
HOLDERS='schemathesis|dredd|portman|newman[[:space:]]+run|pact-broker|pact-verifier|can-i-deploy|spectral[[:space:]]+lint|redocly[[:space:]]+(lint|bundle)|vacuum[[:space:]]+(lint|report|html-report)|openapi-spec-validator|swagger-cli|oasdiff|openapi-typescript|orval|oapi-codegen|openapi-generator|kubb'
|
|
46
|
+
# Ищем в том, что ЗАПУСКАЮТ: конвейер, оболочечные скрипты, сборочные файлы, объявления пакета
|
|
47
|
+
# и манифест самого комплекта. Не в коде: упоминание инструмента в исходнике — не его запуск.
|
|
48
|
+
FOUND=$(grep -rnE "$HOLDERS" $(skip_grep) \
|
|
49
|
+
--include=*.yml --include=*.yaml --include=*.sh --include=*.json --include=*.toml \
|
|
50
|
+
--include=*.ini --include=*.cfg --include=*.mk --include=Makefile --include=Justfile \
|
|
51
|
+
--include=justfile --include=Taskfile.yml --include=*.gradle --include=Jenkinsfile \
|
|
52
|
+
"$DIR" 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$')
|
|
53
|
+
|
|
54
|
+
# ОПРЕДЕЛЕНИЕ ЗАПИСИ КАТАЛОГА — НЕ НАХОДКА, и это касается не только своей записи. Соседняя
|
|
55
|
+
# запись `ci-actually-fails` держит в своём `check.sh` строку со списком запускалок, где
|
|
56
|
+
# перечислены все инструменты про API разом. Проверка находила её и решала, что спецификацию
|
|
57
|
+
# кто-то держит: договор не держал никто, а гейт был ЗЕЛЁНЫМ.
|
|
58
|
+
#
|
|
59
|
+
# Найдено аудитом фич 2026-09-09 — прогоном на настоящем проекте, а не образцами: красный и
|
|
60
|
+
# зелёный образцы лежат по одному, а в проекте записи стоят рядом. Признак определения взят
|
|
61
|
+
# самый надёжный: в той же папке лежит `gate.yml`.
|
|
62
|
+
FOUND=$(printf '%s\n' "$FOUND" | while IFS= read -r L; do
|
|
63
|
+
F="${L%%:*}"
|
|
64
|
+
[ -n "$F" ] || continue
|
|
65
|
+
[ -f "$(dirname "$F")/gate.yml" ] && continue
|
|
66
|
+
printf '%s\n' "$L"
|
|
67
|
+
done | grep -v '^$')
|
|
68
|
+
|
|
69
|
+
if [ -z "$FOUND" ]; then
|
|
70
|
+
printf '%s\n' "$SPECS" | sed 's/$/: спецификацию не держит ни одна команда/'
|
|
71
|
+
echo " почини: заведи арбитра, который краснеет, когда сервер ушёл от договора —"
|
|
72
|
+
echo " «schemathesis run openapi.yaml --url <адрес>» в конвейере. Линтер спецификации"
|
|
73
|
+
echo " (spectral, redocly, vacuum) этого НЕ заменяет: он читает документ, а не сервер,"
|
|
74
|
+
echo " и на сервере, который врёт в каждом поле, остаётся зелёным."
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# --- держит, но провалиться не может -------------------------------------------
|
|
79
|
+
# `--checks not_a_server_error` (и короткое `-c`) оставляет от арбитра одну проверку: «не
|
|
80
|
+
# пятисотка». На замере выше это ровно код 0 при сервере, который врёт в каждом поле. Умолчание
|
|
81
|
+
# у schemathesis — ВСЕ проверки разом, поэтому такое сужение это не настройка, а отключение.
|
|
82
|
+
# КАК ОБЕЗВРЕЖЕН АРБИТР — вопрос к ОБЪЯВЛЕНИЮ запуска, а не к любому файлу, где эта строка
|
|
83
|
+
# встретилась. Граница взята не отсюда: `ci-actually-fails` уже проводит её теми же словами —
|
|
84
|
+
# «провал, погашенный внутри скрипта, это логика скрипта, а не конфиг конвейера». Внутри
|
|
85
|
+
# оболочечного скрипта вердикт решают `set -e`, `trap` и явный `exit`, и по одной строке о нём
|
|
86
|
+
# судить нельзя.
|
|
87
|
+
#
|
|
88
|
+
# Поймано на себе в тот же день: наш `smoke.sh` держит образцы нарушений в heredoc — и новая
|
|
89
|
+
# запись покраснела на собственном наборе проверок. Одна из трёх находок была вообще в
|
|
90
|
+
# КОММЕНТАРИИ, поясняющем замер; отсюда же `drop_comments`.
|
|
91
|
+
RUNS=$(printf '%s\n' "$FOUND" | grep -E '\.(ya?ml|toml|json|mk|gradle):|/(Makefile|Justfile|justfile|Jenkinsfile):' | drop_comments)
|
|
92
|
+
|
|
93
|
+
NARROW=$(printf '%s\n' "$RUNS" | grep -E '(-c|--checks)[[:space:]=]+not_a_server_error([[:space:]]|$|")')
|
|
94
|
+
|
|
95
|
+
# Вторая форма того же: `oasdiff breaking` БЕЗ `--fail-on`. Замер 2026-09-09 на паре
|
|
96
|
+
# спецификаций, где из ответа убрано обязательное поле: инструмент печатает
|
|
97
|
+
# «1 changes: 1 error … removed the required property `email`» — и выходит с НУЛЁМ. С
|
|
98
|
+
# `--fail-on ERR` на той же паре код 1, а на паре без ломающих изменений снова 0.
|
|
99
|
+
# Вывод громкий и красный на вид, конвейер зелёный — самая коварная форма молчания.
|
|
100
|
+
#
|
|
101
|
+
# Форма `uses: oasdiff/oasdiff-action/...` не считается находкой: действие роняет прогон само.
|
|
102
|
+
# Подкоманда `changelog` тоже: она затем и нужна, чтобы напечатать, а не уронить.
|
|
103
|
+
LOUD=$(printf '%s\n' "$RUNS" | grep -E 'oasdiff[[:space:]]+breaking' | grep -v 'oasdiff-action' | grep -v -- '--fail-on')
|
|
104
|
+
|
|
105
|
+
if [ -n "$NARROW$LOUD" ]; then
|
|
106
|
+
[ -z "$NARROW" ] || printf '%s\n' "$NARROW" | sed 's/$/ <- арбитр сужен до «не пятисотка»/'
|
|
107
|
+
[ -z "$LOUD" ] || printf '%s\n' "$LOUD" | sed 's/$/ <- печатает ломающие изменения и выходит с нулём/'
|
|
108
|
+
[ -z "$NARROW" ] || {
|
|
109
|
+
echo " почини: убери «--checks not_a_server_error» — у schemathesis по умолчанию включены"
|
|
110
|
+
echo " ВСЕ проверки, включая response_schema_conformance. Сужение до одной оставляет"
|
|
111
|
+
echo " зелёный прогон на сервере, который врёт в каждом поле ответа."
|
|
112
|
+
}
|
|
113
|
+
[ -z "$LOUD" ] || echo " почини: добавь «--fail-on ERR» — без него oasdiff печатает находки и выходит с нулём."
|
|
114
|
+
echo " провал, погашенный «|| true» или «continue-on-error», — это ci-actually-fails."
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
exit 0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
intent: у спецификации API есть арбитр, и он может провалиться
|
|
2
|
+
intent_en: the API specification has an arbiter, and that arbiter can actually fail
|
|
3
|
+
|
|
4
|
+
# Только там, где договор с чужим кодом уже есть. Библиотеке, консольной программе и монолиту
|
|
5
|
+
# без внешнего интерфейса эта запись не нужна: показанной не тому записи не верят.
|
|
6
|
+
trigger:
|
|
7
|
+
has_api_spec: true
|
|
8
|
+
|
|
9
|
+
recipes:
|
|
10
|
+
any: bash {gate}/check.sh {dir}
|
|
11
|
+
|
|
12
|
+
proof: incidents/README.md, 2026-09-09 «сузили арбитра до самой слабой проверки» — на стенде,
|
|
13
|
+
где сервер врёт в каждом поле ответа, schemathesis с умолчаниями даёт код 1 и три нарушения
|
|
14
|
+
схемы, он же с `-c not_a_server_error` — код 0 и «18 из 18 прошли»; spectral на той же
|
|
15
|
+
спецификации тоже код 0 и жалобы на отсутствие тегов
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Арбитр назван и может провалиться. Оба слова важны:
|
|
2
|
+
# schemathesis с умолчаниями сверяет ОТВЕТЫ сервера со схемой, а не только отсутствие пятисоток;
|
|
3
|
+
# oasdiff без «--fail-on ERR» печатает ломающие изменения и выходит с нулём — замерено.
|
|
4
|
+
name: ci
|
|
5
|
+
on: [push]
|
|
6
|
+
jobs:
|
|
7
|
+
contract:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- run: schemathesis run openapi.yaml --url http://localhost:8000
|
|
12
|
+
- run: oasdiff breaking base.yaml openapi.yaml --fail-on ERR
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
openapi: 3.0.3
|
|
2
|
+
info: { title: Заказы, version: 1.0.0 }
|
|
3
|
+
paths:
|
|
4
|
+
/users/{id}:
|
|
5
|
+
get:
|
|
6
|
+
parameters:
|
|
7
|
+
- { name: id, in: path, required: true, schema: { type: integer } }
|
|
8
|
+
responses:
|
|
9
|
+
'200':
|
|
10
|
+
description: Пользователь
|
|
11
|
+
content:
|
|
12
|
+
application/json:
|
|
13
|
+
schema:
|
|
14
|
+
type: object
|
|
15
|
+
required: [id, email]
|
|
16
|
+
properties:
|
|
17
|
+
id: { type: integer }
|
|
18
|
+
email: { type: string }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Договор лежит, конвейер есть — и ни одна команда договора не касается. Спецификация здесь
|
|
2
|
+
# документация, а не контракт: сервер уходит от неё молча.
|
|
3
|
+
name: ci
|
|
4
|
+
on: [push]
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- run: pytest
|
|
11
|
+
- run: ruff check .
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
openapi: 3.0.3
|
|
2
|
+
info: { title: Заказы, version: 1.0.0 }
|
|
3
|
+
paths:
|
|
4
|
+
/users/{id}:
|
|
5
|
+
get:
|
|
6
|
+
parameters:
|
|
7
|
+
- { name: id, in: path, required: true, schema: { type: integer } }
|
|
8
|
+
responses:
|
|
9
|
+
'200':
|
|
10
|
+
description: Пользователь
|
|
11
|
+
content:
|
|
12
|
+
application/json:
|
|
13
|
+
schema:
|
|
14
|
+
type: object
|
|
15
|
+
required: [id, email]
|
|
16
|
+
properties:
|
|
17
|
+
id: { type: integer }
|
|
18
|
+
email: { type: string }
|
|
@@ -14,7 +14,15 @@ CI=$(find "$DIR/.github/workflows" "$DIR/.gitlab-ci.yml" "$DIR/.circleci" "$DIR/
|
|
|
14
14
|
# Что считается ПРОВЕРКОЙ. Список намеренно закрытый: маскировка бывает законной — необязательная
|
|
15
15
|
# выгрузка отчёта, публикация артефакта, уведомление. Красить всякий `continue-on-error` значит
|
|
16
16
|
# получить гейт, который выключат первым. Красим только гашение того, что выносит вердикт.
|
|
17
|
-
|
|
17
|
+
# ПОЧЕМУ ЗДЕСЬ ЕСТЬ ИНСТРУМЕНТЫ ПРО API И ПОЧЕМУ НЕ ВСЕ ИХ ИМЕНА ЦЕЛИКОМ.
|
|
18
|
+
# Замер 2026-09-09: три шага — фаззер спецификации, детектор ломающих изменений и сверка с
|
|
19
|
+
# ожиданиями потребителей, — все три под `continue-on-error: true`, и эта проверка сказала
|
|
20
|
+
# «чисто», код 0. Список знал `pytest` и `eslint` и не знал ни одного инструмента про API,
|
|
21
|
+
# то есть самый дорогой класс проверок проходил как строка в логе.
|
|
22
|
+
# Имена сокращены до подкоманды там, где слово обиходное: `vacuum` без `lint` совпадает с
|
|
23
|
+
# обслуживанием базы (`psql -c 'VACUUM ANALYZE'`), а шаг обслуживания имеет полное право быть
|
|
24
|
+
# прощающим. Ложный красный дороже пропуска: гейт, который врёт, выключают целиком.
|
|
25
|
+
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)|schemathesis|dredd|oasdiff|spectral lint|redocly (lint|bundle)|vacuum (lint|report|html-report)|pact-broker|pact-verifier|can-i-deploy|portman|newman run|manage.py spectacular'
|
|
18
26
|
|
|
19
27
|
# Закрытый список не поспевает: замер по чужим репозиториям нашёл шаг «Run reviewdog
|
|
20
28
|
# (github-pr-check)» под `continue-on-error: true`, и ни одно имя из списка в нём не звучало.
|
|
@@ -67,7 +67,11 @@ HITS=$(find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null \
|
|
|
67
67
|
# Якорь ссылки «#defining-entry-points» начинается с «#def», за которым идёт «i»: по
|
|
68
68
|
# прежнему правилу это был цвет. Замер 2026-09-08 по шести чужим репозиториям: в uv
|
|
69
69
|
# ложным оказалось именно это, в docs/js/extra.js.
|
|
70
|
-
|
|
70
|
+
# Цвет выписан поразрядно: часть сборок mawk повторители не понимает (в node:22-slim
|
|
71
|
+
# это mawk 1.3.4 20200120), а какая сборка у читателя — мы не знаем
|
|
72
|
+
# {3},{6},{8}, и это условие не срабатывало никогда — гейт молчал на красном образце.
|
|
73
|
+
# Поймано сборкой docker-образа 2026-09-08, а не прогоном: на хосте gawk.
|
|
74
|
+
if (line ~ /#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]([^0-9a-zA-Z_-]|$)/)
|
|
71
75
|
print FILENAME ":" FNR ":" $0
|
|
72
76
|
}
|
|
73
77
|
' 2>/dev/null \
|
|
@@ -65,6 +65,21 @@ else
|
|
|
65
65
|
[ -z "$LESSONS" ] && LESSONS="incidents"
|
|
66
66
|
case "$LESSONS" in http*) LESSONS="" ;; esac # journal по адресу, а не путём — не применимо
|
|
67
67
|
if [ -n "$LESSONS" ]; then
|
|
68
|
+
# СОСТАВ КОММИТА СЧИТАЕТСЯ ДИФФОМ С РОДИТЕЛЕМ, а родителя может не быть видно. При разборе
|
|
69
|
+
# предложения изменений GitHub выкладывает синтетический коммит слияния мелким клоном: сам
|
|
70
|
+
# коммит автора (`HEAD^2`) в клоне есть, а его родителя нет. `git show --name-only` в этом
|
|
71
|
+
# случае считает коммит КОРНЕВЫМ и выдаёт всё дерево — освобождение «трогает только журнал»
|
|
72
|
+
# переставало срабатывать МОЛЧА, и гейт требовал отчёт там, где не должен.
|
|
73
|
+
#
|
|
74
|
+
# Поймано конвейером на записи в собственный журнал 2026-09-09; опыт показал границу точно:
|
|
75
|
+
# глубина 2 — красный, глубина 3 — чисто. Молчать здесь нельзя: пропуск называет себя, иначе
|
|
76
|
+
# выключенная проверка неотличима от работающей.
|
|
77
|
+
if ! (cd "$DIR" && git rev-parse -q --verify "$REF^" >/dev/null 2>&1); then
|
|
78
|
+
echo "мелкий клон: у коммита не видно родителя — состав не определить, проверка пропущена"
|
|
79
|
+
echo " чтобы она работала на предложении изменений:"
|
|
80
|
+
echo " actions/checkout@v4 с fetch-depth: 3 (слияние, его родитель и родитель родителя)"
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
68
83
|
FILES=$(cd "$DIR" && git show --pretty=format: --name-only "$REF" 2>/dev/null | grep -v '^$')
|
|
69
84
|
if [ -n "$FILES" ]; then
|
|
70
85
|
OUTSIDE=$(printf '%s\n' "$FILES" | grep -v "^$LESSONS/" | grep -v "^$LESSONS\$")
|
|
@@ -30,7 +30,11 @@ FILES=$(find "$DIR/$LESSONS" -type f -name '*.md' 2>/dev/null)
|
|
|
30
30
|
# старые записи несут её строкой ">", новые — абзацем "**Вывод.**"; оба варианта законны.
|
|
31
31
|
printf '%s\n' $FILES | while read -r F; do
|
|
32
32
|
awk -v FILE="$F" '
|
|
33
|
-
|
|
33
|
+
# Дата выписана поразрядно, а не как [0-9]{4}: часть сборок mawk повторители не понимает
|
|
34
|
+
# повторители в регулярках, и такая строка не совпадала НИКОГДА. Гейт молча выходил с нулём
|
|
35
|
+
# на собственном красном образце. Поймано сборкой образа 2026-09-08: на хосте gawk, в образе
|
|
36
|
+
# mawk, и приёмка каталога внутри образа отклонила две записи из двадцати.
|
|
37
|
+
/^## [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ { flush(); title = $0; sub(/^## /, "", title); is_entry = 1; has_mark = 0; next }
|
|
34
38
|
/^## / { flush(); is_entry = 0; next }
|
|
35
39
|
(/✅/ || /🔧/ || /📜/ || /👤/) { has_mark = 1 }
|
|
36
40
|
END { flush() }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# mcp-server-resolves
|
|
2
|
+
|
|
3
|
+
**Что ловит.** Объявленный MCP-сервер, которого на деле нет: команда указывает на несуществующий
|
|
4
|
+
файл, либо версия не закреплена и завтра приедет другая.
|
|
5
|
+
|
|
6
|
+
**Почему это отдельная запись.** Сервер, который не поднялся, агенту **никак не виден**: у него
|
|
7
|
+
просто нет этих инструментов, и он молча работает без них. Ни ошибки, ни строки в журнале.
|
|
8
|
+
Это тот же класс, что хук с опечаткой в имени события: настройка выглядит как возможность и
|
|
9
|
+
возможностью не является.
|
|
10
|
+
|
|
11
|
+
## Шишка, из которой она выросла
|
|
12
|
+
|
|
13
|
+
2026-09-08. В проект записали настройку `chrome-devtools-mcp` — выглядела безупречно:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
"chrome-devtools": { "command": "npx", "args": ["-y", "chrome-devtools-mcp@1.9.0"] }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Сервер был мёртв. Puppeteer подхватывал Windows-овый Chrome из `/mnt/c/` и падал с
|
|
20
|
+
`Target closed`. Узнали только потому, что запустили руками и прочитали журнал — обычно так не
|
|
21
|
+
делают: записали конфиг, увидели «сохранено», пошли дальше.
|
|
22
|
+
|
|
23
|
+
## Замер
|
|
24
|
+
|
|
25
|
+
Двадцать настоящих настроек `.mcp.json`, взятых поиском по GitHub 2026-09-08.
|
|
26
|
+
|
|
27
|
+
| | |
|
|
28
|
+
|---|---|
|
|
29
|
+
| конфигов проверено | 20 |
|
|
30
|
+
| покраснело | 9 |
|
|
31
|
+
| находок | 14 |
|
|
32
|
+
| ложных срабатываний | **0** |
|
|
33
|
+
|
|
34
|
+
Все находки — чужие серверы, которые тянутся незакреплёнными при каждом запуске:
|
|
35
|
+
`@modelcontextprotocol/server-github`, `firecrawl-mcp`, `mcp-server-postgres`, `@upstash/context7-mcp`.
|
|
36
|
+
Серверу с таким доступом «сегодня одна версия, завтра другая» — открытая дверь в цепочке поставок.
|
|
37
|
+
|
|
38
|
+
Замер тут же нашёл и два ложных срабатывания, оба починены до объявления записи рабочей:
|
|
39
|
+
|
|
40
|
+
1. `npx tsx rag/src/index.ts` — сервер **свой**, лежит в репозитории и уже под версией. Требовать
|
|
41
|
+
закрепить `tsx` значит краснеть на нормальном укладе, а такой гейт выключают в первый день.
|
|
42
|
+
2. Починка первого съела настоящую находку: `@meridian/docs-mcp` — имя пакета со слэшем, а слэш
|
|
43
|
+
считался признаком локального файла. Видно было только построчным сравнением до и после.
|
|
44
|
+
|
|
45
|
+
## Готовый аналог
|
|
46
|
+
|
|
47
|
+
Готового аналога нет. Проверено 2026-09-08 поиском: линтеры настроек агента существуют
|
|
48
|
+
(`claudelint`, `agent-lint`), но смотрят на права и хуки, а не на MCP. Ближайшее по смыслу —
|
|
49
|
+
наш собственный `deps-are-pinned`: там то же правило про версии, но для зависимостей сборки,
|
|
50
|
+
и файла `.mcp.json` он не видит.
|
|
51
|
+
|
|
52
|
+
## Чего НЕ ловит
|
|
53
|
+
|
|
54
|
+
- **Не запускает серверы.** Мёртвый по любой другой причине сервер — как в шишке выше, где путь
|
|
55
|
+
и версия были верны, а падал сам браузер, — эта проверка не увидит. Запуск чужих команд на
|
|
56
|
+
каждый коммит означает скачивание пакетов и минуты ожидания; такую проверку выключают сразу.
|
|
57
|
+
Живое рукопожатие — то, что делают руками при подключении сервера, и это записано в методичке,
|
|
58
|
+
а не в гейте.
|
|
59
|
+
- **Не судит об относительных путях.** `"command": "./bin/server"` не проверяется: рабочий
|
|
60
|
+
каталог сервера задаётся полем `cwd`, и проверить существование без запуска нельзя.
|
|
61
|
+
- **Не знает, что пакет существует в реестре.** Это делает `no-phantom-package`, но только для
|
|
62
|
+
того, что упомянуто в документации.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Объявленный MCP-сервер, которого на деле нет: команда указывает на файл, которого не
|
|
3
|
+
# существует, либо версия не закреплена и завтра приедет другая.
|
|
4
|
+
#
|
|
5
|
+
# ЗАЧЕМ. Сервер, который не поднялся, агенту НИКАК не виден: у него просто нет этих
|
|
6
|
+
# инструментов, и он молча работает без них. Ни ошибки, ни строки в журнале — та же тишина,
|
|
7
|
+
# что у хука с опечаткой в имени события. Настройка выглядит как возможность и возможностью
|
|
8
|
+
# не является.
|
|
9
|
+
#
|
|
10
|
+
# ПРОВЕРЕНО НА СЕБЕ 2026-09-08. Записали в проект настройку chrome-devtools-mcp, выглядела
|
|
11
|
+
# безупречно. Сервер был мёртв: puppeteer подхватывал Windows-овый Chrome из /mnt/c/ и падал
|
|
12
|
+
# с «Target closed». Узнали только потому, что запустили руками и прочитали журнал; обычно
|
|
13
|
+
# так не делают — записали конфиг, увидели «сохранено», пошли дальше.
|
|
14
|
+
#
|
|
15
|
+
# ЧТО ЭТА ПРОВЕРКА НЕ ДЕЛАЕТ. Она НЕ запускает серверы. Запуск — это исполнение чужих команд
|
|
16
|
+
# на каждый коммит, скачивание пакетов и минуты ожидания; такую проверку выключают в первый
|
|
17
|
+
# день. Здесь ловится только то, что видно без запуска, и этого достаточно для двух самых
|
|
18
|
+
# частых смертей: файла нет и версия плавает.
|
|
19
|
+
DIR="${1:-.}"
|
|
20
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
21
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
22
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
23
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
. "$SKIP_LIB"
|
|
27
|
+
|
|
28
|
+
OUT=""
|
|
29
|
+
for F in "$DIR/.mcp.json" "$DIR/.cursor/mcp.json" "$DIR/.vscode/mcp.json" "$DIR/.claude/mcp.json"; do
|
|
30
|
+
[ -f "$F" ] || continue
|
|
31
|
+
RES=$(awk -v file="$F" -v dir="$DIR" '
|
|
32
|
+
# Номера строк с командой копятся отдельно от текста: разбор идёт в END по всему файлу
|
|
33
|
+
# одной строкой (иначе минифицированный JSON не разобрать), а находку надо привязать к
|
|
34
|
+
# строке — без неё `--since` не сверит путь с дифом и гейт зазеленеет на чужом дифе.
|
|
35
|
+
/"command"[ \t]*:/ { cline[++ci] = NR }
|
|
36
|
+
{ buf = buf $0 " " }
|
|
37
|
+
END {
|
|
38
|
+
n = split(buf, part, /"command"[ \t]*:/)
|
|
39
|
+
for (i = 2; i <= n; i++) {
|
|
40
|
+
chunk = part[i]
|
|
41
|
+
if (!match(chunk, /"[^"]*"/)) continue
|
|
42
|
+
cmd = substr(chunk, RSTART + 1, RLENGTH - 2)
|
|
43
|
+
line = cline[i - 1] ? cline[i - 1] : 1
|
|
44
|
+
|
|
45
|
+
# 1. Абсолютный путь, которого нет. Самая частая смерть у пакетов, чья версия вшита
|
|
46
|
+
# в имя файла: обновились — старый файл удалён, сервер умер, никто не заметил.
|
|
47
|
+
if (substr(cmd, 1, 1) == "/") {
|
|
48
|
+
if (system("test -e \"" cmd "\"") != 0)
|
|
49
|
+
print file ":" line ": команда сервера не существует: " cmd
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# 2. Скачивающий запуск без точной версии. `@latest` и голое имя означают «сегодня
|
|
54
|
+
# одно, завтра другое», а для сервера, которому доверен браузер и файлы, это дверь
|
|
55
|
+
# в цепочке поставок. Правило то же, что у нас для зависимостей.
|
|
56
|
+
if (cmd == "npx" || cmd == "uvx" || cmd == "pipx" || cmd == "bunx" || cmd == "pnpm") {
|
|
57
|
+
args = ""
|
|
58
|
+
if (match(chunk, /"args"[ \t]*:[ \t]*\[[^]]*\]/)) args = substr(chunk, RSTART, RLENGTH)
|
|
59
|
+
if (args == "") continue
|
|
60
|
+
if (args ~ /@latest/) {
|
|
61
|
+
print file ":" line ": версия сервера не закреплена (@latest): " cmd
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
# Имя пакета — первый довод, не начинающийся с дефиса. Точная версия пишется как
|
|
65
|
+
# name@1.2.3 (npm) или name==1.2.3 (python); без неё запуск невоспроизводим.
|
|
66
|
+
m = args
|
|
67
|
+
gsub(/"args"[ \t]*:[ \t]*\[/, "", m); gsub(/\]/, "", m)
|
|
68
|
+
k = split(m, tok, ",")
|
|
69
|
+
|
|
70
|
+
# СЕРВЕР ИЗ СВОЕГО РЕПОЗИТОРИЯ — не находка. `npx tsx rag/src/index.ts` запускает
|
|
71
|
+
# СВОЙ файл, а npx здесь только запускалка: код сервера лежит в репозитории и уже
|
|
72
|
+
# под версией — той же, что и весь проект. Требовать закрепить `tsx` значит краснеть
|
|
73
|
+
# на нормальном укладе, а такой гейт выключают в первый день. Найдено замером по
|
|
74
|
+
# двадцати чужим настройкам с GitHub 2026-09-08: две из десяти находок были ровно
|
|
75
|
+
# этим. Тот же класс, что четыре ложных срабатывания на чужом коде до этого.
|
|
76
|
+
local_entry = 0
|
|
77
|
+
for (j = 1; j <= k; j++) {
|
|
78
|
+
t = tok[j]; gsub(/^[ \t"]+|[ \t"]+$/, "", t)
|
|
79
|
+
# Признак локального входа — расширение файла или явно относительный путь.
|
|
80
|
+
# Просто «есть слэш» не годится: `@scope/name` — это имя пакета в npm, и первая
|
|
81
|
+
# версия этой проверки съела настоящую находку `npx @meridian/docs-mcp`. Поймано
|
|
82
|
+
# тем же замером по двадцати чужим настройкам: находок стало восемь вместо девяти,
|
|
83
|
+
# и пропажу было видно только построчным сравнением до и после.
|
|
84
|
+
if (t ~ /\.(ts|js|mjs|cjs|py|rb|sh)$/ || t ~ /^\// || t ~ /^\.\// || t ~ /^\.\.\//) { local_entry = 1; break }
|
|
85
|
+
}
|
|
86
|
+
if (local_entry) continue
|
|
87
|
+
|
|
88
|
+
for (j = 1; j <= k; j++) {
|
|
89
|
+
t = tok[j]; gsub(/^[ \t"]+|[ \t"]+$/, "", t)
|
|
90
|
+
if (t == "" || substr(t, 1, 1) == "-") continue
|
|
91
|
+
if (t ~ /@[0-9]/ || t ~ /==[0-9]/) break
|
|
92
|
+
print file ":" line ": версия сервера не закреплена: " cmd " " t
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
' "$F")
|
|
99
|
+
[ -n "$RES" ] && OUT="${OUT}${RES}
|
|
100
|
+
"
|
|
101
|
+
done
|
|
102
|
+
|
|
103
|
+
OUT=$(printf '%s' "$OUT" | own_samples_filter "$DIR" | sed '/^$/d')
|
|
104
|
+
if [ -n "$OUT" ]; then
|
|
105
|
+
printf '%s\n' "$OUT"
|
|
106
|
+
echo " почини: закрепи точную версию (пакет@1.2.3) и убедись, что файл команды существует."
|
|
107
|
+
echo " мёртвый сервер агенту не виден: он просто работает без этих инструментов и молчит."
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
exit 0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
intent: объявленный MCP-сервер существует и закреплён по версии — иначе агент молча работает без него
|
|
2
|
+
intent_en: a declared MCP server exists and is version-pinned — otherwise the agent silently works without it
|
|
3
|
+
|
|
4
|
+
# Только там, где агенту подключали внешние инструменты. В проекте без `.mcp.json` проверять
|
|
5
|
+
# нечего, а запись, показанная не тому, стоит доверия всему каталогу.
|
|
6
|
+
trigger:
|
|
7
|
+
has_mcp: true
|
|
8
|
+
|
|
9
|
+
recipes:
|
|
10
|
+
any: bash {gate}/check.sh {dir}
|
|
11
|
+
|
|
12
|
+
samples_for: any
|
|
13
|
+
|
|
14
|
+
proof: incidents/README.md, 2026-09-08 «конфиг был написан верно, а сервер мёртв» — в проект
|
|
15
|
+
записали настройку `chrome-devtools-mcp`, выглядела безупречно; сервер падал с «Target closed»,
|
|
16
|
+
потому что puppeteer подхватывал Windows-овый Chrome из `/mnt/c/`. Узнали только запуском
|
|
17
|
+
руками и чтением журнала: агенту мёртвый сервер не виден вовсе — у него просто нет этих
|
|
18
|
+
инструментов, и он молча работает без них
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"browser": {
|
|
4
|
+
"command": "npx",
|
|
5
|
+
"args": ["-y", "chrome-devtools-mcp@1.9.0"]
|
|
6
|
+
},
|
|
7
|
+
"search": {
|
|
8
|
+
"command": "uvx",
|
|
9
|
+
"args": ["some-search-mcp==0.4.1"]
|
|
10
|
+
},
|
|
11
|
+
"local": {
|
|
12
|
+
"command": "node",
|
|
13
|
+
"args": ["scripts/mcp-server.mjs"]
|
|
14
|
+
},
|
|
15
|
+
"own-code": {
|
|
16
|
+
"command": "npx",
|
|
17
|
+
"args": ["tsx", "rag/src/index.ts"]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"browser": {
|
|
4
|
+
"command": "npx",
|
|
5
|
+
"args": ["-y", "chrome-devtools-mcp@latest"]
|
|
6
|
+
},
|
|
7
|
+
"memory": {
|
|
8
|
+
"command": "/opt/tools/memory-mcp-v0.10.8/bin/memory",
|
|
9
|
+
"args": []
|
|
10
|
+
},
|
|
11
|
+
"search": {
|
|
12
|
+
"command": "npx",
|
|
13
|
+
"args": ["-y", "some-search-mcp"]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# protection-not-removed
|
|
2
|
+
|
|
3
|
+
**Набор объявленных гейтов может только расти.** Убрать гейт из `.aqk.yml` можно — но не молча.
|
|
4
|
+
|
|
5
|
+
## Зачем
|
|
6
|
+
|
|
7
|
+
Комплект ловит агента, когда тот выключает **сигнал**: `# noqa` ловит `gate-not-weakened`,
|
|
8
|
+
`pytest || true` — `ci-actually-fails`, тест, зелёный при любых данных, — `test-has-assertion`.
|
|
9
|
+
|
|
10
|
+
А главный рубильник — сам манифест — не сторожил никто. Замер 2026-09-09 на живой фикстуре:
|
|
11
|
+
|
|
12
|
+
```console
|
|
13
|
+
$ aqk doctor --run --min 1 # четыре гейта, настоящий секрет в коде
|
|
14
|
+
✘ secrets-not-in-code код 1
|
|
15
|
+
код: 1
|
|
16
|
+
|
|
17
|
+
$ grep -v "secrets-not-in-code:" .aqk.yml > tmp && mv tmp .aqk.yml
|
|
18
|
+
$ aqk doctor --run --min 1
|
|
19
|
+
Порог AQK-1 пройден.
|
|
20
|
+
код: 0 # секрет на месте, файлы гейта на диске
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Ни `doctor`, ни `report --since`, ни `vitals`, ни блок состояния для агента этого не заметили.
|
|
24
|
+
Это `pytest || true` этажом выше. И бьёт сильнее обычного обхода: `# noqa` виден в коде, глаз за
|
|
25
|
+
него цепляется, а удалённая строка — это **отсутствие**, а отсутствие не рецензируют.
|
|
26
|
+
|
|
27
|
+
## Как устроено
|
|
28
|
+
|
|
29
|
+
Рядом с реестрами долга лежит `gates-declared.txt` — снимок объявленной защиты. Проверка сверяет
|
|
30
|
+
его с `gates:` манифеста:
|
|
31
|
+
|
|
32
|
+
- имя из снимка пропало из манифеста → **красный**;
|
|
33
|
+
- имя в снимке несёт причину после `#` → снятие названо, прогон зелёный, причина печатается;
|
|
34
|
+
- новые гейты дописываются в снимок сами: набор растёт, укоротить его молча нельзя;
|
|
35
|
+
- снимка нет, но он **был в истории git** → красный: удаление снимка снимает и саму проверку.
|
|
36
|
+
|
|
37
|
+
Снять гейт по-прежнему можно. Требуется одно: назвать причину — как `deprecated` обязан нести
|
|
38
|
+
`superseded_by`. Решение остаётся за человеком и перестаёт быть невидимым.
|
|
39
|
+
|
|
40
|
+
## Готовый аналог
|
|
41
|
+
|
|
42
|
+
**Есть похожие, и они про другое.** [`imbue-ai/ratchets`](https://github.com/imbue-ai/ratchets)
|
|
43
|
+
и [`eslint-seatbelt`](https://www.notion.com/blog/how-we-evolved-our-code-notions-ratcheting-system-using-custom-eslint-rules)
|
|
44
|
+
сторожат **число нарушений**: счётчик по правилу может только уменьшаться.
|
|
45
|
+
|
|
46
|
+
Удаление самого правила они не ловят — и это не догадка, а их документация, проверено
|
|
47
|
+
2026-09-09: «Counts for rules no longer in the resolved enabled set are kept dormant (no
|
|
48
|
+
cleanup). `ratchets tighten` emits a stderr warning naming each orphan». То есть правило,
|
|
49
|
+
убранное из конфига, оставляет счётчик дремать, печатается предупреждение в stderr — **удаление
|
|
50
|
+
не блокируется и объяснения не требует**.
|
|
51
|
+
|
|
52
|
+
Разница принципиальная: у счётчикового храповика удаление правила выглядит как **снижение до
|
|
53
|
+
нуля**, то есть как успех. Ровно та подмена, против которой написан весь комплект.
|
|
54
|
+
|
|
55
|
+
## Чего НЕ ловит
|
|
56
|
+
|
|
57
|
+
- **Гейт, оставленный в манифесте, но выхолощенный до `true`.** Имя на месте, снимок доволен.
|
|
58
|
+
Это ловит `gates-are-runnable` (пустая команда) и `prove` (гейт не краснеет на красном
|
|
59
|
+
образце) — но `lint: "true"` пройдёт обе. Отдельной защиты от подмены команды здесь нет.
|
|
60
|
+
- **Причину, написанную для отвода глаз.** «# снят: не нужен» — законная строка. Проверка
|
|
61
|
+
требует, чтобы решение было НАЗВАНО, а не чтобы оно было верным: верность оценивает человек
|
|
62
|
+
в дифе. Это сознательная граница, та же, что у `deprecated`.
|
|
63
|
+
- **Удаление вместе со снимком в одном коммите на свежем клоне.** Свидетель — история git;
|
|
64
|
+
в репозитории без истории (или при `--depth 1` без этого файла) проверка скажет «снимок
|
|
65
|
+
снят заново», а не «удалён».
|
|
66
|
+
- **Гейты, объявленные пустой командой.** Они не считаются объявленными — защиты они и не
|
|
67
|
+
давали.
|